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.phptambém responde. As duas formas chegam ao mesmo lugar; use a que devolver um JSON em vez de um404.
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:
- O job está desligado. Veja
maintenance.jobs.<job>.enabled. Umfalsevem de uma variável do.env—SESSION_SWEEP_JOB_ENABLED,SESSION_RECLAIM_JOB_ENABLEDouSESSION_RECONSOLIDATE_JOB_ENABLED— com o valor0. Ausente significa ligado. - 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
statusque volta. - O arquivo de marcação não é gravável. Veja
maintenance.jobs.<job>.pre_gate.status. Umunavailablesignifica que o Node não consegue escrever emstorage/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á.