
O pool de conexões que exauriu — e as 38 conexões órfãs que fizeram o hub renderizar vazio
⚡ O hub que respondia, mas estava vazio
Eram 12h de 07/08. O Bug Hunter (o auditor automático que renderiza as rotas e checa se o conteúdo montou) rodou como sempre. Resultado: /hub, /playground, /knowledge, /tools/scrape com #app len=7 — ou seja, o HTML tinha só o <div id="app"></div> vazio, sem nada dentro.
O Arachne respondia HTTP 200. O deploy tava verde. Mas as rotas autenticadas renderizavam vazio.
E no journal: 1.239 erros de QueuePool limit ... timeout 30.00 acumulados desde as 02:57.
🧠 O contexto: pool de conexões é finito
O Arachne usa SQLAlchemy com PostgreSQL. O pool padrão é QueuePool com 10 conexões ativas + 20 de overflow = máximo 30 simultâneas.
Quando o pool enche, novas requisições esperam (timeout 30.00) e depois falham. É o engarrafamento clássico: se cada request devolve a conexão, 30 basta. Se alguma segura a conexão pra sempre, o pool exaure.
O suspeito: o middleware de autenticação por X-API-Key.
🔧 A luta: o generator que nunca era fechado
O auth_middleware fazia uma query pra validar a API key:
# ❌ Antes — o generator vaza a conexão
_session = next(_get_session()) # abre a sessão... e NUNCA fecha
# usa _session pra validar a key
# ... request termina, generator fica aberto
O problema: _get_session() é um generator. next() abre a sessão, mas a conexão só volta ao pool quando o generator é fechado (via finally ou with). Sem fechar, cada request autenticado por X-API-Key vazava 1 transação idle in transaction no PG.
Com o tráfego MCP contínuo (os clientes chamam arachne_* o tempo todo), as conexões órfãs acumularam: 38 conexões presas — algumas há 1.6h, outras 8.6h.
# O que o journal mostrava (1.239 vezes)
sqlalchemy.exc.TimeoutError: QueuePool limit of size 10 overflow 20 reached,
connection timed out, timeout 30.00
# O que o PG via: 38 conexões idle in transaction
SELECT state, COUNT(*) FROM pg_stat_activity
WHERE datname = 'arachne' GROUP BY state;
# 38 idle in transaction ← as órfãs
💡 A resolução: fechar o generator no finally
# ✅ Depois — guarda o generator e fecha no finally
_session_gen = _get_session()
try:
_session = next(_session_gen)
# valida a key
finally:
_session_gen.close() # roda o __exit__ → session.close() → conexão volta ao pool
Uma mudança de 22 linhas. O generator agora é fechado no finally — a conexão volta ao pool imediatamente, não importa o que aconteça no meio.
Mitigação imediata pro incidente: pg_terminate_backend nas 38 conexões órfãs + restart do serviço.
Verificação: Bug Hunter re-rodado → 11/11 checks OK, 0 falhas, /api/auth/me 200.
📊 Métricas
| Métrica | Valor |
|---|---|
| Erros QueuePool no journal | 1.239 (desde 02:57) |
| Conexões órfãs presas | 38 (idle in transaction) |
| Tempo das mais antigas | 8.6h |
| Pool configurado | 10 + 20 overflow = 30 máx |
| Fix | _session_gen.close() no finally (22 linhas) |
| Verificação pós-fix | Bug Hunter 11/11, /api/auth/me 200 |
🎯 Aprendizados
-
Generator SQLAlchemy sem close = leak de conexão —
next(_get_session())semfinallyé uma armadilha. A sessão abre, mas a conexão só volta ao pool quando o generator fecha. -
HTTP 200 não significa “funcionou” — as rotas autenticadas respondiam 200 com HTML vazio. Só o Bug Hunter (que renderiza e checa o DOM) pegou. Health check de status HTTP não basta pra SPA.
-
Bug de pooling é gradual, não binário — 1.239 erros acumulados desde 02:57. Não quebrou de repente; foi sangrando conexão por conexão até exaurir. Monitorar
pg_stat_activityé a rede de proteção. -
Auditoria automática pega o que monitor não vê — o Bug Hunter rodou na hora certa e achou o sintoma (rotas vazias) que levou à causa (pool exaurido).