Skip to main content

Tarefa periódica

Quando usar: manutenção que precisa acontecer de tempos em tempos — reconciliar o que ficou pela metade, expurgar o que passou da retenção, sincronizar um catálogo.

Declarar​

Um método de bean, anotado, com o Ctx da rodada (ou sem parâmetro nenhum):

@Component
@Profile("serve")
static class Periodic {
private final SampleService service;
private final ActionReaper reaper;

Periodic(SampleService service, ActionReaper reaper) {
this.service = service;
this.reaper = reaper;
}

/**
* `scan-reconcile`: varredura cuja Action terminou sem sucesso (esgotada, aposentada) deixaria o
* item "pendente" para sempre; esta tarefa fecha a conta.
*/
@NnsPeriodic(name = "scan-reconcile", every = "1m")
void scanReconcile(Ctx ctx) {
service.reconcileScans(ctx, Duration.ofMinutes(2));
}

/** `action-reaper`: fecha as Actions cuja release saiu do ar. */
@NnsPeriodic(name = "action-reaper", every = "1m")
void reap(Ctx ctx) {
reaper.reap(ctx);
}
}

O nome é único no serviço e aparece na métrica (nns.periodic.runs{task,outcome}) e no log. O every aceita a sintaxe de duração da plataforma (30s, 1m, 1h30m) e um placeholder da configuração declarada: every = "${catalog.sync-every:1h}".

O que o SDK garante​

  • Só a release ativa dispara. O papel é relido a cada rodada — a standby não roda, e quando a release vira ativa, ela começa a rodar sem restart.
  • Primeira espera aleatória em [0, every/10]: réplicas da mesma release não saem juntas.
  • O período é contado do fim da rodada: uma rodada que demora não empilha a próxima.
  • O prazo da rodada é o próprio every.
  • A exceção é contida: um erro numa rodada não mata o agendador e sai no log com error_class.
  • Desligamento: nada de rodada nova a partir do início do desligamento; a rodada em curso é interrompida pelo cancelamento do Ctx.

Uma rodada longa (laço infinito) não é tarefa periódica: para isso existe o laço de fundo (NnsBackground.loop, como o ActionRunner deste módulo).

Como testar​

O teste roda a tarefa cadastrada, pelo nome, sem esperar o relógio. Duas linhas de configuração: nns.periodic.schedule=false registra as tarefas sem agendar (recusado em produção), e PeriodicTasks.runOnce("<nome>") roda uma agora:

new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
ConsoleSdkAutoConfiguration.class, NnsLifecycleAutoConfiguration.class))
.withInitializer(NnsApplication.contextInitializer(NnsCommand.SERVE, config, module))
.withPropertyValues(
"nns.periodic.schedule=false",
"spring.profiles.active=serve",
"spring.autoconfigure.exclude="
+ "br.com.nnumbers.console.sdk.spring.data.NnsDataAutoConfiguration,"
+ "br.com.nnumbers.console.sdk.spring.web.NnsWebAutoConfiguration")
.withBean(CountingService.class, () -> service)
.withBean(ActionReaper.class, () -> reaper)
.withBean(SampleConfiguration.Periodic.class, () -> periodic)
.run(
context -> {
PeriodicTasks tasks = context.getBean(PeriodicTasks.class);
// As duas declarações do módulo estão registradas.
assertThat(tasks.names()).containsExactlyInAnyOrder("scan-reconcile", "action-reaper");
assertThat(tasks.runOnce("scan-reconcile")).isTrue();
assertThat(service.reconciled).hasValue(1);
// Nome que não existe é ERRO de programação, e o gancho diz isso.
assertThatThrownBy(() -> tasks.runOnce("nao-existe"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("nao-existe");
});

O que esse teste pega e uma chamada direta ao método não pegaria: nome errado na anotação, tarefa não registrada, bean ausente.

O serviço de contagem é um dublê que registra a chamada, para o teste não precisar de banco:

static final class CountingService extends SampleService {
final AtomicInteger reconciled = new AtomicInteger();

@Override
public int reconcileScans(Ctx ctx, Duration minAge) {
reconciled.incrementAndGet();
return 0;
}
}

As tarefas que já vêm prontas​

Com banco, migrador e as Sources do SDK, duas tarefas são registradas sozinhas — não escreva as suas:

TarefaO que faz
audit-outbox-purgeExpurga do outbox os eventos já entregues, depois da retenção.
idempotency-purgeExpurga as respostas guardadas de idempotência, depois da retenção.

Erros comuns​

  • @Scheduled do Spring — não conhece o papel: a standby também dispara e as duas releases escrevem no mesmo lugar. Use @NnsPeriodic.
  • Trabalho longo numa tarefa periódica — o prazo da rodada é o every; se ela pode passar disso, o lugar é um laço de fundo com NnsBackground.sleep.
  • Fazer a rodada "pegar tudo" — duas releases da mesma ativa... e uma rodada atrasada depois de um restart: limite o lote (LIMIT, idade mínima) e seja idempotente.
  • Contar com intervalo exato — o primeiro disparo é aleatório dentro de every/10 e o período começa no fim da rodada. Tarefa de minuto em minuto não é relógio de parede.
  • Deixar exceção subir esperando retry — a rodada falhou e o log registrou; o retry é a próxima rodada. Se precisa de mais, trate dentro da tarefa.
  • Esquecer o @Profile("serve") — a tarefa passa a existir também no migrate; o Job não deve agendar nada.
  • Rodar a manutenção na réplica "líder" — a plataforma não tem líder: a tarefa roda em toda réplica da release ativa, então ela precisa ser segura com N réplicas concorrentes.