Skip to content

Torna atômica a reivindicação da notificação de atraso - #92

Merged
Hirley merged 1 commit into
mainfrom
claude-hirley/atraso-claim-atomico
Sep 1, 2026
Merged

Hirley merged 1 commit into
mainfrom
claude-hirley/atraso-claim-atomico

Conversation

@Hirley

@Hirley Hirley commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Fecha a #80.

A rake task selecionava as demandas com atraso_notificado_em nulo, enviava, e só então gravava a marca. O README promete "uma vez por atraso" — e isso valia só no caminho feliz de execução única.

Duas janelas de duplicação vinham dessa ordem:

  1. concorrência — duas execuções simultâneas (cron disparado duas vezes, retry do scheduler, uma réplica a mais) selecionavam a mesma demanda antes de qualquer gravação e notificavam as duas;
  2. morte entre o envio e a gravação — mensagem entregue, processo cai, e a execução seguinte notifica de novo.

Agora a task reivindica antes de enviar, com UPDATE ... WHERE atraso_notificado_em IS NULL. A checagem e a escrita acontecem no mesmo comando: o banco decide quem venceu, e só quem recebe 1 linha afetada envia. A garantia deixa de depender da disciplina de quem agenda.

Onde me afastei da issue

A issue propunha aceitar que qualquer falha "gastasse" a notificação. Fiz diferente: uma falha declarada do envio (notify_atraso devolveu false) devolve a demanda para a próxima execução.

Não é preciso perder o retry para consertar a corrida — a execução concorrente já perdeu na reivindicação, então devolver não reabre nada. O que continua sendo gasto sem sair é a demanda cujo processo morra entre reivindicar e enviar; essa sim é a troca consciente de marcar antes, e está comentada como tal na task.

Sobra uma janela estreita: um transporte que devolva false depois de a mensagem ter sido entregue (timeout na leitura da resposta) faz a próxima execução notificar de novo. É estritamente menor que a janela anterior, e o modo de falha é o mesmo que já existia.

Os specs discriminam de verdade

Os dois novos falhariam contra o código anterior, que é o teste que importa:

  • reivindica a demanda antes de enviar, e não depois — mede se a marca já está gravada no instante em que o envio acontece. Com a ordem antiga: nil.
  • faz uma execução concorrente perder a corrida — simula outra execução tentando reivindicar durante o envio e verifica que ela recebe 0 linhas. Com a ordem antiga: 1.

O spec pré-existente não marca atraso_notificado_em quando o envio falha continua valendo sem alteração — a devolução preserva exatamente o estado que ele já afirmava.

Agendamento

Ganhou seção própria no README, como a issue pedia. O ponto que faltava estar escrito: um deploy sem cron tem a funcionalidade desligada, e nada na aplicação avisa isso.

Registrei também que desde a #77 existe uma terceira via — config/recurring.yml, com o processo worker já de pé — que dispensaria o scheduler externo. É a evolução natural, exigiria embrulhar a task num job, e não foi feita aqui.

Verificação

No container: RuboCop 110 arquivos / 0 ofensas, zeitwerk:check All is good!, RSpec 394 exemplos / 0 falhas (eram 391).

Nada de JS, CSS, HAML ou CSP mudou.

A rake task selecionava as demandas com atraso_notificado_em nulo,
enviava, e só depois gravava a marca. O README promete "uma vez por
atraso", e isso valia só no caminho feliz de execução única.

Duas janelas de duplicação vinham dessa ordem: duas execuções
simultâneas — cron disparado duas vezes, retry do scheduler, uma réplica
a mais — selecionavam a mesma demanda antes de qualquer gravação e
notificavam as duas; e um processo que morresse entre o envio e o
update_column fazia a execução seguinte notificar de novo.

Agora a task reivindica antes de enviar, com
`UPDATE ... WHERE atraso_notificado_em IS NULL`: a checagem e a escrita
acontecem no mesmo comando, o banco decide quem venceu, e só quem recebe
1 linha afetada envia. A garantia deixa de depender da disciplina de
quem agenda.

Uma variação em relação ao que a issue propunha: uma falha DECLARADA do
envio (notify_atraso devolveu false) devolve a demanda para a próxima
execução. A issue aceitava perder a notificação nesse caso, mas não é
preciso — o retry já existia, e mantê-lo não reabre a corrida, porque a
execução concorrente já perdeu na reivindicação. O que continua sendo
gasto sem sair é a demanda cujo processo morra entre reivindicar e
enviar; essa é a troca consciente de marcar antes, e está comentada como
tal.

Sobra uma janela estreita: um transporte que devolva false depois de a
mensagem ter sido entregue (timeout na leitura da resposta) faz a
próxima execução notificar de novo. É estritamente menor que a janela
anterior e o modo de falha é o mesmo que já existia.

Os dois specs novos discriminam de verdade: um mede se a marca já está
gravada no momento em que o envio acontece, e o outro simula a execução
concorrente tentando reivindicar durante o envio e verifica que ela
recebe 0 linhas. Ambos passariam por vacuidade contra o código anterior
— não passam: com a ordem antiga o primeiro veria nil e o segundo, 1.

O agendamento ganhou seção própria no README, como a issue pedia: um
deploy sem cron tem a funcionalidade desligada e nada avisa isso.
Registrei também que desde a #77 existe uma terceira via (config/
recurring.yml, com o worker já de pé) que dispensaria o scheduler
externo — não foi feita, exigiria embrulhar a task num job.

Verificado no container: RuboCop 110 arquivos sem ofensas,
zeitwerk:check limpo, RSpec 394 exemplos e 0 falhas (eram 391).

Closes #80

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Hirley
Hirley merged commit cc4fab0 into main Sep 1, 2026
3 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in @Hirley's task_keeper_api Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant