Skip to content

feat(commit): let registered providers declare their commit handler - #8774

Open
farazshaikh wants to merge 5 commits into
lance-format:mainfrom
farazshaikh:fs/provider-declared-commit-handler
Open

farazshaikh wants to merge 5 commits into
lance-format:mainfrom
farazshaikh:fs/provider-declared-commit-handler

Conversation

@farazshaikh

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #8522, stacked on #8773. Lets a registered ObjectStoreProvider declare which commit handler its store should use, instead of the handler being inferred purely from the URL scheme string.

Changes

  • CommitHandlerType { ConditionalPut, Unsafe } (new, in lance-io) — a small enum a provider returns to declare the concurrency guarantee its store actually offers. ConditionalPut is the Default.
  • ObjectStoreProvider::commit_handler() (new trait method, default ConditionalPut) — a provider whose store cannot offer atomic create-if-not-exists overrides this to return Unsafe.
  • commit_handler_from_url now takes an optional &ObjectStoreRegistry. For a scheme the built-in resolver does not recognize, it consults the registered provider's commit_handler() rather than guessing; with no registered provider it keeps the conflict-safe put-if-not-exists default.
  • The write path threads the session's registry through resolve_commit_handler.

Why

#8773 made the default for unrecognized schemes conflict-safe. This adds the escape hatch Weston asked for: a provider that genuinely can't do put-if-not-exists can opt into UnsafeCommitHandler explicitly, so selection is driven by the provider's real capability rather than a hardcoded scheme list. Per the review discussion, providers return a small fixed set (ConditionalPut / Unsafe); users are not expected to supply arbitrary commit handlers.

Test

test_registered_provider_declares_commit_handler: a provider declaring Unsafe routes to UnsafeCommitHandler, and an unregistered custom scheme still defaults to ConditionalPutCommitHandler.

Verified locally: cargo test -p lance-table, cargo clippy -p lance-io -p lance-table -p lance -p lance-namespace-impls -- -D warnings clean.

@github-actions github-actions Bot added A-encoding Encoding, IO, file reader/writer A-namespace Namespace impls enhancement New feature or request labels Aug 26, 2026
lance-gatekeeper[bot]

This comment was marked as outdated.

@lance-gatekeeper lance-gatekeeper Bot added the K-changes Latest Gatekeeper recommendation requests changes. label Aug 26, 2026
@farazshaikh

Copy link
Copy Markdown
Contributor Author

Reworked so the commit-handler capability is carried on the constructed store rather than re-queried from the registry.

  1. ObjectStore now holds a commit_handler_type: CommitHandlerType, stamped in ObjectStoreRegistry::build_store from provider.commit_handler(). Every store the registry builds or caches carries the capability its provider declared.

  2. Commit-handler resolution derives the default from that captured value instead of a registry lookup. Registering an Unsafe-declaring provider for a known scheme such as memory is now honored, because the choice follows the store, not the scheme name. This also closes the mutable-registry race, since a cached store keeps the capability it was built with. Covered by test_provider_commit_handler_travels_with_store (known-scheme override plus cached store) and test_commit_handler_from_url_honors_store_capability.

  3. The builder and manifest-namespace paths now pass the capability of the store they construct instead of no registry, so their defaults consult the provider too.

Explicit caller-supplied handlers keep precedence, and file/cos/s3+ddb keep their dedicated scheme handlers, since they need scheme-specific parameters and do not reduce to the two-variant enum.

I do not have permission to add the breaking-change label on this repo. Could a maintainer apply it?

@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Aug 26, 2026

@lance-gatekeeper lance-gatekeeper Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ Gate recommendation: request changes.

The store-coupling revision fixes the general registry and cache paths, but handler selection still crosses the authority boundary in two cases: dedicated-scheme overrides bypass the provider declaration, and manifest rewrites derive capability from a different store than the one they commit through. Resolve handlers from the exact committing store in both paths.

Please mark this PR with the breaking-change label.

// scheme such as `memory`, can declare `Unsafe`, and that declaration
// travels with the store it built. Explicit caller-supplied handlers
// are honored before this function is called and never reach here.
_ => match capability {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The wildcard-only dispatch still ignores the constructed store’s declaration for registered overrides of file/file-object-store and cos. ObjectStoreRegistry::insert intentionally permits overriding an existing scheme, so an Unsafe provider under file:// is paired with ConditionalPutCommitHandler on Unix (RenameCommitHandler on Windows). Valid writes can fail, or a backend that cannot enforce create-if-absent can retain the manifest-overwrite risk this capability is meant to declare.

Keep dedicated built-in behavior in the provider/store capability (or an equivalent per-store handler descriptor) and dispatch from the exact store, not the URI string. This is the remaining projection of the earlier authority finding.

Reproducer

At 9e7ae4997, I added and ran:

#[tokio::test]
async fn registered_file_provider_honors_store_capability() {
    let handler = commit_handler_from_url(
        "file:///tmp/ds",
        &None,
        CommitHandlerType::Unsafe,
    )
    .await
    .unwrap();
    assert_eq!(format!("{:?}", handler), "UnsafeCommitHandler");
}

CARGO_TARGET_DIR=/home/agent/tmp/gate8774-target cargo test -p lance-table registered_file_provider_honors_store_capability --lib failed with left ConditionalPutCommitHandler, right UnsafeCommitHandler.

/// Resolve the commit handler for the `__manifest` dataset's storage backend.
async fn manifest_commit_handler(&self) -> Result<Arc<dyn CommitHandler>> {
commit_handler_from_url(&self.root, &None)
commit_handler_from_url(&self.root, &None, self.object_store.commit_handler_type())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This derives the handler from self.object_store, but commit_manifest_overwrite deliberately obtains dataset.object_store(None) and commits through that different store. The public constructors accept the namespace store and session independently, so capability A can select a handler that operates on store B. In the reproduced state, the committing store declares ConditionalPut while this method returns UnsafeCommitHandler; concurrent manifest rewrites can therefore blind-overwrite.

Resolve the handler from the dataset’s exact object store after obtaining it, or carry that store and handler together through the commit.

Reproducer

I used an UnsafeMemoryProvider that delegates store construction and prefix calculation to MemoryStoreProvider but returns CommitHandlerType::Unsafe, then ran this test at 9e7ae4997:

#[tokio::test]
async fn manifest_handler_follows_store_used_for_commit() {
    let root = "memory://gate-repro/namespace";
    let namespace_registry = Arc::new(ObjectStoreRegistry::default());
    namespace_registry.insert("memory", Arc::new(UnsafeMemoryProvider));
    let (object_store, base_path) = ObjectStore::from_uri_and_params(
        namespace_registry,
        root,
        &ObjectStoreParams::default(),
    )
    .await
    .unwrap();

    let dataset_registry = Arc::new(ObjectStoreRegistry::default());
    let session = Arc::new(Session::new(0, 0, dataset_registry));
    let namespace = ManifestNamespace::from_directory(
        root.to_string(), None, Some(session), object_store, base_path,
        true, false, None,
    )
    .await
    .unwrap();

    let dataset = namespace.manifest_dataset.get().await.unwrap();
    let committing_store = dataset.object_store(None).await.unwrap();
    assert_eq!(
        committing_store.commit_handler_type(),
        CommitHandlerType::ConditionalPut,
    );
    let handler = namespace.manifest_commit_handler().await.unwrap();
    assert_eq!(format!("{:?}", handler), "ConditionalPutCommitHandler");
}

CARGO_TARGET_DIR=/home/agent/tmp/gate8774-target cargo test -p lance-namespace-impls manifest_handler_follows_store_used_for_commit --lib failed with left UnsafeCommitHandler, right ConditionalPutCommitHandler.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed in 27e14268e: the handler is now derived inside the retry loop from the same dataset object store used for staging and commit.

@lance-gatekeeper lance-gatekeeper Bot added the K-changes Latest Gatekeeper recommendation requests changes. label Aug 26, 2026
Faraz Shaikh added 5 commits August 27, 2026 12:25
commit_handler_from_url fell through to UnsafeCommitHandler for any scheme
outside the hardcoded allowlist. Concurrent writers to such a scheme could all
target the same manifest version and blindly overwrite it, reporting success
while silently losing updates.

Default the fallback to ConditionalPutCommitHandler instead. Most object stores
(including out-of-tree providers registered at runtime via the ObjectStore
registry) back onto storage that supports atomic create-if-not-exists, so
concurrent commits now fail loudly with a conflict rather than clobbering each
other. A store that genuinely lacks conditional-put support can still opt back
into UnsafeCommitHandler via a caller-supplied commit handler.

Follow-up to lance-format#8522 (per review discussion). A subsequent change will let a
provider declare its commit handler explicitly.
Adds CommitHandlerType { ConditionalPut, Unsafe } and an
ObjectStoreProvider::commit_handler() method (default ConditionalPut) in
lance-io. commit_handler_from_url now takes an optional ObjectStoreRegistry and,
for a scheme the built-in resolver does not recognize, consults the registered
provider's declared handler instead of guessing from the scheme string. The
write path threads the session's registry through resolve_commit_handler.

This lets an out-of-tree provider whose store cannot offer atomic
create-if-not-exists opt into UnsafeCommitHandler explicitly, while everything
else keeps the conflict-safe put-if-not-exists default from the prior commit.

Builds on the default change in the parent commit; test added covering a
provider that declares Unsafe and an unregistered scheme falling back to the
default.
…tore

Carry the provider-declared CommitHandlerType on the ObjectStore the
registry builds, stamped in build_store from provider.commit_handler().
Commit-handler resolution now derives every default from that captured
value instead of re-querying the registry, so:

- overriding a known scheme (registering, for example, an Unsafe
  declaring provider for `memory`) is honored, since the capability
  travels with the store rather than being inferred from the scheme name
- the builder and manifest-namespace paths consult the store they
  construct instead of passing no registry
- a cached store keeps the capability it was built with, closing the
  mutable-registry race

Explicit caller-supplied handlers still take precedence, and the
dedicated file/cos/s3+ddb schemes keep their scheme-specific handlers.
Add two guards so making the commit handler provider-driven cannot
silently change the handler existing stores resolve to:

- lance-io: assert every provider the default registry ships declares
  ConditionalPut, the capability those schemes resolved to before.
- lance-table: build a memory store from the default registry and
  resolve through its captured capability end to end, asserting it still
  yields ConditionalPutCommitHandler.
Manifest rewrites commit through the dataset's own object store, which for
stores like memory:// can be a different instance than the namespace's
self.object_store. Resolve the commit handler from that same committing
store so its captured capability, including a registered override, is the
one that governs the commit.
@farazshaikh
farazshaikh force-pushed the fs/provider-declared-commit-handler branch from 9e7ae49 to 27e1426 Compare August 27, 2026 19:41
@farazshaikh

Copy link
Copy Markdown
Contributor Author

Rebased onto v12.0.0-beta.4 (the branch was conflicting) and addressed the two points.

  1. Manifest rewrites: fixed. manifest_commit_handler now resolves from the dataset's own object store, the exact store the rewrite commits through, instead of self.object_store. So a registered override for that scheme governs the commit. The store is fetched inside the retry loop, and the handler is now derived from that same instance.

  2. Dedicated-scheme handling (file, cos, s3+ddb): kept as scheme arms deliberately. Their handlers (rename, TencentCosCommitHandler, and the s3+ddb external-manifest handler built from the ddbTableName query) cannot be represented by the two-variant CommitHandlerType, and lance-io cannot reference lance-table's handler types since it is the lower layer. Per the earlier maintainer guidance the capability enum is intentionally limited to the two general object-store choices; honoring an override of these built-in schemes needs the deferred store-owned handler-selection refactor, and Tachyon (the driver here) is read-only. I would rather not expand the enum or invert the crate dependency in this PR, but I am happy to take whichever direction a maintainer prefers.

I still do not have permission to add the breaking-change label. Could a maintainer apply it?

@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Aug 27, 2026

@lance-gatekeeper lance-gatekeeper Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ Gate recommendation: request changes.

1 fixed / 1 remains. The author follow-up correctly moves manifest-handler selection to the committing store. Dedicated-scheme overrides still bypass the provider declaration, however, despite the registry’s documented built-in override contract. That can pair a replacement store with commit semantics it cannot support.

A viable revision can distinguish specialized built-in providers from replacements in the store-stamped strategy, or reject those overrides before writes; neither requires reversing the crate dependency.

Please mark this PR with the breaking-change label.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-encoding Encoding, IO, file reader/writer A-namespace Namespace impls enhancement New feature or request K-changes Latest Gatekeeper recommendation requests changes.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant