Skip to content

fix(index): keep HNSW IVF scratch dir alive during partition writing#6980

Open
geserdugarov wants to merge 2 commits into
lance-format:mainfrom
geserdugarov:fix-hnsw-scratch-dir
Open

fix(index): keep HNSW IVF scratch dir alive during partition writing#6980
geserdugarov wants to merge 2 commits into
lance-format:mainfrom
geserdugarov:fix-hnsw-scratch-dir

Conversation

@geserdugarov

@geserdugarov geserdugarov commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes an HNSW-IVF index build bug analogous to #6957, but in the legacy HNSW partition writer path.

Changes

  • Keeps the TempStdDir guard alive for the full write_hnsw_quantization_index_partitions function.
  • Prevents scratch hnsw_part_* files from disappearing before they are read back into the final index files.
  • No new test added: existing index::vector::ivf::tests::test_create_index_nulls already covers IVF_HNSW_PQ and IVF_HNSW_SQ with IndexFileVersion::Legacy.

Testing

  • cargo test -p lance test_create_index_nulls -- --nocapture
  • Result: 10 passed, including the legacy IVF_HNSW_PQ and IVF_HNSW_SQ cases.

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability when building IVF_HNSW indexes with concurrent per-partition jobs.
    • Ensured temporary scratch directories and intermediate files persist correctly during in-flight work, and are cleaned up consistently after errors.
    • Improved error propagation so index build failures stop background work promptly and report late task issues accurately.
  • Tests
    • Added Tokio-based coverage for scratch-directory lifetime, failure draining/abort behavior, and cleanup under isolated temporary directories.

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions github-actions Bot added the bug Something isn't working label May 28, 2026
@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 59 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance/src/index/vector/ivf/io.rs 80.00% 39 Missing and 20 partials ⚠️

📢 Thoughts on this report? Let us know!

@geserdugarov

Copy link
Copy Markdown
Contributor Author

@Xuanwo, @wjones127, hi!
If you don't mind, could you please review this PR also?
Found that we need fix in legacy HNSW partition writer path.
Test related fix was done due to the flaky CI.

@geserdugarov geserdugarov force-pushed the fix-hnsw-scratch-dir branch from 878bf4c to 72fb619 Compare June 4, 2026 07:40
@geserdugarov

Copy link
Copy Markdown
Contributor Author

@Xuanwo, @wjones127, hi!
Gentle remind about this PR.

Comment thread rust/lance/src/index/vector/ivf/io.rs Outdated
let tmp_part_dir = Path::from_filesystem_path(TempStdDir::default())?;
// Bind the guard to keep the scratch directory alive for the whole function;
// dropping it earlier would delete the partition files before they are read back.
let tmp_part_dir_guard = TempStdDir::default();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This adds function-scoped scratch cleanup while the function can still return on the first partition-task error without draining the remaining spawned tasks. Those detached tasks can continue writing under a directory that is being removed, losing later build or I/O errors.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching it! Need some time to work on this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I updated this to drain outstanding tasks on error before the temp guard can drop: remaining handles are kept as Option<JoinHandle<_>>, aborted, and awaited in drain_partition_tasks. I also made the scratch guard shared with partition tasks and removed the detached PQ storage spawn, so aux writes cannot outlive the partition task.

Comment thread rust/lance/src/index/vector/ivf/io.rs Outdated
// Bind the guard to keep the scratch directory alive for the whole function;
// dropping it earlier would delete the partition files before they are read back.
let tmp_part_dir_guard = TempStdDir::default();
let tmp_part_dir = Path::from_filesystem_path(&tmp_part_dir_guard)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The cited existing test does not reach this legacy HNSW writer; HNSW_PQ/SQ creation goes through the V3 builder. This lifetime fix can regress without that test failing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. I added targeted coverage that calls the legacy build_ivf_hnsw_pq_index path in a child process with isolated TMPDIR, then asserts no scratch .tmp* dirs are leaked, plus small tests for guard/drain behavior.

@wjones127 wjones127 self-assigned this Jul 2, 2026
@geserdugarov geserdugarov force-pushed the fix-hnsw-scratch-dir branch 2 times, most recently from 4bc592a to 4b9191a Compare July 7, 2026 04:34
@geserdugarov geserdugarov requested a review from Xuanwo July 7, 2026 04:46
@geserdugarov geserdugarov force-pushed the fix-hnsw-scratch-dir branch from 4b9191a to f52f075 Compare July 9, 2026 06:39
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 582a9508-4a7a-4013-b844-55840b46824f

📥 Commits

Reviewing files that changed from the base of the PR and between f52f075 and c76e78c.

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

📝 Walkthrough

Walkthrough

Refactors IVF_HNSW partition building in io.rs to keep the scratch directory alive across concurrent tasks, drain unfinished work on failure, join PQ and HNSW builds with try_join!, and add tests for lifetime and cleanup behavior.

Changes

Partition build lifetime and cleanup

Layer / File(s) Summary
Scratch dir guard and task tracking
rust/lance/src/index/vector/ivf/io.rs
Creates a shared TempStdDir guard for spawned partition tasks, stores task handles as optional JoinHandles, and wraps the build in a build_result block that can trigger cleanup on failure.
PQ and HNSW build join
rust/lance/src/index/vector/ivf/io.rs
Builds PQ storage directly and combines it with the HNSW build using try_join!.
Concurrency and leak tests
rust/lance/src/index/vector/ivf/io.rs
Adds atomic test imports plus tests for scratch-dir lifetime, draining late failures, and legacy IVF_HNSW_PQ scratch-dir cleanup in a child process.

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

Sequence Diagram(s)

sequenceDiagram
  participant write_hnsw_quantization_index_partitions
  participant TempStdDir
  participant Partition task
  participant drain_partition_tasks

  write_hnsw_quantization_index_partitions->>TempStdDir: create shared scratch guard
  write_hnsw_quantization_index_partitions->>Partition task: spawn with Arc clone
  Partition task->>Partition task: build PQ storage + HNSW via try_join!
  alt success
    Partition task-->>write_hnsw_quantization_index_partitions: return partition result
    write_hnsw_quantization_index_partitions->>write_hnsw_quantization_index_partitions: assemble staged files
  else failure
    write_hnsw_quantization_index_partitions->>drain_partition_tasks: abort and await remaining tasks
    drain_partition_tasks-->>write_hnsw_quantization_index_partitions: collected errors
  end
  write_hnsw_quantization_index_partitions->>TempStdDir: drop guard and remove scratch dir
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: keeping the HNSW IVF scratch directory alive during partition writing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

Actionable comments posted: 6

🤖 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/src/index/vector/ivf/io.rs`:
- Around line 865-873: The test setup in the child-process path is hand-building
Arrow schema and batch objects, which should be replaced with the repository’s
test helper style. Update the code around the FixedSizeListArray setup in the
vector IVF I/O test to use record_batch!() from arrow_array (and any existing
test batch helpers) instead of manually constructing Schema, RecordBatch, and
RecordBatchIterator; keep the surrounding test logic and Dataset::write flow
unchanged.
- Line 795: Move the inline test-only imports out of the function body and into
the existing test module import block so the file follows the same convention as
the other `mod tests` imports. Update the test setup around `PathBuf` and the
other inline imports at the referenced locations in `ivf::io` to be grouped with
the rest of the test imports, keeping the import organization consistent and
avoiding local `use` statements inside functions.
- Line 482: The Vec allocations in the IVF I/O path should be preallocated
because their sizes are already bounded. Update the initialization in the
relevant vector-building code in io.rs, including the `errors` collection and
the other matching vector mentioned in the review, to use
`Vec::with_capacity(...)` with the known upper bounds (`tasks.len()` and
`NUM_SLOW + 1`) instead of empty Vec construction. Use the existing symbols
around the task-processing logic to locate both allocations and keep the
capacity estimate slightly generous if needed.
- Line 372: The semaphore acquisition in the IVF IO build path currently uses an
expect, which can panic inside library code and hide the real failure. Update
the acquire call in the relevant build task around sem.acquire to return the
error through the function’s Result instead of panicking, using proper error
propagation consistent with the surrounding lance::index::vector::ivf::io logic
and existing error types.
- Around line 538-548: The IVF_HNSW quantizer handling in the vector index IO
path still uses panic-prone fallbacks, specifically `aux_writer.unwrap()` and
`unreachable!()` inside the `match quantizer` block. Update this logic to return
a contextual `Error::index` instead of panicking when the auxiliary writer is
missing or the quantizer state is invalid, and thread the error through the
existing `Result` flow from `build_and_write_pq_storage` so the partition task
can fail normally.
- Around line 483-488: The drain loop in the IVF IO task cleanup is awaiting
each aborted handle immediately after aborting it, which delays cancellation of
later tasks; update the logic around the task iteration so all outstanding
handles from tasks are aborted first and only then awaited. Use the existing
task collection in the vector IVF IO path (the loop over tasks and the
handle.abort/handle.await sequence) to split cancellation and waiting into
separate phases.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 206641b6-417f-464a-ac84-c2ecba64e94b

📥 Commits

Reviewing files that changed from the base of the PR and between d217a02 and f52f075.

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

Comment thread rust/lance/src/index/vector/ivf/io.rs Outdated
Comment thread rust/lance/src/index/vector/ivf/io.rs Outdated
Comment thread rust/lance/src/index/vector/ivf/io.rs
Comment thread rust/lance/src/index/vector/ivf/io.rs
Comment thread rust/lance/src/index/vector/ivf/io.rs Outdated
Comment thread rust/lance/src/index/vector/ivf/io.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants