feat: expose queued Function refresh status (#4959)

## Problem

The existing Function `column_status` surface cannot decode the Agent
scheduler's `queued` state or the `failure_threshold_reached` and
`binding_revision_missing` blocked reasons. It also preserves only the
latest refresh job, so a later cancellation can hide an earlier actual
failure.

## Change

- Adds the `queued`, `failure_threshold_reached`, and
`binding_revision_missing` wire states in Rust, Python, and Node. The
wire change is additive, while Rust callers must update exhaustive enum
matches and public struct literals.
- Adds optional `last_failure_job` alongside `last_refresh_job`. The
latest cancellation and the most recent actual failure retain separate
meanings.
- Reuses the canonical Rust `JobFailure` and existing Python/Node
`JobFailureInfo` models for nested job failure details instead of
introducing a parallel status error model.
- Maps the existing public `sql_job_id` field through Node's
`JobFailureInfo`.
- Keeps `column_status`, `column_statuses`, and `wait_fresh` as the
existing API surfaces. There is no new endpoint, row-error list, durable
ledger, or unknown-enum fallback.
- Documents `queued` versus `running` and the separate latest-attempt
and latest-failure jobs in the existing hidden freshness release draft.

The new status values are additive but cross-service: `queued`, the two
new blocked reasons, and `last_failure_job` require matching Agent/QN
and SDK deployments before clients can rely on them. This SDK follow-up
does not change the normal `7cc3904a` dependency pin or claim whole
ENT-3487 completion.

## Validation

The tested private source is `b19c8adbdc936c5976def6c17be47f6d5e3faf3e`,
based on product commit `c9bac25ecb8b73b1bd0256bbea65c2621bcf6a94`. The
final product docs commit changes generated TypeDoc only.

- Rust: 2 selected tests passed, 0 failed, exit 0. The same two Rust
blobs are unchanged from their tested `788ef85b` source through
`c9bac25e`.
- Python: the PyO3 extension built successfully, and the selected native
PyO3 SDK test against a mock HTTP REST transport passed 1/0, exit 0. The
test requests pinned version 42.
- Node: the addon and TypeScript builds passed, Biome passed, and the
selected Jest transport test passed 1/0 with 57 filtered tests. Addon
SHA-256:
`c888e260b61d41e79689ed2f20dd0dc98586a2377ce34f21610b06111734d426`.
- Python extension SHA-256:
`36c961d8a0cbd2e102a7765f2e47efb4c5af6310baa6262c8e3120407463da2b`.
- TypeDoc was generated from the native `native.d.ts` produced by that
Node build. A second generation produced no remaining diff.

The public diff contains only product, test, and generated API
documentation changes. Native runner scripts and preserved
compile-failure logs remain private validation evidence.

Agent/QN production mapping, mixed-version rollout, and the remaining
ENT-3487 server-side work are tracked separately.
This commit is contained in:
Xuanwo
2026-11-11 21:14:30 +08:00
committed by GitHub
parent 7cc3904a7e
commit a356950bce
16 changed files with 394 additions and 55 deletions
+2
View File
@@ -1108,6 +1108,7 @@ class FunctionColumnLastRefreshJob:
state: str
automatic: Optional[bool]
completed_at_millis: Optional[int]
failure: Optional[JobFailureInfo]
class FunctionColumnAutoRefreshStatus:
enabled: bool
@@ -1131,6 +1132,7 @@ class FunctionColumnStatus:
never_computed: int
failed: int
last_refresh_job: Optional[FunctionColumnLastRefreshJob]
last_failure_job: Optional[FunctionColumnLastRefreshJob]
auto_refresh: FunctionColumnAutoRefreshStatus
class RefreshMaterializedViewResult:
+36 -9
View File
@@ -2306,15 +2306,33 @@ def test_remote_function_column_status_passes_pinned_version():
"stale": 2,
"never_computed": 0,
"failed": 0,
"last_refresh_job": {
"job_id": "job-cancelled",
"state": "cancelled",
"automatic": True,
"completed_at_millis": 1790000002000,
},
"last_failure_job": {
"job_id": "job-failed",
"state": "failed",
"automatic": True,
"completed_at_millis": 1790000001000,
"failure": {
"phase": "finalize",
"message": "worker timed out",
"traceback": "Traceback: timeout",
"retryable": True,
"sql_job_id": "sql-job-7",
},
},
"auto_refresh": {
"enabled": True,
"interval_seconds": 60,
"active_job_id": None,
"state": "blocked",
"failure_streak": 3,
"effective_cooldown_millis": 8000,
"blocked_reason": "preparation_backoff",
"active_job_id": "job-queued",
"state": "queued",
"failure_streak": 1,
"next_run_at_millis": None,
"next_run_at_reason": "job_queued_not_started",
},
}
@@ -2350,10 +2368,19 @@ def test_remote_function_column_status_passes_pinned_version():
0,
0,
)
assert result.auto_refresh.state == "blocked"
assert result.auto_refresh.failure_streak == 3
assert result.auto_refresh.effective_cooldown_millis == 8000
assert result.auto_refresh.blocked_reason == "preparation_backoff"
assert result.auto_refresh.state == "queued"
assert result.auto_refresh.active_job_id == "job-queued"
assert result.auto_refresh.failure_streak == 1
assert result.auto_refresh.next_run_at_reason == "job_queued_not_started"
assert result.last_refresh_job.state == "cancelled"
assert result.last_refresh_job.failure is None
assert result.last_failure_job.job_id == "job-failed"
failure = result.last_failure_job.failure
assert failure.phase == "finalize"
assert failure.message == "worker timed out"
assert failure.traceback == "Traceback: timeout"
assert failure.retryable is True
assert failure.sql_job_id == "sql-job-7"
def test_remote_wait_fresh_retries_only_typed_finalization_pending():
+14 -8
View File
@@ -228,7 +228,7 @@ impl From<lancedb::database::JobInfo> for JobInfo {
/// The server's account of why a job failed.
#[pyclass(module = "lancedb._lancedb", get_all, skip_from_py_object)]
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct JobFailureInfo {
phase: Option<String>,
message: Option<String>,
@@ -238,6 +238,18 @@ pub struct JobFailureInfo {
sql_job_id: Option<String>,
}
impl From<lancedb::error::JobFailure> for JobFailureInfo {
fn from(failure: lancedb::error::JobFailure) -> Self {
Self {
phase: failure.phase,
message: failure.message,
traceback: failure.traceback,
retryable: failure.retryable,
sql_job_id: failure.sql_job_id,
}
}
}
#[pymethods]
impl JobFailureInfo {
fn __repr__(&self) -> String {
@@ -341,13 +353,7 @@ impl From<lancedb::database::JobDescription> for JobDescription {
.filter(|result| !result.is_null())
.map(|result| result.to_string()),
_progress_json: Some(progress_json),
failure: description.failure.map(|failure| JobFailureInfo {
phase: failure.phase,
message: failure.message,
traceback: failure.traceback,
retryable: failure.retryable,
sql_job_id: failure.sql_job_id,
}),
failure: description.failure.map(Into::into),
}
}
}
+5
View File
@@ -15,6 +15,7 @@ use crate::{
error::PythonErrorExt,
expr::PyExpr,
index::{CodeAnalyzerOptions, IndexConfig, extract_index_params},
job::JobFailureInfo,
query::{Query, TakeQuery},
table::scannable::PyScannable,
};
@@ -925,6 +926,7 @@ pub struct FunctionColumnLastRefreshJob {
pub state: String,
pub automatic: Option<bool>,
pub completed_at_millis: Option<i64>,
pub failure: Option<JobFailureInfo>,
}
impl From<lancedb::table::FunctionColumnLastRefreshJob> for FunctionColumnLastRefreshJob {
@@ -934,6 +936,7 @@ impl From<lancedb::table::FunctionColumnLastRefreshJob> for FunctionColumnLastRe
state: job.state,
automatic: job.automatic,
completed_at_millis: job.completed_at_millis,
failure: job.failure.map(Into::into),
}
}
}
@@ -985,6 +988,7 @@ pub struct FunctionColumnStatus {
pub never_computed: u64,
pub failed: u64,
pub last_refresh_job: Option<FunctionColumnLastRefreshJob>,
pub last_failure_job: Option<FunctionColumnLastRefreshJob>,
pub auto_refresh: FunctionColumnAutoRefreshStatus,
}
@@ -1017,6 +1021,7 @@ impl From<lancedb::table::FunctionColumnStatus> for FunctionColumnStatus {
never_computed: status.never_computed,
failed: status.failed,
last_refresh_job: status.last_refresh_job.map(Into::into),
last_failure_job: status.last_failure_job.map(Into::into),
auto_refresh: status.auto_refresh.into(),
}
}