1

O comentário do SKIP LOCKED no worker e o LINQ logo abaixo não batem. Where + OrderBy + Take(20) no EF Core vira um SELECT normal. Várias instâncias do BackgroundService pegam as mesmas 20 linhas.

No caso de produção você descreveu o lease SQL, isso resolve. No snippet do worker, sem FOR UPDATE SKIP LOCKED (ou o lease), o MessageId no ServiceBusMessage vira a defesa, e ela mora no consumidor.

Uma SaveChanges no fim do lote também amplia a janela: se o processo morrer no meio dos 20 Send, as que já foram pro broker voltam como Pendente.

Carregando publicação patrocinada...
1

Oi, Florian! Obrigado pela leitura atenta e pela contribuição técnica. Você está certo em um ponto importante: o trecho do worker era um exemplo simplificado e, sem FOR UPDATE SKIP LOCKED ou um lease SQL, a consulta Where + OrderBy + Take(20) no EF Core não bloqueia as linhas; ela vira um SELECT normal. Em múltiplas instâncias do BackgroundService, várias workers podem ler o mesmo lote e publicar os mesmos eventos.

Isso também reforça outra parte da sua observação: no snippet do worker, sem lock/lease, a defesa real deixa de ser o publisher e passa para o consumidor, principalmente via MessageId estável e idempotência por inbox. O artigo já trata isso como um requisito do consumidor, e esse ponto está correto.

Além disso, o seu comentário sobre a janela de falha também é preciso: se o processo morrer depois de alguns SendAsync e antes do SaveChanges, as mensagens já publicadas no broker podem voltar para Pendente na próxima execução. Isso é um trade-off esperado do padrão at-least-once, e justamente por isso o consumidor precisa ser idempotente.

Então, para deixar a ideia mais precisa, o ajuste correto é: o exemplo do worker é didático e não a implementação final para produção; em produção, a seleção das mensagens pendentes deve usar FOR UPDATE SKIP LOCKED ou um lease SQL para evitar duplicação concorrente. O conceito central do artigo continua correto: a outbox resolve a inconsistência entre transação local e publicação externa, enquanto a idempotência no consumidor resolve a duplicidade inerente à entrega at-least-once.

Obrigado pela crítica — ela melhora o texto e deixa o ponto de produção bem mais explícito.