fix(selfhost): guard the dead-letter revive interval against unhandled errors#2597
Conversation
…d errors The AI review flagged a second defect: both backends scheduled reviveDeadLetterJobs directly from setInterval with no error handler of its own. A transient pool/driver/metric failure on that background tick would surface as an unhandled promise rejection (Postgres, async) or an uncaught exception (SQLite, sync), either of which can terminate the self-host process -- turning a bounded revival feature into a process-crash vector. Both backends now wrap the interval callback (reviveDeadLetterJobsSafely) in the exact same try/catch + structured console.error + captureError pattern pump() already uses for the main poll loop, so a failed revive tick just waits for the next interval instead of taking the process down. The public reviveDeadLetterJobs() the tests and any future operator-triggered repair path call directly is untouched -- only the internal timer callback is wrapped. Added a regression test per backend that injects a pool/driver failure specifically on the revive interval's own tick (via fake timers) and asserts the process survives and the failure is logged, mirroring the existing "pump absorbs a ... failure instead of crashing" tests for the main poll loop.
|
Tip 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Gittensory review result - approve/merge recommendedReview updated: 2026-07-02 20:15:21 UTC
✅ Suggested Action - Approve/Merge
Review summary Nits — 5 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2597 +/- ##
==========================================
+ Coverage 95.99% 96.02% +0.02%
==========================================
Files 230 233 +3
Lines 25887 26081 +194
Branches 9410 9474 +64
==========================================
+ Hits 24851 25045 +194
Misses 425 425
Partials 611 611
🚀 New features to boost your workflow:
|
Summary
Follow-up to #2581 (already merged), which shipped automatic dead-letter job revival for both self-host queue backends. A later review pass on that PR flagged a second real defect that landed after the PR's blocking race-condition issue was already fixed and merged, so this is a fresh PR against current
mainrather than an amendment to the closed one.The defect: both backends scheduled
reviveDeadLetterJobsdirectly fromsetIntervalwith no error handler of its own:A transient pool/driver/metric failure on that background tick surfaces as an unhandled promise rejection (Postgres — the function is
asyncand its rejection is discarded viavoid) or an uncaught exception (SQLite — the function is synchronous). Either can terminate the self-host process, turning a feature meant to bound retry storms into a process-crash vector of its own.What changed
Both backends now wrap the interval callback in a new
reviveDeadLetterJobsSafely()that mirrors the exact try/catch + structuredconsole.error+captureErrorpatternpump()already uses for the main poll loop (seeselfhost_queue_pump_crashed— this fix adds the analogousselfhost_queue_dead_letter_revive_crashed). A failed revive tick now just waits for the next interval, same as a failed poll tick waits for the next poll.The public
reviveDeadLetterJobs()— the one tests and any future operator-triggered repair path call directly — is untouched; only the internalsetIntervalcallback is rewired to the safe wrapper, so direct callers still see real errors/rejections if they want to handle them themselves.Tests
Added one regression test per backend that injects a pool/driver failure specifically on the revive interval's own tick (via fake timers +
vi.advanceTimersByTimeAsync) and asserts the process survives and the failure is logged — mirroring the existing"pump absorbs a ... failure instead of crashing"tests already in both suites for the main poll loop.Scope
type(scope): short summaryConventional Commit format, for examplefix(api): restore profile access checks.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coveragelocally — both changedsrc/**files (pg-queue.ts,sqlite-queue.ts) are 100% line-covered; the remaining uncovered branches are pre-existing and unrelated (cross-checked directly against the diff).npm run test:workersnpm run build:mcpnpm run test:mcp-packnpm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderateSafety
ui:openapi:checkconfirms no drift.UI Evidencesection below with screenshots. — N/A, backend-only change.