Repository navigation
fix(queue-storage): report Blocked when any prune statement times out on a lock - #642
Conversation
… on a lock Prune and terminal-count rollup set a transaction-local lock_timeout before taking ACCESS EXCLUSIVE on a slot's partitions, but only the LOCK TABLE and TRUNCATE steps mapped a timeout to Blocked. The timeout also applies to the statements after them: the post-lock liveness checks over the lease and claim partitions, the failed-row carry into the current slot's done_entries and terminal-count deltas, the rollup delta append and the live-counter cleanup. A wait there surfaced as a 55P03 database error, was logged as a prune error and skipped the prune backoff. Run everything after the timeout is set as one block and settle it in one place: commit on success, and on a lock timeout from any statement roll back and report Blocked. Lease and claim prune use the same helper so the rule holds for every ring. The commit duration in PruneDurations is still measured around the commit. The new tests hold a SHARE lock on each relation the queue prune writes after its child locks, and on queue_terminal_live_counts for the rollup; both fail with 55P03 before this change.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
…ocked slot The remaining sealed slots would most likely wait out the same lock timeout, and with a raised prune timeout that holds the maintenance leader for one timeout per slot. The rollup test now seals two slots with pending deltas and expects one blocked slot, then both rolled once the lock is released.
Problem
Queue, lease and claim prune, and the terminal-count rollup, set a transaction-local
lock_timeout(10 ms by default) before they takeACCESS EXCLUSIVEon a slot's partitions. Only theLOCK TABLEstatements and the finalTRUNCATEturned a timeout intoPruneOutcome::Blocked. The timeout applies to every later statement in the transaction as well, and in the queue prune several of them touch relations the prune has not locked:leasesandlease_claims*partitions;done_entrieschild andqueue_terminal_count_deltaschild;queue_terminal_rollup_deltasand deletes the slot'squeue_terminal_live_countsrows.The rollup's post-lock checks and its
queue_terminal_live_countsfold have the same exposure. When one of these statements waited past the timeout, prune returnedErr(Database(55P03 lock_not_available)): the maintenance loop logged it as a prune failure and did not apply the prune backoff, and the rollup pass stopped at that slot. Anything that holds a conflicting lock on one of those relations for longer than the timeout triggers it. Examples are autovacuum's heap truncation (ACCESS EXCLUSIVE, released only when it notices a waiter, every 20 ms), a hosted Postgres's own background workers, and operator DDL.It was seen in CI on the 0.6 line. On AlloyDB Omni,
test_queue_storage_retry_failed_outcome_surfaces_pruned_rowsgot55P03from the carry'sINSERT INTO ... queue_terminal_count_deltas. Onmain, that test already retries aBlockedprune, but this error came back asErrand failed the test the same way.Change
Everything after
set_prune_lock_timeout_txnow runs as one block, and a sharedsettle_prune_txcommits it on success. On a lock timeout from any statement, it rolls the transaction back and returns the caller'sBlockedvalue. The per-statement handling of the lock andTRUNCATEerrors goes away. Lease and claim prune use the same helper. None of their post-lock statements can wait today, but this way the rule holds for every ring.PruneDurations::commitis still measured around the commit.A rollup pass now stops at its first blocked slot instead of trying each remaining slot. Those slots would most likely wait out the same timeout, and with a raised prune timeout (#629/#630) the leader would otherwise wait once per slot. The rollup test seals two slots and expects one blocked slot.
The whole attempt is one transaction, so a
Blockedprune leaves nothing behind. The carried rows, deltas, rollup deltas and counter deletes all roll back, and the slot is pruned intact on a later tick. A test checks this.#629 adds prune outcome metrics. With this change, the late timeouts count as
blocked(and enter the prune backoff) instead oferror.Tests
test_queue_storage_prune_lock_timeout_after_child_locks_reports_blockedtakes aSHARElock in turn on the currentdone_entrieschild, the currentqueue_terminal_count_deltaschild,queue_terminal_rollup_deltasandqueue_terminal_live_counts, while the prune carries a failed row. Each attempt must reportBlocked { slot: 0 }with no rows carried or folded and the terminal-count invariant intact. Once the locks are released the prune must succeed.test_queue_terminal_count_delta_rollup_lock_timeout_after_child_lock_reports_blockeddoes the same for the rollup's live-counter fold, across two sealed slots, and checks that the pass stops after the first.55P03on PostgreSQL 17 and 18.Validation
cargo fmt --all --check,SQLX_OFFLINE=true cargo clippy --all-targets --all-features -- -D warnings.queue_storage_runtime_test, all 214 tests, on privatepostgres:17-alpineandpostgres:18-alpine.0.6 backport: #641.