From 07139324a3cdede0a41ef51dab8622d9b221de2e Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Mon, 20 Jul 2026 10:45:11 +0200 Subject: [PATCH] feat(domain): timeout worker skips an already-resolved registration (refs #102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expire only a still-open (INGEDIEND/IN_BEHANDELING) registration; an already resolved one (expired, or withdrawn/decided while it waited) is left untouched so the job completes without violating the aggregate invariant (§8.6). Closes the S-10a/S-11 race where a withdrawal-while-waiting would loop the expiry job. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Big.Application/ExpireRegistrationWorker.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/services/domain/Big.Application/ExpireRegistrationWorker.cs b/services/domain/Big.Application/ExpireRegistrationWorker.cs index 6e900e7..05f8b94 100644 --- a/services/domain/Big.Application/ExpireRegistrationWorker.cs +++ b/services/domain/Big.Application/ExpireRegistrationWorker.cs @@ -12,9 +12,11 @@ namespace Big.Application; public sealed class ExpireRegistrationWorker(IRegistrationStore store) { /// - /// Process the job. Idempotent: a redelivered job whose registration is already VERLOPEN is a - /// no-op — not persisted again (§8.6, at-least-once delivery). An unknown registration is an error: - /// it throws, leaving the job un-completed for Flowable to redeliver. + /// Process the job. Idempotent and tolerant of races (§8.6, at-least-once delivery): a job whose + /// registration is already resolved — a redelivered expiry (VERLOPEN), or one withdrawn/decided + /// while it waited (INGETROKKEN/INGESCHREVEN/AFGEWEZEN) — is a no-op, so the job still completes + /// rather than throwing into a redelivery loop. Only a still-open registration is expired. An + /// unknown registration is an error: it throws, leaving the job un-completed for Flowable to redeliver. /// public async Task HandleAsync(RegistratieVerlopenJob job, CancellationToken ct = default) { @@ -24,8 +26,9 @@ public sealed class ExpireRegistrationWorker(IRegistrationStore store) ?? throw new InvalidOperationException( $"No registration {job.RegistrationId} for RegistratieVerlopen job {job.JobId}."); - // A redelivered job whose registration is already VERLOPEN completes without persisting again. - if (registration.Status == RegistrationStatus.Verlopen) + // Only a still-open registration lapses; an already-resolved one (expired, or withdrawn/decided + // while it waited) is left untouched so the job can complete without violating the aggregate. + if (registration.Status is not (RegistrationStatus.Ingediend or RegistrationStatus.InBehandeling)) return; registration.Expire();