Skip to content

feat: add index build progress callbacks to segment builders - #86

Merged
jja725 merged 2 commits into
lance-format:mainfrom
u70b3:feat/distributed-index-build-pr3
Sep 21, 2026
Merged

jja725 merged 2 commits into
lance-format:mainfrom
u70b3:feat/distributed-index-build-pr3

Conversation

@u70b3

@u70b3 u70b3 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

E3 of the RFC #55 distributed-build track: an FFI bridge over lance core's IndexBuildProgress trait for the uncommitted segment builders. Build stages (train_ivf, train_quantizer, shuffle, merge_partitions, ...) and per-stage progress are now observable from C/C++.

API

typedef enum {
    LANCE_INDEX_BUILD_PROGRESS_STAGE_START = 0,
    LANCE_INDEX_BUILD_PROGRESS_STAGE_PROGRESS = 1,
    LANCE_INDEX_BUILD_PROGRESS_STAGE_COMPLETE = 2,
} LanceIndexBuildProgressEvent;

typedef void (*LanceIndexBuildProgressCallback)(
    void* callback_ctx, int32_t event, const char* stage,
    uint64_t total, const char* unit, uint64_t completed);

int32_t lance_index_segment_builder_set_progress_callback(
    LanceIndexSegmentBuilder* builder,
    LanceIndexBuildProgressCallback callback,
    void* callback_ctx);
  • total/unit are meaningful only for STAGE_START (0/"" otherwise); completed only for STAGE_PROGRESS. stage is always non-NULL, borrowed, valid for the call duration. Stage names are diagnostic-only, not a stable cross-version contract.
  • callback_ctx may be NULL; setting a callback replaces any previously set callback; it must be set before execute (the builder is single-use), otherwise the setter returns -1.

Contract (documented on the typedef and setter)

  • Invoked from lance-c's internal tokio runtime worker threads; certain stages report concurrently from parallel worker tasks (e.g. tokenized docs), so the callback must be thread-safe and reentrant, non-blocking, and must not call back into any lance_* function.
  • Invocations occur only while execute_uncommitted runs, and this is enforced rather than contractual: a shared retirement gate (ProgressCallbackGate) disables new callback entries and drains in-flight invocations before execute_uncommitted returns — on success, error, and panic paths via a drop guard — so a core worker task detached on an error path (e.g. the inverted index's tokenize_docs workers, whose JoinHandles can be dropped early) degrades to a no-op instead of touching freed context. callback/callback_ctx must remain valid until execute_uncommitted returns.
  • Invoked without a panic guard (house position for user callbacks): it must return normally; unwinding/throwing across this boundary can abort the host process.
  • Advisory only: the callback cannot abort the build (cancellation remains the separate E4 track, blocked on a core hook).

Deviations from the RFC sketch

  • Naming: RFC §7 sketched lance_index_segment_builder_set_progress / user_data; this PR uses set_progress_callback / callback_ctx to match the newer in-tree lance_scanner_set_statistics_callback convention. No semantic change.
  • NULL callback: RFC §12's test plan sketched "NULL callback = no-op"; the setter instead rejects NULL with -1, matching the lance_scanner_set_statistics_callback boundary convention (reject invalid values at the API boundary). Consequence: a set callback can be replaced but not unset — install a no-op callback to disable reporting.

Implementation notes

  • The retirement gate was added in response to review: core's inverted builder clones the progress handle into spawned tokenize_docs workers whose JoinHandles can be dropped early on error paths, which could otherwise invoke the raw C context after the build returned. The gate (SeqCst disable + drain, Arc-shared across every clone core hands to worker tasks) closes that window and makes the documented boundary enforceable.
  • The bridge always returns Ok(()); the only error path is a defensive interior-NUL invariant violation in stage/unit strings (never panics).
  • Direct dependency async-trait added for the #[async_trait] impl of lance_index::progress::IndexBuildProgress (already in the dependency graph transitively via lance-index; Cargo.lock gains only the lance-c edge).

Tests

Gate-level unit tests (src/index_segment.rs):

  • detached_clone_cannot_enter_callback_after_retire — the review reproducer, inverted: a task-owned clone calling after gate retirement is a no-op and never touches the context.
  • retire_drains_in_flight_invocation — retirement disables new entries, then blocks until an in-flight invocation exits.

Rust (tests/c_api_test.rs):

  • test_vector_index_segment_progress_callback — IVF_PQ 256x16 through the segment builder; event codes, per-stage START/COMPLETE pairing, numeric mapping invariants (PROGRESS ⇒ total==0, START ⇒ completed==0), shuffle and merge_partitions observed, shuffle PROGRESS completed <= total with unit == "rows", ctx round-trip.
  • test_scalar_index_segment_progress_callback_sees_load_data — BTree build sees load_data START/COMPLETE.
  • test_vector_index_segment_progress_callback_multi_fragment_subset — 2-fragment dataset, fragment-scoped build with callback active.
  • test_index_segment_builder_progress_callback_edge_cases — NULL builder / NULL callback / NULL callback_ctx (arrives verbatim) / sentinel ctx round-trip / set after execute / set twice (only the replacement receives events).
  • test_index_segment_builder_progress_callback_success_clears_error.

C (tests/cpp/test_c_api.c) and C++ (tests/cpp/test_cpp_api.cpp): test_index_segment_builder_progress each — real compile-and-run coverage, including the fluent progress_callback setter on the C++ side.

Full suite green: cargo fmt, cargo check --all-targets, cargo clippy --all-targets -- -D warnings, cargo test (358 c_api tests incl. the 5 new), cargo test --test compile_and_run_test -- --ignored (C + C++ + static OSS transport).

Refs #55.

@u70b3
u70b3 marked this pull request as ready for review September 20, 2026 01:21
Bridge lance core's IndexBuildProgress trait to a C callback on
LanceIndexSegmentBuilder (E3 of the distributed-build track):

- lance_index_segment_builder_set_progress_callback with START/PROGRESS/
  COMPLETE events; total/unit meaningful on START, completed on PROGRESS.
- Thread-safe/reentrant, non-blocking, no-reentrancy contract; callback and
  context must outlive the builder (conservative: spawned worker tasks and
  error paths may still deliver events).
- Advisory only: the callback cannot abort the build.
- C++ fluent IndexSegmentBuilder::progress_callback wrapper.
- Direct async-trait dependency for the trait impl (already transitive via
  lance-index).

Refs lance-format#55.
@u70b3
u70b3 force-pushed the feat/distributed-index-build-pr3 branch from 253d55f to 44e3a5d Compare September 20, 2026 01:21
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-changes Latest Gatekeeper recommendation requests changes. label Sep 20, 2026
Review on lance-format#86 found the callback lifetime contract was unenforceable on
error paths: lance core's inverted builder clones the progress handle
into spawned tokenize_docs workers whose JoinHandles can be dropped
early, so a detached clone could invoke the raw C callback/context after
execute_uncommitted returned -- a use-after-free once the caller retired
the context.

Add a shared ProgressCallbackGate (Arc-shared across every clone core
hands to worker tasks): execute_uncommitted retires it through a drop
guard on the success, error, and panic exit paths, disabling new
callback entries and draining in-flight invocations before returning;
late detached calls become no-ops. The "invocations only occur while
executing" contract is now enforced rather than advisory, and the
callback/context only need to stay valid until execute_uncommitted
returns (C and C++ docs updated).

Also document that stage names are diagnostic-only and not a stable
cross-version contract.

Regression tests at the gate level: a detached task-owned clone calling
after retirement is a no-op (the review reproducer, inverted), and
retire blocks until an in-flight invocation exits.

@lance-gatekeeper lance-gatekeeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gate recommendation: approve.

The callback-lifetime blocker is fixed in 4a5f53b: a gate shared by all progress clones disables late entries and drains in-flight callbacks before execute_uncommitted returns on success, error, or panic. The C and C++ lifetime contract now matches that enforced boundary.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 20, 2026
@jja725
jja725 merged commit 426a858 into lance-format:main Sep 21, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

K-approved Latest Gatekeeper recommendation permits acceptance.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants