fix(indexer): make stream circuit breaker error-type-aware - #1415
Open
olathedev wants to merge 1 commit into
Open
fix(indexer): make stream circuit breaker error-type-aware#1415olathedev wants to merge 1 commit into
olathedev wants to merge 1 commit into
Conversation
…#1179) The live-stream loop counted every error toward a single hardcoded threshold and terminated the stream permanently once it tripped. A Soroban RPC rolling restart (~10s of consecutive ECONNREFUSED) was enough to halt the indexer until an operator manually restarted it. - Classify errors as retriable (ECONNREFUSED, ECONNRESET, ETIMEDOUT, EPIPE, EHOSTUNREACH, ENETUNREACH/DOWN, EAI_AGAIN, undici timeout codes, 429/5xx RPC responses, pg pool checkout timeouts) vs. persistent, unwrapping the `cause` chain since Node's fetch wraps transport errors in a TypeError. - Track retriable and persistent failures in separate counters via a new StreamCircuitBreaker; only persistent failures can trip it. - Make the threshold configurable via STREAM_CIRCUIT_BREAKER_THRESHOLD (default 10, unchanged). - Add half-open recovery: once open, the breaker waits STREAM_CIRCUIT_BREAKER_PROBE_INTERVAL_MS (default 30s) and lets a single probe through, closing on success instead of requiring a manual restart. - Emit structured stream_circuit_open/half_open/closed and stream_retriable_error metric events on state transitions.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
Author
|
CI is failing on 4 checks here, but all 4 are pre-existing on `main` at the commit this branch is based on (`eb2405b`) and unrelated to the files touched in this PR:
Confirmed by running the CI workflow against `main`'s own HEAD (`eb2405b`) — same 4 failures there. Happy to open separate PRs for these if useful, but keeping them out of scope here to keep this PR focused on #1179. Locally, `services/indexer`'s own `tsc --noEmit`, `eslint`, and `jest` (356 passed) are all clean for the files this PR changes. |
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.
Closes #1179
Problem
The live-stream loop in
services/indexer/src/stream.tstracked a singleconsecutiveFailurescounter that incremented on any error and permanentlybreaks the stream once it hit a hardcoded threshold of 10. That meant:kubectl rollout restart), which produces ~10s of consecutiveECONNREFUSED, halted the indexer for good.Fix
Introduces an error-type-aware
StreamCircuitBreaker(services/indexer/src/stream-circuit.ts):Classification —
isRetriableStreamError()treats transport blips as retriable and lets them retry indefinitely without counting toward the trip threshold:ECONNREFUSED,ECONNRESET,ETIMEDOUT,EPIPE,EHOSTUNREACH,ENETUNREACH,ENETDOWN,EAI_AGAINUND_ERR_CONNECT_TIMEOUT,UND_ERR_HEADERS_TIMEOUT,UND_ERR_BODY_TIMEOUT,UND_ERR_SOCKET)429/5xxRPC responses (a4xxother than 429 is treated as persistent — a malformed request won't fix itself)pgpool exhaustion, matched by message since it carries no error code (e.g. "timeout exceeded when trying to connect", "too many clients already")fetchwraps transport errors in aTypeError, so thecausechain is unwrapped when classifying.ENOTFOUNDis deliberately not retriable (a permanently unresolvable host is a misconfiguration), unlikeEAI_AGAIN(a temporary resolver failure).Separate counters — retriable failures are tracked only for observability; only unclassified/persistent failures increment the counter that can trip the breaker.
Configurable threshold —
STREAM_CIRCUIT_BREAKER_THRESHOLDenv var (default10, unchanged from before).Half-open recovery — tripping the breaker no longer ends the stream. It opens, waits
STREAM_CIRCUIT_BREAKER_PROBE_INTERVAL_MS(default30000), then allows a single probe attempt through. Success closes the breaker and streaming resumes; failure reopens it for another wait. No more manual restarts.Structured metrics —
stream_circuit_open,stream_circuit_half_open,stream_circuit_closed, andstream_retriable_errorare emitted as JSON log lines on every state transition, ready for alerting.Changes
services/indexer/src/stream-circuit.ts(new) — the breaker and error classifier.services/indexer/src/stream.ts— replaced the inline counter/breakwith the new breaker; retriable errors retry-and-continue, persistent errors drive open → half-open → closed.services/indexer/src/config.ts— addedstreamCircuitBreakerThreshold/streamCircuitBreakerProbeIntervalMs, sourced fromSTREAM_CIRCUIT_BREAKER_THRESHOLD/STREAM_CIRCUIT_BREAKER_PROBE_INTERVAL_MS.services/indexer/src/index.ts— wires the new config through tostreamEvents.services/indexer/.env.example— documents the two new env vars.services/indexer/src/__tests__/stream-circuit.test.ts(new) — unit tests for the classifier and breaker state machine.services/indexer/src/__tests__/stream.test.ts— integration tests againststreamEventsitself, including the exact scenario from the issue.Acceptance criteria
ECONNREFUSEDerrors followed by a successful fetch → breaker never trips, stream continues.Test plan
npx jestinservices/indexer— all 356 tests pass (2 pre-existing skips unrelated to this change), including:stream-circuit.test.ts— classifier coverage for every retriable/persistent code,cause-chain unwrapping, and the breaker's closed → open → half-open → closed transitions.stream.test.ts— new#1179describe block: 15xECONNREFUSEDfollowed by success never opens the breaker; a run of unclassified errors opens it at the configured threshold; the breaker recovers through a half-open probe instead of terminating the stream.npx tsc --noEmit— no new type errors (the two pre-existing errors insearch.ts/utils.tsare unrelated and present onmainbefore this change).npx eslinton all changed files — clean.