Skip to content

fix(scanner): honor batch_readahead to bound v2 scan decode concurrency#7632

Merged
yanghua merged 4 commits into
lance-format:mainfrom
zhangyue19921010:fix-filtered-read-batch-readahead
Jul 13, 2026
Merged

fix(scanner): honor batch_readahead to bound v2 scan decode concurrency#7632
yanghua merged 4 commits into
lance-format:mainfrom
zhangyue19921010:fix-filtered-read-batch-readahead

Conversation

@zhangyue19921010

@zhangyue19921010 zhangyue19921010 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Background & Motivation

Under concurrent-scan workloads like Spark, a single large worker (Executor) typically runs many Tasks in parallel (in Lance's read path, 1 Task = 1 fragment = 1 scan). The problem: each Task independently sizes its own scan concurrency from the core count of the current Executor / host.

Concretely in Lance, the decode concurrency of the v2 read path FilteredReadExec (plan name LanceRead) — the try_buffered(num_threads) — is unconditionally set to get_num_compute_intensive_cpus() = num_cpus − 2.

total decode concurrency per Executor ≈ (concurrent Tasks) × (num_cpus − 2)

Why It Was Deprecated, and the Gap It Left Behind

Scanner.batch_readahead is marked Ignored in v2 and newer format — a deliberate decision, not an oversight. In v1 it controlled two things: prefetch depth and decode concurrency. v2 replaced prefetch with a byte-budget model (io_buffer_size + fragment_readahead), so its prefetch role became meaningless and was rightly dropped.

The gap: v2 split the decode-concurrency role into FilteredReadExec's threading mode (try_buffered(num_threads)), hard-coded to get_num_compute_intensive_cpus(). The only override today is the process-wide LANCE_CPU_THREADS env var — far too coarse, since it governs every compute-intensive path at once (vector/KNN search, index building, take, update/merge-insert, …), not just scan decode concurrency. With no per-scan knob, there's no way to rein in the over-parallelization above.

What This Change Does & Its Impact

Have new_filtered_read pass Scanner.batch_readahead through as FilteredReadThreadingMode::OnePartitionMultipleThreads(batch_readahead), reattaching batch_readahead to the decode-concurrency dimension that v2 had left without a knob.

Summary by CodeRabbit

  • Bug Fixes

    • Invalid batch_readahead / batchReadahead values (0 or less) are now rejected consistently across Java, Python, and Rust.
    • Error messages now clearly state batch_readahead must be greater than 0.
    • Filtered scan execution now applies batch_readahead to control decoding parallelism more reliably.
  • Tests

    • Strengthened batch_readahead tests to validate deterministic scanned content in addition to row counts.
    • Added Rust coverage for default behavior, custom values, and rejection of zero-parallelism configurations.

@github-actions github-actions Bot added A-java Java bindings + JNI bug Something isn't working labels Jul 6, 2026
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.00000% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance/src/io/exec/filtered_read.rs 42.85% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

@yanghua yanghua left a comment

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.

Left some comments.


// Bound the decode fan-out by `batch_readahead`.
read_options = read_options.with_threading_mode(
FilteredReadThreadingMode::OnePartitionMultipleThreads(self.batch_readahead),

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.

Do we need to make sure self.batch_readahead is not equal to 0?

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.

checked.

/// This controls how decode work is parallelized. For the default single-partition
/// scan, the parameter of [`FilteredReadThreadingMode::OnePartitionMultipleThreads`]
/// bounds how many batch-decode tasks are buffered in flight (via `try_buffered`).
pub fn with_threading_mode(mut self, threading_mode: FilteredReadThreadingMode) -> Self {

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.

Do we allow OnePartitionMultipleThreads(0) / MultiplePartitions(0) or any check the input parameter?

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.

added

@github-actions github-actions Bot added the A-python Python bindings label Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b2426e7-c0a0-449c-a2d5-970b8bc57cfd

📥 Commits

Reviewing files that changed from the base of the PR and between 750d071 and 6c196e2.

📒 Files selected for processing (1)
  • rust/lance/src/io/exec/filtered_read.rs
💤 Files with no reviewable changes (1)
  • rust/lance/src/io/exec/filtered_read.rs

📝 Walkthrough

Walkthrough

batch_readahead now requires a value greater than zero across Java, Python, and Rust APIs. Rust filtered reads apply the configured value to decode threading, expose that mode through FilteredReadOptions, reject zero parallelism, and add validation and data-integrity tests.

Changes

Batch readahead behavior

Layer / File(s) Summary
Positive batch readahead validation
java/src/main/java/org/lance/ipc/ScanOptions.java, python/python/lance/dataset.py, rust/lance/src/dataset/scanner.rs
Java, Python, and Rust reject zero or non-positive batch_readahead values and document the positive-value requirement.
Filtered-read decode threading
rust/lance/src/dataset/scanner.rs, rust/lance/src/io/exec/filtered_read.rs
Scanner construction passes batch_readahead into filtered-read threading options, and filtered-read options validate that configured parallelism is nonzero.
Batch readahead coverage
rust/lance/src/dataset/scanner.rs, java/src/test/java/org/lance/ScannerTest.java
Tests verify default and configured decode threading, zero-value rejection, row counts, and deterministic scanned id sums.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scanner
  participant FilteredReadOptions
  participant FilteredReadExec
  Scanner->>FilteredReadOptions: set decode threading from batchReadahead
  FilteredReadOptions->>FilteredReadExec: provide threading mode
  FilteredReadExec->>FilteredReadExec: reject zero parallelism
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 matches the main change: using batch_readahead to bound v2 scan decode concurrency.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
java/src/main/java/org/lance/ipc/ScanOptions.java (1)

105-105: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Javadoc doesn't document the new batchReadahead > 0 constraint.

The constructor validates batchReadahead > 0 (lines 137-138), but the @param batchReadahead Javadoc (line 105), getBatchReadahead() Javadoc (lines 270-274), and Builder.batchReadahead() Javadoc (lines 559-564) still only say "Number of batches to read ahead." / "the number of batches to read ahead." with no mention of the positive-value requirement, so callers have no way to discover this constraint without hitting the runtime exception.

As per coding guidelines, "Copy Rust documentation about defaults, constraints, and invariants into Javadoc for binding classes."

📝 Suggested doc update
   /**
    * Set the batch readahead.
    *
    * `@param` batchReadahead Number of batches to read ahead.
+   *     Must be greater than 0.
    * `@return` Builder instance for method chaining.
    */
   public Builder batchReadahead(int batchReadahead) {

Also applies to: 271-277, 559-568

🤖 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 `@java/src/main/java/org/lance/ipc/ScanOptions.java` at line 105, Update the
Javadocs for the ScanOptions constructor’s batchReadahead parameter,
getBatchReadahead(), and Builder.batchReadahead() to explicitly state that
batchReadahead must be greater than zero, preserving the existing description
while documenting the validation constraint consistently.

Source: Coding guidelines

java/src/test/java/org/lance/ScannerTest.java (1)

419-455: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a batchReadahead(0) rejection case

ScannerTest only covers the low-readahead happy path. Add an assertThrows(IllegalArgumentException.class, () -> new ScanOptions.Builder().batchReadahead(0).build()) case so the zero-value validation stays covered here.

🤖 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 `@java/src/test/java/org/lance/ScannerTest.java` around lines 419 - 455, Add a
validation test in ScannerTest, alongside testDatasetScannerBatchReadahead, that
asserts new ScanOptions.Builder().batchReadahead(0).build() throws
IllegalArgumentException using assertThrows.
🧹 Nitpick comments (1)
python/python/lance/dataset.py (1)

6184-6193: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

batch_readahead docstring lacks an example.

The updated docstring documents the new constraint but has no usage example or links to related methods.

As per coding guidelines, "All public APIs must have documentation with examples, and documentation should link to relevant structs and methods" for **/*.{rs,md,py} files.

🤖 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 `@python/python/lance/dataset.py` around lines 6184 - 6193, Add a usage example
and relevant cross-references to the public `batch_readahead` docstring, showing
a valid positive value and linking to related scanner configuration methods or
APIs, while preserving the documented greater-than-zero constraint.

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/src/io/exec/filtered_read.rs`:
- Around line 1490-1499: Validate the thread count before accepting it in
`with_threading_mode` (or centrally in `FilteredReadExec::try_new`), rejecting
`OnePartitionMultipleThreads(0)` and `MultiplePartitions(0)` with
`Error::invalid_input_source`. Ensure the guard applies to all public
construction paths so `get_stream` never calls `try_buffered` with zero.

---

Outside diff comments:
In `@java/src/main/java/org/lance/ipc/ScanOptions.java`:
- Line 105: Update the Javadocs for the ScanOptions constructor’s batchReadahead
parameter, getBatchReadahead(), and Builder.batchReadahead() to explicitly state
that batchReadahead must be greater than zero, preserving the existing
description while documenting the validation constraint consistently.

In `@java/src/test/java/org/lance/ScannerTest.java`:
- Around line 419-455: Add a validation test in ScannerTest, alongside
testDatasetScannerBatchReadahead, that asserts new
ScanOptions.Builder().batchReadahead(0).build() throws IllegalArgumentException
using assertThrows.

---

Nitpick comments:
In `@python/python/lance/dataset.py`:
- Around line 6184-6193: Add a usage example and relevant cross-references to
the public `batch_readahead` docstring, showing a valid positive value and
linking to related scanner configuration methods or APIs, while preserving the
documented greater-than-zero constraint.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5fa73b89-be82-47ef-a13c-41a77af2c5d8

📥 Commits

Reviewing files that changed from the base of the PR and between 78a814b and 24a8fc3.

📒 Files selected for processing (5)
  • java/src/main/java/org/lance/ipc/ScanOptions.java
  • java/src/test/java/org/lance/ScannerTest.java
  • python/python/lance/dataset.py
  • rust/lance/src/dataset/scanner.rs
  • rust/lance/src/io/exec/filtered_read.rs

Comment thread rust/lance/src/io/exec/filtered_read.rs

@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: 1

🧹 Nitpick comments (2)
rust/lance/src/io/exec/filtered_read.rs (2)

1486-1497: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document both threading modes with a runnable example.

This public builder documents only OnePartitionMultipleThreads and has no example. Add a no_run example using the current signature and explain or link both enum variants.

As per coding guidelines, all public Rust APIs must have documentation with examples and links to relevant structs and methods.

🤖 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/io/exec/filtered_read.rs` around lines 1486 - 1497, Expand the
documentation for FilteredReadExec::with_threading_mode to describe and link
both FilteredReadThreadingMode variants, including their respective behavior.
Add a runnable `no_run` Rust example using the current builder signature and
showing threading mode configuration, with links to relevant types and methods.

Source: Coding guidelines


3933-3939: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Parameterize the mode cases with rstest.

These cases differ only by input. Replace the loop with named #[case::...] parameters and one test body.

As per coding guidelines, Rust tests whose cases differ only by inputs must use rstest with readable case names.

🤖 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/io/exec/filtered_read.rs` around lines 3933 - 3939, The test
currently loops over two threading modes that differ only by input; refactor it
to use an rstest parameterized test with readable named cases such as
#[case::...] for each FilteredReadThreadingMode value. Keep a single test body
that receives the mode parameter and uses it to build FilteredReadOptions and
invoke FilteredReadExec::try_new.

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/src/io/exec/filtered_read.rs`:
- Around line 3939-3946: Update the test around FilteredReadExec::try_new to
retain the returned error instead of converting it directly to a string, then
match it as Error::InvalidInput and assert that the contained message includes
"must be greater than 0"; ensure other variants fail the test.

---

Nitpick comments:
In `@rust/lance/src/io/exec/filtered_read.rs`:
- Around line 1486-1497: Expand the documentation for
FilteredReadExec::with_threading_mode to describe and link both
FilteredReadThreadingMode variants, including their respective behavior. Add a
runnable `no_run` Rust example using the current builder signature and showing
threading mode configuration, with links to relevant types and methods.
- Around line 3933-3939: The test currently loops over two threading modes that
differ only by input; refactor it to use an rstest parameterized test with
readable named cases such as #[case::...] for each FilteredReadThreadingMode
value. Keep a single test body that receives the mode parameter and uses it to
build FilteredReadOptions and invoke FilteredReadExec::try_new.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: af689aef-007a-413d-bbdd-19c122f50a17

📥 Commits

Reviewing files that changed from the base of the PR and between 24a8fc3 and 750d071.

📒 Files selected for processing (1)
  • rust/lance/src/io/exec/filtered_read.rs

Comment thread rust/lance/src/io/exec/filtered_read.rs Outdated
@zhangyue19921010

Copy link
Copy Markdown
Contributor Author

HI @yanghua Thanks a lot for your review. All comments are addressed. PTAL ~

@yanghua yanghua left a comment

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.

+1

@yanghua
yanghua merged commit 787f759 into lance-format:main Jul 13, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-java Java bindings + JNI A-python Python bindings bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants