@@ -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.
6383pub 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) ]
19531983mod 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