Skip to content

Fecha os desvios do padrao: frescura consultavel, cancelamento cooperativo e ADR 0005 - #5

Open
johnenderson wants to merge 3 commits into
mainfrom
feat/frescura-exposta-e-cancelamento-cooperativo
Open

Fecha os desvios do padrao: frescura consultavel, cancelamento cooperativo e ADR 0005#5
johnenderson wants to merge 3 commits into
mainfrom
feat/frescura-exposta-e-cancelamento-cooperativo

Conversation

@johnenderson

Copy link
Copy Markdown
Owner

Fecha os tres desvios conscientes que o ADR 0004 registrou contra o padrao Asynchronous Request-Reply: dois com codigo, um com ADR proprio.

A lib nao esta publicada, entao a quebra na SPI entra direto.

Desvio 2 — o gate deixou de ser garantia

Como a lib nao serve dados, o cliente le a base do consumidor e nada o impede de ler antes de a carga terminar. "So leia quando estiver quente" virou conselho.

A SPI JobFreshness deixa o endpoint de dominio declarar de quando sao os dados:

@GetMapping("/contas/aptas")
ResponseEntity<ContasResponse> aptas() {
    var contas = repository.buscarAptas();          // SQL de dominio, otimizado
    return ResponseEntity.ok(new ContasResponse(
            contas,
            freshness.lastRefreshedAt("contas").orElse(null),   // "dados de"
            freshness.isFresh("contas")));                      // dentro da janela?
}
  • lastRefreshedAt(type): quando a ultima carga concluiu — a idade real dos dados. Vazio se nenhuma concluiu; carga que falhou nao conta como dado quente.
  • isFresh(type): se essa conclusao esta dentro da janela configurada. Sempre false sem janela, pela mesma razao que toda submissao dispara carga nesse caso — prometer frescura ali seria mentir.

O desvio continua existindo (ler dado morno segue possivel), mas deixa de ser silencioso.

Desvio 3 — cancelar nao interrompe a rotina

Agora e cooperativo: JobContext.isCancelled() permite a rotina parar num ponto que ela escolhe.

Nao interrompemos a thread de proposito. Abortar no meio de um lote deixaria a base do consumidor parcialmente atualizada, sem ninguem para consertar — so a rotina sabe onde e seguro parar.

public void handle(JobContext ctx) {
    for (var lote : contas.emLotes(500)) {
        if (ctx.isCancelled()) {
            return;                  // para num ponto consistente
        }
        contas.reavaliar(lote);
    }
}
  • Quebra de contrato: JobHandler.handle() passa a ser handle(JobContext ctx).
  • Cada chamada le o storage — perguntar entre lotes, nao a cada item.
  • Rotinas que ja chamam progress periodicamente nao precisam disto: o false devolvido por progress carrega a mesma informacao, sem leitura extra.
  • Parar nao muda estado: o job permanece CANCELLED, e o complete que a lib tenta em seguida e recusado pelo UPDATE condicional. Nao existe "descancelar".

Desvio 1 — offload para uma fila (ADR 0005, nao implementado)

docs/adr/0005-dispatch-por-fila-na-propria-tabela.md desenha a alternativa: a propria tabela async_jobs como fila, com FOR UPDATE SKIP LOCKED, invertendo o dispatch de push para pull.

O ADR registra o ganho (distribuicao real entre instancias; queda de instancia deixa de depender de redispatch-after), o custo (latencia de um ciclo de poll, carga constante no banco, mais uma peca viva) e o criterio de quando implementar.

Nao implementado agora porque o ganho e balanceamento de fila, e para uma rotina de reaquecimento com single-flight ligado ha um job ativo por escopo — nao existe fila a balancear. As alternativas descartadas (broker dedicado, LISTEN/NOTIFY, advisory locks) estao registradas com o motivo.

No-op silencioso encontrado no caminho

freshness.enabled=true sem coalesce-in-flight=true nao fazia nada: a janela localiza a ultima carga concluida pela coalescing_key, e essa coluna so e gravada quando o coalescing esta ligado. A configuracao pedia para poupar carga e nada acontecia, sem aviso.

Agora falha o startup com a explicacao. Nao e acidental que os dois andem juntos — ambos falam do mesmo escopo: coalescing cobre carga em andamento, frescor cobre carga ja concluida.

Cobertura

AsyncJobProcessorAdapterOut nao tinha teste de unidade direto; ganhou um: start recusado, type desconhecido, excecao da rotina, fire-and-forget nao completado e os quatro casos de isCancelled. Mais o E2E que cancela um job com a rotina rodando e verifica que ela ve o cancelamento, para, e o estado terminal permanece.

./mvnw clean test
Tests run: 143, Failures: 0, Errors: 0, Skipped: 0

Fecha os dois desvios do ADR 0004 que dependiam de codigo, e registra o terceiro
como ADR proprio.

JobFreshness (desvio 2). Como a lib nao serve dados, nada impede o cliente de ler
a base antes de a carga terminar: so leia quando estiver quente e conselho, nao
garantia. A SPI resolve deixando o endpoint de dominio declarar de quando sao os
dados que devolve — lastRefreshedAt(type) da a idade real, isFresh(type) diz se
esta dentro da janela. O desvio continua existindo, mas deixa de ser silencioso.

isFresh e falso quando nao ha janela configurada, pela mesma razao que toda
submissao dispara carga nesse caso. Prometer frescura ali seria mentir.

JobContext.isCancelled (desvio 3). Cancelar nao interrompe a thread da rotina, e
isso e deliberado: abortar no meio deixaria a base do consumidor parcialmente
atualizada, sem ninguem para consertar. Quem sabe onde e seguro parar e a rotina,
entao ela pergunta entre lotes. JobHandler.handle passa a receber o JobContext.

Parar nao muda estado: o job permanece CANCELLED, e o complete que a lib tenta em
seguida e recusado pelo UPDATE condicional. Rotinas que ja chamam progress
periodicamente nao precisam de isCancelled — o false do progress carrega a mesma
informacao, sem leitura extra.

ADR 0005 (desvio 1). Dispatch por fila na propria tabela com FOR UPDATE SKIP
LOCKED, com o criterio de quando implementar. Nao implementado: o ganho e
distribuicao entre instancias, e para uma rotina de reaquecimento com
single-flight ligado ha um job ativo por escopo — nao existe fila a balancear.

Correcao de um no-op silencioso encontrado no caminho: freshness.enabled=true sem
coalesce-in-flight=true nao fazia nada, porque a janela procura a ultima carga
concluida pela coalescing_key, que so e gravada quando o coalescing esta ligado.
A configuracao pedia para poupar carga e nada acontecia. Agora falha o startup.

Novo teste de unidade do AsyncJobProcessorAdapterOut, que nao tinha cobertura
direta: start recusado, type desconhecido, excecao da rotina, fire-and-forget e
os quatro casos de isCancelled.

Suite: 143 testes, 0 falhas.
O SonarCloud sinalizou 8 metodos handle(JobContext) vazios como suspeitos
(java:S1186), todos em handlers de teste que nao precisam de efeito — o teste
verifica outra coisa (ciclo de vida, formato de type, indexacao por registry).
Adicionado o comentario que a propria regra pede, explicando por que o corpo
vazio e intencional em cada caso.

Um dos oito nao tinha por que existir: nullHandler (type "null-test") em
AsynchronousRequestReplyPatternApplicationTests estava registrado como bean mas
nenhum teste submetia esse type — codigo morto. Removido em vez de comentado;
a suite continua 143/143 depois da remocao, confirmando que nada dependia dele.
A correcao anterior colocou o comentario na linha acima da assinatura de
handle(JobContext ctx); a regra java:S1186 exige um comentario aninhado,
ou seja, dentro das chaves do metodo. O SonarCloud reanalisou o PR e continuou
sinalizando as 7 ocorrencias restantes pelo mesmo motivo. Corrigido movendo
cada comentario para dentro do corpo.
@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant