Skip to content

Change the version of C++ used for protoc to the earliest supported. - #147

Merged
allada merged 1 commit into
TraceMachina:masterfrom
chrisstaite-menlo:older_c++
Jul 8, 2023
Merged

allada merged 1 commit into
TraceMachina:masterfrom
chrisstaite-menlo:older_c++

Conversation

@chrisstaite-menlo

@chrisstaite-menlo chrisstaite-menlo commented Jul 8, 2023 •

Copy link
Copy Markdown
Collaborator

This allows builds to continue to work on older platforms.

This closes #146.


This change is Reviewable

This allows builds to continue to work on older platforms.
@aaronmondal

Copy link
Copy Markdown
Contributor

I feel like this is a good change at the moment. We should strive to make this at least C++17 to be in line with what larger repos use, (e.g. Google and LLVM set it to 17), but since this is just used for an external dependency it hardly matters at the moment.

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

:lgtm:

Reviewed 1 of 1 files at r1, all commit messages.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @chrisstaite-menlo)

@allada
allada merged commit b62338b into TraceMachina:master Jul 8, 2023
@chrisstaite-menlo
chrisstaite-menlo deleted the older_c++ branch July 10, 2023 21:02
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
…k GOAWAY-stuck pool

## The wedge

Production blobs reported "not found in any worker" because pooled h2
`Channel`s with stale post-`GOAWAY` state were being silently reused.
The originating worker emitted
`GOAWAY(b"too_many_internal_resets", ENHANCE_YOUR_CALM, Library)`
(fleet-wide pattern: artist, citizen, isotope, julius, garlic, domino,
forget). The h2 client `Channel` retained the dead connection in its
internal `Buffer`/`Reconnect` layers; a subsequent `bs_client(channel)
.read()` call failed synchronously on attempt 1 with
`Code::Internal "Tried to send while stream is closed"`, 0 bytes written.

`worker_proxy_store::try_read_from_worker` saw `Code::Internal` (not
`Code::NotFound`/`DataLoss`), did NOT evict the locality entry, fell
through to the next peer — but its pool was independently affected by
the same fleet-wide GOAWAY storm. After exhausting peers, the server
returned "not found in any worker" → Bazel got `FAILED_PRECONDITION` →
build failed.

The same pool wedges slow-store writes (worker→server): 8-min retry
delays observed for the same digest's mirror write.

## Why the existing recovery didn't fire

`connection_manager::ResponseFuture::poll` already evicts the channel
when the *request future itself* returns `Err` (non-streaming RPCs).
But for streaming RPCs (ByteStream `Read`), errors surface inside the
response stream items (`Some(Err(status))`), not in the request future.
The `Connection` is dropped at the end of `read_internal`'s acquisition
statement — long before the streaming body errors out — and `Drop`
silently re-pools the dead channel as healthy. The next caller pulls
the same dead clone and fails identically.

## The fix

Two complementary mechanisms:

1. **Drop-side eviction** — `Connection::notify_transport_error()` sets
   a flag. `Connection::Drop` checks the flag: if set, sends `Error` +
   `Dropped(None)` to the worker (evicts channel + queues reconnect +
   releases concurrency slot) instead of `Dropped(Some(channel))`.
   Use this when the call site holds the `Connection` past the failure
   moment (currently unused — exists for future call sites that want
   to keep the lease alive across the streaming body).

2. **Pool-side coarse eviction** —
   `ConnectionManager::evict_idle_channel(reason)` removes any one
   currently-idle channel from `available_channels` and queues a
   reconnect for that endpoint slot. Called from
   `GrpcStore::evict_pool_on_transport_err` whenever a transport-shaped
   error (`Code::Internal | Unavailable | Unknown`, classified by
   `looks_like_dead_channel`) bubbles up from a streaming retry path.
   Repeated calls (one per failed retry) progressively replace every
   dead clone in the pool without flushing healthy ones.

   Wired into:
   - `get_part_single_stream` retry loop: both `read_internal` setup
     errors AND streaming-body `Some(Err(status))` errors.
   - `get_part_parallel` per-chunk retry loop: same two sites.
   - `write` retry loop: belt-and-suspenders alongside the existing
     auto-eviction in `ResponseFuture::poll`.

## Dual-direction coverage

Confirmed: `GrpcStore` is the same code on both sides — workers use it
as the slow-store backend (worker→server) and the server uses it as
peer-fetch transport (server→worker, via `WorkerProxyStore`). Both
construct `Transport::Tcp(ConnectionManager::new(...))` in
`GrpcStore::new`, so the fix applies symmetrically without
direction-specific code.

## Tests

Two unit tests in `connection_manager` (real local TCP listener, count
distinct accepts before/after the operation):

* `evict_idle_channel_triggers_reconnect_against_real_endpoint` —
  asserts that calling `evict_idle_channel` triggers exactly one
  additional TCP `accept()` (the reconnect dial) within ~4s.
  Mutation step: comment out the `EvictIdle` arm body — test fails
  with the explicit "accept_count is still N (baseline N)" panic.

* `notify_transport_error_evicts_channel_on_drop` — acquires a
  Connection, calls `notify_transport_error`, drops it, asserts that
  a fresh TCP accept happens within ~4s. Mutation step: comment out
  the `if self.transport_error` branch in `Drop` — test fails
  identically.

Wrapped under explicit deadlines (3s for initial dial, 4s for
reconnect) so a regression manifests as a clear timeout, not a hang.
All 5 connection_manager tests pass; existing 4 grpc_store_test
integration tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
Extends the base h2-stale-channel fix (255cfc09) with reviewer-driven
hardening and the producer-side mitigation surfaced by the GOAWAY
root-cause investigator.

## Connection-manager (consumer-side) hardening

* **Removed dead `notify_transport_error` Drop-side machinery.** Zero
  callers existed; production wedge is unreachable from the leased-
  connection path because `read_internal` drops the Connection at the
  end of its acquisition statement (long before the streaming body
  errs). Pool-side eviction with per-endpoint targeting + dedup is
  the working path.
* **Per-endpoint targeting.** `EvictIdle` now carries
  `endpoint_uri: Option<String>`. With multi-endpoint pools, an evict
  for endpoint A no longer pops an idle channel for endpoint B
  (`evict_idle_targets_specified_endpoint_only` test).
* **Per-endpoint dedup gate.** Concurrent EvictIdle requests within
  `EVICT_DEDUP_WINDOW = 500ms` are absorbed; a 32-way GOAWAY storm
  produces 1 reconnect, not 32 (`evict_idle_storm_dedups_to_single_eviction`
  test, validates with mutation).
* **Tightened `looks_like_dead_channel` predicate.** Added
  `Code::ResourceExhausted` (defensive cover for ENHANCE_YOUR_CALM if
  it ever surfaces directly). Gated `Code::Internal` on a message-
  content check (`"Tried to send while stream is closed"` plus a small
  set of h2-shaped variants) so server-app `make_err!(Internal,...)`
  no longer evicts healthy channels. Production message verified
  against buildcache journal 2026-04-25.

## Sibling-bug fix

* `running_actions_manager.rs:319` (GetTree streaming consumption)
  now calls `grpc_store.evict_pool_on_transport_err` on streaming-body
  status errors. Same pattern as `get_part_single_stream`. Made
  `evict_pool_on_transport_err` `pub` so external streaming consumers
  can apply the recovery.

## Producer-side GOAWAY fix (worker_proxy_store + config)

* **`parallel_chunk_count` default 64 → 16.** Each blob ≥ 8MiB opens
  N concurrent streams against the same peer; race-mode aborts emit
  RST_STREAM per stream. 64 × multiple workers → >1024 RST/sec
  trips hyper's `max_local_error_reset_streams = 1024` budget and
  produces `GOAWAY(too_many_internal_resets, ENHANCE_YOUR_CALM,
  Remote)`. 16 keeps useful parallelism (4× over single-stream)
  while shrinking the abort multiplier 4×.
* **Cooperative loser cancellation.** New
  `WorkerProxyStore::cancel_loser_racer` drops the loser's `rx` first
  (causing the producer's next `tx.send` to fail cleanly), then
  `abort()`s only if the task doesn't exit within a 50ms grace
  window. Replaces all 4 `peer_handle.abort()` / `server_handle.abort()`
  call sites in the race-mode `tokio::select!`. Server-streaming
  Read RPCs still emit RST_STREAM on stream-drop (no END_STREAM
  send-path on one-way receive), but the cooperative path produces
  cleaner drops from the producer's natural exit instead of
  mid-poll cancellation, which tonic/hyper handles better.

## Comment trim

Net ~84 fewer comment lines across `connection_manager.rs` and
`grpc_store.rs`. Removed: 14-line `EvictIdle` essay, 17-line
`evict_idle_channel` rationale, 27-line `looks_like_dead_channel`
exclusion list, 6-line `connection_tx` field comment, 5 inline
callsite comments collapsed to 1 line each. Doc comments retained
for public APIs and non-obvious behavior (per CLAUDE.md).

## Tests

* `looks_like_dead_channel_classifies_codes` — table-driven over
  all gRPC codes plus message variants. Mutation verified.
* `evict_idle_storm_dedups_to_single_eviction` — 32 concurrent
  EvictIdle on a 4-channel pool. Mutation (remove dedup gate)
  produces 2+ extra accepts; with dedup, exactly 1.
* `evict_idle_targets_specified_endpoint_only` — 2 endpoints,
  evict only B. Mutation (ignore endpoint_uri filter) fails the
  test (B doesn't reconnect because A was popped).
* Existing 4 grpc_store integration tests + 6 connection_manager
  tests still pass. Existing 45 worker_proxy_store unit tests
  still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
- stores.rs:1400 doc comment "Default: 64" → "Default: 16" with TraceMachina#147
  rationale (h2 RST_STREAM budget under race-loser drops)
- grpc_store.rs:150 message regex "connection error" → "h2 connection
  error" so MongoDB/peer-app Internal errors carrying "connection
  error: ..." don't false-positive evict the local h2 channel pool

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
… actual blob bytes (Bug B)

Production wedge mechanism (audit
.claude/reviews/wedge-mechanism-mirror-blobs/audit.md, 2026-04-25):
server-side `GrpcStore::get_part` for digests f479989b...-183 and
ddb73a...-183 was called with `length: Some(10_485_760)` (10 MiB —
the bytestream `read_limit` forwarded from a Bazel client RPC). The
pre-fix `effective_length = length.unwrap_or(...)` took that value at
face value, exceeded the 8 MiB `parallel_chunk_read_threshold`, and
routed into `get_part_parallel` with `chunk_count=8`. The math split
the 183-byte blob into 8 ~1.25 MiB sub-ranges, the worker correctly
served chunk 0's 183 bytes + clean Status::OK trailer and EOFed
chunks 1-7 (offsets past blob size). The post-7dc4f0ec `CleanShort`
classifier folds those EOF chunks back into Ok, so the data DOES
arrive — but the splitter still wastes 7 RPCs per read of every tiny
blob, and under concurrent contention the parallel path produces the
"Tried to send while stream is closed" / "in
GrpcStore::get_part_parallel write" wedge shape observed in
production logs (TraceMachina#147 producer-side h2 RST_STREAM bursts amplified
by race-loser JoinHandle::abort()).

Two-layer fix:

1. Call site (`grpc_store.rs:get_part`): clamp `effective_length` to
   `digest.size_bytes() - offset`. `length` is documented as "at most
   N bytes from this offset" — clamping to the actual remaining bytes
   is semantically correct, mirrors the existing pattern in
   `get_action_result_as_part` (`length.unwrap_or(default_len).min(
   default_len)`), and naturally keeps tiny blobs on the
   single-stream path.
2. Defense in depth inside `get_part_parallel`: take a `blob_size`
   parameter and clamp `chunk_count` per the audit-fix formula —
   `chunk_count.min(blob_remaining.div_ceil(threshold).max(1))`. A
   blob smaller than the parallel threshold collapses to
   chunk_count=1; a blob between 8 MiB and 16 MiB gets 2 chunks
   regardless of `parallel_chunk_count=16`. This protects the
   parallel splitter against any future caller that reaches it
   directly (tests, or a hypothetical caller bypassing `get_part`)
   without re-introducing the wasted-RPC pattern.

Test changes (same commit so the test exercises the production
scenario):
- `TinyBlobByteStream` now honors `read_limit` per REAPI semantics
  (was returning the whole tail for any offset).
- Test config switched to production defaults
  (`parallel_chunk_read_threshold = 8 MiB`,
  `parallel_chunk_count = 8`) so the pre-fix code path enters parallel
  iff `length` exceeds 8 MiB — exactly the production wedge shape.

Verification:
- Pre-fix run: `chunk_count=8 requested_chunk_count=8`, test FAILS
  on `expected ≤2 ReadRequest RPCs ... got 8`.
- Post-fix run: `parallel chunked` log absent, single-stream path
  used, test PASSES.
- Mutation step (per CLAUDE.md): comment out either clamp
  individually, test goes red on the same assertion. Documented in
  the test docstring.

Sibling-bug audit (per CLAUDE.md):
- `grpc_store.rs:get_part_parallel` — only chunk-splitter in the
  codebase; fixed in this commit.
- `batch_read_blobs` — forwards the whole request unsplit; not
  affected.
- `memory_store.rs:get_part` — already clamps via
  `length.unwrap_or(default_len).min(default_len)`.
- `filesystem_store.rs:read_file_entry_bytes` — naturally clamped
  by file size on read.
- `compression_store.rs`, `dedup_store.rs`, `redis_store.rs` use
  `length.unwrap_or(MAX)` without an upstream clamp, but they iterate
  over actual stored bytes — not RPC splits — so wasted-RPC class
  doesn't apply. No sibling fix needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
…ket B)

`looks_like_dead_channel` already covered the production wedge signature
("Tried to send while stream is closed") and "h2 connection error", but
hyper / tonic frequently surface the post-GOAWAY shape as the SHORT
form `connection error: the server sent GOAWAY` — without the "h2 "
prefix. The classifier therefore failed to mark the channel as dead
and the caller would not retry on a fresh connection (TraceMachina#147 false-
negative reproducer in the existing
`looks_like_dead_channel_classifies_codes` test).

Fix: add a `m.contains("connection error")` matcher inside the
`Code::Internal` arm. The existing app-level guard (`"verify size
mismatch in CAS upload"`) does not contain that substring and remains
correctly classified as NOT a dead channel. Sibling-bug audit: no other
sites in the workspace duplicate this classifier (`worker_proxy_store`
has its own `is_connection_error` for socket-level codes, scoped
differently); no other `make_err!(Code::Internal, "...connection
error...")` callsites inside our trust boundary that the broadened
match could mis-classify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
…ation for prod h2 channel-reuse trace

Adds info!/warn! logs to disambiguate the four hypotheses for the
production wedge (109k events of "Tried to send while stream is closed"
since restart with no eviction-side logs):

  (a) evict_pool_on_transport_err is never called on the failure path
  (b) evict IS called but the predicate doesn't match / channel pool
      returns the same stale clone
  (c) the err is wrapped between buf_channel send-error and the predicate
  (d) retrier classification is independent of looks_like_dead_channel

New trace points:
- ConnectionManager EvictIdle: dedup-skip and no-victim paths promoted
  from debug! to info! so we observe whether eviction requests are
  swallowed.
- Connection::channel_id_for_log() public accessor exposes the
  (endpoint_idx, connection_idx) pair.
- read_internal: log channel-id for every TCP/Dual-TCP acquire keyed
  on resource_name (join key with downstream get_part logs).
- evict_pool_on_transport_err: log every entry including no-op path,
  with predicate result and msg-head — directly tests hypothesis (c).
- get_part_single_stream writer.send failure: log code+msg-head+
  looks_like_dead+attempt — disambiguates writer-side termination
  (downstream reader dropped) from upstream channel staleness.

Volume estimate: ~10 errors/sec * (1 acquire + 1 fail = 2 lines), so
~20 lines/sec at peak — within "cheap enough to ship permanently"
budget. All info!/warn!, no error!, no allocation in tight loops
beyond what tracing fields already do. Existing unit tests all pass
(looks_like_dead_channel_classifies_codes + 6 connection_manager
tests).
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
… constructor + helpers

Adds the wire-stable discriminator detail message that distinguishes
TraceMachina#212 Q8 backpressure from genuine dead-h2-channel ResourceExhausted.
Without this, every per-blob mpsc-full event would evict the underlying
h2 channel and reproduce the production TraceMachina#147 stale-channel-reuse trace
(commit 7b157d6).

- worker_api.proto: new `BackpressureSignal { Reason reason; uint64
  retry_after_ms }` message in the existing
  com.github.trace_machina.nativelink.remote_execution package.
  Reasons: REASON_UNSPECIFIED, GLOBAL_CHUNK_BUDGET_EXHAUSTED,
  PER_BLOB_MPSC_FULL. Wire-stable per design §3.
- genproto/com.github.....pb.rs: hand-written prost-equivalent Rust for
  the new message (codegen is Bazel-driven; this matches the existing
  convention of committing generated .pb.rs files). Centralizes the
  type_url as `BACKPRESSURE_SIGNAL_TYPE_URL` so producer + consumer can
  never drift.
- nativelink-error: `Error::resource_exhausted_backpressure(msg, any)`
  mirroring the existing `not_found_with_detail` pattern. Same
  decoupling rationale (no nativelink-util dep cycle).
- nativelink-store/chunked_signal.rs (NOT feature-gated; both peers
  must be able to recognise the discriminator regardless of whether
  THIS process emits it):
  * `encode_backpressure_signal_any(reason, retry_after_ms)` — Phase 2
    producer helper.
  * `error_has_backpressure_signal(&err)` — receiver-side classifier
    used by the Phase 1 `looks_like_dead_channel` tightening (next
    commit).
  Tests cover encode-decode roundtrip + signal-present / -absent /
  other-detail-only scenarios.
- nativelink-store: prost-types added as a direct dep so the helper
  compiles without leaning on a transitive re-export.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
…ssureSignal

The historic classifier (TraceMachina#147) treated any `Code::ResourceExhausted` as
a dead h2 channel and triggered eviction. With TraceMachina#212 Q8 admission about
to start emitting tagged `Code::ResourceExhausted` for honest
backpressure (per-blob mpsc full or global byte budget exhausted),
that classifier would tear down the underlying h2 channel at
backpressure rate — replaying the production TraceMachina#147 stale-channel-reuse
trace whenever the global budget saturates.

Fix per design §13.1.1 point 2: the `Code::ResourceExhausted` arm
becomes "dead channel iff the error does NOT carry a
`BackpressureSignal` detail." Bare ResourceExhausted (the legacy h2
ENHANCE_YOUR_CALM shape) continues to evict; tagged ResourceExhausted
does not.

This is a Phase-1 prerequisite — the tightening MUST land before any
Phase 2 admission code emits the tagged status, otherwise the first
backpressure event takes down the connection.

Test extends the existing `looks_like_dead_channel_classifies_codes`
with a sibling `..._respects_backpressure_signal` covering both
reasons (GlobalChunkBudgetExhausted, PerBlobMpscFull) plus the
absence-of-discriminator preservation case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
…ort_err for chunked path

Per perf-optimizer BLOCK B2 (TraceMachina#212 Phase 2.4 worker WriteChunked
client): the legacy ByteStream write/get_part paths call
evict_pool_on_transport_err on transport-shaped failures (the TraceMachina#147
fix that prevents reuse of GOAWAY'd channels). The chunked-write
path skipped this entirely — a GOAWAY-shaped failure would leave the
dead channel sitting in ConnectionManager's pool for the next
request to pick up and immediately re-fail.

Wire the call into update_via_chunked_inner. The
looks_like_dead_channel classifier (used inside
evict_pool_on_transport_err) is the SAME one the legacy paths use
and explicitly excludes ResourceExhausted+BackpressureSignal — so
an honest backpressure rejection from the chunked-write handler
(Q8) does NOT trigger eviction.

Adds chunked_grpc_eviction_test integration test: bind an in-process
WorkerApi server that returns Code::Unavailable, build a GrpcStore
against it with chunked-writes enabled, push a CHUNK_SIZE blob,
assert the TraceMachina#147 trace fires with predicate_matched=true. Adds
chunked_size_cap_test for the B1 part 1 cap.
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
…acity from MemoryStore

Phase 2.6 extends Phase 1's wire-stable BackpressureSignal contract
to a second producer site (in addition to the chunked admission +
WriteChunked handler emission landed in Phase 2.2/2.3). When a
MemoryStore write would force eviction of a recently-inserted blob
to make room, the operator can now opt the store into emitting
Code::ResourceExhausted with a MemoryStoreAtCapacity discriminator
detail INSTEAD of the historic silent-evict behavior. The existing
looks_like_dead_channel classifier in grpc_store.rs:159 already
respects the discriminator (Phase 1 wire-up), so no client-side
change is required for this signal to be classified as transient
backpressure rather than a dead h2 channel.

Architectural-change discipline: changing MemoryStore semantics from
silent-evict to backpressure-error IS architectural per CLAUDE.md.
The change is gated behind BOTH the chunked_fast_slow feature flag
AND a default-OFF runtime kill-switch
(set_emit_backpressure_for_test). The historic behavior is preserved
bit-identically until an operator explicitly toggles the gate, so
this commit ships the mechanism without committing operators to
the new semantics. A follow-up tracker will wire JSON-config plumbing
once the user signs off on the production rollout shape.

Implementation:
- nativelink-util/src/moka_evicting_map.rs: new
  would_exceed_capacity(incoming_bytes) predicate. Best-effort
  snapshot of moka's eventually-consistent weighted_size; biases
  toward emitting backpressure earlier (never silently overruns).
- nativelink-store/src/memory_store.rs: AtomicBool kill-switch +
  check_backpressure_gate() shared by update() and update_oneshot().
  No-op compile-time stub when chunked_fast_slow is OFF.
- nativelink-proto/...: new MEMORY_STORE_AT_CAPACITY = 3 enum value
  (additive — existing 1 and 2 unchanged) committed in the prior
  Phase 2.6 commit.

Asymmetric-contract test coverage (CLAUDE.md mandatory practice for
state-mutating side effects on borrowed values):
- Under-action: emits ResourceExhausted+signal when kill-switch ON +
  at capacity.
- Over-action: NEVER fires when kill-switch OFF (production default);
  silent-evict path preserved.
- Production composition: VerifyStore(MemoryStore) propagates the
  detail through the wrapper without swallowing it.
- Classifier round-trip: producer-emitted error is recognized by
  error_has_backpressure_signal so looks_like_dead_channel correctly
  treats it as backpressure (NOT a dead channel — replaying TraceMachina#147
  would be catastrophic at backpressure rate).

All four tests run under tokio::time::timeout(5s) deadlock detector
with specific assertion messages naming the contract violation.
Mutation step verified: forcing the gate condition to false makes
3 of 4 tests fail (the OFF/preserves-silent-evict test correctly
remains green because its assertion IS the silent-evict path).
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
…S4 partial-state + S5 digest-mismatch

Six new tests covering the contract surfaces the reviewer findings
called out:

**B1 cap (in `bazel_facing_internal_chunking_test.rs`):**
`pin_budget_cap_rejects_admission_with_pinned_bytes_exhausted_signal`
— a PinBudget with cap = 2*CHUNK rejects the 3rd chunk's admission
with `Code::ResourceExhausted` + `BackpressureSignal{ reason:
PinnedBytesExhausted }`. Mutation-tested: `if false` around the
pin-budget acquire → red-fail with the specific anti-TraceMachina#203 message.

**S3 classifier (in `chunked_signal.rs`):**
`new_fixup_variants_roundtrip_and_classify` — both new variants
(`MemoryStoreAtCapacity` from Phase 2.6 + `PinnedBytesExhausted` from
B1) round-trip through proto encode/decode AND are detected by
`error_has_backpressure_signal` so `looks_like_dead_channel` (in
grpc_store) classifies them correctly. Updated the existing
`looks_like_dead_channel_respects_backpressure_signal` test to also
exercise both new variants. Mutation-tested: type_url comparison set
to `false` → red-fail with the specific TraceMachina#147 regression-risk message.

**S4 partial-state (in `bazel_facing_internal_chunking_test.rs`):**
`fast_tier_ok_dispatch_err_records_failed_slow_write` — when chunked
dispatch admission fails (PinBudget=0) but fast-tier write succeeds:
the update returns Err, the fast-tier replica remains visible
(≥2-replica invariant during the recovery window), AND the digest is
in `failed_slow_writes` for retry-on-reconnect. Mutation-tested:
removing the `failed_slow_writes.insert` in
`update_via_chunked_dispatcher`'s err branch → red-fail with the
specific recovery-hook message.

**S5 digest-mismatch on async-commit (in `bazel_facing_internal_chunking_test.rs`):**
`async_commit_digest_mismatch_blocks_canonical_cas_landing` — when
the bytes streamed into the chunked driver hash to a DIFFERENT digest
than declared, the (β) async-commit returns Ok at admission, then the
driver's e2e SHA-256 verify rejects the commit. The canonical CAS
file MUST NOT appear on disk. Mutation-tested: bypassing the e2e
SHA-256 check → red-fail with the specific message about lying bytes
landing.

**Plus accessor additions:**
- `ChunkedReadRegistry::pin_hits_total/pin_misses_total/pin_partial_misses_total`
  (test introspection of the metric counters).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
…romote 1 to warn!

`fast_slow_store` was the dominant log source in today's 10-min anomaly sweep
(14,486 lines / 10 min, ~24/sec — single-handedly larger than every other
crate combined). Each `populate run_producer` invocation emitted 12+ per-call
breadcrumbs at info! level (head_result Ok/Err, data_stream entry/Ok/Err,
slow_store.get entry/Ok/Err, fast_store.update entry/Ok/Err, join3 returned,
send_eof/send_error, run_producer exit Ok/Err). With ~1,440 populate calls
per 10 min in steady state, this alone accounted for ~17K lines of the
volume — dwarfing the 6 sites TraceMachina#262 already demoted in the same file.

Same shape as TraceMachina#262: per-call info! breadcrumbs from TraceMachina#147-trace work that
are no longer load-bearing in steady-state and compound under burst.
Same risk profile: log volume × mimalloc retention contributes to RSS
growth (TraceMachina#255).

Demote (a) — happy-path per-call breadcrumbs that fire once per populate:
  - line 1921 populate head_result Ok       (info → debug)
  - line 1927 populate head_result Err      (info → debug; NotFound is normal)
  - line 1980 populate data_stream branch entry            (info → debug)
  - line 2039 populate data_stream branch Ok               (info → debug)
  - line 2047 populate data_stream branch Err              (info → debug)
  - line 2075 populate slow_store.get branch entry         (info → debug)
  - line 2082 populate slow_store.get branch Ok            (info → debug)
  - line 2087 populate slow_store.get branch Err           (info → debug)
  - line 2101 populate fast_store.update branch entry      (info → debug)
  - line 2111 populate fast_store.update branch Ok         (info → debug)
  - line 2116 populate fast_store.update branch Err        (info → debug)
  - line 2130 populate join3 returned                      (info → debug)
  - line 2175 populate calling streaming_writer.send_eof   (info → debug)
  - line 2188 populate calling streaming_writer.send_error (info → debug)
  - line 2199 populate run_producer exit Ok                (info → debug)
  - line 3508 update_oneshot: background slow write spawned (info → debug)
  - line 3590 update_oneshot: background slow write complete (info → debug)

Promote (c) — populate failure summary, anomalous condition, replaces the 5
intermediate per-step Err lines so operators get exactly ONE warn! per failed
populate (not 5):
  - line 2204 populate run_producer exit failed — slow-store populate did
    not deliver the blob to the fast tier (info → warn!)

Keep (b) — state transitions and once-per-major-event lines stay info!:
  - line 834  chunked admission complete (transfer with throughput)
  - line 1187 shutdown flush enumerated
  - line 1313 shutdown flush completed (transfer with throughput)
  - lines 1488/1497 chunked-read cascade ENABLED/DISABLED (kill-switch state)

Anomaly observability is preserved by:
  - the Err-summary warn! at line 2204 (covers ALL populate failures)
  - the existing PHANTOM BLOB warn! at line 2159 (has-then-NotFound race)
  - the existing background-slow-write watchdog warn! at line 3542
  - the existing background-slow-write FAILED error! at line 3604
  - all pre-existing warn!/error! sites untouched

No tests assert on any of the touched log strings (verified with
`grep -rn logs_contain nativelink-store/tests/`); no test breakage.
The file_gets_cleans_up_on_cache_eviction test in filesystem_store_test.rs
hangs on origin/main without these changes — pre-existing flakiness,
not caused by this diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
…in_flight (1/64)

Invariant violated: two high-volume INFO log sites fire on every read
and every chunked blob admission (~302/10m and ~708/10m respectively),
increasing log overhead without providing signal beyond what the
closed TraceMachina#147 investigation needed and without a volume bound on the
durability path log.

Mechanism that violates it: (a) grpc_store.rs:read_internal emitted
"channel acquired (TraceMachina#147 trace)" at info! on every read — 4 arms (TCP /
QUIC / Dual-TCP / Dual-QUIC); TraceMachina#147 was fixed in 5adf34a and f47a7bd,
so this is leftover instrumentation with no open follow-up. (b)
chunked_write_handler.rs:bazel_facing_v1_dispatcher reaper inline code
emitted "chunked_in_flight removed" (outcome=reaper_decrement) at info!
on every chunked blob admission — the other two paired sites (registered
+ Drop-removal) were already demoted to debug! by the dafcf8b sweep
and compile out in prod (release_max_level_info); this site was added
post-sweep and missed it.

Mechanism that re-establishes it after the fix: (a) remove all 4
read_internal info! calls and the sole-purpose resource_for_log local
binding; ep_idx/conn_idx/post_conn are retained (used in map_err and
return tuples). (b) gate the reaper_decrement info! behind
chunked_inflight_log_sampled(&dig): a deterministic pure function that
folds the first 8 bytes of the digest packed hash into a u64 key and
tests key % 64 == 0 — same digest always yields the same bool so the
(already-debug) registered and removed lines remain paired if the other
sites are ever restored; volume drops from ~708/10m to ~11/10m.

Composite invariants this fix interacts with:
- FL-688 durability path: the "chunked_in_flight removed" signal
  distinguishes writer paths (TraceMachina#247/TraceMachina#477 DS disambiguation). Preserved
  at 1/64 sampling — enough for journal diagnosis, not per-blob.
- release_max_level_info compile-out: debug! sites compile out; the
  remaining info! at the reaper site now has bounded volume.
- refcount bookkeeping logic: unchanged; only the info! emission is
  gated — the map lock, decrement, per-digest wakeup, and FSS-wide
  notify are unconditional.
- No per-RPC timeouts on internal paths: unchanged.

Test that proves the fix re-establishes the invariant:
- sampling_tests::sample_period_constant_is_64: numeric-constant
  discipline — literal 64 at declaration site.
- sampling_tests::sampling_is_deterministic_per_digest: same digest →
  same bool across 5 repeated calls (register/remove pair safety).
- sampling_tests::sampling_rate_is_approximately_1_in_64: 6400 distinct
  synthetic digests → 50–150 true (expected 100); bespoke messages for
  mutation A (% 1 → 6400) and mutation B (false → 0).

Behavior changes:
- grpc_store.rs read_internal: 4 info! removed entirely (~302/10m →
  0). No data-path change; ep_idx/conn_idx/post_conn retained for
  error-path tuple. Verified in: nativelink-store --lib 98/98 pass.
- chunked_write_handler.rs reaper_decrement: info! sampled 1/64
  (~708/10m → ~11/10m). Refcount bookkeeping, wakeup logic, and
  registry semantics: NO change. Verified in: nativelink-service
  --lib sampling_tests 3/3 pass + mutation A/B red-verified.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rejuvenile added a commit to rejuvenile/nativelink that referenced this pull request Aug 19, 2026
…onnect::Reconnect

Root cause (confirmed from prod logs + code): the worker slow-store GrpcStore's
QUIC leg (nativelink-util/src/tls_utils.rs::h3_channel) is a pool of 32 members,
each `tower::buffer::Buffer::new(h3_channel, 1024)`. `tonic_h3`'s inner
`RequestSender::poll_ready` (h3-util-0.0.5 client_conn.rs:159) re-dials on a dead
connection but returns `Err` when that reconnect's `conn.connect()` fails — e.g.
after a quinn idle/connection timeout while the server is momentarily
unreachable. A plain `tower::buffer::Buffer` PERMANENTLY poisons on the first
inner `poll_ready` `Err`: its worker stores a `ServiceError` and exits
(worker.rs:186-202), replaying "buffered service failed: {inner}" instantly to
all clones forever. There was NO rebuild path — `grpc_store.rs::
evict_pool_on_transport_err` is `Transport::Quic(_) => {}`. Prod:
instant_fail_total=35682 vs slow_fail_total=42 — ~42 genuine timeouts latched all
32 members → every single-stream QUIC read instant-failed for hours →
input-fetch fails → action fails → full re-execution.

Fix (verified against tower-0.5.3 source): wrap each pool member's H3 channel in
`tower::reconnect::Reconnect` INSIDE the `Buffer`:
`Buffer::new(Reconnect::with_connection(initial_channel, maker, ()), 1024)`.
`Reconnect::poll_ready` NEVER returns `Err` (reconnect/mod.rs:84-140): a
Connected inner `poll_ready` error loops → `State::Idle` → `make_service`
(transparent reconnect); a make error returns `Ready(Ok)` and stashes the error
for the next `call()` (per-request error, not poison). The `Buffer` poisons ONLY
on an inner `poll_ready` `Err`, so with `Reconnect` inside it the `Buffer` can
NEVER poison. A new `H3ChannelMaker: Service<()>` rebuilds one fresh H3 channel
synchronously (fresh UDP socket + quinn Endpoint) per make; `with_connection`
seeds the eagerly-built initial channel so the existing eager UDP-buffer tuning +
one-warn-per-pool diagnostic and the initial socket-bind timing are unchanged —
only RECONNECTS are lazy. The pool stays 32 independent self-healing members with
identical round-robin behavior. Transport-wide: covers BOTH the single-stream
read path and the write path (both traverse h3_channel).

The existing TraceMachina#147/#FU/TraceMachina#2-Fix-C latch DETECTION + abort machinery in grpc_store.rs
is retained (correct fast-fail); the fix just makes the latch not happen.

Invariant being violated: a transient slow-store (QUIC) readiness failure must
not permanently latch the transport — a subsequent read must succeed — so it
cannot cause unnecessary duplicate action execution or truncated streamed data.
Mechanism that violates it: tls_utils.rs h3_channel wrapping each member as a
bare `Buffer::new(h3_channel, 1024)`, which poisons permanently on the first
inner `RequestSender::poll_ready` `Err` (tower buffer/worker.rs:186-202).
Mechanism that re-establishes it after the fix: tls_utils.rs h3_channel now wraps
each member as `Buffer::new(Reconnect::with_connection(initial_channel, maker,
()), 1024)`; `Reconnect::poll_ready` (tower reconnect/mod.rs:84-140) never
surfaces an `Err` to the `Buffer`, transparently re-making a fresh H3 channel via
`H3ChannelMaker` on a Connected `poll_ready` error.
Composite invariants this fix interacts with: (1) the per-RPC-timeout policy —
unchanged (no timeouts added; liveness stays keepalive + bounded Buffer queue of
1024/member); (2) the round-robin pool selection in `QuicChannel`
(poll_ready/call pairing) — unchanged, still 32 members; (3) the
`looks_like_latched_pool` / abort-threshold fast-fail in grpc_store.rs — retained
and now rarely reached; (4) in-flight stream integrity — `RequestSender::call`
clones its `send_request`, so a reconnect in a later `poll_ready` cannot truncate
an in-flight read.
Test that proves the fix re-establishes the invariant:
tls_utils::reconnect_seam_tests::buffer_with_reconnect_self_heals_after_transient_error
(deterministic seam) and tests/quic_reconnect_selfheal_test.rs::
quic_read_self_heals_after_transient_transport_failure (real quinn/h3).

Behavior changes:
- QUIC READ path (single-stream, Transport::Quic and Transport::Dual QUIC leg): a
  transient quinn timeout / failed inner reconnect → transparent Reconnect
  rebuild instead of a permanent "buffered service failed" latch. Verified safe
  in compositions: WorkerProxyStore → VerifyStore → ExistenceCacheStore →
  SizePartitioning → cas_FAST_SLOW_STORE(MemoryStore→FilesystemStore) with the
  worker slow-store GrpcStore over Dual(TCP+QUIC); read TraceMachina#1 over a live server
  still succeeds identically, and post-failure reads now self-heal
  (nativelink-store grpc tests green; real-transport test green).
- QUIC WRITE path (sibling poison — same h3_channel): identical transparent
  reconnect instead of permanent latch. Verified safe: batch/small-RPC writes and
  chunked write RPCs that route over the QUIC leg now recover from a transient
  timeout rather than instant-failing until process restart. No write ordering,
  ack, or durability semantics change (no fsync/sync primitives; mirror_blobs +
  BIS ack unchanged; Buffer queue still capped at 1024/member).
- Pool construction: the initial 32 members are still built eagerly (same UDP
  bind + tune_quic_udp_buffers + one-warn-per-pool). Only the RECONNECT path is
  new/lazy. No change to non-QUIC (TCP) transport, and all new code is
  `#[cfg(feature = "quic")]`-gated (non-quic build unchanged).
- New buffering fields: none on any network path (the Buffer cap stays 1024;
  H3ChannelMaker holds only cloned config/uri/name + an AtomicU64 generation
  counter — no owned-bytes buffers).

Reproduce-first TDD: Test A (seam) — plain Buffer permanently latches after a
transient poll_ready Err (documents the bug); Buffer+Reconnect self-heals;
mutation dropping Reconnect fails with the bespoke never-poison message. Test B
(real quinn/h3) — dead-port-first forces the exact `poll_ready` Err at the
transport seam; fixed build heals; mutation dropping Reconnect fails end-to-end
with the production signature "buffered service failed: timed out" in
GrpcStore::read (quic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

Unable to build on Ubuntu 20.04

3 participants