2026-06-03_cadencia-trigger-server-env-stale-systemd¶
Incidente: trigger_server.py daemon rodando 4 dias com env stale — todo tenant novo falhava 403 GHL create_location silenciosamente¶
Data: 03/06/2026 Severidade: Alta Projeto: Cadência Duração do impacto: ~20h (02/06 23:40 → 03/06 19:30) Tags: #backend #ghl #provisioning #env-stale #daemon #systemd #silenciosa #regressao
O que aconteceu¶
Durante o onboarding do tenant Marinella/Grupo WGL (73779837-93ee-4693-9b3e-9a5ae75a4875) em 03/06 18:07, o provisionamento da subconta GHL falhou silenciosamente com provisioning_status="partial" — mesmo erro do incident 02/06 (GHL_COMPANY_ID errado → 403 Forbidden), apesar do fix daquele incident já estar aplicado há ~18h.
Investigação revelou um segundo tenant afetado na mesma janela: Mel Quevedo / Horus Marcas e Patentes (1063c105-b096-46cf-b29c-efe657683314, provisionado 17:54). Bug silencioso — log captura como WARN (não FATAL), zero alertas, frontend não bloqueia próximas fases do onboarding.
Causa raiz¶
Bug estrutural distinto do incident 02/06 (que era .env com valor errado em disco). Aqui o .env estava correto desde 02/06 23:40, mas o processo daemon nunca recarregou em memória.
-
trigger_server.pydaemon longo: iniciado em 30/05 01:26 vianohup python3 trigger_server.py >> log &(PID 2164301, uptime 4d 17h 58min). Lê/cadencia/.envna inicialização e mantém todas as vars emos.environem memória. -
trigger_server.py:run_script()line ~30: invoca scripts filhos viasubprocess.run(cmd, capture_output=True, timeout=timeout)sem parâmetroenv=. Sem isso, subprocess herda env do parent (o daemon stale). -
provision_tenant.py:55(env loader): lê.envcomos.environ.setdefault(k.strip(), v.strip()).setdefault()não sobrescreve se a chave já existe — e ela existe (herdada do parent stale). Logo,provision_tenant.pyLÊ o.envcorrigido mas usa o valor antigo já em memória do parent. -
provision_tenant.py:create_ghl_location(): montabody = {'companyId': os.environ.get('GHL_COMPANY_ID', ''), ...}=yXnyB5pagLHLdjEvGpYe(valor pré-fix). GHL responde 403 "Forbidden resource" porqueGHL_AGENCY_TOKENnão tem permissão nessa company.
Cadeia: daemon iniciado 30/05 → fix .env 02/06 23:40 sem restart → subprocess herda env stale → setdefault impede correção → 403 silencioso em todos provisionings desde então.
Por que não foi detectado¶
- Erro silencioso por design:
provision_tenant.pytrata o 403 comoWARN, segue com DNS e blog, marcaprovisioning_status=partial. Frontend não bloqueia onboarding nas fases seguintes (PR #7 do 02/06 só validou pré-condições notrigger-generation, não nas fases pós-OB). - Nenhuma instrumentação de alerta: 0 webhooks, 0 mensagem Stevo, 0 dashboard de "tenants com partial". Bug só apareceria quando cliente reclamasse de email/scoring não funcionando.
- Incident 02/06 não checou o uptime do daemon: o fix testou
curl /locations/com env do shell SSH (que lê.envatualizado), confirmou 201 Created e considerou resolvido — sem verificar se o daemon em memória também tinha o env atualizado. provision_tenant.pyreportavaUsing static GHL_AGENCY_TOKEN (fallback)mas não logava ocompanyIdno payload — escondeu a evidência decisiva.
Como foi corrigido¶
Fix em 4 camadas (todos aplicados 03/06 19:00-19:43):
1. Reprovision manual dos 2 tenants afetados¶
TRIGGER_SECRET='L9rgVfb...'
curl -s -X POST http://localhost:39090/provision \
-H 'Content-Type: application/json' \
-d '{"tenant_id":"73779837-...","secret":"'$TRIGGER_SECRET'"}'
# Marinella: location_id=O8AQNUoPg9oIa4vle6Fo, status=OK
# Horus: location_id=mxAthP6SiONbBLB8rhnI, status=OK
2. Patch em 9 scripts /cadencia/pipeline/*.py (sed em massa)¶
os.environ.setdefault(k.strip(), v.strip()) → os.environ[k.strip()] = v.strip()
Scripts patchados: provision_tenant.py, trigger_server.py, seinfeld_generate.py, blog_generate.py, linkedin_generate.py, instagram_generate.py, newsletter_generate.py, sync_accounts.py, backfill_images.py. Backups .bak-env-stale-20260603-1942.
3. Systemd unit cadencia-trigger.service criada¶
[Unit]
Description=Cadencia Growth Trigger Server (on-demand pipeline)
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/cadencia/pipeline
EnvironmentFile=/cadencia/.env
ExecStart=/usr/bin/python3 /cadencia/pipeline/trigger_server.py
Restart=always
RestartSec=5
StandardOutput=append:/cadencia/logs/trigger_server.log
StandardError=append:/cadencia/logs/trigger_server.log
[Install]
WantedBy=multi-user.target
EnvironmentFile=/cadencia/.env força systemd a reler o .env em todo restart. Restart=always evita downtime se o processo cair. Habilitado via systemctl enable cadencia-trigger.service. PID 1125397, smoke test OK.
4. Backup pré-mudança¶
/cadencia/pipeline/*.py.bak-env-stale-20260603-1942 para rollback rápido se necessário.
Prevenção¶
Checklist / regras pra evitar recorrência¶
- [ ] Após mudar
/cadencia/.envem produção:sudo systemctl restart cadencia-trigger.service(obrigatório, não opcional) - [ ] Antes de declarar "fix de
.envaplicado":ps -eo etime,cmd | grep <daemon>e compararetimecom timestamp da última mudança no.env. Se etime > idade do fix → daemon ainda stale. - [ ] Daemons Python que dependem de
.env: SEMPRE rodar via systemd comEnvironmentFile=, nuncanohup ... &manual. - [ ] Em qualquer script Python que lê
.env: usaros.environ[k] = v, NUNCAos.environ.setdefault(k, v). setdefault propaga env stale do parent. - [ ] Adicionar step 6 ao
/validar-deploy-vps: compararmtimede.envvsetimede processos dependentes.
Pattern correto (Python)¶
# ❌ ERRADO — herda env stale do parent
env_path = Path('/cadencia/.env')
for line in env_path.read_text().splitlines():
if '=' in line and not line.startswith('#'):
k, v = line.split('=', 1)
os.environ.setdefault(k.strip(), v.strip()) # NÃO sobrescreve
# ✅ CERTO — força valor do .env, ignora herança do parent
for line in env_path.read_text().splitlines():
if '=' in line and not line.startswith('#'):
k, v = line.split('=', 1)
os.environ[k.strip()] = v.strip()
Pattern correto (subprocess)¶
# ❌ ERRADO — subprocess herda env do parent (pode estar stale se parent é daemon)
subprocess.run(cmd, capture_output=True)
# ✅ CERTO se precisar isolar — passar env limpo lido do .env
clean_env = dict(os.environ)
# OR: clean_env = {**os.environ, **load_env_from_file('/cadencia/.env')}
subprocess.run(cmd, capture_output=True, env=clean_env)
Gotcha novo¶
G019 — Daemon Python lendo .env via setdefault herda env stale do parent. subprocess.run sem env= propaga env do daemon longo-vivo. Mudanças no .env em disco não chegam ao processo até restart. Fix: systemd EnvironmentFile + os.environ[k] = v.
Regra atualizada em¶
- [x]
pd-framework/times/produto/cadencia/EXPERTISE.md— adicionar G019 (pendente) - [x]
pd-framework/incidents/INDEX.md(este incident) - [ ]
/validar-deploy-vps— adicionar step de mtime check (sugerido)
Commits relacionados¶
- VPS
/cadencia/pipeline/— 9 arquivos patchados viased, backups.bak-env-stale-20260603-1942(não commitados — diretório VPS Master, sync comcadencia-growthGitHub pendente) /etc/systemd/system/cadencia-trigger.service— criado via heredoc
Links relacionados¶
- Incident pai (causa correlata, fix incompleto):
2026-06-02_cadencia-aprovou-e-nao-gerou-4-causas-raiz-convergentes.md - Tenants afetados:
- Marinella/Grupo WGL —
73779837-93ee-4693-9b3e-9a5ae75a4875(reprovisionada 19:29) - Mel Quevedo/Horus Marcas e Patentes —
1063c105-b096-46cf-b29c-efe657683314(reprovisionada 19:41) - PDL-25 (OAuth GHL bloqueado — força fallback estático e expõe esse bug)
Registrado via sistema de incidentes. Ver INDEX.md para histórico completo.