Repository navigation
Conversation
psteinroe
force-pushed
the
fix/claim-token-fencing
branch
from
October 2, 2026 23:23
e0d610a to
ebfc1c5
Compare
…e heartbeat Every claim sets a fresh claim_token. Settlement, saveStep, registerEventWait and clearWaitingState require it alongside locked_by, so a retried or late request from an earlier claim by the same orchestrator can no longer release or settle a newer claim. A connection error is ambiguous, so getExecutions and the heartbeat now only retry rollback errors. A claim whose response was lost is recovered like an expired lease: workers track the executions they hold, and the heartbeat releases rows locked by its orchestrator that are not held and were locked more than one heartbeat interval ago. returnExecutions built one postgres.js query and re-awaited it on every retry, so retries never re-sent the statement. Build it per attempt.
psteinroe
force-pushed
the
fix/claim-token-fencing
branch
from
October 2, 2026 23:37
ebfc1c5 to
a788065
Compare
This branch has not been deployed
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.
Claims and claim-dependent writes were fenced only by
locked_by = orchestrator_id, and every call retried ambiguous connection errors. As a result, a lost claim response could orphan a batch until restart, and an old request from an earlier claim could change a newer claim of the same execution. This PR adds a per-claim fencing token and recovers orphaned claims the way a lease expires. It also fixesreturnExecutionsretries, which never re-sent anything.Claim token
Every claim sets a fresh
claim_token(gen_random_uuid()). Settlement,saveStep,registerEventWaitandclearWaitingStaterequire it alongsidelocked_by. Before this, a retried or late request from an earlier claim could act on a newer claim by the same orchestrator. Tests reproduce both harmful cases:releasedresult released the running claim. That decrementedattempts, cleared the lock, and let a second worker run the execution at the same time.registerEventWaitcall created a fresh subscription and released the running claim. That claim's own result was then dropped, and the execution waited for an event that never came.dispatchCustomEventsstays onlocked_by. It only inserts deduplicated delivery rows and never changes the source's claim, which existing tests already cover.attemptscan't serve as the token because suspension decrements it.Claims are not retried after ambiguous errors; orphans are released by the heartbeat
A connection error doesn't tell us whether the statement committed.
getExecutionstherefore retries only rollback errors (40001,40P01,55P03), and a failed poll is followed by the next poll. A claim that committed but whose response was lost is recovered like an expired lease:locked_by/locked_atexactly like stale-orchestrator recovery.This is how other queues handle it. None of them makes the claim itself idempotent; they bound how long an orphaned claim can stay locked:
40001,40P01,57P03,EHOSTUNREACH,ETIMEDOUT), and releases any job locked for more than 4 hours.retry_countfor the stale-settle problem (#925).Earlier revisions made the claim retry idempotent with a client token and an advisory lock. That was dropped in favour of this simpler lease-style recovery.
Settlement retries
returnExecutionsbuilt one postgres.js query and passed() => queryto the retry loop. A postgres.js query runs only once, so every retry re-awaited the same rejection with backoff up to 60s until shutdown. The query is now built inside the retry callback.Verification
the heartbeat releases a claim the orchestrator does not hold: a claim made for the orchestrator but never handed to its worker survives a heartbeat within the grace. Past the grace it is released and run by the worker, while the execution the worker holds stays locked throughout.retries a settlement whose connection was terminated: the backend is killed withpg_terminate_backendmid-statement.fences a replayed release after the same orchestrator re-claims the executiondoes not let a replayed registration release a later claimjust lint,just format,bun run typecheckand the fullbun testare clean.Follow-ups
None. The broader concern about connection-error retries of writes that aren't idempotent (#55) was resolved by #63.