Repository navigation
feat(commit): let registered providers declare their commit handler - #8774
farazshaikh wants to merge 5 commits into
Conversation
|
Reworked so the commit-handler capability is carried on the constructed store rather than re-queried from the registry.
Explicit caller-supplied handlers keep precedence, and I do not have permission to add the |
There was a problem hiding this comment.
❌ 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 { |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in 27e14268e: the handler is now derived inside the retry loop from the same dataset object store used for staging and commit.
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.
9e7ae49 to
27e1426
Compare
|
Rebased onto v12.0.0-beta.4 (the branch was conflicting) and addressed the two points.
I still do not have permission to add the |
There was a problem hiding this comment.
❌ 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.
Summary
Follow-up to #8522, stacked on #8773. Lets a registered
ObjectStoreProviderdeclare which commit handler its store should use, instead of the handler being inferred purely from the URL scheme string.Changes
CommitHandlerType { ConditionalPut, Unsafe }(new, inlance-io) — a small enum a provider returns to declare the concurrency guarantee its store actually offers.ConditionalPutis theDefault.ObjectStoreProvider::commit_handler()(new trait method, defaultConditionalPut) — a provider whose store cannot offer atomic create-if-not-exists overrides this to returnUnsafe.commit_handler_from_urlnow takes an optional&ObjectStoreRegistry. For a scheme the built-in resolver does not recognize, it consults the registered provider'scommit_handler()rather than guessing; with no registered provider it keeps the conflict-safe put-if-not-exists default.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
UnsafeCommitHandlerexplicitly, 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 declaringUnsaferoutes toUnsafeCommitHandler, and an unregistered custom scheme still defaults toConditionalPutCommitHandler.Verified locally:
cargo test -p lance-table,cargo clippy -p lance-io -p lance-table -p lance -p lance-namespace-impls -- -D warningsclean.