Repository navigation
sccache server placement is a verdict axis; compile pool becomes an explicit budget row - #7019
gunbai-bot[bot] wants to merge 18 commits into
Conversation
…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>
eb771e8 to
8d88a45
Compare
…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>
|
Fixed in The reported issue: What fixing it by execution then surfaced — the more serious defect. With the arity corrected, Notably the repo had already predicted this: the pre-existing Both paths now route through the single Verification (all by execution, local
Worth flagging for the eventual flip: when One process note: a whole-tree — sent from wise-crab-547 |
…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>
|
Thanks for the approve and for the 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 Then I tried to verify it and found What landed instead addresses the actual defect you named — the arbitrary 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 Verified by execution:
Two incidental notes for the shell→intent thread you flagged: — sent from wise-crab-547 |
|
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. You were exactly right, including about the consequence:
You were also right that my witness only exercised observe. Now:
Discriminating RED, run rather than asserted: restoring the old stats-only 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: So the authority now lives in 3. Orphan evidence row — fixed. Real inconsistency, and it was debris from a reverted approach. Verification: emitted script parses ( One pre-existing item I noticed but did not touch, since it predates this PR: — sent from wise-crab-547 |
|
CI failure on The failure: exactly 1 of 2137 discovery witnesses — Provenance, verified rather than inferred:
Resolution: the fix simply needed to reach this branch. Auto-sync has since merged main again at
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 — sent from wise-crab-547 |
# Conflicts: # dag/extdeps/cache/sccache.dag
|
This PR carries an incidental fix to This branch rewrites 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 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 The ask: when you next merge One genuine exception: if the 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. |
|
Heads-up: this branch carries a competing fix for the main-red in This PR has rewritten 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,
Verified by execution against merged main 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 (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 |
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>
…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>
* 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>
The defect
sccacheexecutes 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.
BuildCacheDaemonObservationhad one success arm,DaemonStatsOk, and the realizer's observe step wassccache --show-statsreturning 0 →observe=ok→exit 0. Responsiveness was conflated with correct placement — DESIGN §5 state-space conflation — and it fabricated a plausibleProvisionConverged.Measured consequence on srv1: the CI server was lazily auto-spawned inside dead slot srv1-10's cgroup, orphaned by
KillMode=processwhen the fleet narrowed 10→5 slots, and kept alive indefinitely bySCCACHE_IDLE_TIMEOUT=0. All five live slots' compiles executed under one dead slot's 15G high / 16G max caps: 229,780memory.maxbreach 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 onDaemonStatsOk.ProvisionConvergedis reachable only fromServerUnitOwned. New refusalsRefusalServerLazySpawned/RefusalPlacementUnknown. Unknown placement refuses rather than assuming owned — unobservable placement is ignorance, not evidence.extdeps.cache.sccacheassccache_server_process_facts(client auto-spawn,SCCACHE_IDLE_TIMEOUT=0, compiles-as-children), each with aVendorCitation— not minted locally.The realizer actually observes it (
host_build_cache_provision_script)/procand comparing against the managed slice. Both non-owned cases exit 1 with a typed receipt line.if:build_cache_read_back_body()never emitted its closingfi, 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)compile_poolis now an explicit child of the host allocation tree and is subtracted inrunner_slice_cap.extdeps.jobserver.jobserver_build_pool_bytes(tokens ×mib_per_job) — reused verbatim, no new literal.CompilePoolPlacementmakes 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 toCompilePoolSliceDeclaredalongside 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).ServerLazySpawneddrives the witness to false. The perturbation was run, observed red, and reverted.bash -n→BASH_PARSE_OK, 64 lines, 4 placement markers. This check is itself discriminating — it returnedBASH_PARSE_FAILbefore thefifix.runner_placement_holds→ true,allocation_conserves_when_pools_fit→ true (live plan path unchanged, as designed).Not in scope
The managed
ctrl-sccache-ci.serviceunit + slice, the converge knob, and theCompilePoolSliceDeclaredflip are the follow-on. This PR makes the bad state refusable and countable; it does not yet provision the unit. STEP 2 (removingci_release_build_script()'s-u RUSTC_WRAPPERabsorbing fallback) remains operator-sequenced after live read-back, per the existing design anchor.