fix(github-webhook): bound reviewer-wake lock-timeout retries and record dead_lettered (BLO-21582) - #1003
Conversation
1 similar comment
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. The synced skill bodies were unavailable in this runtime, so the same checks were applied directly to the exact diff and head files. Important Issues (1)
Strengths
Recommended Action
This PR is authored by |
allyblockcast
left a comment
There was a problem hiding this comment.
Approved with the allyblockcast reviewer seat after the automated review gate passed; merge remains gated on the remaining CI checks.
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Critical Issues (0)Important Issues (2)
Suggestions (1)
Strengths
Recommended Action
The PR is authored by |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. The synced skill bodies were unavailable in this runtime, so the same checks were applied directly to the exact diff and current-head files. Prior Findings Dispositioned (3)
Important Issues (2)
Strengths
Recommended Action
This PR is authored by |
…nd the lock-exhaustion fallback recheck (BLO-21582) Ally review follow-up on this branch (PR #1003, review at issuecomment-5182720378) found two still-live gaps in the previous commit's withPrReviewerTaskLock: 1. The deadline raced the WHOLE transaction returned by db.transaction(), including action(tx) itself, not just pool checkout + the advisory-lock probe. If the lock was acquired near the deadline, the handler could abandon a live action() that later commits a wake -- while the catch block, having observed no wake yet, recorded received+dead_lettered and answered reviewerWakeFired: false. The late action then incremented received again and queued the wake, producing both a false dead-letter and broken funnel counts. Fixed by resolving a dedicated `lockProbeSettled` promise the instant the pg_try_advisory_xact_lock probe itself settles, before action(tx) ever runs, and racing ONLY that against the deadline. Once the probe reports the lock is ours, we await the in-flight transaction (running action) to completion unconditionally instead of racing it further. 2. The lock-exhaustion fallback recheck (findExistingPrReviewerWake / findActivePrReviewerForTask / selectPrReviewerAgentId) ran outside any deadline. A saturated pool that timed out the lock probe could just as easily stall these reads indefinitely, defeating the whole point of the request-wide budget and GitHub's response-window protection. Fixed with a small additional budget (PR_REVIEWER_TASK_LOCK_FALLBACK_BUDGET_MS, 1s) appended to the lock deadline. A timeout on either read is treated as "unknown" -- distinct from a confirmed "no equivalent wake" / "no active reviewer" -- and falls through to the pre-existing conservative default (record the delivery as lost) rather than blocking the response further. Two new integration tests: - Delays heartbeat.wakeup() (via a slow penstockAvailabilityGate) past the 4s lock budget after the advisory lock is acquired with zero contention, and asserts a single terminal outcome (reviewerWakeFired: true, one received/queued pair, no dead-letter) with no metric movement after the response is sent. - Forces the lock probe to exhaust its budget via genuine advisory-lock contention AND separately blocks the fallback recheck's own read with an ACCESS EXCLUSIVE table lock on agent_wakeup_requests held far longer than the fallback budget -- proving the bound, not the lock's eventual release, is what lets the response return (well under the 8s both locks are held for), while still recording the delivery as lost. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Pushed 1. 2. The lock-exhaustion fallback recheck ( New tests (both passing against the fix, both in
Full Note: this PR had been added to the merge queue (position 27) sometime after the review comment landed, which would have shipped the still-broken code — I dequeued it to push this commit. Re-queue once this pass looks good. |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. The published skill bodies were unavailable in this runtime, so the same checks were applied directly to the exact diff and current-head files. Prior Findings Dispositioned (2)
Important Issues (2)
Strengths
Recommended Action
This PR is authored by |
…ord dead_lettered (BLO-21582) withPrReviewerTaskLock's per-PR advisory-lock acquisition can time out (2s budget) when the current holder is itself stalled acquiring the second pooled connection heartbeat.wakeup() needs (see the comment on withPrReviewerTaskLock) -- reproduced live in production during a burst of concurrent webhook deliveries. That timeout landed in the outer catch and returned false BEFORE the `received` counter a few lines further in ever incremented, so the loss was invisible to the entire paperclip_github_review_request_delivery_total funnel: not `received`, not `queued`, not `dead_lettered`. A review request that "routed correctly" on every webhook-side log vanished with zero record anywhere, while the handler still answered GitHub 200 so GitHub's own redelivery-on-failure never fired either. Adds a bounded retry (3 attempts, 300ms/900ms backoff) around the lock acquisition -- safe to re-run because the guarded closure re-checks existingWake before doing anything -- and, once every attempt is exhausted, records dead_lettered directly so this loss is finally counted by the funnel invariant the BLO-18859 observability work already built (received == queued + suppressed + dead_lettered). Two new integration tests reproduce genuine cross-session advisory-lock contention against the embedded test Postgres: one proves a contention window shorter than the retry budget self-heals, the other proves an exhausted one is recorded as dead_lettered rather than silently dropped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… false dead-letters on lock exhaustion (BLO-21582) Ally review follow-up on this branch (issue comment 5177920386): - Replace the 3-attempt x fresh-2s-each retry loop (worst case ~7.2s, and only bounded *after* each db.transaction() returned, so a stalled pool checkout wasn't bounded at all) with a single request-wide 4s deadline that withPrReviewerTaskLock races pool checkout + the lock probe against directly. - On lock exhaustion, recheck for an equivalent durable wake (or confirm no reviewer was ever active) before recording dead_lettered, so a concurrent duplicate delivery that already completed the wake no longer produces a false loss alert. Falls back to the pre-existing received+dead_lettered recording only when neither recheck explains the outcome. Adds a regression test for the false-dead-letter case and updates the two existing lock-contention tests for the new single-budget timing.
…nd the lock-exhaustion fallback recheck (BLO-21582) Ally review follow-up on this branch (PR #1003, review at issuecomment-5182720378) found two still-live gaps in the previous commit's withPrReviewerTaskLock: 1. The deadline raced the WHOLE transaction returned by db.transaction(), including action(tx) itself, not just pool checkout + the advisory-lock probe. If the lock was acquired near the deadline, the handler could abandon a live action() that later commits a wake -- while the catch block, having observed no wake yet, recorded received+dead_lettered and answered reviewerWakeFired: false. The late action then incremented received again and queued the wake, producing both a false dead-letter and broken funnel counts. Fixed by resolving a dedicated `lockProbeSettled` promise the instant the pg_try_advisory_xact_lock probe itself settles, before action(tx) ever runs, and racing ONLY that against the deadline. Once the probe reports the lock is ours, we await the in-flight transaction (running action) to completion unconditionally instead of racing it further. 2. The lock-exhaustion fallback recheck (findExistingPrReviewerWake / findActivePrReviewerForTask / selectPrReviewerAgentId) ran outside any deadline. A saturated pool that timed out the lock probe could just as easily stall these reads indefinitely, defeating the whole point of the request-wide budget and GitHub's response-window protection. Fixed with a small additional budget (PR_REVIEWER_TASK_LOCK_FALLBACK_BUDGET_MS, 1s) appended to the lock deadline. A timeout on either read is treated as "unknown" -- distinct from a confirmed "no equivalent wake" / "no active reviewer" -- and falls through to the pre-existing conservative default (record the delivery as lost) rather than blocking the response further. Two new integration tests: - Delays heartbeat.wakeup() (via a slow penstockAvailabilityGate) past the 4s lock budget after the advisory lock is acquired with zero contention, and asserts a single terminal outcome (reviewerWakeFired: true, one received/queued pair, no dead-letter) with no metric movement after the response is sent. - Forces the lock probe to exhaust its budget via genuine advisory-lock contention AND separately blocks the fallback recheck's own read with an ACCESS EXCLUSIVE table lock on agent_wakeup_requests held far longer than the fallback budget -- proving the bound, not the lock's eventual release, is what lets the response return (well under the 8s both locks are held for), while still recording the delivery as lost. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
b1ea8bd to
d286645
Compare
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. The published skill bodies were unavailable in this runtime, so the same high-confidence checks were applied directly to the exact diff and current-head files. Prior Findings Dispositioned (2)
Important Issues (1)
Strengths
Recommended Action
This PR is authored by |
Thinking Path
Linked Issues or Issue Description
Refs #21582
Refs #18859
Paperclip issue: https://paperclip.blockcast.net/BLO/issues/BLO-21582
Paperclip issue: https://paperclip.blockcast.net/BLO/issues/BLO-18859
What Changed
PrReviewerTaskLockTimeoutErrorso lock acquisition timeouts can be retried without retrying unrelated webhook errors.PR_REVIEWER_TASK_LOCK_BUDGET_MS), racing pool checkout + the advisory-lock probe against it directly, instead of a 3-attempt x fresh-2s-each loop that could reach ~7.2s and only checked elapsed time after eachdb.transaction()call returned (so a stalled pool checkout wasn't bounded at all).dead_letteredso the existingPaperclipGithubReviewRequestDeadLetteredalert can see the loss -- but first rechecks (unlocked) for an equivalent durable wake or a confirmed absence of any active reviewer, so a concurrent duplicate delivery whose equivalent wake already completed is treated as the same silent no-op the lock-guarded idempotency check produces, not a false loss.Verification
server/src/__tests__/github-webhook.test.tslocally against embedded Postgres: all 115 tests pass, including the three BLO-21582 lock-contention tests (recovery within budget, exhaustion without an equivalent wake, and exhaustion with a pre-existing equivalent wake correctly treated as a no-op).tsc --noEmitfor theserverworkspace: no errors in the touched files.Risks
Model Used
GPT-5 Codex with repository inspection, production log analysis supplied in the PR body, GitHub Actions log inspection, and command execution. Follow-up pass by Claude Sonnet 5 (PlatformSREEngineer) addressing Ally's review feedback (unbounded end-to-end retry latency and a false dead-letter on lock exhaustion when a concurrent duplicate delivery already completed the equivalent wake).
Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template