Skip to content

feat: add batch blob range reads#7864

Merged
Xuanwo merged 5 commits into
mainfrom
xuanwo/blob-v2-batch-range-api
Jul 21, 2026
Merged

feat: add batch blob range reads#7864
Xuanwo merged 5 commits into
mainfrom
xuanwo/blob-v2-batch-range-api

Conversation

@Xuanwo

@Xuanwo Xuanwo commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Blob v2 exposes per-value range reads through BlobFile, while Dataset.read_blobs can batch only whole values. Python workloads that need row-specific windows therefore either schedule one call per blob or materialize complete values, losing centralized planning, bounded concurrency, and backpressure.

This adds a dataset-level row-specific range contract for row IDs, indices, and addresses. Each request carries its row selector, offset, and length, so duplicates, out-of-order requests, and multiple ranges for one row remain unambiguous. Validation, physical-range planning, source grouping, coalescing, bounded scheduling, ordering, and error semantics stay in Rust; Python remains a thin collecting wrapper.

Python user-path benchmark

Real Amazon S3 validation ran in one CPython 3.12.13 process on a c7i.4xlarge in us-east-2. The workload used four 500 MiB Blob v2 values and 64 deterministic 100 KiB windows, with concurrency 16 and a 64 MiB I/O buffer. Both methods used the same requests and objects for ten repetitions, and method order rotated by repetition. The reported p50 and p95 use linear interpolation across those ten runs.

The existing-path baseline used pre-opened BlobFile handles and a prewarmed 16-worker ThreadPoolExecutor, so handle lookup and executor startup were excluded in its favor. Its measured interval covered submitting 64 synchronous BlobFile.read_range calls through receiving every bytes result. The PR path measured one synchronous Dataset.read_blob_ranges call through receiving its collected result list. Payload validation ran outside both measured intervals.

Python user-path metric Existing ThreadPoolExecutor + BlobFile.read_range This PR: Dataset.read_blob_ranges Benefit
64-range workload p50 wall-clock latency (lower is better) 201.19 ms 81.51 ms 2.47x speedup
64-range workload p95 wall-clock latency (lower is better) 377.65 ms 103.74 ms 3.64x speedup
Logical throughput p50 (higher is better) 31.07 MiB/s 76.80 MiB/s 2.47x throughput

Storage I/O diagnostic

A separate Rust scheduler diagnostic at the same implementation commit measured the physical I/O for the same logical workload. It is used only to validate range efficiency, not as a Python latency baseline.

Storage path Logical bytes Physical bytes p50 Physical requests p50 Read amplification p50
read_blob_ranges 6,553,600 6,556,844 72 1.0005x
Whole-value read_blobs control 6,553,600 2,097,155,244 136 320.0005x

The range API therefore reduced aggregate read amplification by 319.84x relative to the only existing dataset-level batch API. Direct S3 is intentionally omitted from the benefit table because it is a storage diagnostic control, not a Python user path.

No client, OS, or S3 cache flush was performed; first-pass labels are observational and Amazon S3 server-side cache state is uncontrolled. Both measurements used the current PR head, 9c95e0324.

@github-actions github-actions Bot added A-python Python bindings enhancement New feature or request labels Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 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: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 0aa82751-c0d1-41dd-926f-6c0b3159e086

📥 Commits

Reviewing files that changed from the base of the PR and between 9c95e03 and 9b61418.

📒 Files selected for processing (5)
  • python/python/lance/dataset.py
  • python/python/tests/test_blob.py
  • python/src/dataset.rs
  • rust/lance/src/dataset.rs
  • rust/lance/src/dataset/blob.rs

📝 Walkthrough

Walkthrough

Changes

Adds planned blob byte-range reads across Rust and Python. Requests support row IDs, addresses, or indices, optional I/O buffer sizing and ordering, range validation, mixed blob sources, null handling, and bounded execution batches.

Blob range reads

Layer / File(s) Summary
Range-read contracts and dataset entry point
rust/lance/src/dataset/blob.rs, rust/lance/src/dataset.rs
Defines range request/result types and exposes a dataset builder for row-selected blob range reads.
Range planning, batching, and execution
rust/lance/src/dataset/blob.rs
Plans physical ranges, bounds payload batches, maps scheduler results to requests, and updates full-blob reads to use the batching pipeline.
Python API wiring and coverage
python/python/lance/..., python/src/dataset.rs, python/python/tests/test_blob.py
Adds the Python API and type stub, dispatches selector modes, and tests ordering, validation, nulls, empty batches, and mixed blob sources.

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

Sequence Diagram(s)

sequenceDiagram
  participant PythonDataset
  participant Dataset
  participant ReadBlobRangesBuilder
  participant BlobReadScheduler
  PythonDataset->>Dataset: Submit row, offset, and length requests
  Dataset->>ReadBlobRangesBuilder: Select rows and configure options
  ReadBlobRangesBuilder->>BlobReadScheduler: Submit physical ranges in bounded batches
  BlobReadScheduler-->>ReadBlobRangesBuilder: Return range payloads
  ReadBlobRangesBuilder-->>PythonDataset: Return request index, row address, and bytes
Loading

Possibly related PRs

  • lance-format/lance#7861: Changes concurrent dispatch of drained blob-range batches in the related blob range execution path.

Suggested reviewers: bubblecal, jackye1995, wjones127, zhangyue19921010

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding batch blob range reads.
Description check ✅ Passed The description is directly related to the changeset and explains the new batch range-read API and its motivation.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch xuanwo/blob-v2-batch-range-api

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

@Xuanwo
Xuanwo marked this pull request as ready for review July 20, 2026 17:21

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

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
python/python/lance/dataset.py-2279-2287 (1)

2279-2287: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Describe this as one API call, not one I/O operation.

Line 2280 promises a single planned I/O operation, but range planning can split work across payload batches and blob sources, issuing multiple physical reads. Say “one planned API call” instead.

Proposed fix
-        Read row-specific blob-local byte ranges with one planned I/O operation.
+        Read row-specific blob-local byte ranges with one planned API call.
🤖 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 2279 - 2287, Update the
docstring for the row-specific blob range-reading method to describe the
operation as one planned API call rather than one planned I/O operation. Keep
the remaining explanation of planning, grouping, coalescing, and scheduling
unchanged.
🤖 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.

Other comments:
In `@python/python/lance/dataset.py`:
- Around line 2279-2287: Update the docstring for the row-specific blob
range-reading method to describe the operation as one planned API call rather
than one planned I/O operation. Keep the remaining explanation of planning,
grouping, coalescing, and scheduling unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: a3b24ec9-b698-4423-93a1-b27084e3c051

📥 Commits

Reviewing files that changed from the base of the PR and between af6f0f7 and 8f0baee.

📒 Files selected for processing (6)
  • python/python/lance/dataset.py
  • python/python/lance/lance/__init__.pyi
  • python/python/tests/test_blob.py
  • python/src/dataset.rs
  • rust/lance/src/dataset.rs
  • rust/lance/src/dataset/blob.rs

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.34653% with 78 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance/src/dataset/blob.rs 90.39% 55 Missing and 22 partials ⚠️
rust/lance/src/dataset.rs 83.33% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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

I found two issues that should be addressed in the new range-read path.

Comment thread rust/lance/src/dataset/blob.rs
Comment thread rust/lance/src/dataset/blob.rs Outdated

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

Found two correctness issues that reproduce on the current head.

Comment thread rust/lance/src/dataset/blob.rs
Comment thread rust/lance/src/dataset/blob.rs Outdated
@AyushExel
AyushExel requested a review from BubbleCal July 21, 2026 05:34

@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

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

@Xuanwo
Xuanwo merged commit aea6ded into main Jul 21, 2026
33 checks passed
@Xuanwo
Xuanwo deleted the xuanwo/blob-v2-batch-range-api branch July 21, 2026 08:59
wjones127 pushed a commit that referenced this pull request Jul 21, 2026
Blob v2 exposes per-value range reads through `BlobFile`, while
`Dataset.read_blobs` can batch only whole values. Python workloads that
need row-specific windows therefore either schedule one call per blob or
materialize complete values, losing centralized planning, bounded
concurrency, and backpressure.

This adds a dataset-level row-specific range contract for row IDs,
indices, and addresses. Each request carries its row selector, offset,
and length, so duplicates, out-of-order requests, and multiple ranges
for one row remain unambiguous. Validation, physical-range planning,
source grouping, coalescing, bounded scheduling, ordering, and error
semantics stay in Rust; Python remains a thin collecting wrapper.

### Python user-path benchmark

Real Amazon S3 validation ran in one CPython 3.12.13 process on a
`c7i.4xlarge` in `us-east-2`. The workload used four 500 MiB Blob v2
values and 64 deterministic 100 KiB windows, with concurrency 16 and a
64 MiB I/O buffer. Both methods used the same requests and objects for
ten repetitions, and method order rotated by repetition. The reported
p50 and p95 use linear interpolation across those ten runs.

The existing-path baseline used pre-opened `BlobFile` handles and a
prewarmed 16-worker `ThreadPoolExecutor`, so handle lookup and executor
startup were excluded in its favor. Its measured interval covered
submitting 64 synchronous `BlobFile.read_range` calls through receiving
every `bytes` result. The PR path measured one synchronous
`Dataset.read_blob_ranges` call through receiving its collected result
list. Payload validation ran outside both measured intervals.

| Python user-path metric | Existing `ThreadPoolExecutor` +
`BlobFile.read_range` | This PR: `Dataset.read_blob_ranges` | Benefit |
| --- | ---: | ---: | ---: |
| 64-range workload p50 wall-clock latency (lower is better) | 201.19 ms
| 81.51 ms | 2.47x speedup |
| 64-range workload p95 wall-clock latency (lower is better) | 377.65 ms
| 103.74 ms | 3.64x speedup |
| Logical throughput p50 (higher is better) | 31.07 MiB/s | 76.80 MiB/s
| 2.47x throughput |

### Storage I/O diagnostic

A separate Rust scheduler diagnostic at the same implementation commit
measured the physical I/O for the same logical workload. It is used only
to validate range efficiency, not as a Python latency baseline.

| Storage path | Logical bytes | Physical bytes p50 | Physical requests
p50 | Read amplification p50 |
| --- | ---: | ---: | ---: | ---: |
| `read_blob_ranges` | 6,553,600 | 6,556,844 | 72 | 1.0005x |
| Whole-value `read_blobs` control | 6,553,600 | 2,097,155,244 | 136 |
320.0005x |

The range API therefore reduced aggregate read amplification by 319.84x
relative to the only existing dataset-level batch API. Direct S3 is
intentionally omitted from the benefit table because it is a storage
diagnostic control, not a Python user path.

No client, OS, or S3 cache flush was performed; first-pass labels are
observational and Amazon S3 server-side cache state is uncontrolled.
Both measurements used the current PR head, `9c95e0324`.

(cherry picked from commit aea6ded)
Xuanwo added a commit that referenced this pull request Jul 23, 2026
## Why

Blob selection APIs currently expose an implementation detail of the
physical read planner: null descriptors are omitted because they
schedule no payload I/O. This makes result cardinality depend on
descriptor contents, prevents callers from distinguishing null values
from omitted rows, and leaves `read_blobs`, `read_blob_ranges`, and
`take_blobs` with inconsistent contracts.

This is a follow-up to #7864 and resolves the remaining null behavior
described in #7899.

## Contract

All blob selection APIs now return one logical result per selector or
range request. Null blobs are represented explicitly, while valid empty
blobs and empty ranges remain non-null empty values. The physical
planner still avoids payload I/O for nulls; the logical result layer
restores their selection positions.

## Breaking change

This intentionally changes public return types and observable result
cardinality:

- Rust `take_blobs*` APIs return `Vec<Option<BlobFile>>`, and
`ReadBlob::data` / `ReadBlobRange::data` are `Option<Bytes>`.
- Python blob APIs return `Optional` values for null blobs.
- Java `takeBlobs*` lists retain every requested position and may
contain null elements.

Callers that relied on null selections being omitted must now handle
explicit null results.

BREAKING CHANGE: Blob selection APIs now preserve every selector or
request and represent null blob values explicitly instead of omitting
them.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-python Python bindings enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants