Repository navigation
fix: never exit the process on an unhandled database rejection - #516
Merged
Merged
Conversation
kuan121
approved these changes
Oct 2, 2026
2 tasks done
2 of 7 tasks
cybele-ripple
added a commit
that referenced
this pull request
Oct 6, 2026
#519) ## High Level Overview of Change `calculateAgreement()` runs one promise per validator inside a single `Promise.all`. If any single validator's promise rejected, the whole `Promise.all` rejected immediately, which skipped the post-batch cleanup (`purgeHourlyAgreementScores()`, `chains.purgeChains()`, and the `reported_at` advance) for every validator that hour, not just the one that failed. Catch each validator's promise individually inside `calculateAgreement()`'s loop, so the batch-level purge/reset always runs regardless of a single validator's failure Add a `.catch` to `getHourlyAgreementScores` (the one DB read in `shared/database/agreement.ts` with no error handling, unlike every write in that file) ### Context of Change validator-history-service tracks per-validator agreement scores on an hourly cycle. After scoring all validators for the hour, it's supposed to clear its in-memory ledger-chain tracking so the next hour starts clean. [#516](#516) (merged 2026-10-02) added an `unhandledRejection` handler so a Knex pool timeout no longer crashes the process. That fix surfaced this issue: `getHourlyAgreementScores` can still throw on a transient DB error, and since it was awaited inside one of the per-validator promises in `calculateAgreement()`'s Promise.all, a single rejection aborted the whole batch's cleanup step — not just that one validator's score. With the cleanup skipped, each chain's accumulated ledger set never gets cleared and keeps growing hour over hour, while the validations window (`validationsByPublicKey`) stays bounded to 2 hours. That mismatch is what produces the observed symptom in prod: agreement scores that look stuck or wrong, with missed growing relative to a roughly flat validated count. ### Type of Change <!-- Please check relevant options, delete irrelevant ones. --> - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactor (non-breaking change that only restructures code) - [x] Tests (You added tests for code that already exists, or your new feature included in this PR) - [ ] Documentation Updates - [ ] Release ## Before / After - **Before**: one validator's agreement calculation failing (e.g. a transient DB read error) silently skipped `purgeHourlyAgreementScores()` / `chains.purgeChains()` / the `reported_at` reset for the entire batch that hour, letting chain state accumulate unbounded over time. - **After**: a single validator's failure is logged and contained to that validator; the batch-level purge and `reported_at` advance run every hour regardless. ## Test Plan Current tests pass and added `test/database/agreement-score-errors.test.ts`, `test/chains/agreement-batch-isolation.test.ts`
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.
High Level Overview of Change
Stops
vhs-connectionscrash-looping when the Knex pool saturates.unhandledRejectionhandler in all three services (connection-manager,crawler,api) so a failed database call is logged instead of exiting the processsaveValidator: catch the second statement, which had no error handlerwsHandling.tsandagreement.tsmin: 2,max: 20) — the knex default of 10 was the ceilingacquireConnectionTimeoutfrom 120s to 30s so a backlog surfaces instead of queueingContext of Change
vhs-connectionswas restarting 90–144 times per 24h across dev, staging and prod, withreason=Error(not OOMKilled)saveValidator's second statement had no.catch():void agreement.handleValidation(...)— so aKnexTimeoutErrorpropagated out as an unhandled rejection. Since Node 15 that throws, which exits the processvoidsites and 6 unguarded timer callbacks, all calling functions with no internaltry/catch. Patching each individually would leave the next unfound path to crash the same way, so the global handler is the actual fix and the targeted.catch()calls just keep errors attributable in logsmaxuses headroom that already existsuncaughtExceptionis deliberately not trapped — a synchronous throw can leave the process in an indeterminate state where continuing is worse than restartingType of Change
Before / After
KnexTimeoutError→ unhandled rejection → process exit → k8s restart → repeat