Skip to content

fix(processing): retry publish with exponential backoff, dead-letter on exhaustion - #476

Merged
Xhristin3 merged 2 commits into
XStreamRollz:mainfrom
irefavor15-dotcom:fix/343-session-publish-retry
Jul 29, 2026
Merged

fix(processing): retry publish with exponential backoff, dead-letter on exhaustion#476
Xhristin3 merged 2 commits into
XStreamRollz:mainfrom
irefavor15-dotcom:fix/343-session-publish-retry

Conversation

@irefavor15-dotcom

Copy link
Copy Markdown
Contributor

Summary

Closes #343

StreamSession.pump() previously called this.fail(error) on the very first publish() failure, which cleared the entire queue and moved the session to errored. A single transient API hiccup caused permanent data loss for every event behind the failed one.

Root Cause

// Before (session.ts pump())
} catch (err) {
  this.queue.unshift(next)  // put event back
  this.fail(error)           // → clears queue, errors session
  return
}

Fix

Each event publish is now wrapped in an exponential-backoff retry loop. After exhausting the retry budget the individual event is dead-lettered and the loop continues — the session stays running and the rest of the queue is processed normally.

attempt 1 → fail → wait 100ms
attempt 2 → fail → wait 200ms
attempt 3 → fail → wait 400ms
attempt 4 → DEAD-LETTER (emit 'dead-letter', log ERROR, continue queue)

The backoff is capped at 5 s per attempt. fail() is still available for coordinator-level failures (e.g. lock errors) but is no longer called from inside the publish loop.

Changes

xstreamroll-processing/src/session.ts

  • New maxPublishRetries constructor param (default 3)
  • pump() inner loop replaced with backoff-retry logic
  • Emits 'dead-letter'(event: StreamEvent, err: Error) on exhaustion
  • Session remains in running state after dead-lettering

xstreamroll-processing/src/session-registry.ts

  • Added maxPublishRetries?: number to SessionRegistryOptions
  • Passes options.maxPublishRetries to new StreamSession() in spawn()

xstreamroll-processing/src/config.ts

Added PROCESSING_PUBLISH_MAX_RETRIES to the zod env schema (default '3', min 0).

xstreamroll-processing/src/worker.ts

Reads env.PROCESSING_PUBLISH_MAX_RETRIES and passes it as maxPublishRetries to the SessionRegistry.

xstreamroll-processing/.env.example

Documents PROCESSING_PUBLISH_MAX_RETRIES with notes on the fail-fast mode (=0).

xstreamroll-processing/__tests__/session.test.ts

  • Updated existing test: session no longer errors on a single failure; replaced with a maxPublishRetries=0 dead-letter test and a fail()-is-still-wired test
  • 4 new tests in a dedicated describe block:
    1. Retries on transient failure and eventually publishes successfully
    2. Dead-letters after retry exhaustion and continues with the rest of the queue
    3. dead-letter event carries the original StreamEvent and the Error
    4. Session stays running and accepts new events after dead-lettering

Test Results

PASS __tests__/session.test.ts (5.07 s)
  StreamSession
    ✓ starts in the idle state
    ✓ transitions to running on start()
    ✓ enqueues and publishes events in order
    ✓ publishes null latency for invalid event timestamps
    ✓ rejects new events when not running
    ✓ transitions to stopped via stop()
    ✓ emits state and processed events
    ✓ dead-letters an event and stays running when maxPublishRetries=0
    ✓ explicit fail() still transitions to errored and emits error event
    ✓ is a no-op when stop() is called on a stopped session
  StreamSession — publish retry and dead-letter (issue #343)
    ✓ retries on publish failure and eventually succeeds
    ✓ dead-letters after exhausting retries and continues processing remaining queue
    ✓ emits dead-letter event with the original StreamEvent and the error
    ✓ session stays running after dead-lettering and accepts new events

Tests: 19 passed

…on exhaustion (XStreamRollz#343)

Previously, the first publish() failure called fail() which dropped the
entire queue and errored the session. This caused permanent data loss for
all queued events on any transient API error.

Changes:
- Rewrite StreamSession.pump() to retry each event with exponential
  backoff (100ms * 2^attempt, capped at 5s) up to maxPublishRetries times.
- After exhausting the retry budget, dead-letter the individual event
  (emit 'dead-letter', log at ERROR level) and continue processing the
  rest of the queue — the session stays running.
- The session only transitions to 'errored' via the explicit fail() path,
  which is now reserved for coordinator-level failures (e.g. lock errors).
- Add maxPublishRetries constructor param to StreamSession (default 3).
- Add maxPublishRetries option to SessionRegistryOptions and thread it
  through spawn() so the worker can configure it via env var.
- Add PROCESSING_PUBLISH_MAX_RETRIES env var (default 3) to config.ts
  zod schema and document in .env.example.
- Wire MAX_PUBLISH_RETRIES into the SessionRegistry constructor in worker.ts.
- Update session.test.ts: replace the old 'errors on first failure' test
  with dead-letter + fail-fast variants; add 4 new tests covering:
  retry-then-success, dead-letter-and-continue, dead-letter event shape,
  and session remaining healthy after dead-lettering.

Closes XStreamRollz#343
@Xhristin3
Xhristin3 merged commit 3a4c01b into XStreamRollz:main Jul 29, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

processing: StreamSession silently drops the entire queue on first publish failure

2 participants