0028 — Heartbeat do integrador-client, alerta de silêncio e CENTRAL_API_TOKEN¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-10 · Decidido em: 2026-09-10
Decisão local do central-backend. Não confundir com o ADR central 0028 da casa (sync event-driven das docs), que o
application.ymltambém cita.
Contexto¶
O integrador-client é um jar Java 8 que cada cliente roda on-premise (Windows, Agendador de Tarefas)
— um app com N instalações, uma por cliente. Uma instalação que para (máquina desligada, jar
travado, .properties quebrado) ninguém vê: o write-back para de chegar e o sintoma aparece horas
ou dias depois, relatado pelo cliente. A casa também não sabe, sem acesso remoto, qual versão
roda em cada cliente nem quais fluxos estão ligados. E não há cadastro de instalação: o cliente
recebe o jar e começa a rodar.
Decisão¶
- Heartbeat em dois saltos, pelo integrador-server do cliente. O client manda o heartbeat
(versão, fluxos, hostname) no startup e a cada 60 min ao integrador-server do próprio cliente
(
int.<cliente>.xadm.biz, destino que o firewall já libera, com o Bearer que o write-back já usa). O integrador-server carimba ocliente_id(envCLIENTE_ID) e repassa ao central emPOST /api/integrador/heartbeat. Carimbar no integrador-server cumpre a norma de tenant (identidade do contexto autenticado, nunca do corpo): cada instância é de um cliente só. - Contrato congelado entre os 4 repos, com amostras canônicas copiadas como fixture de teste em quem produz e em quem consome. A casa durável de K2–K4 (o que o central expõe) é o OpenAPI publicado e o contrato da API REST; a de K1 (client → integrador-server) é do integrador-server.
- Token M2M próprio,
CENTRAL_API_TOKEN, com filtro próprio (IntegradorAuthFilter) que só abre/api/integrador/**. Nome pelo destino (<DESTINO>_API_TOKEN), um valor compartilhado por todos os integrador-servers. Não reusa oAUTH_INTEGRATOR_TOKEN(sync de senhas pAbast), que abre todo o/api/admin/**. Emprod, token vazio recusa o boot (IntegradorTokenGuard). - Registro automático por upsert: uma linha por cliente em
integrador_instancias, criada no 1º heartbeat. Sem@Transactionale sem lock — a leitura prévia só escolhe o log ("registrado" ou "voltou").cliente_idfora declientes→422 cliente_desconhecido+WARN(misconfiguração do integrador-server), sem linha. - Silêncio medido em horas úteis: 07:00–22:00, seg–sex, sem feriado nacional, em
America/Sao_Paulo. Passou de 6h úteis, oSilencioJob(a cada 15 min) alerta uma vez por episódio. Carnaval e Corpus Christi (ponto facultativo) contam como feriado — menos alerta falso. Régua no código, sem property. - Eleição por claim condicional, não por lock:
UPDATE … SET alertado_em = :agora WHERE alertado_em IS NULL AND ultimo_heartbeat_em = :visto. Só a réplica que vira a linha alerta; um heartbeat que chegou entre a leitura e o claim faz o claim falhar (sem alerta falso). - Alerta = evento do GlitchTip do central com fingerprint por cliente
(
["integrador-client-silencioso", <cliente_id>], tagcliente), viaSentry.captureMessage, maisWARNno log — nuncaLOG.error, que oSentryAppenderduplicaria. Uma issue por cliente: o alerta padrão do GlitchTip notifica a cada evento, então reincidências notificam do mesmo jeito. - Um relógio só (
Clockinjetado,ClockFactory) carimba, reivindica e mede. O instante gravado é o do servidor, nunca o do client. - API admin sem paginação e com
DELETEidempotente.GET /api/admin/integradoresé lookup fixo pequeno (no máximo uma linha por cliente), a isenção de paginação da norma — mesmo precedente doGET /api/admin/clients. ODELETEresponde204com ou sem a linha: um404numa requisição com credencial vira ERROR no GlitchTip (regra daxadm-comum-web0.7.1+), e cada corrida benigna abriria issue. É o botão "Parar de acompanhar" da aba Deploy do central-ui.
Alternativas descartadas¶
- Client direto ao central. Exigiria liberar um destino novo no firewall de cada cliente e distribuir um token novo em cada instalação.
- Heartbeat pela fila do PowerSync. É canal de dado de negócio; misturar sinal de vida nele acopla a observabilidade à saúde do próprio canal que ela observa.
- Fallback do
cliente_idpara oCLIENTEdo integrador-server. OCLIENTEé nome de exibição (Maxsul,On Petro Trading…) e nunca passaria na regex; o fallback esconderia a misconfiguração. - Reusar o
AUTH_INTEGRATOR_TOKEN. Abriria todo o/api/admin/**a cada integrador-server. - Fingerprint por episódio (issue nova a cada silêncio). Não notifica mais do que por cliente e aumenta a exposição ao corte do smoke pós-deploy (issue nova reprova; reincidência só avisa).
pg_try_advisory_lockpara eleger a réplica. Mesmo efeito com lock de sessão e conexão presa; o claim condicional é idempotente por construção.- Outbox, retry ou histórico de heartbeats. O heartbeat é periódico — perder um é irrelevante, o próximo cobre. O estado atual basta para "está vivo?".
Consequências¶
CENTRAL_API_TOKENé obrigatório nos dois recursos do central (jar e native), como oCENTRAL_DEPLOY_TOKEN— sem ele o container não sobe. Ver o runbook de deploy e o runbook do heartbeat.- O bind do
String[]na@Querynativa (com@TypeDef(STRING_ARRAY)no parâmetro) é provado na JVM pelo teste; no binário native, só pelo passo 1 do smoke real depois da release. - O primeiro alerta da vida de um cliente dentro da janela do smoke pós-deploy pode reprovar um
deploy do central (o central não carimba
releaseno Sentry, então a camada 3 corta por tempo). OinitialDelay = 20mdo job tira a réplica nova dessa janela; o risco residual está aceito. - Regra de notificação do GlitchTip do projeto do central tem de ser por evento, não só "issue nova" — senão as reincidências não notificam. Ver provedor GlitchTip.
- Apagar não desliga: parar de acompanhar uma instalação cujo jar ainda roda só dura até o
próximo heartbeat. O desligamento de verdade é no client (
heartbeat.intervaloMinutos=0).