Skip to content

Kill stalled tier2 segments instead of slow ones - #860

Merged
maoueh merged 1 commit into
developfrom
feature/kaiko-problematic-eth-mainnet-substreams
Aug 1, 2026
Merged

Kill stalled tier2 segments instead of slow ones#860
maoueh merged 1 commit into
developfrom
feature/kaiko-problematic-eth-mainnet-substreams

Conversation

@maoueh

@maoueh maoueh commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Context

A customer's map_token_balances substreams on eth-mainnet was reconnecting in a loop for hours while delivering ~0 blocks. Every attempt died with:

error during init_stores_and_backprocess: run_parallel_process failed: parallel processing run:
scheduler run: segment [25652000-25653000] failed while streaming data:
rpc error: code = DeadlineExceeded desc = deadline_exceeded: request active for too long

Traced end to end:

  1. service/tier2.go watchdog fires once a segment exceeds segmentExecutionTimeout (60min), cancelling with ErrRequestActiveForTooLong.
  2. Tier1's worker maps codes.DeadlineExceeded to a RetryableErr.
  3. But orchestrator/work/worker.go:161 intercepts first: streamOutput && upstream.DataSent() makes it fatal with zero retries — correctly so, since those blocks already reached the client and retrying would duplicate them.

The module makes roughly a thousand eth_call per block (the rpc:eth_call metric counts batches, not individual calls). Segments were landing right at the edge of the 60 minute budget. A segment that misses is never cached, so the retry redoes all of it and races the same clock again — the request could never complete regardless of how many times the client reconnected.

Change

Tier2 now aborts a segment when it stops making progress rather than when it merely takes a long time.

  • New stall timeout, 10min by default (WithSegmentStallTimeout), reset on every block processed.
  • segmentExecutionTimeout demoted to an absolute backstop, default raised 60min → 4h.
  • Stall wins when both deadlines blow — it is the more actionable diagnosis.
  • Decision extracted into segmentWatchdog (service/segment_watchdog.go) so it is testable against wall-clock scenarios rather than only inside the ProcessRange goroutine.

A wedged segment is still killed promptly. Because a single block is already bounded by blockExecutionTimeout (3min), the stall timeout cannot be tripped by one legitimately slow block.

Observability: request active for a long time gained a since_last_progress field, and a stall-kill reports request stalled, no block progress, distinct from request active for too long.

Also in here

Two smaller fixes found while tracing:

  • Stats.processedBlocks is now atomic. It is written by the block-processing goroutine and read by the watchdog goroutine. That cross-goroutine read already existed for the slow-query log, but control flow now depends on it.
  • Tier1 shutdown reported as Internal instead of Unavailable. The endpoint is shutting down, please reconnect error is wrapped several times on its way up from the scheduler and was matched with a pointer comparison (err == errShuttingDown), so it never took the Unavailable path. Clients lost the reconnect signal during every tier1 rollout.

Testing

  • service/segment_watchdog_test.go — covers the customer scenario (one block per 5min for 3h survives, where the old budget killed it at 60min), stall detection, deadline reset on progress, the absolute backstop, precedence, and disabled timeouts.
  • service/error_shutting_down_test.go — asserts Unavailable for both the bare and the four-deep-wrapped shutdown error.
  • go build ./... clean; service, metrics, orchestrator, pipeline, app pass; -race clean on service, metrics, pipeline.

Reviewer notes

  • The 4h backstop is a judgement call, not a derived number. It is what makes this workload survivable. A segment can now hold a tier2 worker slot up to 4h instead of 1h — a real fleet-wide capacity change.
  • worker.go:161 is deliberately untouched. This change avoids tripping the no-retry path rather than softening it.
  • This is a resilience fix, not a cure. ~1000 eth_call/block remains the root cause on the module side; caching token metadata in a store would be the real fix there.

Tier2 aborted a segment once it exceeded a fixed 60 minute budget. That is
fatal to workloads that are expensive but healthy: a segment making thousands
of eth_call per block can take slightly longer than the budget while still
advancing block by block, get killed, and -- since a killed segment is never
cached -- have its retry redo the same work and hit the same wall. Such a
request never completes, no matter how many times the client reconnects.

Replace the fixed budget with a stall timeout (10 minutes, resets on every
block processed) and demote segmentExecutionTimeout to an absolute backstop,
raising its default from 60 minutes to 4 hours. A wedged segment is still
killed promptly, and because a single block is already bounded by
blockExecutionTimeout (3 minutes), the stall timeout cannot be tripped by one
legitimately slow block.

The decision is extracted into segmentWatchdog so it can be tested against
wall-clock scenarios instead of only inside the ProcessRange goroutine.

Stats.processedBlocks becomes atomic: it is written by the block-processing
goroutine and read by the watchdog goroutine. That read already existed for
the slow-query log, but control flow now depends on it.

Also fix a tier1 shutdown during parallel backprocessing being reported to the
client as Internal rather than Unavailable. The "endpoint is shutting down,
please reconnect" error is wrapped several times on its way up from the
scheduler, and was matched with a pointer comparison, so it never took the
Unavailable path and clients lost the reconnect signal during a rollout.
@sduchesneau

Copy link
Copy Markdown
Contributor

🔍 Vulnerabilities of ghcr.io/streamingfast/substreams:854d0df

📦 Image Reference ghcr.io/streamingfast/substreams:854d0df
digestsha256:01d21563577b4df6a435a8ddb59e85f571af444055c57fbf4b1a7fb1f406d98f
vulnerabilitiescritical: 0 high: 0 medium: 0 low: 0
platformlinux/amd64
size120 MB
packages381
📦 Base Image ubuntu:24.04
also known as
  • noble
  • noble-20260610
digestsha256:52df9b1ee71626e0088f7d400d5c6b5f7bb916f8f0c82b474289a4ece6cf3faf
vulnerabilitiescritical: 0 high: 0 medium: 17 low: 5

@maoueh
maoueh merged commit ce43387 into develop Aug 1, 2026
11 checks passed
@maoueh
maoueh deleted the feature/kaiko-problematic-eth-mainnet-substreams branch August 1, 2026 02:52
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.

2 participants