Documentation Conceitos

O cron de manutenção do Node

Uma visita que terminou precisa passar a constar como terminada. Enquanto isso não acontece, a sessão continua aberta no banco: a contagem de sessões do dia está alta demais, a duração média está errada e o consolidado de 7 dias está calculado por cima de linhas que ainda vão mudar. Nada disso é um evento perdido — é um número que ainda não fechou.

Quem faz esse fechamento é a manutenção do Node: um punhado de tarefas curtas que rodam em intervalos de segundos, fora do caminho de ingestão. Elas têm dois relógios possíveis, e um Node é saudável com qualquer um dos dois.

Esta página é para quem opera um Node próprio, tem acesso ao shell do servidor e está decidindo se vale a pena colocar uma linha no crontab.

O que é um job de manutenção

Um job de manutenção é uma tarefa que não pertence a nenhuma requisição. Ela não atende um visitante nem responde a um evento: ela arruma o estado que as requisições deixaram para trás. O Node tem três, e cada um trata de um problema diferente.

Job O que ele faz Cadência
session_sweep Fecha sessões que ficaram sem atividade tempo suficiente para serem consideradas encerradas. a cada 60 s
session_reclaim Recupera sessões que começaram a fechar e travaram no meio — um processo que morreu, um deploy no instante errado. A sessão volta para a fila em vez de ficar presa. a cada 60 s
session_reconsolidate Recalcula os números de uma sessão já fechada quando algo chegou atrasado e a marcou para recontagem. a cada 300 s

Cada job é independente. Ele é acionado sozinho, tem o próprio prazo, o próprio lote e o próprio registro de saúde.

Dois relógios, e o Node é saudável com os dois

O Node aciona a manutenção de duas maneiras:

  • Gatilho de tráfego (traffic_tick) — ligado por padrão, sem configuração nenhuma. Depois de ingerir um evento, o Node verifica se algum job está vencido e, se estiver, roda um. É assim que um Node recém-instalado se mantém desde o primeiro minuto.
  • Cron local (local_cron) — uma chamada autenticada que o seu crontab faz, no ritmo que você definir.

Um Node cujo crontab é mais lento que o intervalo — ou que não tem crontab nenhum — cai no gatilho de tráfego, e isso é o projeto funcionando, não uma degradação.

Dito sem rodeios:

  • um Node sem crontab nenhum continua se mantendo sozinho, usando o tráfego dos visitantes como relógio;
  • um crontab mais lento que o intervalo não é erro de configuração — significa apenas que mais trabalho anda em cima do tráfego;
  • o cron existe para que um Node de baixo tráfego não precise esperar um visitante aparecer.

O crontab é uma recomendação porque um temporizador é mais constante que o tráfego em um Node parado, não porque o Node precise de um para estar correto.

O que o cron melhora — e o que ele não melhora

O cron não faz uma sessão fechar mais cedo.

Uma sessão só se torna elegível para fechamento 1920 segundos (32 minutos) depois da última atividade: 1800 s de inatividade mais 120 s de carência. Esse piso é do modelo de sessão, não do agendador, e nenhum crontab o encurta.

O que muda é o depois. A sessão fica elegível em um instante qualquer; quem a fecha é o próximo acionamento da manutenção. Com o intervalo antigo de 1800 s, uma sessão que ficava elegível logo depois de uma passagem esperava a passagem seguinte — o fechamento caía em algum ponto entre cerca de 32 e 62 minutos, e você não tinha como saber onde. Com o intervalo de 60 s, a mesma sessão fecha entre 32 e 33 minutos.

Isso é ganho de previsibilidade, não de velocidade. O piso continua o mesmo; o que sumiu foi a espera imprevisível em cima dele.

O crontab recomendado

Uma linha por minuto, uma linha por job:

* * * * * curl -sS -X POST "https://SEU-NODE/public/local-maintenance.php" -H "X-Master-Key: SUA_MASTER_KEY" -H "Content-Type: application/json" -d '{"job":"session_sweep"}' > /dev/null
* * * * * curl -sS -X POST "https://SEU-NODE/public/local-maintenance.php" -H "X-Master-Key: SUA_MASTER_KEY" -H "Content-Type: application/json" -d '{"job":"session_reclaim"}' > /dev/null
*/5 * * * * curl -sS -X POST "https://SEU-NODE/public/local-maintenance.php" -H "X-Master-Key: SUA_MASTER_KEY" -H "Content-Type: application/json" -d '{"job":"session_reconsolidate"}' > /dev/null

Troque SEU-NODE pelo domínio do seu Node e SUA_MASTER_KEY pelo valor de MASTER_KEY do arquivo .env dele — a mesma chave que você usa para entrar no Console.

Três regras que não são opcionais:

  • A Master Key viaja no cabeçalho X-Master-Key — nunca na URL, nunca no corpo. URL aparece em log de servidor, em log de proxy e no histórico do shell; cabeçalho, não.
  • O corpo é exatamente {"job":"<nome>"}. Nenhuma outra chave é aceita: um corpo com campos a mais é recusado.
  • Um job por chamada. É por isso que são três linhas, e não uma.

* * * * * é a cadência recomendada para os dois jobs de 60 s. session_reconsolidate roda a cada 300 s, então */5 * * * * já basta — chamá-lo a cada minuto não faz mal, ele simplesmente responde not_due.

Se o seu Node estiver instalado de forma que a raiz do domínio já aponte para a pasta pública, https://SEU-NODE/local-maintenance.php também responde. As duas formas chegam ao mesmo lugar; use a que devolver um JSON em vez de um 404.

O contrato do endpoint

Situação Resposta
Chamada correta 200 com o envelope do job
Método diferente de POST 405 · {"error":"method_not_allowed"}
Chave ausente ou errada 403 · {"error":"forbidden_invalid_key"}
Corpo fora do formato, ou job desconhecido 422 · {"error":"job_not_registered"}
O job rodou e falhou 500 com o envelope, "status":"failed"

O envelope de uma chamada bem-sucedida tem esta forma:

{
  "job": "session_sweep",
  "status": "ran",
  "trigger": "local_cron",
  "claimed_at": 1757500000,
  "next_eligible_at": 1757500060,
  "duration_ms": 41,
  "result": {
    "claimed": 12,
    "closed": 12,
    "reconsolidated": 0,
    "conflicts": 0,
    "errors": 0,
    "had_more": false
  }
}

status responde o que aconteceu:

status Significado
ran O job rodou. result traz os números.
not_due Ainda não venceu. É a resposta normal da maioria dos minutos.
deferred Outro acionamento pegou o job primeiro. Nada a fazer.
skipped_budget Sobrou menos prazo do que a menor fatia útil de trabalho. O job volta no próximo acionamento.
failed O job levantou um erro. Vem com 500.

Um not_due não é um erro, e não deve acionar alerta nenhum. Um cron de um minuto sobre um job de 60 s passa a maior parte do tempo respondendo exatamente isso.

Orçamentos: o que o Node gasta em cada acionamento

Toda passagem de manutenção é limitada por um orçamento de tempo, além do tamanho do lote. O orçamento depende de quem está pagando a conta:

Acionamento Orçamento Quem espera
o cron 3000 ms ninguém — não há visitante do outro lado
tráfego, resposta já entregue (PHP-FPM) 800 ms ninguém — a página já foi enviada
tráfego, resposta ainda não entregue 120 ms o visitante, antes de a página sair

Nenhum visitante paga mais do que o orçamento. Quando há fila acumulada, o Node gasta mais acionamentos, e não um acionamento mais longo: a passagem para no prazo, marca que ainda havia trabalho e reprograma o job para voltar em segundos em vez de esperar o intervalo inteiro.

E quando não há nada vencido, não há custo relevante: uma requisição sem trabalho pendente custa uma leitura de arquivo e zero consultas ao banco.

Lotes, validação e o que não existe

Cada job trabalha em lotes de até 100 sessões por passagem, e nenhuma passagem ultrapassa o teto rígido de 500, seja qual for o valor pedido. Esses dois números e as cadências da tabela lá em cima são fixos no código do Node — não há nada para você ajustar neles.

Os três orçamentos aceitam ajuste pelo .env do Node — ST_MAINT_BUDGET_LOCAL_CRON_MS, ST_MAINT_BUDGET_TRAFFIC_DETACHED_MS e ST_MAINT_BUDGET_TRAFFIC_INLINE_MS. Cada um tem uma faixa própria, e a validação é explícita: um valor que não seja inteiro, ou que esteja fora da faixa, volta para o padrão em vez de ser aplicado. A saúde do Node mostra as duas coisas lado a lado — o que você escreveu (configured), o que está valendo (effective) e o motivo da recusa (fallback) — para que uma configuração ignorada não passe por configuração aplicada.

Não existe tela de configuração e não existe configuração remota. Nada disso aparece no Console, e o Supreme não muda esses valores no seu Node. Cadência e lote são fixos no código do Node; os orçamentos moram no .env do seu servidor e em nenhum outro lugar. Se você foi procurar a tela, ela não existe — não é uma tela que falta, é uma decisão.

Diagnóstico: o bloco maintenance da saúde

O endpoint autenticado de saúde traz um bloco maintenance com o estado por job:

curl -s "https://SEU-NODE/public/api/health.php" -H "X-Master-Key: SUA_MASTER_KEY"

A leitura útil está em separar duas perguntas que parecem a mesma:

1. O que este servidor consegue fazer — sondado na hora da chamada, em maintenance.runtime:

Campo O que diz
sapi Como o PHP está rodando. fpm-fcgi é a única forma capaz de entregar a resposta antes da manutenção.
terminator A função que entrega a resposta cedo, quando existe.
terminator_internal Se essa função é a nativa do PHP, e não uma substituta declarada por outro código.

2. O que de fato aconteceu da última vez — gravado, em maintenance.jobs.<job>.last:

Campo O que diz
last_status ran, not_due, deferred, skipped_budget, failed — ou never_run.
last_trigger local_cron ou traffic_tick: quem acionou.
last_response_clock detached (a resposta já tinha saído) ou inline_fallback (não tinha).
last_response_reason Por que aquele relógio, e não o outro.
last_duration_ms Quanto a passagem levou.
claimed / errors Quantas sessões a passagem pegou e quantos erros houve.

Elas são separadas de propósito. Um servidor que consegue entregar a resposta cedo e cuja última passagem mesmo assim rodou inline é um estado real e normal — é o que acontece quando quem acionou foi o cron, porque no cron não há visitante nenhum esperando. Nesse caso last_response_reason diz local_cron_sync, e não há nada a corrigir. Julgar a capacidade pelo último registro leva a caçar um problema que não existe.

Se um job nunca roda

last_status preso em never_run, ou um last_claimed_at velho, tem uma lista curta de causas:

  1. O job está desligado. Veja maintenance.jobs.<job>.enabled. Um false vem de uma variável do .env — SESSION_SWEEP_JOB_ENABLED, SESSION_RECLAIM_JOB_ENABLED ou SESSION_RECONSOLIDATE_JOB_ENABLED — com o valor 0. Ausente significa ligado.
  2. Não há crontab e não há tráfego. Os dois relógios estão parados ao mesmo tempo. Um Node sem visitantes e sem crontab não tem o que o acione. Chame o endpoint uma vez à mão e veja o status que volta.
  3. O arquivo de marcação não é gravável. Veja maintenance.jobs.<job>.pre_gate.status. Um unavailable significa que o Node não consegue escrever em storage/cache/ — corrija a permissão da pasta e o job volta sozinho no acionamento seguinte.

O interruptor SESSION_SWEEP_TICK_ENABLED

Essa variável do .env desliga o gatilho de tráfego inteiro:

  • ausente — ligado. É o padrão, e é o que você quer na maioria dos casos.
  • 0 — desligado. Nenhuma requisição de visitante aciona manutenção alguma.

O cron continua funcionando com ela desligada. Desligar o gatilho e manter o crontab é uma configuração legítima: toda a manutenção passa a andar no temporizador, e nenhuma requisição de visitante paga por ela. Desligar o gatilho sem ter crontab, por outro lado, deixa o Node sem relógio nenhum — e aí as sessões realmente param de fechar.

maintenance.tick_enabled mostra em qual dos dois estados o Node está.