Skip to content

feat(indexer): exactly-once pipeline, backpressure, WebSocket fanout, gap detection#604

Merged
devJaja merged 7 commits into
Epta-Node:mainfrom
distributed-nerd:feat/indexer-exactly-once-pipeline
Jun 23, 2026
Merged

feat(indexer): exactly-once pipeline, backpressure, WebSocket fanout, gap detection#604
devJaja merged 7 commits into
Epta-Node:mainfrom
distributed-nerd:feat/indexer-exactly-once-pipeline

Conversation

@distributed-nerd

Copy link
Copy Markdown
Contributor

Closes #533

Summary

Reworks the indexer's event stream into a production-grade, exactly-once pipeline with backpressure, in-process fanout, and gap detection. Replaces the fixed-interval getEvents poll in services/indexer/src/stream.ts.

What changed

Exactly-once ingestion

  • New raw_events staging table (migration 006_raw_events.sql): (id BIGSERIAL, ledger_sequence BIGINT, event_index INT, contract_id TEXT, topic TEXT[], data JSONB, processed_at TIMESTAMPTZ, PRIMARY KEY(ledger_sequence, event_index)).
  • Each batch runs in a single serialisable transaction: stage into raw_events (ON CONFLICT DO NOTHING) → project into domain tables via the same tx client → advance indexer_state.processed_cursor as the last statement before COMMIT. A crash rolls everything back together, so replay yields no duplicate domain rows. (src/pipeline.ts)

Backpressure & adaptive polling

  • Token-bucket rate limiter for RPC calls — default 10 req/s, RPC_RATE_LIMIT_PER_SEC. (src/ratelimit.ts)
  • Adaptive poll interval: doubles (up to 5s) on an empty batch, halves (down to 100ms) on a full page. (src/poller.ts)
  • 429 responses trigger exponential backoff with retries; the window is re-fetched so no events are dropped.

Internal pub/sub fanout

  • In-process EventBus (Node EventEmitter) with typed per-event-type channels plus a wildcard. (src/bus.ts)
  • /ws WebSocket handler subscribes to the bus and pushes { type, payload } JSON frames, with type subscriptions, 15s ping/pong heartbeat, and clean disconnection. (src/ws.ts)
  • WebSocket message schema documented in docs/indexer/WEBSOCKET_API.md.

Gap detection

  • After each batch, verifies batch[0].ledger_sequence == cursor + 1. On a gap (e.g. RPC node failover mid-sequence) it emits a structured gap_detected log and backfills the missing range before continuing. (src/gap.ts)

Tests

  • Exactly-once: crash simulated between raw ingest and domain write → no duplicate rows on restart; idempotent replay; bus publishes only after commit.
  • Backpressure: mocked 429 → verified exponential backoff and no dropped events.
  • WebSocket: clients receive events within 200ms of ingestion, including under a synthetic burst.
  • Unit tests for the bus, token bucket, adaptive poller, and gap detection.

npm test95 passed (13 suites), including all pre-existing indexer handler tests. tsc typechecks clean.

Acceptance criteria

  • No duplicate domain rows under any crash scenario
  • Adaptive polling implemented and tested
  • WebSocket push implemented with documented schema
  • Gap detection emits structured log and triggers backfill
  • All existing indexer tests pass

Notes

  • The pipeline's domainProcessor is an injectable seam: the exactly-once mechanism is fully implemented/tested, but wiring the existing typed handlers requires Soroban XDR decoding, which is out of scope for this issue (the previous index.ts also only logged events). A clear marker shows where decoded handlers plug in.
  • The existing Dockerfile runs npm ci --omit=dev before tsc build (which needs dev @types/typescript) — a pre-existing issue left untouched here.

shaaibu7 added 6 commits June 22, 2026 19:55
Migration 006 introduces the exactly-once backbone:
- raw_events (PK ledger_sequence, event_index) for idempotent staging
- indexer_state.processed_cursor advanced only after the domain commit
…gap detection

- pipeline.ts: stage raw_events + project to domain tables + advance cursor in
  a single serialisable transaction; ON CONFLICT DO NOTHING for idempotency
- bus.ts: in-process EventEmitter pub/sub with typed + wildcard channels
- ratelimit.ts: token-bucket limiter (default 10 req/s, env-configurable)
- poller.ts: adaptive poll interval (double when idle, halve on full page)
- gap.ts: mid-stream ledger gap detection
Rewrite streamEvents with backpressure (token bucket + adaptive interval),
429 exponential backoff that never drops events, and gap detection that emits
a structured gap_detected log and backfills the missing range before
continuing. Parses per-ledger event_index from the RPC event id.
- ws.ts: bridge the event bus to /ws clients as { type, payload } frames,
  with type subscriptions, 15s ping/pong heartbeat, and clean disconnect
- index.ts: wire stream -> IngestPipeline -> bus -> WebSocket, resume from the
  committed cursor, and serve /health + /ws over an HTTP server
- pipeline: crash between raw ingest and domain write -> no duplicate rows on
  restart; idempotent replay; bus publish only after commit
- stream: 429 -> exponential backoff with no dropped events
- ws: events reach clients within 200ms, including under a synthetic burst
- unit tests for bus, token bucket, adaptive poller, and gap detection
- docs/indexer/WEBSOCKET_API.md: full /ws message schema
- README + .env.example: exactly-once pipeline, backpressure, fanout, gap
  detection, and new env vars
- add ws + @types/ws dependencies
@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown

@shaaibu7 is attempting to deploy a commit to the Jaja's projects Team on Vercel.

A member of the Team first needs to authorize it.

@devJaja devJaja self-requested a review June 22, 2026 21:13
@devJaja

devJaja commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Solid implementation @distributed-nerd , resolve the conflicts , fix the CI check failing

@devJaja

devJaja commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Nice implementation @distributed-nerd

LGTM

@devJaja devJaja merged commit dcd70bf into Epta-Node:main Jun 23, 2026
2 of 4 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.

feat(indexer): build a real-time, resumable, exactly-once Soroban event pipeline with PostgreSQL WAL-based deduplication and sub-second latency SLA

3 participants