feat(functions): expose retry policies in Python and Rust (#4873)

Expose `FunctionRetryPolicy` on Function declarations and refresh
overrides, together with `error_policy` and recorded attempt counts.
Python uses the Rust validation rules before sending a request; local
refreshes reject these Enterprise options.

The policy bounds calls and backoff, supports
class/message/provider-code matching, and can disable retries with
`max_attempts=1`. Batch retry and skip operate on the whole failed call.
Requires Enterprise server support for the new policy.

Validation: rebuilt the Python extension; 47 targeted Python tests and
10 Rust tests passed. Python `make check`, Rust formatting, and `cargo
clippy --features remote --tests --examples -- -D warnings` passed. The
additional repository-root Ruff sweep reports existing errors under
`docs/web-tests`; the Python workspace checks pass.

Draft user guidance covers the supporting server requirement, safe
repetition of partial work and whole-batch exhaustion. The guide
sections stay hidden until release. Vale 3.24.0 passed for the added
draft prose; no paired deployed SDK/server retry acceptance was run.
Companion server: https://github.com/lancedb/sophon/pull/9812.

BREAKING CHANGE: Rust callers constructing `FunctionRegistrationRequest`
or `FunctionErrorRecord` with struct literals must supply the new
`retry_policy` or `attempts` field, respectively.
This commit is contained in:
Yang Cen
2026-11-06 02:30:49 +08:00
committed by GitHub
parent 3d790e751d
commit 6687db979d
19 changed files with 528 additions and 11 deletions
+1
View File
@@ -145,6 +145,7 @@ pub fn _lancedb(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(table::tokenize, m)?)?;
m.add_function(wrap_pyfunction!(permutation::async_permutation_builder, m)?)?;
m.add_function(wrap_pyfunction!(util::validate_table_name, m)?)?;
m.add_function(wrap_pyfunction!(util::validate_function_retry_policy, m)?)?;
m.add_function(wrap_pyfunction!(util::parse_field_path, m)?)?;
m.add_function(wrap_pyfunction!(query::fts_query_to_json, m)?)?;
m.add_function(wrap_pyfunction!(expr_col, m)?)?;
+26 -4
View File
@@ -46,8 +46,22 @@ fn refresh_column_options(
batch_size: Option<i64>,
gpu_class: Option<String>,
gpu_count: Option<i64>,
retry_policy: Option<String>,
error_policy: Option<String>,
) -> PyResult<RefreshColumnOptions> {
let mut options = RefreshColumnOptions::default();
options.retry_policy = retry_policy
.map(|json| {
serde_json::from_str(&json)
.map_err(|error| PyValueError::new_err(format!("invalid retry_policy: {error}")))
})
.transpose()?;
options.error_policy = error_policy
.map(|value| {
serde_json::from_value(serde_json::Value::String(value))
.map_err(|error| PyValueError::new_err(format!("invalid error_policy: {error}")))
})
.transpose()?;
options.batch_size = batch_size
.map(usize::try_from)
.transpose()
@@ -611,6 +625,7 @@ impl From<lancedb::table::RefreshColumnResult> for RefreshColumnResult {
#[pyclass(module = "lancedb._lancedb", get_all, from_py_object)]
#[derive(Clone, Debug)]
pub struct FunctionErrorRecord {
pub attempts: Option<u32>,
pub job_id: String,
pub fragment_id: u64,
pub row_offset: Option<u32>,
@@ -653,6 +668,7 @@ impl FunctionErrorRecord {
impl From<lancedb::function::FunctionErrorRecord> for FunctionErrorRecord {
fn from(record: lancedb::function::FunctionErrorRecord) -> Self {
Self {
attempts: record.attempts,
job_id: record.job_id,
fragment_id: record.fragment_id,
row_offset: record.row_offset,
@@ -2126,15 +2142,18 @@ impl Table {
})
}
#[pyo3(signature = (column, *, batch_size=None, gpu_class=None, gpu_count=None))]
#[pyo3(signature = (column, *, batch_size=None, gpu_class=None, gpu_count=None, retry_policy=None, error_policy=None))]
pub fn refresh_column(
self_: PyRef<'_, Self>,
column: String,
batch_size: Option<i64>,
gpu_class: Option<String>,
gpu_count: Option<i64>,
retry_policy: Option<String>,
error_policy: Option<String>,
) -> PyResult<Bound<'_, PyAny>> {
let options = refresh_column_options(batch_size, gpu_class, gpu_count)?;
let options =
refresh_column_options(batch_size, gpu_class, gpu_count, retry_policy, error_policy)?;
let inner = self_.inner_ref()?.clone();
future_into_py(self_.py(), async move {
let result = inner
@@ -2145,15 +2164,18 @@ impl Table {
})
}
#[pyo3(signature = (column, *, batch_size=None, gpu_class=None, gpu_count=None))]
#[pyo3(signature = (column, *, batch_size=None, gpu_class=None, gpu_count=None, retry_policy=None, error_policy=None))]
pub fn refresh_column_async(
self_: PyRef<'_, Self>,
column: String,
batch_size: Option<i64>,
gpu_class: Option<String>,
gpu_count: Option<i64>,
retry_policy: Option<String>,
error_policy: Option<String>,
) -> PyResult<Bound<'_, PyAny>> {
let options = refresh_column_options(batch_size, gpu_class, gpu_count)?;
let options =
refresh_column_options(batch_size, gpu_class, gpu_count, retry_policy, error_policy)?;
let inner = self_.inner_ref()?.clone();
future_into_py(self_.py(), async move {
let job = inner
+11
View File
@@ -70,3 +70,14 @@ pub fn parse_field_path(path: &str) -> PyResult<Vec<String>> {
/// A wrapper around a LanceDB type to allow it to be used in Python
#[derive(Debug, Clone)]
pub struct PyLanceDB<T>(pub T);
/// Validate the Function retry wire contract using the same rules as Rust APIs.
#[pyo3::pyfunction]
pub fn validate_function_retry_policy(json: &str) -> pyo3::PyResult<()> {
let policy: lancedb::function::FunctionRetryPolicy =
serde_json::from_str(json).map_err(|error| {
pyo3::exceptions::PyValueError::new_err(format!("invalid retry_policy: {error}"))
})?;
policy
.validate()
.map_err(|error| pyo3::exceptions::PyValueError::new_err(error.to_string()))
}