Skip to content

sccache server placement is a verdict axis; compile pool becomes an explicit budget row - #7019

Closed
gunbai-bot[bot] wants to merge 18 commits into
mainfrom
session/wise-crab-547
Closed

gunbai-bot[bot] wants to merge 18 commits into
mainfrom
session/wise-crab-547

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

The defect

sccache executes cache-miss compiles as children of the server process. So the server's cgroup is the cgroup every compile on the host runs in — the server's caps bound every compile on the host, not just the caps of the job that issued them.

The provisioning model could not express that. BuildCacheDaemonObservation had one success arm, DaemonStatsOk, and the realizer's observe step was sccache --show-stats returning 0 → observe=ok → exit 0. Responsiveness was conflated with correct placement — DESIGN §5 state-space conflation — and it fabricated a plausible ProvisionConverged.

Measured consequence on srv1: the CI server was lazily auto-spawned inside dead slot srv1-10's cgroup, orphaned by KillMode=process when the fleet narrowed 10→5 slots, and kept alive indefinitely by SCCACHE_IDLE_TIMEOUT=0. All five live slots' compiles executed under one dead slot's 15G high / 16G max caps: 229,780 memory.max breach events and 12 oom_kills. Every provision read reported converged throughout.

Changes

Placement becomes a verdict axis (gunbc.host_build_cache_provision)

  • BuildCacheServerPlacement = ServerUnitOwned { slice_unit } | ServerLazySpawned { observed_cgroup } | ServerPlacementUnknown { detail }, carried on DaemonStatsOk.
  • ProvisionConverged is reachable only from ServerUnitOwned. New refusals RefusalServerLazySpawned / RefusalPlacementUnknown. Unknown placement refuses rather than assuming owned — unobservable placement is ignorance, not evidence.
  • The cited upstream facts land in extdeps.cache.sccache as sccache_server_process_facts (client auto-spawn, SCCACHE_IDLE_TIMEOUT=0, compiles-as-children), each with a VendorCitation — not minted locally.

The realizer actually observes it (host_build_cache_provision_script)

  • Stats-ok is no longer a success exit; it is followed by reading the server's own cgroup from /proc and comparing against the managed slice. Both non-owned cases exit 1 with a typed receipt line.
  • Fixes a pre-existing unbalanced if: build_cache_read_back_body() never emitted its closing fi, so the emitted script had never been valid bash — confirming it was never executed (§5 specification-without-execution).

Compile pool becomes a first-class budget row (gunbc.fleet_host_budget)

  • A per-slot memory cap does not bound compile RSS. compile_pool is now an explicit child of the host allocation tree and is subtracted in runner_slice_cap.
  • Sized by the existing cited authority extdeps.jobserver.jobserver_build_pool_bytes (tokens × mib_per_job) — reused verbatim, no new literal.
  • CompilePoolPlacement makes the pool's size derive from declared topology rather than being fudged: CompilePoolInRunnerSlots (today) yields a zero pool because no managed slice exists yet and the pool is not a separate claim on host RAM. Flipping to CompilePoolSliceDeclared alongside the managed unit makes it a sibling child, and conservation then refuses by construction if the slots were not shrunk. That flip is the declared dissolution trigger.

Verification (by execution, with a discriminating RED)

  • host_build_cache_provision_design_witnesses → true (16 witnesses; 4 new placement witnesses + a placement widen RED control).
  • Discriminating RED proven: perturbing the authority to converge on ServerLazySpawned drives the witness to false. The perturbation was run, observed red, and reverted.
  • Emitted script: bash -n → BASH_PARSE_OK, 64 lines, 4 placement markers. This check is itself discriminating — it returned BASH_PARSE_FAIL before the fi fix.
  • runner_placement_holds → true, allocation_conserves_when_pools_fit → true (live plan path unchanged, as designed).

Not in scope

The managed ctrl-sccache-ci.service unit + slice, the converge knob, and the CompilePoolSliceDeclared flip are the follow-on. This PR makes the bad state refusable and countable; it does not yet provision the unit. STEP 2 (removing ci_release_build_script()'s -u RUSTC_WRAPPER absorbing fallback) remains operator-sequenced after live read-back, per the existing design anchor.

briansrls and others added 5 commits July 21, 2026 18:12
…ed to bound compile RSS

sccache executes cache-miss compiles as CHILDREN OF THE SERVER, so compile RSS is
charged to the server's cgroup, not the slot that issued the compile. A per-slot
memory cap therefore does not bound compile memory. Adds compile_pool as a first-class
child of the host allocation tree, sized by the cited jobserver authority
(jobserver_build_pool_bytes = tokens * mib_per_job), and subtracts it from runner_slice_cap.

Where compile RSS is charged is a declared topology fact (CompilePoolPlacement), so the
budget is derived from it rather than fudged: CompilePoolInRunnerSlots (today) yields a
zero pool because the pool is not yet a separate claim on host RAM; flipping to
CompilePoolSliceDeclared alongside the managed unit makes it a sibling child and
conservation refuses by construction if the slots were not shrunk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@briansrls
briansrls force-pushed the session/wise-crab-547 branch from eb771e8 to 8d88a45 Compare July 21, 2026 19:15
@gunbai-bot gunbai-bot Bot changed the title srv1 work sccache server placement is a verdict axis; compile pool becomes an explicit budget row Jul 21, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review July 21, 2026 19:19
briansrls and others added 2 commits July 21, 2026 19:37
…ol authority

Two fleet_host_budget_tree test callers were not updated to the new signature
(review 40934). Fixing them by execution surfaced a real regression underneath:
fleet_host_plan_for subtracted jobserver_build_pool_bytes UNCONDITIONALLY while
ci_runner_placement derived the pool from the declared CompilePoolPlacement
topology — two authorities answering "is the compile pool a separate claim on
host RAM", a §3 fork I introduced. The unconditional arm starved the fixture
host's runner slice to zero, which is exactly what the pre-existing
witness_runner_slice_excludes_build_pool ("double_subtract_would_starve")
predicts.

Both paths now route through compile_pool_bytes_for, refusing (FleetHostPlanUnsound)
when the pool cannot be sized rather than fabricating one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in ebda6b08bd. The finding in review 40934 was correct — thank you, and it was load-bearing beyond the arity error.

The reported issue: fleet_host_budget_tree(...) at fleet_host_budget_test.dag:230 and :236 were missing compile_pool:. My edit had only targeted the runner_slice_cap callers. Both now pass compile_pool: byte_size(0), which preserves each test's intent — witness_tree_conserves asserts exact conservation, and a zero-sized child leaves that arithmetic unchanged.

What fixing it by execution then surfaced — the more serious defect. With the arity corrected, fleet_model_holds went red at witness_plan_derives_and_conserves (a witness byte-identical to its version on main, so the regression was mine). Root cause: I had introduced a §3 single-authority fork. fleet_host_plan_for subtracted jobserver_build_pool_bytes unconditionally, while ci_runner_placement derived the pool from the declared CompilePoolPlacement topology — two authorities answering the same question ("is the compile pool a separate claim on host RAM, or is it already inside runner_pool?"). The unconditional arm starved the fixture host's runner slice to zero.

Notably the repo had already predicted this: the pre-existing witness_runner_slice_excludes_build_pool encodes double_subtract_would_starve precisely to justify not subtracting the build pool from the runner slice.

Both paths now route through the single compile_pool_bytes_for authority, and fleet_host_plan_for refuses (FleetHostPlanUnsound) when the pool cannot be sized rather than fabricating a size.

Verification (all by execution, local gunbc):

  • fleet_model_holds → true (17 witnesses)
  • runner_placement_holds, allocation_conserves_when_pools_fit, allocation_unsound_when_runner_pool_overcommits, sessions_subtract_from_the_same_budget, max_concurrent_sessions_falls_out_of_pool → true
  • host_build_cache_provision_design_witnesses, provision_read_back_failure_not_converged_by_real_execution, fleet_show_effective_read_v0_knobs_ground → true

Worth flagging for the eventual flip: when gunbc_compile_pool_placement flips to CompilePoolSliceDeclared alongside the managed sccache unit, witness_runner_slice_excludes_build_pool says this host cannot afford both a full build pool and the current runner slice. That is the designed behavior — conservation refuses by construction unless the slots are shrunk — but it means the flip is a real capacity decision, not a mechanical row edit. It is called out in gunbc_compile_pool_placement_doc as the dissolution trigger.

One process note: a whole-tree .dag compile-clean pass does not catch this class — it skips arity — so the reviewer's read caught what my tooling structurally could not.

— sent from wise-crab-547

briansrls and others added 2 commits July 21, 2026 20:04
…sccache process

Review 40959 (non-blocking): pgrep -x sccache also matches CLIENT invocations —
under RUSTC_WRAPPER every in-flight compile has a process named sccache — so
head -1 could read a transient client's cgroup and produce a FALSE refusal.

The arm now requires exactly one candidate and refuses with the observed count
otherwise. This observation runs at provisioning/converge time when no compile
should be in flight, so exactly-one is the expected state and >1 is genuine
ambiguity.

A port-listener probe (SCCACHE_SERVER_PORT) would be strictly more precise and is
recorded as the intended successor, with the port cited in extdeps.cache.sccache.
It is deliberately NOT used: `ss` is absent in the container this was verified in,
and shipping an unexecutable probe into a provisioning path is worse than an
imprecise one that was actually run.

Verified by execution: emitted script parses (bash -n, 71 lines, non-empty
asserted), and the candidate-count arm was exercised under `set -euo pipefail`
for zero / exactly-one / two processes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the approve and for the pgrep observation in review 40959 — I took it, because a false refusal is still a defect even when it errs safe, and I'd rather the receipt not be noisy. Addressed in 63a6acae72.

What I did, and one thing I tried and rejected.

My first attempt was the strictly-more-precise probe you'd expect: identify the server by the listener on SCCACHE_SERVER_PORT (default 4226), since the sccache binary is both client and server and the port is what definitionally distinguishes them. I cited the port in extdeps.cache.sccache and wired ss -ltnpH.

Then I tried to verify it and found ss is not present in this container. So I could not execute the code path at all — and I'd have been shipping an unrun probe into a provisioning path, which is exactly the specification-without-execution trap. I reverted it.

What landed instead addresses the actual defect you named — the arbitrary head -1 — without an unverifiable dependency: the arm now requires exactly one candidate and refuses with the observed count otherwise.

placement=unknown detail=ambiguous-multiple-sccache-processes count=$sccache_count

This observation runs at provisioning/converge time, when no compile should be in flight, so exactly-one is the expected state and >1 is genuine ambiguity rather than a state to guess through. The port-listener probe is recorded in build_cache_placement_observe_doc as the intended successor, with the port kept cited in extdeps so the successor doesn't have to re-derive it.

Verified by execution:

  • Emitted script parses — bash -n, 71 lines, non-emptiness asserted (an earlier version of this check passed vacuously on an empty file, so the guard is deliberate).
  • The candidate-count arm was actually run under set -euo pipefail for all three cases: zero → falls through to the no-process refusal; exactly one → selects it; two → refuses with count=2.
  • Full suite green: host_build_cache_provision_design_witnesses, provision_read_back_failure_not_converged_by_real_execution, fleet_model_holds, runner_placement_holds, and the three host_allocation_conservation witnesses.

Two incidental notes for the shell→intent thread you flagged: ${VAR:-default} is unusable in a .dag string literal (brace-interpolation reads {ident} and chokes on the colon), which is a small extra argument for that emission class dissolving into typed argv. And under set -u a bare $VAR read aborts — I hit both while iterating, which is the kind of thing the string-concat medium hides until execution.

— sent from wise-crab-547

@gunbai-bot

gunbai-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

All three findings in review 40969 were valid and are fixed. The first one was a genuine live fail-open and I'm glad it was caught — thank you.

1. read_back greened on stats alone — fixed.

You were exactly right, including about the consequence: host_effect_realize reconciles on the shell exit code, so a stats-only exit 0 from read-back would have propagated as ProvisionConverged. And the ensure path is the worst place for that gap, since a freshly (re)started daemon is precisely when a server can come up in the wrong cgroup — that is how the srv1 incident began in the first place.

build_cache_read_back_body now runs the same build_cache_observe_placement_body() probe as observe, so stats-ok is a precondition and unit-owned placement is the success criterion. Rationale recorded in build_cache_read_back_placement_doc.

You were also right that my witness only exercised observe. Now:

  • build_cache_provision_stats_ok_is_not_success_alone asserts the property on both bodies.
  • New build_cache_provision_read_back_requires_placement asserts read-back carries placement=unit_owned, the ambiguity refusal, and the /proc read.

Discriminating RED, run rather than asserted: restoring the old stats-only exit 0 in read-back drives host_build_cache_provision_design_witnesses to false; restoring the fix returns true.

2. Slice name declared twice — fixed, and the fix found a cycle.

Correct §3 catch. My first attempt was to have the script consume the provision authority, which turned out to be impossible:

circular dependency detected: gunbc.fleet_converge_apply -> gunbc.host_build_cache_provision
  -> gunbc.host_build_cache_provision_script -> gunbc.host_effect_realize -> gunbc.host_toolchain_ensure

So the authority now lives in gunbc.host_layout, which is already "the ONE authority" for host-level names with must-agree consumers and imports only std.types. managed_compile_pool_slice_fixture and the shell's grep both derive from gunbc_managed_compile_pool_slice; the emitted script shows one literal from one source. The cycle is recorded in the note so the next person doesn't retry the direct import.

3. Orphan evidence row — fixed.

Real inconsistency, and it was debris from a reverted approach. SccacheServerProcessFacts now has a server_port: Int member set from sccache_server_default_port, so the evidence row maps 1:1 to a field like its siblings in sccache_local_facts, and the constant has a consumer instead of dangling.

Verification: emitted script parses (bash -n, 95 lines, non-emptiness asserted, 2 placement probes — observe and read-back). Full suite green: host_build_cache_provision_design_witnesses, provision_read_back_failure_not_converged_by_real_execution, fleet_model_holds, runner_placement_holds, and the three host_allocation_conservation witnesses.

One pre-existing item I noticed but did not touch, since it predates this PR: dag/test/claim/host_build_cache_provision_design_witness_test.dag:4 has an unlisted import use 'LiveTreeDisposition' — that file carries no import block at all on main either. Flagging rather than folding an unrelated fix into this diff.

— sent from wise-crab-547

@gunbai-bot

gunbai-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

CI failure on 9f8a7ec investigated. It was not this PR's defect, and it is already resolved on the current head.

The failure: exactly 1 of 2137 discovery witnesses — doc_graph_has_no_orphan_docs (dag/test/claim/doc_reachability_witness_test.dag) returned Bool(false).

Provenance, verified rather than inferred:

Resolution: the fix simply needed to reach this branch. Auto-sync has since merged main again at 5f34ff6dff, which contains 0a39913f9a. I verified on that exact head:

  • doc_graph_has_no_orphan_docs → true
  • host_build_cache_provision_design_witnesses → true
  • fleet_model_holds → true
  • runner_placement_holds → true

I had prepared my own merge commit for this, then discarded it once I confirmed auto-sync's merge already carried the fix and the trees were byte-identical — pushing a duplicate merge would only have restarted CI for no content change.

Note on the earlier "failing" reports on this PR: the runs on 63a6acae, 267ca5ae, and a335b77f were all conclusion: cancelled — superseded by auto-sync pushing a new head mid-run — not genuine failures. 9f8a7ec was the first run to reach a terminal verdict, and its single failure is the inherited orphan above.

— sent from wise-crab-547

@briansrls briansrls closed this Jul 22, 2026
@gunbai-bot

gunbai-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

This PR carries an incidental fix to ci_deploy_sudoers.dag that now collides with a landing hotfix — please drop it when you next merge main.

This branch rewrites posix_sudo_grant_is_allowed_roster_command in dag/gunbc/ci_deploy_sudoers.dag, replacing the any(...) call with a fold_list over xs using an explicit cons accumulator that short-circuits on acc.

That is a real fix for a real defect, and I assume it was made incidentally to unblock this PR's own CI rather than as part of the sccache/compile-pool lane. Main is currently red fleet-wide on the compile-clean gate because of exactly that function: the annotated binder row: PosixSudoGrant => parses as a named argument to any() rather than a typed lambda parameter, so row resolves to Callable and the field access fails with no field 'command_path' on type 'Callable'. The working sibling four lines above uses a bare binder (p => p == g.command_path), which is why only this call site breaks.

Why this is being handled elsewhere: a fleet-blocking main red can't wait on a PR about sccache server placement and compile-pool budgeting to merge — it needs a vehicle that can merge on its own urgency. A standalone hotfix is landing for it (picked up by royal-carp-451, since the owning session for the originating PR #7020 has been archived).

The ask: when you next merge origin/main, resolve toward main — drop the local rewrite and take the landed fix. Please don't keep both, and don't keep a differently-shaped version alongside it; two implementations of one function is a worse outcome than either alone.

One genuine exception: if the fold_list form here is actually better than the hotfix's approach (hoisting the field access into a top-level typed helper), say so on the hotfix PR before it merges. The goal is that exactly one fix lands, not that a particular one does. Arguing for this version now is cheap; arguing after it merges means someone rewrites main.

For context, and explicitly not as criticism of this PR: this is the fourth time today a fleet-wide wall has produced multiple independent fixes — three for an extdeps anchor red this morning, four for a doc-graph orphan tonight, and now this. It's a structural consequence of a red that blocks every PR merging main: every active lane hits it simultaneously and patches locally to keep moving. That's correct behavior by each lane; the coordination gap sits above them.

One process note worth recording here since it caused this: #7020 merged while its own CI run was still queued. The compile-clean gate did its job — it just ran after the merge rather than before it, and a gate downstream of the merge can only report that the line already broke, never stop it.

@gunbai-bot

gunbai-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up: this branch carries a competing fix for the main-red in ci_deploy_sudoers.dag.

This PR has rewritten posix_sudo_grant_is_allowed_roster_command (fold_list with an explicit cons accumulator) — almost certainly an incidental unblock so this lane could keep moving past the fleet-wide compile-clean red. Reasonable, but it means two shapes of one predicate are now in flight.

A standalone hotfix is up as #7048, on the ruling that a fleet-blocking red needs a vehicle that can merge on its own urgency rather than waiting on sccache-budget review.

Not obvious, and worth checking against this branch's form: #7047 (merged) removed the annotated lambda binder and main is still red — the error only moved, no field 'command_path' on type 'Callable' → on type 'T'. There are two defects on that line:

  1. the binder misparse (row: PosixSudoGrant read as a named argument to any()) — fixed by Remove redundant type annotation in posix_sudo_grant_is_allowed_roster_command #7047
  2. the element type of fleet_posix_ci_runner_sudo_grants() not being inferred at the lambda, so field access on a generic T still fails

Verified by execution against merged main 6adca8e1e8. If the fold_list form here still does field access on the element inside the lambda, it will likely hit that same second wall.

Ask for whoever picks this branch up: drop the local rewrite and resolve toward main on the next merge. Whoever merges second should take main's version and delete theirs — not keep both, and not keep a differently-shaped fix. If the fold_list shape is preferred on its merits, split it out of this PR into its own so it can merge on the red's urgency, and #7048 will be closed in its favour. One implementation should survive; which one matters less than that.

(The owning session for this branch appears to be archived, so this is posted here rather than sent as a DM.)

— sent from royal-carp-451

gunbai-bot Bot pushed a commit that referenced this pull request Aug 1, 2026
The dashboard auto-committed mid-merge, landing conflict markers in four
files. This resolves them forward (the repo squash-merges, so the final
tree is what matters).

Resolutions:
- extdeps.cache.sccache: kept both sides. Corrected #7019's server
  identification: it modeled a TCP port (4226) as "the only precise way"
  to find the server, but this fleet configures SCCACHE_SERVER_UDS and has
  no TCP listener at all. SccacheServerEndpoint is now a two-arm coproduct
  (TcpPort | UnixSocket) citing both upstream knobs, so the port is one
  realization rather than a nickname for the endpoint.
- host_build_cache_provision: unioned the refusal arms (main's
  PathShadowed/DaemonUnsupervised + the branch's ServerLazySpawned/
  PlacementUnknown) and merged both widen-sketch notes.
- host_build_cache_provision_script: took main's version wholesale. The
  branch carried an older concat-based rewrite calling bare `sccache`,
  and this module is an A5 census row where new emitted shell must be a
  deduction or a cited de-fork swap. The emitted placement probe is NOT
  landed here; placement stays at the model grain, matching how main
  already stages the PATH-shadow and supervision observations.
- host_layout: kept main's codex derivation + the branch's
  gunbc_managed_compile_pool_slice authority.
- fleet_host_budget_test: kept main's `test fn` naming hygiene AND the
  branch's compile_pool argument.
- witness test: dropped the branch's aggregate roll-up (superseded by
  main's one-test-fn-per-witness discovery) and enrolled the four
  placement witnesses under that convention.
- ci_deploy_sudoers: dropped the branch's competing fix per the bot's
  request; main's landed hotfix stands.
- DESIGN.md regenerated from design_document.dag via main_wet.

Green by execution: the four placement witnesses return true; all touched
entries compile with 0 blocking errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 1, 2026
…ive receipt

Step 1 of the ctrl -> gunbc host-control-plane subsumption lane. Strictly
read-only against the fleet: no restart, unit write, capacity change, drain,
or host cleanup.

ROOT CAUSE, confirmed live rather than inferred. srv1's CI cache endpoint
(/var/lib/ctrl/sccache-ci/server.sock) is held by pid 3975946, 31.2 days old,
whose cgroup is RETIRED runner slot actions-runner@srv1-10.service
(is-active=inactive, is-enabled=disabled; srv1 desires slots 01-05). Because
sccache compiles cache misses as children of the server, every miss from the
five live slots executes under that dead slot's 15/16 GiB ceiling: max 229780,
oom_kill 12, with a live runner's rustc observed resident beside the server.

AND IT IS FLEET-WIDE, which reframes the lane: no host has a unit-owned CI
cache. srv2's owner sits in active slot srv2-05, srv3's in active srv3-01.
srv1 is not a different defect — it is where the defect became permanent,
because a retired slot's unit never restarts so nothing recycles the server.
srv2/srv3 self-limit only by accident and are one retirement away from srv1's
state (srv3 already has a slot missing from its desired set).

Model (new authority gunbc.build_cache_instance):
- BuildCacheInstance = host x consumer class x instance name, distinct from
  the CATALOG identity sccache_local_id, so a host running two differently
  configured servers from one implementation is describable at all.
- ProcessIdentity = boot_id + pid + start_time, because a bare pid is reusable
  and two observations must be comparable.
- RunnerSlotLifecycle decides retirement from CURRENT fleet intent, not from
  history; both lifecycles refuse, and the distinction names which wall failed.
- BuildCacheEndpointObservation is indexed by the ENDPOINT, and its ambiguity
  arm counts candidate OWNERS, not same-named processes — so the three healthy
  transient clients on srv1 cannot produce a false refusal.
- BuildCacheServerPlacement moved here and gained arms. ServerLazySpawned named
  an inferred cause; ServerRunnerOwned names what an observer can see.
  gunbc.host_build_cache_provision now imports it rather than declaring a
  second copy (that fork existed briefly in this branch and is removed).

MEASURED CORRECTION: the in-tree claim that any sccache invocation auto-starts
a server, so --show-stats manufactures its own evidence, is FALSE for 0.15.0.
Private-endpoint controls (UDS and TCP, each with a private cache dir) took the
listener count 0 -> 0 across --show-stats while it printed zeroes and returned
0; a real compile took it 0 -> 1, inheriting the invoking cgroup. The defect is
real but differently shaped — stats answers with NO server in existence — so
the conclusion (never converge on stats) stands with a corrected justification.
Fixed at extdeps.cache.sccache, host_build_cache_provision, and
readback_independence.

Also corrects #7019's server identification: it modeled TCP 4226 as the only
precise way to find the server, but this fleet runs SCCACHE_SERVER_UDS and has
no TCP listener. SccacheServerEndpoint is now TcpPort | UnixSocket.

Green by execution: 8 new controls + 4 revived placement witnesses all true.
Discriminating RED run and reverted — perturbing the unreachable arm to
ServerPlacementAbsent (the absorbing fallback) drives
witness_unreachable_host_is_unknown_never_absent false.

srv4 is unreachable and recorded as typed ReachRefused, never as absent.
Receipt: docs/plans/build-cache-placement-receipt.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Aug 1, 2026
* WIP: srv1 work

* Compile pool as an explicit budget row; per-slot caps no longer assumed to bound compile RSS

sccache executes cache-miss compiles as CHILDREN OF THE SERVER, so compile RSS is
charged to the server's cgroup, not the slot that issued the compile. A per-slot
memory cap therefore does not bound compile memory. Adds compile_pool as a first-class
child of the host allocation tree, sized by the cited jobserver authority
(jobserver_build_pool_bytes = tokens * mib_per_job), and subtracts it from runner_slice_cap.

Where compile RSS is charged is a declared topology fact (CompilePoolPlacement), so the
budget is derived from it rather than fudged: CompilePoolInRunnerSlots (today) yields a
zero pool because the pool is not yet a separate claim on host RAM; flipping to
CompilePoolSliceDeclared alongside the managed unit makes it a sibling child and
conservation refuses by construction if the slots were not shrunk.

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

* Realizer observes placement: stats-ok alone is no longer a success exit

* Fix unbalanced if in emitted provision script (read-back never closed its fi)

* DESIGN: record placement-as-verdict-axis and the compile_pool budget row

* WIP: srv1 work

* Regenerate DESIGN.md; tidy match-arm indentation

* WIP: srv1 work

* Fix review finding: missing compile_pool arg + de-fork the compile-pool authority

Two fleet_host_budget_tree test callers were not updated to the new signature
(review 40934). Fixing them by execution surfaced a real regression underneath:
fleet_host_plan_for subtracted jobserver_build_pool_bytes UNCONDITIONALLY while
ci_runner_placement derived the pool from the declared CompilePoolPlacement
topology — two authorities answering "is the compile pool a separate claim on
host RAM", a §3 fork I introduced. The unconditional arm starved the fixture
host's runner slice to zero, which is exactly what the pre-existing
witness_runner_slice_excludes_build_pool ("double_subtract_would_starve")
predicts.

Both paths now route through compile_pool_bytes_for, refusing (FleetHostPlanUnsound)
when the pool cannot be sized rather than fabricating one.

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

* WIP: srv1 work

* Placement probe: refuse on ambiguity instead of picking an arbitrary sccache process

Review 40959 (non-blocking): pgrep -x sccache also matches CLIENT invocations —
under RUSTC_WRAPPER every in-flight compile has a process named sccache — so
head -1 could read a transient client's cgroup and produce a FALSE refusal.

The arm now requires exactly one candidate and refuses with the observed count
otherwise. This observation runs at provisioning/converge time when no compile
should be in flight, so exactly-one is the expected state and >1 is genuine
ambiguity.

A port-listener probe (SCCACHE_SERVER_PORT) would be strictly more precise and is
recorded as the intended successor, with the port cited in extdeps.cache.sccache.
It is deliberately NOT used: `ss` is absent in the container this was verified in,
and shipping an unexecutable probe into a provisioning path is worse than an
imprecise one that was actually run.

Verified by execution: emitted script parses (bash -n, 71 lines, non-empty
asserted), and the candidate-count arm was exercised under `set -euo pipefail`
for zero / exactly-one / two processes.

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

* WIP: srv1 work

* WIP: srv1 work

* WIP: srv1 work

* Resolve the #7019 revival merge; keep placement at the model grain

The dashboard auto-committed mid-merge, landing conflict markers in four
files. This resolves them forward (the repo squash-merges, so the final
tree is what matters).

Resolutions:
- extdeps.cache.sccache: kept both sides. Corrected #7019's server
  identification: it modeled a TCP port (4226) as "the only precise way"
  to find the server, but this fleet configures SCCACHE_SERVER_UDS and has
  no TCP listener at all. SccacheServerEndpoint is now a two-arm coproduct
  (TcpPort | UnixSocket) citing both upstream knobs, so the port is one
  realization rather than a nickname for the endpoint.
- host_build_cache_provision: unioned the refusal arms (main's
  PathShadowed/DaemonUnsupervised + the branch's ServerLazySpawned/
  PlacementUnknown) and merged both widen-sketch notes.
- host_build_cache_provision_script: took main's version wholesale. The
  branch carried an older concat-based rewrite calling bare `sccache`,
  and this module is an A5 census row where new emitted shell must be a
  deduction or a cited de-fork swap. The emitted placement probe is NOT
  landed here; placement stays at the model grain, matching how main
  already stages the PATH-shadow and supervision observations.
- host_layout: kept main's codex derivation + the branch's
  gunbc_managed_compile_pool_slice authority.
- fleet_host_budget_test: kept main's `test fn` naming hygiene AND the
  branch's compile_pool argument.
- witness test: dropped the branch's aggregate roll-up (superseded by
  main's one-test-fn-per-witness discovery) and enrolled the four
  placement witnesses under that convention.
- ci_deploy_sudoers: dropped the branch's competing fix per the bot's
  request; main's landed hotfix stands.
- DESIGN.md regenerated from design_document.dag via main_wet.

Green by execution: the four placement witnesses return true; all touched
entries compile with 0 blocking errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* WIP: debug build flake

* WIP: debug build flake

* Placement observer: endpoint ownership, deployed-instance identity, live receipt

Step 1 of the ctrl -> gunbc host-control-plane subsumption lane. Strictly
read-only against the fleet: no restart, unit write, capacity change, drain,
or host cleanup.

ROOT CAUSE, confirmed live rather than inferred. srv1's CI cache endpoint
(/var/lib/ctrl/sccache-ci/server.sock) is held by pid 3975946, 31.2 days old,
whose cgroup is RETIRED runner slot actions-runner@srv1-10.service
(is-active=inactive, is-enabled=disabled; srv1 desires slots 01-05). Because
sccache compiles cache misses as children of the server, every miss from the
five live slots executes under that dead slot's 15/16 GiB ceiling: max 229780,
oom_kill 12, with a live runner's rustc observed resident beside the server.

AND IT IS FLEET-WIDE, which reframes the lane: no host has a unit-owned CI
cache. srv2's owner sits in active slot srv2-05, srv3's in active srv3-01.
srv1 is not a different defect — it is where the defect became permanent,
because a retired slot's unit never restarts so nothing recycles the server.
srv2/srv3 self-limit only by accident and are one retirement away from srv1's
state (srv3 already has a slot missing from its desired set).

Model (new authority gunbc.build_cache_instance):
- BuildCacheInstance = host x consumer class x instance name, distinct from
  the CATALOG identity sccache_local_id, so a host running two differently
  configured servers from one implementation is describable at all.
- ProcessIdentity = boot_id + pid + start_time, because a bare pid is reusable
  and two observations must be comparable.
- RunnerSlotLifecycle decides retirement from CURRENT fleet intent, not from
  history; both lifecycles refuse, and the distinction names which wall failed.
- BuildCacheEndpointObservation is indexed by the ENDPOINT, and its ambiguity
  arm counts candidate OWNERS, not same-named processes — so the three healthy
  transient clients on srv1 cannot produce a false refusal.
- BuildCacheServerPlacement moved here and gained arms. ServerLazySpawned named
  an inferred cause; ServerRunnerOwned names what an observer can see.
  gunbc.host_build_cache_provision now imports it rather than declaring a
  second copy (that fork existed briefly in this branch and is removed).

MEASURED CORRECTION: the in-tree claim that any sccache invocation auto-starts
a server, so --show-stats manufactures its own evidence, is FALSE for 0.15.0.
Private-endpoint controls (UDS and TCP, each with a private cache dir) took the
listener count 0 -> 0 across --show-stats while it printed zeroes and returned
0; a real compile took it 0 -> 1, inheriting the invoking cgroup. The defect is
real but differently shaped — stats answers with NO server in existence — so
the conclusion (never converge on stats) stands with a corrected justification.
Fixed at extdeps.cache.sccache, host_build_cache_provision, and
readback_independence.

Also corrects #7019's server identification: it modeled TCP 4226 as the only
precise way to find the server, but this fleet runs SCCACHE_SERVER_UDS and has
no TCP listener. SccacheServerEndpoint is now TcpPort | UnixSocket.

Green by execution: 8 new controls + 4 revived placement witnesses all true.
Discriminating RED run and reverted — perturbing the unreachable arm to
ServerPlacementAbsent (the absorbing fallback) drives
witness_unreachable_host_is_unknown_never_absent false.

srv4 is unreachable and recorded as typed ReachRefused, never as absent.
Receipt: docs/plans/build-cache-placement-receipt.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Address review 46182: correct the stale managed_compile_pool_slice_note citations

The review flagged that the note cites ServerUnitOwned.slice_unit, which this
PR renamed and moved. Verifying it showed all THREE of the note's claims had
gone stale in one edit:

1. ServerUnitOwned.slice_unit -> ServerManagedUnitOwned { unit } in
   gunbc.build_cache_instance, and placement is decided against the instance's
   intended_unit (a service) rather than this slice.
2. 'the emitted shell greps the observed cgroup for it' — the script carries
   ZERO compile-pool references; the branch's emitted placement probe was
   deliberately not landed (A5 census row: emitted shell may shrink, not grow).
3. The two-consumer drift hazard and the import-cycle homing rationale were
   both premised on that missing consumer.

Rewritten against the real consumers (build_cache_instance's two instance
constructors plus the provision-module fixture) and stating that nothing
provisions this slice yet, so the row is not misread as describing the fleet.

Also deletes srv1_stranded_server_cgroup_fixture, orphaned when its only
consumer moved to an inline RunnerSlotIdentity. It was additionally wrong:
it guessed ghrunner@srv1-10.service where the observed cgroup is
actions-runner@srv1-10.service.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Grant publication placement for the three paths this PR adds

Main landed the P-B publication placement gate (#7560) after this branch's
merge-base, so the three files added here reached CI with no roster envelope
and the gate correctly refused: AddedPathPublishGrantAbsent on
build_cache_instance.dag, build_cache_placement_observation_test.dag and
docs/plans/build-cache-placement-receipt.md.

These are ordinary public substrate and docs, so the fix is to declare the
grants rather than to touch the gate. The gate's own discriminating controls
stay green either way: added_granted_public_path_greens_gate and
added_ungranted_public_path_reds_gate both true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* WIP: debug build flake

* Couple slot+lifecycle into a coproduct; dissolve the gate's second verdict mapping

Two findings from review 46217, both confirmed against the code.

EndpointOwnerObserved carried `unit` beside `owning_runner_slot:
RunnerSlotIdentity?` and `runner_slot_lifecycle: RunnerSlotLifecycle?`.
Three loose fields admitted states no observation can produce, and one of
them was live: placement_of_owner matched the slot first, so an Absent slot
carrying a Present lifecycle skipped the runner arm entirely and reached the
unit comparison, where a cgroup whose unit equalled intended_unit returned
ServerManagedUnitOwned. A runner-owned endpoint could be reported correctly
placed because two fields disagreed.

Replaced by EndpointOwnerContext = OwnerInRunnerSlot { slot, lifecycle }
| OwnerInSystemdUnit { unit } | OwnerContextUndecided { detail }. The
contradictory state is now unwritable rather than checked (DESIGN 4b/5,
construction over validation), and the unclassifiable cgroup gets a positive
arm carrying its own located reason instead of a half-filled record.

build_cache_provision_gate_accepts was a full second traversal of
BuildCacheDaemonObservation and, inside DaemonStatsOk, of
BuildCacheServerPlacement -- the same observation-to-verdict mapping that
provision_build_cache_verdict and placement_verdict already compute, written
twice with either copy free to drift (DESIGN 2/3). It now takes catalog_id,
calls the fold, and compares shapes: converged-vs-refused plus the refusal
cause via discriminant, leaving the located reason string as diagnostic
payload rather than contract. A placement arm added to the coproduct can no
longer be forgotten here, because there is nowhere left to forget it.

Green by execution: 42/42 witnesses across both modules, both entries
compile at 0 blocking errors. The new control
witness_undecided_owner_context_refuses_and_keeps_the_located_reason asserts
the replacement arm refuses and preserves the observer's reason; there is
deliberately no witness for the contradictory state, since the type no
longer admits it.

Also completes 2f1ff43, a mid-edit dashboard auto-commit that captured the
coproduct without its fixtures (review 46224).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* WIP: debug build flake

* Resolve roster conflict by dedup; grant #7550's two lean paths

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: gunbai-bot[bot] <289086189+gunbai-bot[bot]@users.noreply.github.com>
Co-authored-by: Brian Searls <briansearls1@gmail.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.

1 participant