fix(python): improve function failure diagnostics (#4790)

## Summary

- Make `AsyncJob.wait(timeout)` and `Job.wait(timeout)` raise a
descriptive `TimeoutError` containing the job ID and elapsed timeout.
Registry jobs explain that work continues on the server and include the
`db.open_job(id).wait()` reattach call. SQL query handles instead say to
keep the original handle because query IDs cannot be reopened through
the registry jobs API.
- Render `JobFailureInfo` and `FunctionErrorRecord` with Python values
instead of Rust `Option` wrappers.
- Decode and expose the server's `row_address` on `FunctionErrorRecord`,
and document how to retry recorded rows with `REFRESH COLUMN ... WHERE
_rowaddr IN (...)` before compaction or rewrite.
- Warn when an import inside a packaged Function body resolves to
undeclared local source. The `udf` documentation explains why arbitrary
base-image coverage and distribution-to-import-name mapping cannot be
validated reliably by the SDK.

`row_address` is optional, so records from older servers still decode
with `None`. Rust code that constructs `FunctionErrorRecord` literals
must now supply this new field.

Part of ENT-3448 and ENT-3442. This consumes the server-side diagnostics
from lancedb/sophon#9509 and logical row addresses from
lancedb/sophon#9512.

## Validation

- Rust `function_errors` remote decoding and
legacy-missing-`row_address` tests.
- Rust/PyO3 clippy for `lancedb` and `lancedb-python`.
- Python extension build, pyright, and ruff.
- Targeted Python job, Function packaging, Function error, and job
detail tests: 62 passed, plus timeout-handle tests: 3 passed.
This commit is contained in:
Xuanwo
2026-11-02 06:44:48 +08:00
committed by GitHub
parent 9786f2b63b
commit 6a255926d7
11 changed files with 306 additions and 14 deletions
+14 -4
View File
@@ -200,10 +200,20 @@ pub struct JobFailureInfo {
#[pymethods]
impl JobFailureInfo {
fn __repr__(&self) -> String {
format!(
"JobFailureInfo(phase={:?}, message={:?}, retryable={:?})",
self.phase, self.message, self.retryable
)
let phase = self
.phase
.as_ref()
.map_or_else(|| "None".to_string(), |value| format!("{value:?}"));
let message = self
.message
.as_ref()
.map_or_else(|| "None".to_string(), |value| format!("{value:?}"));
let retryable = match self.retryable {
Some(true) => "True",
Some(false) => "False",
None => "None",
};
format!("JobFailureInfo(phase={phase}, message={message}, retryable={retryable})")
}
}
+12 -3
View File
@@ -596,6 +596,7 @@ pub struct FunctionErrorRecord {
pub job_id: String,
pub fragment_id: u64,
pub row_offset: Option<u32>,
pub row_address: Option<u64>,
pub column: String,
pub function: String,
pub function_version: String,
@@ -608,12 +609,19 @@ pub struct FunctionErrorRecord {
#[pymethods]
impl FunctionErrorRecord {
pub fn __repr__(&self) -> String {
let row_offset = self
.row_offset
.map_or_else(|| "None".to_string(), |value| value.to_string());
let row_address = self
.row_address
.map_or_else(|| "None".to_string(), |value| value.to_string());
format!(
"FunctionErrorRecord(job_id={:?}, fragment_id={}, row_offset={:?}, column={:?}, \
error_type={:?}, error_message={:?})",
"FunctionErrorRecord(job_id={:?}, fragment_id={}, row_offset={}, row_address={}, \
column={:?}, error_type={:?}, error_message={:?})",
self.job_id,
self.fragment_id,
self.row_offset,
row_offset,
row_address,
self.column,
self.error_type,
self.error_message
@@ -627,6 +635,7 @@ impl From<lancedb::function::FunctionErrorRecord> for FunctionErrorRecord {
job_id: record.job_id,
fragment_id: record.fragment_id,
row_offset: record.row_offset,
row_address: record.row_address,
column: record.column,
function: record.function,
function_version: record.function_version,