Preparar e prever antes de executar
Guarda o código completo abaixo como run.py numa pasta de trabalho. Precisas de Python 3.13 ou posterior e de um executável NGINX com os módulos HTTP de proxy e cache. Define DR_NGINX_BIN com o caminho desse executável e executa python3 run.py --output evidence.json. O script cria portas locais temporárias, uma origem fictícia e uma cache isolada. Antes de executar, escreve a tua previsão para cada grupo: pedidos à origem, estados de cache e corpo recebido. Não reutilizes esta configuração como configuração do portal de produção. Os cabeçalhos X-Lab são controlos didáticos definidos apenas neste ensaio.
Separar conectividade de reutilização
No primeiro grupo, dois GET a /fresh produzem MISS e HIT. A origem recebe apenas um pedido e os corpos coincidem. Esta combinação permite explicar onde ocorreu a reutilização. Um cliente sem cache de corpos envia efetivamente os dois pedidos ao proxy; por isso não atribuas o segundo resultado ao browser. Regista o percurso concreto e a contagem antes de generalizar. Um HIT também não demonstra que um dado de negócio continua adequado ao processo que o consome. Neste ensaio o corpo é uma cadeia sintética, sem dependências de base de dados ou regras de autorização.
Construir uma matriz de representações
O grupo /query usa fund=A e fund=B, seguidos de uma repetição de A. Espera duas idas à origem e reutilização da representação A no terceiro pedido. Depois compara pt, en, pt e en em /variant, onde a origem envia Vary: Accept-Language. Os corpos devem acompanhar o idioma em cada leitura. A grelha tem duas perguntas distintas: o recurso correto foi identificado e a variante correta foi escolhida? Num portal fictício de fundos, uma otimização que perde o parâmetro fund pode entregar dados do fundo errado mesmo com latência excelente. Define os resultados funcionais antes de medir a melhoria de tempo.
Investigar respostas que não ficam guardadas
Os grupos /nostore, /private e /cookie fazem dois pedidos iguais a cada endpoint. Nesta configuração, todos chegam à origem e dão MISS. Usa os cabeçalhos registados e a configuração efetiva para explicar cada resultado, em vez de começar por limpar diretórios ou aumentar disco. A origem pode estar a comunicar limites legítimos de reutilização. Para um incidente fictício de baixo hit ratio, escreve uma hipótese, a observação que a distingue de avaria e a mudança que não deves fazer sem rever o contrato. Retirar sinais de personalização apenas para subir uma métrica pode introduzir um problema de isolamento mais grave.
Ler, guardar e invalidar são decisões distintas
Em /bypass, preenche primeiro a cache com v1. A origem muda para v2 e um pedido com X-Lab-Bypass obtém a versão nova; a leitura normal seguinte dá HIT v2. Depois a origem muda para v3. Só X-Lab-Nostore continua a permitir HIT v2. Com os dois cabeçalhos, o cliente recebe v3, mas a leitura normal posterior permanece em v2. Desenha três colunas: de onde veio o corpo, o pedido guardou uma resposta e a entrada anterior desapareceu? Esta sequência impede anunciar uma limpeza da cache só porque um diagnóstico conseguiu ler a origem. O script não executa uma API de purge.
Entregar uma conclusão reproduzível
O registo desta revisão usa NGINX 1.30.5 e Python 3.13.1. O ficheiro de evidência contém configuração, pedidos à origem, respostas ao cliente, logs por salto e hashes do script e binário. Compara os resultados dos dez grupos com as previsões e explica uma previsão errada. Não esperes igualdade de portas ou datas entre execuções. O processo NGINX termina e a pasta temporária é removida no fim; confirma esses campos. A execução não envolve TLS, browser, CDN, autenticação ou carga concorrente. Termina com uma nota de handover que diga o que foi observado, em que ambiente e que hipótese continua por testar.
"""Original DR cache experiment. Only synthetic data and IPv4 loopback.
Usage: DR_NGINX_BIN=/path/to/nginx python3 run.py --output evidence.json
Requires Python 3.13+ and NGINX with HTTP proxy/cache modules; no installation.
"""
import argparse, hashlib, http.client, http.server, json, os, pathlib, platform
import signal, socket, subprocess, tempfile, threading, time
from datetime import datetime, timezone
def run(binary):
events, checks, responses = [], {}, []
state = {'version': 'v1', 'outage': False}
class Origin(http.server.BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
def log_message(self, *args):
pass
def do_GET(self):
route = self.path.split('?')[0]
events.append({'path': self.path, 'language': self.headers.get('Accept-Language'),
'ifNoneMatch': self.headers.get('If-None-Match')})
status, headers, body = 200, {'Cache-Control': 'max-age=60'}, self.path
if route == '/variant':
headers['Vary'] = 'Accept-Language'
body = 'language:' + self.headers.get('Accept-Language', 'none')
elif route == '/nostore':
headers['Cache-Control'] = 'no-store'
elif route == '/private':
headers['Cache-Control'] = 'private, max-age=60'
elif route == '/cookie':
headers['Set-Cookie'] = 'synthetic=1; Path=/'
elif route == '/bypass':
body = 'version:' + state['version']
elif route == '/revalidate':
headers.update({'ETag': '"fixture-v1"', 'Cache-Control': 'max-age=1'})
body = 'validated-body'
if self.headers.get('If-None-Match') == '"fixture-v1"':
status, body = 304, ''
headers['Cache-Control'] = 'max-age=60'
elif route in ('/stale', '/strict'):
headers['Cache-Control'] = 'max-age=1'
body = 'last-known-value'
if state['outage']:
status, body = 503, 'origin-unavailable'
headers['Cache-Control'] = 'no-store'
payload = body.encode('utf-8')
self.send_response(status)
for key, value in headers.items():
self.send_header(key, value)
if status != 304:
self.send_header('Content-Length', str(len(payload)))
self.send_header('Connection', 'close')
self.close_connection = True
self.end_headers()
if payload:
self.wfile.write(payload)
origin = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Origin)
origin.daemon_threads = True
thread = threading.Thread(target=origin.serve_forever, daemon=True)
thread.start()
# Reserve an ephemeral candidate. NGINX binding failure is fatal, never take over an existing service.
with socket.socket() as s:
s.bind(('127.0.0.1', 0))
port = s.getsockname()[1]
process = None
result = {}
def check(name, details, valid):
checks[name] = {'passed': bool(valid), **details}
if not valid:
raise AssertionError(name + ': ' + json.dumps(details))
try:
with tempfile.TemporaryDirectory(prefix='dr-http-cache-') as tmp:
root = pathlib.Path(tmp)
config = '''daemon off;
master_process on;
worker_processes 1;
error_log "ROOT/error.log" notice;
pid "ROOT/nginx.pid";
events { worker_connections 64; }
http {
access_log "ROOT/access.log" evidence;
log_format evidence escape=json '{"path":"$request_uri","status":"$status","upstream":"$upstream_status","cache":"$upstream_cache_status"}';
proxy_temp_path "ROOT/proxy-temp";
proxy_cache_path "ROOT/cache" keys_zone=lab:1m max_size=10m inactive=2m;
server {
listen 127.0.0.1:PORT;
proxy_cache lab;
proxy_cache_key "$scheme$proxy_host$request_uri";
proxy_cache_valid 200 60s;
proxy_cache_revalidate on;
proxy_cache_bypass $http_x_lab_bypass;
proxy_no_cache $http_x_lab_nostore;
add_header X-DR-Cache $upstream_cache_status always;
location / { proxy_pass http://127.0.0.1:ORIGIN; }
location = /stale {
proxy_cache_use_stale error timeout http_503;
proxy_pass http://127.0.0.1:ORIGIN;
}
}
}
'''.replace('ROOT', str(root)).replace('PORT', str(port)).replace('ORIGIN', str(origin.server_port))
# log format must be declared before the access_log that names it.
lines = config.splitlines()
ai = next(i for i,l in enumerate(lines) if 'access_log ' in l)
lines[ai], lines[ai+1] = lines[ai+1], lines[ai]
config = '\n'.join(lines) + '\n'
conf = root/'nginx.conf'
conf.write_text(config)
subprocess.run([binary, '-t', '-p', str(root)+'/', '-c', str(conf)], check=True, capture_output=True, text=True)
output = open(root/'process.log', 'w')
try:
process = subprocess.Popen([binary, '-p', str(root)+'/', '-c', str(conf)], stdout=output, stderr=output)
for _ in range(100):
if process.poll() is not None:
raise RuntimeError((root/'process.log').read_text())
try:
with socket.create_connection(('127.0.0.1', port), timeout=.2):
break
except OSError:
time.sleep(.05)
else:
raise TimeoutError('NGINX readiness timeout')
def get(path, headers=None):
client = http.client.HTTPConnection('127.0.0.1', port, timeout=5)
try:
client.request('GET', path, headers=headers or {})
r = client.getresponse()
row = {'path':path, 'status':r.status, 'cache':r.getheader('X-DR-Cache'), 'body':r.read().decode()}
responses.append(row)
return row
finally:
client.close()
def count(route):
return sum(e['path'].split('?')[0] == route for e in events)
a,b = get('/fresh'),get('/fresh')
check('fresh-response-reused-without-origin', {'states':[a['cache'],b['cache']], 'originRequests':count('/fresh')}, [a['cache'],b['cache']]==['MISS','HIT'] and a['body']==b['body'] and count('/fresh')==1)
a,b,c = get('/query?fund=A'),get('/query?fund=B'),get('/query?fund=A')
check('query-string-separates-cache-keys', {'states':[x['cache'] for x in (a,b,c)],'originRequests':count('/query'),'bodies':[x['body'] for x in (a,b,c)]}, a['body']!=b['body'] and a['body']==c['body'] and count('/query')==2 and c['cache']=='HIT')
rows = [get('/variant', {'Accept-Language':l}) for l in ['pt','en','pt','en']]
check('vary-separates-language-representations', {'states':[r['cache'] for r in rows],'bodies':[r['body'] for r in rows],'originRequests':count('/variant')}, [r['cache'] for r in rows]==['MISS','MISS','HIT','HIT'] and [r['body'] for r in rows]==['language:pt','language:en','language:pt','language:en'] and count('/variant')==2)
for route,label in [('nostore','no-store-not-retained'),('private','private-not-shared'),('cookie','set-cookie-not-cached-by-this-configuration')]:
rows = [get('/'+route),get('/'+route)]
check(label, {'states':[r['cache'] for r in rows],'originRequests':count('/'+route)}, [r['cache'] for r in rows]==['MISS','MISS'] and count('/'+route)==2)
get('/bypass');state['version']='v2'
a,b = get('/bypass',{'X-Lab-Bypass':'1'}),get('/bypass')
check('bypass-can-populate-new-cached-response', {'bypassState':a['cache'],'nextState':b['cache'],'nextBody':b['body']},a['cache']=='BYPASS' and b['cache']=='HIT' and b['body']=='version:v2')
state['version']='v3'
a=get('/bypass',{'X-Lab-Nostore':'1'})
b=get('/bypass',{'X-Lab-Bypass':'1','X-Lab-Nostore':'1'})
c=get('/bypass')
check('read-bypass-and-write-suppression-are-separate', {'noCacheOnlyState':a['cache'],'combinedState':b['cache'],'combinedBody':b['body'],'laterCachedBody':c['body']},a['cache']=='HIT' and b['cache']=='BYPASS' and b['body']=='version:v3' and c['body']=='version:v2' and c['cache']=='HIT')
a=get('/revalidate');get('/stale');get('/strict');time.sleep(2.1)
b=get('/revalidate');state['outage']=True
stale,strict=get('/stale'),get('/strict')
process.send_signal(signal.SIGQUIT);process.wait(timeout=10)
logs=[json.loads(l) for l in (root/'access.log').read_text().splitlines() if l.strip()]
rv=[l for l in logs if l['path']=='/revalidate'][-1]
check('upstream-304-revalidates-body-for-client-200', {'clientStatus':b['status'],'upstreamStatus':rv['upstream'],'cacheState':b['cache'],'bodyRetained':a['body']==b['body'],'conditionalValidatorObserved':any(e['ifNoneMatch']=='"fixture-v1"' for e in events)}, b['status']==200 and rv['upstream']=='304' and b['cache']=='REVALIDATED' and b['body']==a['body'])
sl=[l for l in logs if l['path']=='/stale'][-1]
st=[l for l in logs if l['path']=='/strict'][-1]
check('stale-policy-masks-origin-failure-in-client-status', {'staleClientStatus':stale['status'],'staleUpstreamStatus':sl['upstream'],'staleCacheState':stale['cache'],'strictClientStatus':strict['status'],'strictUpstreamStatus':st['upstream']},stale['status']==200 and sl['upstream']=='503' and stale['cache']=='STALE' and strict['status']==503 and st['upstream']=='503')
result={'executedAt':datetime.now(timezone.utc).isoformat(), 'nginxVersion':subprocess.run([binary,'-V'],capture_output=True,text=True,check=True).stderr.strip(), 'pythonVersion':platform.python_version(),'checks':checks,'passed':sum(c['passed'] for c in checks.values()),'failed':sum(not c['passed'] for c in checks.values()),'configuration':config,'originEvents':events,'responses':responses,'accessLog':logs,'scriptSha256':hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest(),'binarySha256':hashlib.sha256(pathlib.Path(binary).read_bytes()).hexdigest(),'scope':'Actual NGINX with one worker and synthetic Python origin on IPv4 loopback. GET only. No TLS, browser cache, authentication, CDN, production traffic, commercial purge API, load test or independent specialist review.'}
finally:
if process is not None and process.poll() is None:
process.send_signal(signal.SIGQUIT)
try: process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill();process.wait(timeout=5)
output.close()
result['temporaryDirectoryRemoved']=not root.exists()
result['childExited']=process.returncode is not None
return result
finally:
origin.shutdown();origin.server_close();thread.join(timeout=5)
if __name__ == '__main__':
parser=argparse.ArgumentParser();parser.add_argument('--output',required=True);args=parser.parse_args()
binary=os.environ.get('DR_NGINX_BIN')
if not binary or not pathlib.Path(binary).is_file():
parser.error('DR_NGINX_BIN must identify an existing NGINX executable')
result=run(str(pathlib.Path(binary).resolve()))
pathlib.Path(args.output).write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps({'passed':result['passed'],'failed':result['failed'],'temporaryDirectoryRemoved':result['temporaryDirectoryRemoved'],'childExited':result['childExited']}))
Num portal fictício, /query?fund=A e /query?fund=B têm corpos diferentes. Uma chave que perde fund pode misturar dados sem produzir erro HTTP.
Armadilhas comuns
Confundir bypass com purge, remover private para subir o hit ratio ou assumir que um cabeçalho personalizado afeta todas as caches do percurso.
Tópicos relacionados: O pedido e a representação pretendida · Cache, variantes e validação · Diagnóstico e orçamento de tempo
Uma cache correta precisa de selecionar o corpo certo, respeitar o contrato de reutilização e tornar observáveis as decisões de leitura e armazenamento.
Referência: NGINX HTTP proxy module: cache controls · DR HTTP/HTTPS 2026-09; HTTP RFCs 9110–9114; selected TLS 1.3 and NGINX/curl guidance