Skip to content

fix(index): avoid caching store-bound scalar indices#7962

Open
u70b3 wants to merge 3 commits into
lance-format:mainfrom
u70b3:fix-session-cache-store-lifecycle
Open

fix(index): avoid caching store-bound scalar indices#7962
u70b3 wants to merge 3 commits into
lance-format:mainfrom
u70b3:fix-session-cache-store-lifecycle

Conversation

@u70b3

@u70b3 u70b3 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • let IndexStore implementations declare whether live indices holding their readers are safe to share through a Session cache
  • bypass the default live ScalarIndex cache for LanceIndexStore, so FTS and other default scalar plugins always bind readers to the current object store
  • preserve portable-state caching for btree, bitmap, and label-list plugins and keep decoded posting/page data reusable
  • add a multi-fragment A -> B -> A credential-rotation regression covering I/O ownership, results, cache inventory, and credential-key leakage

Lifecycle

Depends on #7944. Together, the two PRs remove the known store-bound vector and scalar entries from Session caches. This resolves stale-generation retention deterministically without adding TTL/TTI or process-local object-store identities.

Testing

  • cargo fmt --all -- --check
  • cargo check -p lance-index-core -p lance-index -p lance --lib
  • cargo test -p lance --lib test_scalar_cache_uses_current_object_store -- --nocapture
  • cargo test -p lance-index test_btree_index_state_reconstruct_and_plugin_cache
  • cargo test -p lance-index test_bitmap_cache_fast_path
  • cargo clippy --all --tests --benches -- -D warnings

The current #7944 base contains an unrelated blob merge-insert test compile error (Option<BlobFile>::read). The scalar regression and full clippy were run with a local-only six-line unwrap fix for that base error; that workaround is not included here.

Closes #7958
Closes #7959

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Index caching now avoids retaining store-bound scalar and vector objects across object-store generations. Portable IVF state remains cacheable, while readers are reopened against the current store. Regression tests verify cache isolation, current-store IO, and secret-free cache keys.

Changes

Index cache isolation

Layer / File(s) Summary
Scalar cache eligibility
rust/lance-index-core/src/scalar.rs, rust/lance-index/src/scalar/..., rust/lance/src/index.rs
IndexStore reports whether bound indices can be cached; LanceIndexStore disables this path, and scalar cache behavior plus object-store isolation are tested.
Portable IVF state and vector opening
rust/lance/src/index.rs
Legacy live vector caching is removed. IVF state remains codec-backed, while vector construction, bandwidth configuration, cache namespacing, and insertion use the selected object store.
Fresh readers and cache isolation validation
rust/lance/src/index/vector/ivf/v2.rs
IVF reconstruction reopens readers using cached metadata, adds a partition-suppressing test backend, and verifies portable state reuse, current-store reads, and secret-free cache keys.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Dataset
  participant IndexCache
  participant ObjectStore
  participant VectorIndex
  Dataset->>IndexCache: open index
  IndexCache-->>Dataset: portable IVF state or cache miss
  Dataset->>ObjectStore: open readers for current store
  ObjectStore-->>VectorIndex: index data
  VectorIndex-->>Dataset: reconstructed index
  Dataset->>IndexCache: store portable IVF state
Loading

Possibly related PRs

Suggested reviewers: xuanwo, jackye1995, westonpace, wjones127, gstamatakis95

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning [#7958] The PR removes some store-bound cache paths, but it does not add TTL/TTI or generation-aware invalidation for stale entries. Add TTL/TTI or a generation-aware invalidation path for store-bound Session cache entries while keeping store-free entries shared.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Clear and specific: it summarizes the main change, preventing caching of store-bound scalar indices.
Description check ✅ Passed The description matches the code changes and objectives around scalar cache isolation and regression coverage.
Linked Issues check ✅ Passed [#7959] Scalar/FTS caches now opt out for store-bound stores, and the regression test covers credential rotation.
Out of Scope Changes check ✅ Passed The vector cache and test changes align with the linked cache-isolation work and don't introduce unrelated behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@u70b3
u70b3 force-pushed the fix-session-cache-store-lifecycle branch from f1a2cca to f6fdf31 Compare July 24, 2026 05:05

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
rust/lance/src/index/vector/ivf/v2.rs (1)

6570-6585: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add #[cfg_attr(coverage, coverage(off))] to this test utility.

NoPartitionCacheBackend and its CacheBackend impl are test-only scaffolding and should be excluded from coverage instrumentation.

♻️ Suggested annotations
+#[cfg_attr(coverage, coverage(off))]
 #[derive(Debug)]
 struct NoPartitionCacheBackend(lance_core::cache::MokaCacheBackend);
 #[async_trait::async_trait]
+#[cfg_attr(coverage, coverage(off))]
 impl lance_core::cache::CacheBackend for NoPartitionCacheBackend {

As per coding guidelines: "disable coverage for test utilities with #[cfg_attr(coverage, coverage(off))]".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/index/vector/ivf/v2.rs` around lines 6570 - 6585, Add
#[cfg_attr(coverage, coverage(off))] to the NoPartitionCacheBackend test utility
and its CacheBackend implementation, excluding both scaffolding components from
coverage instrumentation.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance-index-core/src/scalar.rs`:
- Around line 249-257: Expand the documentation for
can_cache_store_bound_indices with a compiling usage example that demonstrates
both true and false implementations, and link to the relevant cache and
index-opening APIs using their actual symbols and signatures. Keep the example
synchronized with the public contract and clarify when implementers should
override the default.

In `@rust/lance/src/index.rs`:
- Around line 2942-2975: Replace the manually constructed test fixtures in the
batch setup around RecordBatchIterator with arrow_array’s record_batch!() macro.
Remove the redundant Schema, Arc, and RecordBatch::try_new boilerplate while
preserving the existing id and text values and resulting RecordBatchIterator
behavior.

---

Nitpick comments:
In `@rust/lance/src/index/vector/ivf/v2.rs`:
- Around line 6570-6585: Add #[cfg_attr(coverage, coverage(off))] to the
NoPartitionCacheBackend test utility and its CacheBackend implementation,
excluding both scaffolding components from coverage instrumentation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 6335ac2e-99de-4a65-815f-d9354fafa37f

📥 Commits

Reviewing files that changed from the base of the PR and between f1a2cca and f6fdf31.

📒 Files selected for processing (5)
  • rust/lance-index-core/src/scalar.rs
  • rust/lance-index/src/scalar/lance_format.rs
  • rust/lance-index/src/scalar/registry.rs
  • rust/lance/src/index.rs
  • rust/lance/src/index/vector/ivf/v2.rs

Comment on lines +249 to +257
/// Whether a live index holding this store can be shared through a session cache.
///
/// Implementations whose readers capture connection state or renewable
/// credentials should return `false`. Serializable index state can still be
/// cached and reconstructed with the current store.
fn can_cache_store_bound_indices(&self) -> bool {
true
}

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add a usage example for this public cache-safety contract.

Document both return values with a compiling example and links to the relevant cache/opening API so implementers can safely override this method.

As per coding guidelines, “Document all public APIs with examples and links to relevant structs and methods; keep examples synchronized with actual signatures.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-index-core/src/scalar.rs` around lines 249 - 257, Expand the
documentation for can_cache_store_bound_indices with a compiling usage example
that demonstrates both true and false implementations, and link to the relevant
cache and index-opening APIs using their actual symbols and signatures. Keep the
example synchronized with the public contract and clarify when implementers
should override the default.

Source: Coding guidelines

Comment thread rust/lance/src/index.rs
Comment on lines +2942 to +2975
let test_dir = TempStrDir::default();
let schema = Arc::new(Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("text", DataType::Utf8, false),
]));
let batches = vec![
RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int32Array::from(vec![0, 1, 2, 3])),
Arc::new(StringArray::from(vec![
"alpha common",
"beta common",
"gamma common",
"alpha common",
])),
],
)
.unwrap(),
RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int32Array::from(vec![4, 5, 6, 7])),
Arc::new(StringArray::from(vec![
"beta common",
"gamma common",
"alpha common",
"beta common",
])),
],
)
.unwrap(),
];
let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), schema);

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use record_batch!() for the test fixtures.

This manually repeats Schema, Arc, and RecordBatch::try_new boilerplate. Build these batches with record_batch!() instead.

As per coding guidelines, “Use record_batch!() from arrow_array to construct RecordBatch in tests instead of manual Schema/Arc/try_new boilerplate.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/index.rs` around lines 2942 - 2975, Replace the manually
constructed test fixtures in the batch setup around RecordBatchIterator with
arrow_array’s record_batch!() macro. Remove the redundant Schema, Arc, and
RecordBatch::try_new boilerplate while preserving the existing id and text
values and resulting RecordBatchIterator behavior.

Source: Coding guidelines

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/lance/src/index.rs (1)

2400-2557: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

IVF_RQ opens against the wrong directory.

Every other IVF variant branch (IVF_FLAT, IVF_PQ, IVF_SQ, IVF_HNSW_FLAT, IVF_HNSW_SQ, IVF_HNSW_PQ) passes the locally-resolved index_dir (from self.indice_files_dir(&index_meta), which honors index_meta.base_id for e.g. shallow-cloned datasets). IVF_RQ instead passes self.indices_dir(), ignoring any base redirection. For an IVF_RQ index whose metadata points at a different base (cloned dataset), this will resolve to the wrong location and fail to open.

🐛 Proposed fix
                     "IVF_RQ" => {
                         let ivf = IVFIndex::<FlatIndex, RabitQuantizer>::try_new(
                             object_store.clone(),
-                            self.indices_dir(),
+                            index_dir,
                             uuid.to_owned(),
                             frag_reuse_index,
                             self.metadata_cache.as_ref(),
                             index_cache,
                             file_sizes,
                         )
                         .await?;
                         Ok(wrap_ivf(ivf))
                     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/index.rs` around lines 2400 - 2557, Update the IVF_RQ branch
in the index-opening match to pass the locally resolved index_dir to
IVFIndex::try_new, matching every other IVF variant. Do not use
self.indices_dir(), so index metadata base redirection remains honored.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rust/lance/src/index.rs`:
- Around line 2400-2557: Update the IVF_RQ branch in the index-opening match to
pass the locally resolved index_dir to IVFIndex::try_new, matching every other
IVF variant. Do not use self.indices_dir(), so index metadata base redirection
remains honored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 5c7e1498-9d16-4f7a-9f13-3d6df6327c64

📥 Commits

Reviewing files that changed from the base of the PR and between f6fdf31 and 9404ed7.

📒 Files selected for processing (1)
  • rust/lance/src/index.rs

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

Labels

A-index Vector index, linalg, tokenizer bug Something isn't working

Projects

None yet

1 participant