Skip to content

fix(io): publish object store metrics for local reads and writes#7994

Open
wjones127 wants to merge 1 commit into
lance-format:mainfrom
wjones127:fix-local-writer-metrics
Open

fix(io): publish object store metrics for local reads and writes#7994
wjones127 wants to merge 1 commit into
lance-format:mainfrom
wjones127:fix-local-writer-metrics

Conversation

@wjones127

Copy link
Copy Markdown
Contributor

The optimized local paths — LocalWriter, LocalObjectReader, the io_uring readers, and the local copy / recursive-delete shortcuts — go straight to the filesystem, so they never reach MeteredObjectStore and published no metrics at all. Writing a dataset to a local path produced zero object store metrics even though the IO tracker recorded it.

IOTracker now carries the metrics base label of the store it belongs to (its store prefix, the same value MeteredObjectStore is given) and hands out an IoMetricsGuard for IO that bypasses the object_store layer. Each bypassing path now records requests, bytes, latency, errors and the in-flight gauge under the same labels, so local IO aggregates with cloud IO:

path operation
LocalWriter (one put per file, from open to durable at its final path) put
LocalObjectReader::get_range / get_all / streamed chunks get
LocalObjectReader::size (the local equivalent of a HEAD) head
UringReader / UringCurrentThreadReader get_range / get_all get
local ObjectStore::copy copy
local ObjectStore::remove_dir_all (one request, like delete_stream) delete

Two things worth a second opinion:

  • A caller-supplied store (ObjectStoreParams::object_store, deprecated) got no metered wrapper at all, so it published nothing either. It is now wrapped like a registry-built store. This would double-count if a caller passed in a store that Lance had already metered.
  • IoStats is left alone. The local copy, recursive delete and size lookup were never counted there and still aren't, so existing IO assertions are unaffected; adding them would shift IO counts across the test suite.

The io_uring readers have no coverage here — they need Linux plus a working ring, so the new tests exercise the non-uring local paths only.

Fixes #7993

@github-actions github-actions Bot added A-encoding Encoding, IO, file reader/writer bug Something isn't working labels Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Local filesystem readers, writers, io_uring paths, and copy/delete shortcuts now record object-store metrics through IOTracker, including operation outcomes, latency, byte counts, labels, and in-flight state. Tests cover successful, failed, and persisted operations.

Changes

Local I/O metrics

Layer / File(s) Summary
IOTracker metrics guards
rust/lance-io/src/utils/tracking_store.rs
Adds optional metrics labels, timed operation guards, outcome recording, and named IOTracker fields.
Local object-store wiring
rust/lance-io/src/object_store.rs
Rebases trackers for local stores and records local copy and delete operations.
Local and io_uring read metrics
rust/lance-io/src/local.rs, rust/lance-io/src/object_reader.rs, rust/lance-io/src/uring/*
Records get and head outcomes for local, streamed, and io_uring reads, including failed reads with zero transferred bytes.
Local writer lifecycle metrics
rust/lance-io/src/object_writer.rs
Carries a put-operation guard through writer shutdown and persistence before recording the result.
Metrics behavior validation
rust/lance-io/src/object_store/metrics.rs
Adds documentation and tests for local reads, writes, errors, shortcuts, labels, and in-flight gauges.

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

Sequence Diagram(s)

sequenceDiagram
  participant ObjectStore
  participant LocalObjectReader
  participant LocalFilesystem
  participant IOTracker
  ObjectStore->>LocalObjectReader: open with local_io_tracker()
  LocalObjectReader->>IOTracker: begin_io("get")
  LocalObjectReader->>LocalFilesystem: perform blocking read
  LocalFilesystem-->>LocalObjectReader: return Result and bytes
  LocalObjectReader->>IOTracker: record outcome and byte count
Loading

Suggested reviewers: xuanwo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: publishing object store metrics for local reads and writes.
Description check ✅ Passed The description directly describes the local metrics gap and the fix applied to local IO paths.
Linked Issues check ✅ Passed The changes address #7993 by adding metrics for local writes and reader paths that bypass MeteredObjectStore.
Out of Scope Changes check ✅ Passed The added metrics plumbing, tests, and local shortcut handling align with the issue and stated objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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.

The optimized local paths — `LocalWriter`, `LocalObjectReader`, the io_uring
readers, and the local `copy` / recursive-delete shortcuts — go straight to the
filesystem, so they never reach `MeteredObjectStore` and published no metrics at
all. Writing a dataset to a local path produced zero object store metrics even
though the IO tracker recorded it.

`IOTracker` now carries the metrics `base` label of the store it belongs to and
hands out an `IoMetricsGuard` for IO that bypasses the `object_store` layer.
Every such path records requests, bytes, latency, errors and the in-flight gauge
under the same labels the metered store uses, so local IO aggregates with cloud
IO. A caller-supplied `ObjectStore` (the deprecated `ObjectStoreParams::object_store`)
is now metered too.

`IoStats` is unchanged: the local `copy`, recursive delete and size lookup were
never counted there and still aren't, so existing IO assertions are unaffected.

Fixes lance-format#7993

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wjones127
wjones127 force-pushed the fix-local-writer-metrics branch from abda9ee to 4b64b21 Compare July 24, 2026 21:53
@wjones127
wjones127 marked this pull request as ready for review July 24, 2026 21:54
@wjones127
wjones127 requested a review from hamersaw July 24, 2026 21:54

@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

Note

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

🟡 Other comments (1)
rust/lance-io/src/object_store/metrics.rs-1517-1536 (1)

1517-1536: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert on the error variant/message, not just is_err().

test_local_read_error_is_counted only checks reader.get_range(0..100).await.is_err()). As per coding guidelines: "Assert on both the error variant and the message content in tests; do not check only is_err()."

✅ Proposed fix
             let reader = store.open(&path).await.unwrap();
             // Reading past the end of the file fails.
-            assert!(reader.get_range(0..100).await.is_err());
+            let err = reader.get_range(0..100).await.unwrap_err();
+            assert!(
+                matches!(err, object_store::Error::Generic { .. }),
+                "unexpected error variant: {err:?}"
+            );
🤖 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-io/src/object_store/metrics.rs` around lines 1517 - 1536, Update
test_local_read_error_is_counted to capture the get_range error and assert both
its expected error variant and message content instead of checking only
is_err(). Preserve the existing metrics assertions and the out-of-bounds read
scenario.

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-io/src/utils/tracking_store.rs`:
- Around line 104-144: The local-bypass metrics label must honor BaseLabelMode
scoping and avoid per-operation allocation. In
rust/lance-io/src/utils/tracking_store.rs lines 104-144, update
with_metrics_base to materialize the same scoped label used by
ObjectStoreMetricsExt::metered(), including the configured off/full/scheme
behavior. In rust/lance-io/src/object_store.rs lines 653-661, precompute and
cache the scoped local IOTracker alongside the existing io_tracker/store_prefix
state, then reuse it across open, open_with_size, create, copy_impl, and
remove_dir_all instead of calling local_io_tracker() for each operation.

---

Other comments:
In `@rust/lance-io/src/object_store/metrics.rs`:
- Around line 1517-1536: Update test_local_read_error_is_counted to capture the
get_range error and assert both its expected error variant and message content
instead of checking only is_err(). Preserve the existing metrics assertions and
the out-of-bounds read scenario.
🪄 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: 7e76efbb-517b-4558-9642-4ee3f9999754

📥 Commits

Reviewing files that changed from the base of the PR and between e0fb830 and 4b64b21.

📒 Files selected for processing (8)
  • rust/lance-io/src/local.rs
  • rust/lance-io/src/object_reader.rs
  • rust/lance-io/src/object_store.rs
  • rust/lance-io/src/object_store/metrics.rs
  • rust/lance-io/src/object_writer.rs
  • rust/lance-io/src/uring/current_thread.rs
  • rust/lance-io/src/uring/reader.rs
  • rust/lance-io/src/utils/tracking_store.rs

Comment on lines +104 to +144

/// Label the metrics published through [`Self::begin_io`] with the prefix of
/// the store this tracker belongs to, so IO that bypasses the `object_store`
/// layer carries the same `base` label as the store's metered operations.
#[cfg(feature = "metrics")]
pub fn with_metrics_base(mut self, base: &str) -> Self {
self.metrics_base = Some(base.into());
self
}

/// Without the `metrics` feature there is nothing to label.
#[cfg(not(feature = "metrics"))]
pub fn with_metrics_base(self, _base: &str) -> Self {
self
}

/// Begin an operation that talks to storage without going through the
/// `object_store` layer, and so is invisible to the `MeteredObjectStore`
/// wrapper: the optimized local reads and writes go straight to the
/// filesystem. `operation` must be one of the labels that wrapper uses
/// (`get`, `put`, `head`, ...) so this IO aggregates with the rest.
///
/// The returned guard keeps the in-flight gauge raised until it is dropped.
#[cfg(feature = "metrics")]
pub fn begin_io(&self, operation: &'static str) -> IoMetricsGuard {
IoMetricsGuard {
state: self.metrics_base.as_ref().map(|base| IoMetricsState {
_in_flight: InFlightGuard::new(base, operation),
base: base.clone(),
operation,
start: Instant::now(),
}),
}
}

/// Without the `metrics` feature there is nothing to publish.
#[cfg(not(feature = "metrics"))]
pub fn begin_io(&self, _operation: &'static str) -> IoMetricsGuard {
IoMetricsGuard {}
}
}

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Local-bypass metrics label ignores BaseLabelMode scoping and is re-allocated on every local I/O call.

with_metrics_base stores the raw, unscoped store_prefix as-is, unlike ObjectStoreMetricsExt::metered() which applies scoped_base(parse_base_label_mode(...), prefix) (see metrics.rs test_scoped_base/test_base_label_defaults_to_scheme). Because local_io_tracker() calls with_metrics_base afresh on every local read/write/copy/delete, this also means: (1) the runtime LANCE_METRICS_BASE_LABEL_MODE (off/full/scheme) opt-out isn't honored for local bypass paths the way it is for cloud/MeteredObjectStore paths, and (2) a new Arc<str> is allocated on every single local I/O call instead of once per ObjectStore.

  • rust/lance-io/src/utils/tracking_store.rs#L104-L144: apply the same BaseLabelMode/scoped_base logic used by .metered() when materializing metrics_base (or accept an already-scoped label from the caller) instead of storing the raw prefix.
  • rust/lance-io/src/object_store.rs#L653-L661: precompute and cache the scoped local IOTracker once (e.g., alongside the io_tracker/store_prefix fields at construction) instead of calling local_io_tracker() — and re-allocating — on every open/open_with_size/create/copy_impl/remove_dir_all call.
📍 Affects 2 files
  • rust/lance-io/src/utils/tracking_store.rs#L104-L144 (this comment)
  • rust/lance-io/src/object_store.rs#L653-L661
🤖 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-io/src/utils/tracking_store.rs` around lines 104 - 144, The
local-bypass metrics label must honor BaseLabelMode scoping and avoid
per-operation allocation. In rust/lance-io/src/utils/tracking_store.rs lines
104-144, update with_metrics_base to materialize the same scoped label used by
ObjectStoreMetricsExt::metered(), including the configured off/full/scheme
behavior. In rust/lance-io/src/object_store.rs lines 653-661, precompute and
cache the scoped local IOTracker alongside the existing io_tracker/store_prefix
state, then reuse it across open, open_with_size, create, copy_impl, and
remove_dir_all instead of calling local_io_tracker() for each operation.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

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

Labels

A-encoding Encoding, IO, file reader/writer bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LocalWriter ignores object store metrics

1 participant