fix(python): retry transient job polling errors

Fixes #4773.

Python job polling now retries transient connection, send, and read failures with capped exponential backoff under the caller's original timeout. If the deadline expires while disconnected, `wait()` raises `ConnectionError` with the job ID, reconnect guidance, and the original cause; after recovery, ordinary timeouts remain `TimeoutError`. Terminal job errors, cancellation, and malformed responses continue to surface immediately.

Validation: exact-head CI passed across Rust all-features, Python on Linux, macOS, and Windows, NodeJS, deny, and packaging checks. The hosted macOS wheel suite passed 3049 tests (162 skipped, 70 deselected), including sync and async recovery, timeout reuse, cancellation, terminal errors, and error preservation.
This commit is contained in:
lancedb-gatefixer[bot]
2026-11-03 05:05:30 +08:00
committed by GitHub
parent 5ca8d836e7
commit ea9194a7f3
11 changed files with 474 additions and 11 deletions
+25 -1
View File
@@ -71,6 +71,7 @@ impl<T> PythonErrorExt<T> for std::result::Result<T, LanceError> {
request_id,
status_code.map(|s| s.as_u16()),
err.http_error_details(),
is_transport_error(source.as_ref()),
)?;
if let Some(cause) = source.source() {
@@ -170,7 +171,14 @@ fn http_from_rust_error(
let details = err
.downcast_ref::<LanceError>()
.and_then(LanceError::http_error_details);
let py_err = new_py_http_error(py, message, request_id, status_code, details)?;
let py_err = new_py_http_error(
py,
message,
request_id,
status_code,
details,
is_transport_error(err),
)?;
// Reset the traceback since it doesn't provide additional information.
let py_err = py_err.call_method1(intern!(py, "with_traceback"), (PyNone::get(py),))?;
@@ -189,6 +197,7 @@ fn new_py_http_error<'py>(
request_id: &str,
status_code: Option<u16>,
details: Option<&HttpErrorDetails>,
is_transport_error: bool,
) -> PyResult<Bound<'py, PyAny>> {
let error_code = details.map(|details| details.error_code.as_str());
let resource_kind = details.map(|details| details.resource_kind.as_str());
@@ -205,5 +214,20 @@ fn new_py_http_error<'py>(
resource_kind,
resource_name,
namespace_path,
is_transport_error,
))
}
// Preserve the typed transport classification across the Python boundary.
// A missing status code alone also covers malformed job responses, which
// waiting longer cannot repair.
fn is_transport_error(err: &(dyn std::error::Error + 'static)) -> bool {
if let Some(error) = err.downcast_ref::<reqwest::Error>() {
return error.is_connect()
|| error.is_timeout()
|| error.is_request()
|| error.is_body()
|| error.is_decode();
}
err.source().is_some_and(is_transport_error)
}