Skip to content

Demote CDC chunks in the eviction order after splice - #2627

Open
erneestoc wants to merge 2 commits into
TraceMachina:mainfrom
erneestoc:ec/chunk-demotion-after-splice
Open

Demote CDC chunks in the eviction order after splice#2627
erneestoc wants to merge 2 commits into
TraceMachina:mainfrom
erneestoc:ec/chunk-demotion-after-splice

Conversation

@erneestoc

@erneestoc erneestoc commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Problem (production failure)

A cold iOS RBE build on a size-capped filesystem CAS with CDC chunking
failed deterministically: four actions reported the same missing 78-byte
intermediate output, and the Buildkite retry failed immediately on the
same digest. With build-without-the-bytes the client cannot re-upload an
evicted intermediate output, so a mid-build eviction of a
still-referenced blob is fatal.

Three code facts compose into the failure:

  1. Chunks are permanent first-class CAS blobs: a spliced blob occupies
    ~2× its size (chunk blobs + assembled blob) in the same store.
  2. The splice's existence checks and chunk reads promote every chunk
    in the store's LRU (sizes_for_keys(..., peek=false)), so chunks
    monopolize the LRU head while early-build small blobs form the
    eviction tail.
  3. Eviction has no refcounting: the assembled blob's insert-time
    eviction pass reclaims from the tail regardless of what queued
    actions still reference.

Reproduced in an integration test: a 20MiB-capped store holding a small
early blob plus a 12MiB blob — a plain upload keeps the small blob
alive; a chunk+splice of the same bytes evicts it.

Mechanism

  • EvictingMap::demote: moves entries to the least-recently-used end of
    the eviction order — no callbacks, no size changes, absent keys
    ignored (lru::LruCache::demote).
  • StoreDriver::demote_keys: default no-op eviction-ordering hint;
    overridden by the filesystem and memory stores, forwarded through
    fast_slow. Wrappers that don't forward it simply leave ordering
    unchanged — there is deliberately no advertisement bit to keep
    forwarding contracts trivial.
  • SpliceBlob demotes all chunk entries once their contents are fully
    consumed, before the EOF releases the assembled blob for commit, so
    the commit's own eviction pass evicts the reproducible chunks first
    (the ordering matters: demoting after the commit is too late). The
    no-op splice fast path demotes likewise, undoing the promotion done by
    its existence check.

Chunks remain in the store as dedup hints for future incremental
uploads; under pressure they are now the first casualties instead of the
last, and SplitBlob regenerates layouts on demand if they are gone.

Scope

This predates the open chunked-uploads PR #2595 — any chunking client
(Bazel --experimental_remote_cache_chunking against a NativeLink CAS
since #2497) can trigger it. Operator guidance regardless of this fix: a
size-capped CAS whose eviction can outpace a build's working set is
unsafe under build-without-the-bytes; with chunking, budget roughly 2×
the large-output working set as headroom.

Tests

  • splice_evicts_demoted_chunks_not_unrelated_blobs: verified to FAIL
    without the demotion (the small blob is evicted by the splice's
    commit) and pass with it; also byte-verifies the assembled blob and
    asserts real eviction pressure (some chunks reclaimed).
  • demote_moves_entry_to_eviction_front, demote_absent_key_is_noop
    (EvictingMap unit tests).
  • Suites: cargo check --tests --workspace clean; bazel
    util+store+service+config 68/68 with clippy pedantic + nightly rustfmt
    aspects.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC


This change is Reviewable

A spliced blob stores its bytes twice in the same CAS: the chunk blobs
(retained as dedup hints for future incremental uploads) plus the
assembled blob, and the splice's existence checks and chunk reads promote
every chunk in the store's LRU. Under a size-capped store the assembled
blob's own insert-time eviction pass therefore reclaims space from
unrelated, still-referenced blobs while the freshly-promoted chunks
survive — reproduced in production as a deterministic NotFound on a
78-byte intermediate output that a build-without-the-bytes client cannot
re-upload.

Add EvictingMap::demote (move entries to the least-recently-used end of
the eviction order without firing callbacks or changing sizes) exposed
via a default-no-op StoreDriver::demote_keys, overridden by the
filesystem and memory stores and forwarded through fast_slow. SpliceBlob
demotes all chunk entries once their contents are fully consumed —
before the EOF releases the assembled blob for commit — so the commit's
eviction pass evicts the reproducible chunks first; the no-op splice
fast path demotes likewise. Regression test reproduces the failure
(fails without the demotion) with an unrelated small blob surviving a
splice that exceeds the store cap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXVtatcR9YMecBiu9RjwpC
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nativelink Ready Ready Preview, Comment Jul 27, 2026 1:01am
nativelink-aidm Ready Ready Preview, Comment Jul 27, 2026 1:01am

Request Review

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.

2 participants