Skip to content

Commit 702246c

Browse files
committed
fix(validity): don't bisect range on transient transport failures
The admission-shed guard already avoided range bisection when the self-hosted prover pool was full. Extend the same no-bisection retry path to transient transport failures: a gRPC UNAVAILABLE / "tcp connect error" means the backend was unreachable (e.g. the network-gateway is down, or the router reports both backends unavailable), not that the proof failed. Bisecting a range because the backend was momentarily unreachable needlessly fragments a range that is fine. gRPC semantics define UNAVAILABLE as a retryable transient condition, so this holds for both the self-hosted and Succinct paths — a range only needs bisecting on a deterministic proof failure (execution unexecutable / too big), which surfaces as a different error. Reset such requests to Unrequested and retry the same range next loop. Add is_transient_transport_error + unit tests covering gateway-down, Succinct transient-unavailable, low-level connect errors, and the deterministic-failure negative case.
1 parent 10b940f commit 702246c

1 file changed

Lines changed: 80 additions & 11 deletions

File tree

validity/src/proposer.rs

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@ fn is_admission_shed_error(e: &anyhow::Error) -> bool {
5959
format!("{e:?}").contains(ADMISSION_SHED_MARKER)
6060
}
6161

62+
/// Whether a failed proof-request task failed because the prover backend was
63+
/// unreachable / transiently unavailable (a transport or connectivity fault)
64+
/// rather than because the proof itself failed. gRPC defines `UNAVAILABLE` as a
65+
/// retryable transient condition, and a dead backend surfaces as
66+
/// `status: Unavailable, message: "tcp connect error"` (e.g. the self-hosted
67+
/// network-gateway is down, or the router reports both backends unavailable).
68+
///
69+
/// Such a failure must be retried as-is, NOT marked Failed: marking it Failed
70+
/// feeds range bisection, which needlessly fragments a range that is perfectly
71+
/// fine — the backend was simply unreachable. A range only needs bisecting when
72+
/// the proof fails *deterministically* (execution unexecutable / too big),
73+
/// which surfaces as a different error, never as gRPC `UNAVAILABLE`. This holds
74+
/// for both the self-hosted and Succinct paths.
75+
fn is_transient_transport_error(e: &anyhow::Error) -> bool {
76+
let rendered = format!("{e:?}");
77+
rendered.contains("status: Unavailable") ||
78+
rendered.contains("tcp connect error") ||
79+
rendered.contains("error trying to connect")
80+
}
81+
6282
/// Configuration for the driver.
6383
pub struct DriverConfig {
6484
pub network_prover: Option<Arc<NetworkProver>>,
@@ -1522,18 +1542,28 @@ where
15221542
error = ?e,
15231543
"Task failed with error"
15241544
);
1525-
// A self-hosted admission shed means the prover pool
1526-
// was momentarily full — the request never reached a
1527-
// backend. Retry the SAME range next loop instead of
1528-
// marking it Failed, which would count toward range
1529-
// bisection and fragment a range that is fine. Only
1530-
// the self-hosted gateway emits this marker; Succinct
1531-
// never does, so its path is unchanged.
1532-
if is_admission_shed_error(&e) {
1545+
// Some failures must NOT bisect the range: a
1546+
// self-hosted admission shed (prover pool momentarily
1547+
// full) and a transient transport failure (the
1548+
// backend was unreachable — e.g. the gateway is down,
1549+
// gRPC `Unavailable` / "tcp connect error"). In both
1550+
// cases the request never produced a proof, so the
1551+
// range is fine; marking it Failed would feed range
1552+
// bisection and needlessly fragment it. Reset it to
1553+
// Unrequested and retry the SAME range next loop.
1554+
let no_bisect_reason = if is_admission_shed_error(&e) {
1555+
Some("self-hosted admission shed (prover pool full)")
1556+
} else if is_transient_transport_error(&e) {
1557+
Some("transient transport failure (backend unreachable)")
1558+
} else {
1559+
None
1560+
};
1561+
if let Some(reason) = no_bisect_reason {
15331562
warn!(
15341563
request_id = request.id,
15351564
request_type = ?request.req_type,
1536-
"self-hosted admission shed; resetting to Unrequested for retry (no bisection)"
1565+
reason,
1566+
"resetting to Unrequested for retry (no bisection)"
15371567
);
15381568
if let Err(reset_err) = self
15391569
.driver_config
@@ -1543,7 +1573,7 @@ where
15431573
{
15441574
warn!(
15451575
error = ?reset_err,
1546-
"Failed to reset shed request to Unrequested"
1576+
"Failed to reset request to Unrequested"
15471577
);
15481578
}
15491579
continue;
@@ -1951,7 +1981,7 @@ mod contiguous_block_tests {
19511981

19521982
#[cfg(test)]
19531983
mod admission_shed_tests {
1954-
use super::is_admission_shed_error;
1984+
use super::{is_admission_shed_error, is_transient_transport_error};
19551985

19561986
#[test]
19571987
fn detects_self_hosted_admission_shed_only() {
@@ -1977,4 +2007,43 @@ mod admission_shed_tests {
19772007
let wrapped = shed.context("make_proof_request failed");
19782008
assert!(is_admission_shed_error(&wrapped));
19792009
}
2010+
2011+
#[test]
2012+
fn detects_transient_transport_errors() {
2013+
// The production symptom: the network-gateway is down, so the router (or
2014+
// the SDK) surfaces a gRPC UNAVAILABLE with a tcp connect error. This
2015+
// must be treated as retryable-without-bisection.
2016+
let gateway_down =
2017+
anyhow::anyhow!("status: Unavailable, message: \"tcp connect error\", details: []");
2018+
assert!(is_transient_transport_error(&gateway_down));
2019+
2020+
// A Succinct-side transient unavailable is also transport-class: retry,
2021+
// don't bisect (the range is fine, the backend was momentarily down).
2022+
let succinct_unavailable = anyhow::anyhow!(
2023+
"status: Unavailable, message: \"succinct network temporarily unavailable\""
2024+
);
2025+
assert!(is_transient_transport_error(&succinct_unavailable));
2026+
2027+
// Lower-level connect failures (before a gRPC status is formed).
2028+
let connect_err =
2029+
anyhow::anyhow!("error trying to connect: tcp connect error: Connection refused");
2030+
assert!(is_transient_transport_error(&connect_err));
2031+
2032+
// Detected through anyhow context wrapping too.
2033+
let wrapped = gateway_down.context("make_proof_request failed");
2034+
assert!(is_transient_transport_error(&wrapped));
2035+
2036+
// A genuine deterministic proof failure is NOT transport-class — it must
2037+
// still bisect the range.
2038+
let unexecutable = anyhow::anyhow!("proof generation failed: execution unexecutable");
2039+
assert!(!is_transient_transport_error(&unexecutable));
2040+
2041+
// An admission shed carries UNAVAILABLE, so it is also transport-class —
2042+
// the handler checks the shed predicate first for a distinct log, but
2043+
// either way the range is not bisected.
2044+
let shed = anyhow::anyhow!(
2045+
"status: Unavailable, message: \"x-sp1-admission-shed: pool at capacity\""
2046+
);
2047+
assert!(is_transient_transport_error(&shed));
2048+
}
19802049
}

0 commit comments

Comments
 (0)