Batch small worker output uploads into BatchUpdateBlobs - #2592
Draft
erneestoc wants to merge 2 commits into
Draft
Conversation
Worker output upload opens one ByteStream Write stream per output file, paying a fixed per-stream cost for every small blob. This adds StoreDriver::update_many (default: loop over update_oneshot, zero behavior change), an opt-in GrpcSpec.experimental_write_batching that packs small blobs into BatchUpdateBlobs RPCs (3MiB budget with a 256-byte per-entry overhead charge, digest dedup within a call, per-entry status isolation: retryable entry errors fall back to the streaming path, non-retryable propagate), a FastSlowStore::update_many that registers in-flight slow writes and forwards the batch to both tiers, and a running_actions_manager seam that queues output files at or below 128KiB and publishes them with batched update_many calls. The worker seam only activates when the store chain advertises the new StoreOptimizations::SubscribesToUpdateMany (FastSlowStore forwards its slow tier's advertisement); with the flag unset every store keeps the existing per-file streaming path. Measured (real gRPC over TCP, byte-verified): BatchUpdateBlobs vs stream-per-blob 15.5x at 4KiB x2048 (30us -> 1us/blob), 3.6x at 32KiB, 1.4x at 256KiB, on a zero-RTT loopback floor; real network RTT widens the gap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC
erneestoc
force-pushed
the
ec/output-upload-batching
branch
from
July 22, 2026 01:29
52ccd1c to
8547c95
Compare
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
erneestoc
marked this pull request as draft
July 22, 2026 19:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The worker uploads every output file as its own
ByteStream.Writestream.Each stream carries a fixed setup cost (measured ~1.6ms on the read-side
investigation; ~30µs/blob even on zero-RTT loopback), so actions with many
small outputs pay it N times. This is the write-side twin of the merged
read coalescing (#2540).
Measured (M-series, real ByteStream+CAS servers over TCP, byte-verified)
Stream-per-blob (64-wide, today's shape) vs
BatchUpdateBlobsbatches(3MiB cap, 256B/entry overhead — same constants as #2540):
Zero-RTT loopback floor; real network RTT widens the gap (each avoided
stream saves a full setup round trip).
Mechanism
StoreDriver::update_many(Vec<(StoreKey, Bytes)>)with a defaultloop-over-
update_oneshotimplementation — zero behavior change forstores that don't override — plus
StoreOptimizations::SubscribesToUpdateManyso call sites can gate batching work.
GrpcSpec.experimental_write_batching(max_blob_size_bytesdefault 128KiB,
max_batch_bytesdefault 3MiB). When set,GrpcStore::update_manypacks small blobs intoBatchUpdateBlobsrequests (per-entry overhead charge, ambient digest function, digest
dedup within a call) reusing the existing retried
batch_update_blobs.Per-entry status isolation: retryable per-entry errors fall back to the
streaming path; non-retryable errors propagate.
FastSlowStore::update_manyforwards batching to the slow tier when itadvertises support, registering the same in-flight-slow-write guards as
update()so concurrenthas()visibility is preserved.running_actions_managercollects output files≤128KiB into a
SmallFileBatcher(32MiB in-flight cap) and publishesthem with one
update_manyper action; large files keep the existingstreaming/whole-file path. Only active when the store chain advertises
SubscribesToUpdateMany— behavior is byte-identical otherwise.Trade-offs / notes
update_oneshotrather than
update_with_whole_file's rename-without-fsync, trading alocal fast-tier write for N-1 avoided slow-tier streams. The flag is
opt-in and aimed at remote (gRPC) slow tiers where stream setup dominates.
partition upload traffic (this PR reroutes small worker outputs off
ByteStream; Add opt-in join-the-flight ByteStream write dedup #2591 dedups concurrent ByteStream streams). Batches and
streams for the same digest are reconciled by existing store-level dedup;
no double-single-flight is built.
Tests
grpc_write_batching_test.rs: 8 tests incl. batch packing, per-entryretryable fallback, non-retryable propagation, threshold routing, digest
dedup, FastSlow chain E2E against a fake gRPC CAS.
small_outputs_batched_when_store_subscribes: realaction execution, asserts exactly one
update_manywith deduped items.cargo check --tests --workspaceclean; bazel store+config+worker+util 62/62 with clippypedantic + nightly rustfmt aspects.
Review-round fixes (pre-PR, 8-area adversarial review)
CacheMetricsStorenow forwardsupdate_many— it forwardsoptimized_for, and the store factory wraps every backend in it, sowithout this the default production composition advertised batching but
dispatched the per-blob loop (regression test:
cache_metrics_wrapper_preserves_batching).error_if!:max_batch_bytesmust covermax_blob_size_bytes+ 256B overhead, otherwise an over-budgetsingle-entry request bypasses the per-entry streaming fallback
(regression test:
rejects_blob_threshold_larger_than_batch_budget).digest; FastSlow guard lifecycle cancel-safe; deferred-upload window
closes before the ActionResult is stored (AC write happens after
upload_results); ambient digest-function propagation matches every
existing GrpcStore RPC; packing always makes progress; drop-mid-flush is
RAII-clean.
Second review round (8-angle adversarial review, pre-PR)
Fixed:
intermediary's gRPC message-size limit could reject a 3MiB batch that
individual streams would survive; previously this hard-failed the action).
Regression test:
whole_rpc_failure_falls_back_to_streaming.buffer_unordered(4)) and theoversized/fallback path uploads with width-32 concurrency, restoring the
old per-file overlap (a lowered
max_blob_size_bytesno longer routesmid-size outputs into a serial loop).
FastSlowStore::update_manywrites with width-16concurrency instead of the serial default loop; a redundant full-Vec
clone removed.
batcher, write packer, and constructor validation; the worker's 128KiB
small-file threshold now references the exported config default so the
two cannot drift.
Known limitations (documented in code):
has()on a batch member blocks until the batch settles (coarser thanupdate(); window shrunk by concurrent dispatch; per-chunk completion isa possible follow-up requiring an API change).
beyond that); no cross-action global budget yet.
CacheMetricsStoreare all-or-nothing on partialfailures (accepted approximation).
optimized_for(SubscribesToUpdateMany)must alsoforward
update_many— contract now documented on the optimization enum;wrappers using the default
falseare safe (feature quietly inactivebehind them).
🤖 Generated with Claude Code
https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC
This change is
Review update (2026-07-22)
The batching path was hardened after adversarial review:
rpc_timeout_s; timeout failures retry and then fall back to streaming.Focused Cargo and Bazel store/worker suites, formatting checks, and
git diff --checkpass. The feature remains opt-in and is still intended primarily for many-small-output actions.