Pular para conteúdo

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 docs do pipeline.yml (GitHub), ao publicar no Garage, faz POST /api/ci/docs-published {slug, alvo} no central (Bearer CENTRAL_DEPLOY_TOKEN, molde do /api/ci/deploy). Best-effort, não-fatal — se o aviso falha, o job docs segue 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 o docs-published.
  • O nginx sincroniza só o prefixo. O sync-apps.sh ganha uma função sync_slug <slug> que faz rclone sync só de docs-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.sh refatorado (função sync_slug + /_sync + backstop + debounce) e Dockerfile ganha o listener (nginx location /_sync + fcgiwrap CGI, ou fallback busybox-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 job docs.
  • Encaixe constitucional: o central é o dono do trigger (§7/ §9/0026); a auth M2M segue <DESTINO>_API_TOKEN (seguranca).