Skip to content

feat: Add batch TOUCH operation with optimized implementation#477

Closed
lcian wants to merge 7 commits into
mainfrom
lcian/batch-exists
Closed

feat: Add batch TOUCH operation with optimized implementation#477
lcian wants to merge 7 commits into
mainfrom
lcian/batch-exists

Conversation

@lcian

@lcian lcian commented May 13, 2026

Copy link
Copy Markdown
Member

This adds a new TOUCH operation that, most importantly, can be batched.
Semantics: if an object exists, and has a TTI expiration policy, refresh its TTI.

The Backend trait gains a new method touch_batch that can provide an optimized implementation for executing multiple TOUCH operations. The default implementation just calls get_metadata in a loop and converts the result.
The GCS backend has an "optimized" implementation that executes the requests to GCS with bounded concurrency (20).

HighVolumeBackend also gains a new method get_tiered_metadata_batch with a similar default implementation.
The Bigtable backend has an optimized version where only two requests are used, a ReadRows to get the rows and a MutateRows to bump the TTI on all of them in bulk.

The name is inspired by the UNIX touch command, which bumps the date a file was last modified, although it's also different because it can also create files, which our operation doesn't.
More discussion on the naming is welcome.
The previous name was EXISTS, which IMO didn't really make it clear that this bumps TTI, so TOUCH might make that a bit clearer.

As a bonus, this also adds a HEAD API to the client, needed because with the way we have implemented batching, each batchable operation also needs its own non-batch implementation, and for TOUCH this is simply a HEAD request where we discard the metadata.

lcian added 3 commits May 4, 2026 11:26
Add an `exists` operation for batch requests that checks whether objects
exist, returning 200/404 without payload. Key optimization: instead of
N individual backend calls, Bigtable uses 1-3 RPCs (batch ReadRows for
metadata, optional payload fetch for stale TTI subset, batch MutateRows
for TTI bumps). GCS uses concurrent fetch_gcs_metadata calls. Tiered
storage coordinates HV metadata batch → LT exists batch for tombstones.

The batch endpoint partitions exists ops from others, routes exists
through a dedicated `service.check_exists_batch()` path, and merges
results with the streaming executor output.
@lcian lcian changed the title lcian/batch exists feat(service): Add batch exists operation with optimized backend support May 13, 2026
@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.25000% with 189 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.99%. Comparing base (d08783e) to head (b9c175b).

Files with missing lines Patch % Lines
objectstore-service/src/backend/bigtable.rs 54.09% 84 Missing ⚠️
objectstore-server/src/endpoints/batch.rs 59.57% 38 Missing ⚠️
clients/rust/src/many.rs 17.24% 24 Missing ⚠️
clients/rust/src/touch.rs 0.00% 13 Missing ⚠️
objectstore-service/src/service.rs 0.00% 13 Missing ⚠️
objectstore-service/src/backend/testing.rs 0.00% 8 Missing ⚠️
objectstore-service/src/streaming.rs 84.90% 8 Missing ⚠️
objectstore-server/src/extractors/batch.rs 97.14% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #477      +/-   ##
==========================================
- Coverage   86.91%   85.99%   -0.93%     
==========================================
  Files          77       79       +2     
  Lines       11398    11900     +502     
==========================================
+ Hits         9907    10233     +326     
- Misses       1491     1667     +176     
Components Coverage Δ
Rust Backend 90.66% <69.53%> (-1.15%) ⬇️
Rust Client 77.64% <39.34%> (-2.46%) ⬇️
Python Client 86.36% <ø> (ø)

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread objectstore-server/src/endpoints/batch.rs Outdated
@lcian lcian changed the title feat(service): Add batch exists operation with optimized backend support feat: Add batch exists operation with optimized implementation May 15, 2026
@lcian lcian changed the title feat: Add batch exists operation with optimized implementation feat: Add batch TOUCH operation with optimized implementation May 15, 2026
Comment thread clients/rust/src/many.rs
use objectstore_types::metadata::{Compression, Metadata};
use percent_encoding::NON_ALPHANUMERIC;
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
use reqwest::multipart::Part;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is all just plumbing.

Comment on lines +110 to 113
Operation::Get(_) | Operation::Touch(_) => {
objectstore_types::auth::Permission::ObjectRead
}
Operation::Insert(_) => objectstore_types::auth::Permission::ObjectWrite,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Touch operations are unconditionally exempt from rate limiting despite triggering expensive backend writes

In objectstore-server/src/endpoints/batch.rs (line 55), all Touch operations skip the rate limiter (!matches!(op, Operation::Touch(_))) with the comment 'lightweight existence checks', but each touch triggers real backend mutations: a GCS metadata rewrite or a Bigtable MutateRows call. An authenticated caller with ObjectRead permission can issue unlimited batch requests with up to 1,000 touch keys each, with no throttle, flooding the backend storage tier.

Verification

Bypass at objectstore-server/src/endpoints/batch.rs:55 — the per-operation rate-limit check is skipped for Operation::Touch. Touch requires only ObjectRead permission (streaming.rs:108–110). state.service.touch_batch (service.rs) acquires a single concurrency permit for the whole batch; the Bigtable backend executes a ReadRows + MutateRows that writes a GC/TTI policy on every matching row, and the GCS backend performs up to 20 concurrent metadata rewrites per batch. MAX_OPERATIONS=1000 caps per-request size but no inter-request throttle applies, while every other op kind (Get/Insert/Delete) still passes through rate_limiter.check. Any read-token holder can therefore flood the storage tier with unlimited backend mutations.

Identified by Warden security-review · 6WH-KV2

@lcian

lcian commented May 15, 2026

Copy link
Copy Markdown
Member Author

Unfortunately this can create internal inconsistency issues as we're using MutateRows and we could override a new tombstone update, leaving the new LT object orphaned.
Recreating a dumber version of this now.

@lcian lcian closed this May 15, 2026
matt-codecov pushed a commit that referenced this pull request May 18, 2026
This adds the HEAD operation to the Rust client API, and makes it
batchable.

The intent is to use this in `sentry-cli` to do a batch check for
existing Snapshots, bumping their TTI, and subsequently only upload the
missing ones. This is possible given that we use the hash of the object
as its key for snapshots.
Note that we will also need to change the expiration policy for
Snapshots to TTI instead of TTL (currently used).

A singular HEAD request maps to a HEAD HTTP request.
A batch HEAD request goes through the existing
`objectstore-service/src/streaming.rs` pipeline that executes operations
with concurrency bounded to 10% of the existing service concurrency
limit.

This is the "dumb" version of the much more optimized
#477.
In that PR, I optimized the implementation down to the backend level,
most importantly using `ReadRows` (plural) and `WriteRows` for Bigtable.
Unfortunately, that creates some possible inconsistency scenarios where,
ultimately, LT blobs could remain orphaned. `WriteRows` is not
conditional, so it could overwrite a new tombstone that comes in between
the `ReadRows` to read all tombstones and the `WriteRows` to bump the
TTI of existing ones.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant