feat: make the notification queue durable with claim leases - #879
Open
Evaristus023 wants to merge 1 commit into
Open
Evaristus023 wants to merge 1 commit into
Evaristus023 wants to merge 1 commit into
Conversation
|
@Evaristus023 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
The listener already stores scheduled notifications in
scheduled_notificationsand dequeues them with an atomicUPDATE ... WHERE id IN (SELECT ...)claim, but the claim had no heartbeat:lock_expires_atwas written once at dequeue time and never extended, while both schedulers callrecoverStaleLocks()at the top of every poll cycle. Any delivery that outlivedSCHEDULER_LOCK_TIMEOUT_MS(60s by default) was therefore reset toPENDINGby the next poll and handed to another worker while the first worker was still sending it — the same notification delivered twice, and a job that was still running counted as a failed attempt.This change turns the claim into a durable, renewable lease:
next_retry_atwindow is no longer claimed immediately by the main scheduler while the retry scheduler owns it;Related Issue
Closes #780
Changes
Persistent claim + heartbeat
[ADD]
listener/src/services/notification-claim-lease.tsNotificationClaimLeaserenews a claim everylockTimeoutMs / 3viacalculateLeaseRenewIntervalMsand is strictly dependency-free (timers and the renewal function are injectable), so the renewal policy is unit-testable without a database.falsemeans the claim is gone: the lease is marked lost, the owner is notified once, and it never renews again.stop()clears the timer and drains the in-flight renewal, so the database can be closed immediately afterwards.startClaimLease()returnsnullfor repositories that have norenewLock, so existing test doubles keep working unchanged.[MODIFY]
listener/src/services/scheduled-notification-repository.tsfetchAndLockPendingNotifications(dequeue) now skips rows whosenext_retry_atis still in the future, so backoff state is respected and only one dequeue path owns a retrying job.renewLock(id, processorId, lockTimeoutMs)heartbeat: extendslock_expires_atonly while the row is stillPROCESSINGand owned by that processor, and reports whether the lease is still held.[MODIFY]
listener/src/services/notification-scheduler.tsfinallyblock next toworkerManager.completeJob, logging lost leases and failed renewals.[MODIFY]
listener/src/services/retry-scheduler.tsprocessRetry.Tests
[ADD]
listener/src/services/notification-claim-lease.test.tsstop()draining, idempotentstart/stop, and bothstartClaimLeasebranches.[ADD]
listener/src/services/scheduled-notification-queue.test.tsretry_count/last_errorand is not returned by either dequeue path; a permanently failed job keepsretry_count,last_error,error_details,processing_started_at,processing_completed_atand is neither claimable nor renewable.Verification Results
scheduled_notifications; verified by dequeue after close → reopen, and by requeueing the job of a worker that died mid-flightretry_count,last_error,error_details,processing_started_at,processing_completed_atpreserved; terminal jobs are not claimable or renewable and their backoff window is honouredCloses #780