mirror of
https://github.com/lancedb/lancedb.git
synced 2026-11-13 14:16:54 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user