0028 — Docs sync event-driven pelo central¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-12 · Decidido em: 2026-08-31
Contexto¶
O container de docs (nginx:1.27-alpine + rclone) serve docs.xadm.biz e roda
scripts/sync-apps.sh em loop a cada ~90s: rclone lsf garage:docs-sites + rclone sync por
app (~36k objetos / ~20 apps). O LIST/HEAD massivo do rclone mantém o garage em ~3 cores
constantes — não é scrub/resync, é carga de request do poll — o maior hog de CPU do host
(backlog item 134). E as docs quase nunca mudam (só num publish de release/master) → o poll é
desperdício quase-total.
Decisão¶
O sync das docs é event-driven: o publish dispara, o relógio não. O central-backend — que já é o dono dos triggers de deploy (control-plane, 0026) — passa a ser o dono do trigger de sync das docs também.
- O publish avisa. O job
docsdopipeline.yml(GitHub), ao publicar no Garage, fazPOST /api/ci/docs-published {slug, alvo}no central (BearerCENTRAL_DEPLOY_TOKEN, molde do/api/ci/deploy). Best-effort, não-fatal — se o aviso falha, o jobdocssegue verde e o backstop cobre. - O central cutuca o nginx. O container docs expõe um endpoint HTTP interno (rede do
Coolify, não roteado pelo Traefik)
POST /_sync?slug=<slug>, autenticado por um token próprio (DOCS_API_TOKEN, ≠ o de deploy — outra fronteira de confiança). O central chama ao receber odocs-published. - O nginx sincroniza só o prefixo. O
sync-apps.shganha uma funçãosync_slug <slug>que fazrclone syncsó dedocs-sites/<slug>/(o nginx já tem rclone + read-key + o script — menor superfície nova). A guarda de slug anti-traversal ([a-z0-9-]+) é preservada; debounce por slug (trailing-edge) coalesce publishes em rajada. - Backstop. O poll de 90s sai; entra reconcile completo no boot do container + poll lento de 60min — rede de baixo para sinal perdido / publish fora do pipeline (edição web no Forgejo).
Escopo faseado. Esta ADR é a Fase 1 (repo docs: sync-apps.sh + /_sync + backstop + o
delta do pipeline.yml). A Fase 2 (o endpoint /api/ci/docs-published no central-backend, com
filtro /api/ci/** no molde do /api/ci/deploy) tem decisão/spec própria no repo do central-backend.
Alternativas descartadas¶
- Central roda o rclone. O central-backend ganharia rclone + read-key + rodaria o sync do prefixo direto. Preterido — duplica a key/rclone que o nginx já tem; mais superfície.
- nginx observa fila/marcador no garage. O nginx pollaria um marcador leve no garage. Preterido — ainda é poll (mais leve, mas não zero) e não usa o dono natural do trigger (o central).
- Marcador em volume compartilhado. O central escreve um marcador num volume que o nginx observa. Mais enxuto (sem listener/token), mas exige volume compartilhado + watcher e não é síncrono. Preterido — o endpoint HTTP é síncrono e sem estado compartilhado; fica como reserva se o listener provar caro.
- rclone incremental server-side. Reescrever o rclone para diff server-side. Preterido — o event-driven já mata o custo; não vale a reescrita.
Consequências¶
- Garage ~3 cores → perto de zero (pico só no publish real) — alívio direto do CPU do host (item 134), sem tocar em dado.
sync-apps.shrefatorado (funçãosync_slug+/_sync+ backstop + debounce) e Dockerfile ganha o listener (nginxlocation /_sync+fcgiwrapCGI, ou fallbackbusybox-extras; interno-only- Bearer). Nenhum dos dois é template rastreado (central-runtime) — sem bump por eles; o bump é a capacidade nova.
- Delta no
pipeline.yml(template rastreado): o step POST best-effort no jobdocs. - Encaixe constitucional: o central é o dono do trigger (§7/
§9/0026); a auth M2M segue
<DESTINO>_API_TOKEN(seguranca).