Firehose block stream can hang indefinitely with no idle/read timeout → subgraph silently stops indexing until restart
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 52/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Tranquilo
- Stack tecnológico
- grpc, rust
- Área
- backend, distributed-systems, networking
Línea de trabajo
Comienza con graph/src/blockchain/firehose_block_stream.rs, especialmente con el establecimiento del stream y el bucle de recepción; después inspecciona graph/src/blockchain/block_stream.rs y core/src/subgraph/runner/mod.rs para ver cómo se propaga la suspensión. Revisa graph/src/firehose/endpoints.rs en busca del comportamiento existente de timeout y keepalive. La tarea estará terminada cuando un stream de firehose inactivo se detecte y se recupere sin reasignación manual, mientras los streams normales continúan indexando.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Summary
A subgraph fed by a firehose block stream can stop indexing indefinitely and silently if the firehose upstream keeps the HTTP/2 stream open but stops sending frames (no message, no error, no end-of-stream). The subgraph stays health: healthy, is not paused, sets no fatalError, emits zero logs, and its latestBlock is frozen. Only a manual restart / reassignment (which opens a fresh stream) recovers it.
The block-stream receive loop has no per-message idle/read timeout, and HTTP/2 keepalive pings are intentionally disabled, so there is nothing to detect or break a half-open / app-hung upstream.
Affected versions
Confirmed present and unchanged from v0.42.1 through v0.44.0 and current master. The relevant code has not changed since it was introduced.
Symptom (observed in production)
graphman info --status:Paused: false,Health: healthy,fatalError: null.latestBlockfrozen for hours; no forward progress.- No log lines for the deployment anywhere (all pods) for the entire stall.
- Other subgraphs on the same node/chain keep indexing normally (node is fine).
graphman reassign <hash> <node>(fresh runner → fresh stream) recovers it immediately.- Correlated with the firehose's upstream block source being briefly unstable (e.g. a node restart), which can leave individual gRPC streams half-open.
Root cause
1. No idle timeout on the stream receive loop.
graph/src/blockchain/firehose_block_stream.rs (master: establishment timeout at L246, receive loop at L258):
let req = endpoint.clone().stream_blocks(request, &headers);
let result = tokio::time::timeout(Duration::from_secs(120), req).await; // guards ESTABLISHMENT only
match result {
Ok(stream) => {
let mut last_response_time = Instant::now(); // only feeds a metric
for await response in stream { // <-- no timeout / select! around each .next()
...
}
- The
tokio::time::timeout(120s, req)wraps only the initialstream_blocksestablishment future, not the per-frame iteration. - Inside
for await response in streamthere is notokio::time::timeout, noselect!, no idle deadline.last_response_timeis only passed to a metric (observe_response) and never used to trip a timeout. - If the upstream holds the HTTP/2 stream open and sends nothing,
for awaitnever yields, and the hang propagates cleanly up the whole consumer chain with no error and no log:BufferedBlockStream(graph/src/blockchain/block_stream.rs:62) only pumps the stream into an mpsc channel — no timeout of its own.- The runner's
block_stream.next().await(core/src/subgraph/runner/mod.rs:327) has notokio::time::timeoutand noselect!. - The only wakeup for the suspended runner task is its
Cancelable/CancelGuard, which is fired only by unassign/reassign. This is exactly whygraphman reassign(fresh runner → fresh stream) is the only thing that recovers it.
2. HTTP/2 keepalive intentionally disabled (no liveness probe for a half-open stream).
graph/src/firehose/endpoints.rs (~L241–250 on master):
// Note: Do not set `http2_keep_alive_interval` or `http2_adaptive_window`, as these will
// send ping frames, and many cloud load balancers will drop connections that frequently
// send pings.
let endpoint = endpoint_builder
.initial_connection_window_size(Some((1 << 31) - 1))
.connect_timeout(Duration::from_secs(10))
.tcp_keepalive(Some(Duration::from_secs(15)))
// Timeout on each request, so the timeout to establish each 'Blocks' stream.
.timeout(Duration::from_secs(120));
- HTTP/2 keepalive pings are deliberately off, so there is no application-level liveness probe.
tcp_keepalive(15s)detects a fully dead peer (no TCP ACKs) but not a half-open / app-hung peer whose OS keeps the socket alive — the common case when the upstream is mid-restart behind a proxy/LB.connect_timeoutand.timeout(120s)(per the code's own comment) bound only stream establishment, not mid-stream idle.
This is a known, deliberately worked-around limitation, not an oversight. The original comment on the connection-window tuning (PR #3818, 2022-08-08) states:
"We run multiple block streams on a same connection, and a problematic subgraph with a stalled block stream might consume the entire window capacity for its http2 stream and never release it. If there are enough stalled block streams to consume all the capacity on the http2 connection, then all subgraphs using this same http2 connection will stall."
The mitigation chosen at the time was to set the connection window to the maximum (so a few stalled streams don't starve others) — it makes the connection tolerate stalled streams but never recovers an individual stalled stream.
Why existing guards don't cover it
connect_timeout/.timeout(120s)— establishment only (see comment in code).tcp_keepalive(15s)— dead peer only, not half-open.- No
GRAPH_*env var bounds the streaming-loop idle time.GRAPH_FIREHOSE_FETCH_BLOCK_TIMEOUT_SECS(firehose_block_fetch_timeout) only guards the reorg block-refetch RPC, not the main stream loop. GRAPH_KILL_IF_UNRESPONSIVEdoes not catch this: it is a tokio-runtime/threadpool contention watchdog (ping/pong over the whole runtime,node/src/launcher.rs). A runner suspended on an idle.awaitconsumes no thread, so the watchdog stays satisfied. (The reporter of #4146 hadGRAPH_KILL_IF_UNRESPONSIVE=true; it detected nothing.)
Related / prior art (none fixes this)
- #4146 "Indexing stucks until restart" (closed, 2022) — near-identical symptom (stops indexing, no errors, clear logs, different subgraph each time, only a full restart fixes it), but on the RPC poller and closed without a root cause. Symptomatic precedent.
- #3810 — added the firehose
connect_timeout(establishment-only; explicitly not mid-stream). - #3855 "firehose: Set a timeout for grpc requests" — added the
.timeout(120s), motivated explicitly by "we might have seen requests hanging" — but it bounds only stream establishment. - #3822 "firehose: Set tcp keepalive" ("to detect firehose restarts and such") — TCP-level only; misses a half-open / app-hung peer.
- #3818 — connection window max + the "do not set http2_keep_alive_interval" comment quoted above.
- #4190 "Store connection issue prevents subgraph indexing until graph-node is restarted" — same shape (silent stall until restart) on the Postgres writer path.
Note: maintainers have already reached for hang mitigations twice (#3855 request timeout, #3822 TCP keepalive), but both bound only connection establishment / dead-socket detection — neither recovers a mid-stream idle hang.
Proposed fix (either, or both)
- Idle timeout on the receive loop — wrap each
stream.next()intokio::time::timeout(idle_deadline, …)(or aselect!against a reset-on-message deadline) inside the loop; on elapse, drop the stream and reconnect with backoff (the reconnect path already exists). Makeidle_deadlineenv-configurable (e.g.GRAPH_FIREHOSE_STREAM_IDLE_TIMEOUT_SECS), disabled by default to preserve current behavior. - Re-enable a conservative HTTP/2 keepalive —
http2_keep_alive_interval+keep_alive_timeout, gated behind an env var so operators whose network path is not behind a ping-dropping LB (e.g. an in-cluster service) can opt in. This directly negates the failure mode the 2022 comment was avoiding, without forcing it on everyone.
Option 1 is topology-independent and recommended as the primary fix.
Reproduction
- Point a subgraph at a firehose endpoint.
- While it streams, make the firehose upstream hold the gRPC stream open but stop emitting frames (e.g. pause/stall the upstream block source, or interpose a proxy that stops forwarding frames without closing the stream).
- Observe: the subgraph freezes with
health: healthy, no logs, nofatalError, frozenlatestBlock, indefinitely. - Restart/reassign → recovers immediately.
Environment
- graph-node v0.42.1 (also verified unchanged on master / v0.44.0).
- Ethereum mainnet subgraphs consuming a firehose block stream (StreamingFast
firehose-ethereum), eth_call provider separate.
- Lenguaje dominante
- Rust
- Estrellas
- 3.2k
- Forks
- 1.1k
- Merge medio
- 4 d 1 h
- PR fusionados (30 d)
- 1
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de graphprotocol/graph-node
-
current: include emits an all-null bucket for dimensionless aggregations, nulling the whole response Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
graphprotocol/graph-node#6719 ·
-
RUSTSEC-2026-0194: Quadratic run time when checking a start tag for duplicate attribute names Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
graphprotocol/graph-node#6673 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
graphprotocol/graph-node#6650 · 1 comentario ·
-
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
graphprotocol/graph-node#6722 ·
-
Dificultad 3/5 1-2 días Aptitud para principiantes 68/100
graphprotocol/graph-node#6721 ·
Todos los issues de graphprotocol/graph-node
Issues similares
-
risk:low runtime status:in-progress type:test
Dificultad 1/5 Menos de una hora Aptitud para principiantes 92/100
zeroclaw-labs/zeroclaw#11023 ·
-
good first issue refactor
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
EricSpencer00/Resilient#4835 · 1 comentario ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
bisq-network/bisq-musig#204 ·
-
agent:ready documentation
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
cesarferreira/stax#890 ·