feat(jni,io): default-open Session reuse + ObjectStoreRegistry single-flight#6839
Open
LuciferYang wants to merge 3 commits into
Open
feat(jni,io): default-open Session reuse + ObjectStoreRegistry single-flight#6839LuciferYang wants to merge 3 commits into
LuciferYang wants to merge 3 commits into
Conversation
b85fba9 to
d26a288
Compare
…-flight
Coalesce concurrent cold `Dataset.open` calls in the JNI layer so 144
parallel opens against the same URI reuse one `ObjectStore` instead of
each rebuilding the credential chain and HTTP client.
# JNI default-open path (lance-jni)
`BlockingDataset::open` without an explicit `Session` now constructs a
fresh per-call `Session` whose `ObjectStoreRegistry` is the
process-wide `GLOBAL_OBJECT_STORE_REGISTRY` (a `LazyLock<Arc<...>>`).
Sharing only the registry — not the `Session` — keeps the asymmetric
caching shape: the registry coalesces expensive `ObjectStore` builds
(credential probe, IMDS, TLS handshake), while metadata/index caches
remain per-call so callers do not share eviction policy or cache size
with other tenants.
Bare-URI opens (empty storage options, no provider, no namespace
commit handler) collapse onto a single cache entry per URI: the first
caller's resolved default-credential chain becomes the credentials
used by every subsequent caller for the lifetime of that
`Arc<ObjectStore>`. This cross-tenant credential bleed is intentional
under the bare-URI invariant; callers that need per-tenant isolation
can opt out via `LANCE_JNI_DISABLE_DEFAULT_REGISTRY_SHARING={1,true,yes}`,
pass tenant-distinguishing storage options, or supply an explicit
`Session`.
The opt-out env var is parsed once into an `AtomicU8` tri-state
(`UNINIT/ENABLED/DISABLED`) on first read, with a truthy whitelist
(`1|true|yes` — anything else falls back to enabled and warns). A
`DisableSharingTestGuard` RAII helper resets the atomic for unit
tests so the env-var probe can be exercised deterministically.
# Registry single-flight (lance-io)
`ObjectStoreRegistry::get_store` adds a per-key build-lock so concurrent
cold builds for the same `CacheKey` serialize behind one in-flight
`build()` instead of all racing the credential chain. The lock map is
`Mutex<HashMap<CacheKey, Arc<tokio::sync::Mutex<()>>>>`; each caller
acquires its slot via a `BuildLockSession` RAII guard whose `Drop`
releases the inner mutex first, then opportunistically removes the
HashMap entry if it observes `strong_count == 1`. This ordering
guarantees that panics, errors, and cancellations all reclaim the
build-lock entry without leaking a permanently-locked key.
Failures are not cached — when a cold build returns `Err`, the next
waiter retries from scratch (one-Err-per-N-waiters retry contract).
An opportunistic sweep gated by `cold_attempt_nth.is_multiple_of(64)`
amortizes HashMap shrinkage across cold-path attempts so a long-lived
process does not accumulate stale entries from short-lived URIs.
The `CacheKey` uses `sanitized_authority` derived from `Url::host()`
(IPv6 brackets preserved, port preserved, userinfo stripped) so URIs
that differ only in embedded credentials still collapse onto the same
cache slot.
# Tests
- `lance-io`: hit/miss, single-flight coalescing, panic-path RAII
cleanup, error-path RAII cleanup, `sanitized_authority` over IPv6
/ userinfo / non-default port, opportunistic sweep counter.
- `lance-jni`: `parse_disable_value` truthy whitelist + warn path,
`DisableSharingTestGuard` ordering, end-to-end coalescing test
that drives 144 concurrent default-opens through the JNI layer
and asserts a single `ObjectStore` is built.
# Configuration
`java/README.md` documents the `LANCE_JNI_DISABLE_DEFAULT_REGISTRY_SHARING`
env var, the bare-URI credential-bleed invariant, and the three
opt-out paths (env var, tenant-distinguishing storage options,
explicit `Session`).
d26a288 to
9e5b879
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #6838
Coalesces concurrent cold
Dataset::openagainst the same URI so N parallel opens reuse oneObjectStoreinstead of each rebuilding the credential chain and HTTP client.Two changes, both required:
lance-io— registry single-flight.ObjectStoreRegistry::get_storegains a per-CacheKeytokio::sync::Mutexso concurrent cold builds serialize behind one in-flightbuild(). The lock map entry is freed via aBuildLockSessionRAII guard that drops the inner mutex first, then removes the HashMap entry if it observesstrong_count == 1, making panics / errors / cancellations all reclaim the slot. Failures are not cached (one-Err-per-N-waiters retry); an opportunistic sweep gated oncold_attempt_nth % 64 == 0amortizes HashMap shrinkage.lance-jni— default-open registry sharing.BlockingDataset::openwithout an explicitSessionnow uses a process-wideLazyLock<Arc<ObjectStoreRegistry>>(theSessionitself is still per-call so metadata/index caches stay isolated). This is what makes thelance-iofix actually coalesce across JNI default opens — without it each open had its own registry and had nothing to coalesce against.Bare-URI opens (empty storage options, no provider, no namespace handler) collapse onto one cache entry per URI: the first caller's resolved default-credential chain becomes the credentials used by every subsequent caller for the lifetime of that
Arc<ObjectStore>. This is documented injava/README.mdalong with three opt-outs:LANCE_JNI_DISABLE_DEFAULT_REGISTRY_SHARING={1,true,yes}SessionBenchmark
144 concurrent opens against the same URI, semaphore-gated to 12, S3 → MinIO :9000,
BENCH_SHARED_SESSION=1, run via the standalone reproducer in the Tests section below:ad9f38227no-sharedmode (each open creates its ownSession) is unchanged between branches, as expected.Tests
lance-io: hit/miss, single-flight coalescing, panic + error RAII cleanup,sanitized_authority(IPv6 / userinfo / non-default port), opportunistic sweep counter.lance-jni: env-var truthy whitelist + warn path,DisableSharingTestGuardordering, end-to-end test driving 144 concurrent default-opens through the JNI layer and asserting a singleObjectStorebuild.rust/lance/examples/concurrent_open_bench.rs:concurrent_open_bench.rs