fix(backend): route poison events to DLQ and back off batch growth on zero progress (#302) - #493
Open
solaawojobi00-bit wants to merge 8 commits into
Conversation
Resolves eslint no-empty errors on the two empty catch blocks in recordDeadLetter, fixing the failing Backend (Node.js) CI check on Stellar-Search#302.
AbuJulaybeeb
requested changes
Aug 25, 2026
AbuJulaybeeb
left a comment
Contributor
There was a problem hiding this comment.
KIndly update branch
Contributor
Author
CI has been fixed and branch updated. Kindly Merge Thanks |
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.
Fix Poison Event Stuck Batch Head Loop & Adaptive Sizing Hot Loop (#302)
Problem
In the event-sourcing dispatcher (
EventStoreService), per-event projection dispatch failures caught duringprocessBatchwere recorded by incrementingfailedCountand leavingprocessed = false. BecausegetUnprocessedselects withWHERE processed = false ORDER BY occurred_at ASC, version ASC, a poison event permanently remained at the head of every subsequent batch.Meanwhile,
recordBatchOutcomeinterpreted a saturated batch (total >= limit) as a growing backlog and exponentially doubledbatchSizetowardsEVENT_STORE_MAX_BATCH_SIZE(2000), whilenextDelayMsreturned the 10ms catch-up interval (EVENT_STORE_CATCHUP_INTERVAL_MS) regardless of whether any events succeeded. This transformed a stuck backlog into a maximum-batch, near-zero-delay hot loop against Postgres while completely blocking downstream events.Scenarios
event_dead_letterand markedprocessed=true, dead_lettered=true. Downstream events proceed.batchSizedoubled up to 2000.processed === 0 && total > 0, batch sizing backs off towards baseline (EVENT_STORE_BATCH_SIZE) rather than doubling.EVENT_STORE_CATCHUP_INTERVAL_MS, spinning the event loop against DB.alignedpoll interval (EVENT_STORE_POLL_INTERVAL_MS, 500ms) on zero progress, eliminating hot loops.event_dead_letterstores payload, error message, and stack trace; queryable viagetDeadLetterEvents()andgetDeadLetterCount().Solution
attempts,last_error,dead_lettered, anddead_lettered_atcolumns toevent_stream.event_dead_lettertable storing failed events with aggregate info, payload, attempts, error message, and stack trace.EVENT_STORE_MAX_ATTEMPTS(default: 3) in environment configuration and schema.recordDeadLetteratomically writes toevent_dead_letterand updatesevent_streamso it is no longer retrieved bygetUnprocessed.recordBatchOutcometo checkmadeProgress(processed > 0). Iftotal > 0 && !madeProgress,batchSizebacks off towardsEVENT_STORE_BATCH_SIZE.nextDelayMsso thatEVENT_STORE_CATCHUP_INTERVAL_MS(10ms) is only returned whensaturated && madeProgress. When zero progress is made, it returns the standard poll interval.getDeadLetterEvents(limit)andgetDeadLetterCount()query helpers onEventStoreService.deadLetteredandmaxAttemptsingetSchedulerStats().Changes
backend/src/db/schema.sql: Addedattempts,last_error,dead_lettered,dead_lettered_atcolumns toevent_streamand defined theevent_dead_lettertable with indices.backend/src/config/env.js&backend/.env.example: AddedEVENT_STORE_MAX_ATTEMPTSschema validation (default: 3) and documented optional setting.backend/src/eventSourcing/eventStore.js: Added retry tracking, dead-letter routing, backoff handling on zero progress, and operator query methods.backend/src/eventSourcing/eventStore.test.js: Added comprehensive unit test coverage for poison events, DLQ routing, backoff, and operator queries.Regression Tests
retries failing events and increments attempt count below maxAttemptsroutes to dead-letter queue after N failed attempts without blocking streamone permanently failing event neither blocks subsequent events nor loops after DLQbacks off batch size instead of growing when saturated batch processes 0 eventsnextDelayMs returns poll interval instead of catch-up interval when processed === 0Testing Output
Notes for Reviewers
Closes #302