Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
59f9098
feat(pace-cc): measure a compile's peak memory, children included (#1…
christianparpart Oct 28, 2026
e555159
feat(pace-cc): record queue wait and peak memory per invocation (#1728)
christianparpart Oct 28, 2026
1360d69
fix(pace-cc): document log v3 and keep the peak-memory tally optional…
christianparpart Oct 28, 2026
5efb113
feat(distributed): peak-memory estimates keyed by cache key and TU sh…
christianparpart Oct 28, 2026
4abfdc9
fix(pace-cc): range-based page walk in the peak-memory test helper (#…
christianparpart Oct 28, 2026
40315bc
test(distributed): make the PeakEstimates eviction cases able to fail…
christianparpart Oct 28, 2026
50aa0c8
style: apply the pinned clang-format to the branch's sources (#1728)
christianparpart Oct 28, 2026
5ca2287
feat(distributed): admission queue with per-machine budgets and backf…
christianparpart Oct 28, 2026
c06e852
fix(distributed): reserve for a queue head on every machine it may ru…
christianparpart Oct 28, 2026
bcbf522
feat(distributed): dedup by store, connection-scoped release, failove…
christianparpart Oct 28, 2026
7c834ca
fix(distributed): order the admission queue by an explicit key, index…
christianparpart Oct 28, 2026
2e42342
feat(wire): admission verbs and wire version 16 (#1728)
christianparpart Oct 28, 2026
e685fce
fix(wire): list MACHINE-BUDGET among the proven-identity verbs, revie…
christianparpart Oct 28, 2026
6fc1fee
docs: MachineBudget in the joining-verb and capacity-verb prose (#1728)
christianparpart Oct 28, 2026
22f2e84
feat(pace-node): machine budget flags and per-round MachineBudget rep…
christianparpart Oct 28, 2026
ca782b3
fix(pace-node): require the measurement sink, wire it under test, rev…
christianparpart Oct 28, 2026
30a4aba
fix(pace-node): skip the budget dial only when the presence dial reac…
christianparpart Oct 28, 2026
bd7b10d
feat(pace-node): leader parks Admit requests and admits against machi…
christianparpart Oct 28, 2026
227b6d0
fix(distributed): a worker refusal keeps its ticket, a vanished worke…
christianparpart Oct 28, 2026
a376469
fix(pace-node): a refused admission waits again on its connection, ju…
christianparpart Oct 28, 2026
18cd4ee
test(pace-node): bound ReadAnswer as a whole, spell NetError fully (#…
christianparpart Oct 28, 2026
21afbcf
fix(pace-node): no C-style loop in the admission pump or park, drop a…
christianparpart Oct 28, 2026
2a3f758
test(pace-node): re-gate a proven caller through the production fold,…
christianparpart Oct 28, 2026
147ee06
fix(pace-node): a worker refusal on a demoted node is answered NotLea…
christianparpart Oct 28, 2026
ed20a49
feat(pace-node): forward Admit inline to the leader (#1728)
christianparpart Oct 28, 2026
bac2945
build: core-cpp 0.6.0 -> 0.10.0 (#1728)
christianparpart Oct 28, 2026
5126f2f
build(ranges-seam): derive views:: selections, refuse std::views::str…
christianparpart Oct 28, 2026
0faa49d
refactor(pace-cc): PeakMemoryChild strides through core::ranges::Stri…
christianparpart Oct 28, 2026
2855f75
fix(pace-node): a voter that follows forwards Admit inline, never to …
christianparpart Oct 28, 2026
d9c9620
feat(pace-cc): hold an admit connection while compiling (#1728)
christianparpart Oct 28, 2026
024fc80
fix(pace-cc): bound SETTLE separately and harden the admit session th…
christianparpart Oct 28, 2026
c016f6f
fix(pace-cc): retire the admit loop's socket and pointer on every exi…
christianparpart Oct 28, 2026
ecb5702
feat(pace-cc): queue every cache miss at the leader, fail open withou…
christianparpart Oct 28, 2026
66d4611
test(pace-cc): initialise every MissSeams member in the harness (#1728)
christianparpart Oct 28, 2026
fa2e741
test(pace-cc): no C-style loop in the admit session scripts (#1728)
christianparpart Oct 28, 2026
11dbef6
fix(pace-node): an admission grant carries the worker key, lifetime a…
christianparpart Oct 28, 2026
203cdb7
fix(pace-cc): record a local grant's reason, give a failed worker's g…
christianparpart Oct 28, 2026
a15ecdc
test(wire): hold the encoded grant while a case splits it (#1728)
christianparpart Oct 28, 2026
dd6714d
fix(pace-node): an admitted no-worker grant moves the leader's counte…
christianparpart Oct 28, 2026
ebcb966
fix(pace-cc): hold a plan refusal until a queue grants, settle a fail…
christianparpart Oct 28, 2026
8fb8654
feat(pacectl): live-stats fleet shows the compile queue and its clien…
christianparpart Oct 28, 2026
584a1ce
fix(pacectl): the queue ledger reports what admission counts as in us…
christianparpart Oct 28, 2026
84ac4a9
test: fleet admission end-to-end (#1728)
christianparpart Oct 28, 2026
6809c6d
test: collect the admission case's keys by hand, clang over libstdc++…
christianparpart Oct 28, 2026
5dbec4f
docs: fleet compile queue rules and operator docs (#1728)
christianparpart Oct 28, 2026
ed70e56
test: tighten the fleet admission budget case, split the killed-clien…
christianparpart Oct 28, 2026
b660774
fix: clang-tidy findings on the fleet compile queue (#1728)
christianparpart Oct 28, 2026
2716196
fix(pace-cc): clang-tidy on the admit session, stats and process runn…
christianparpart Oct 28, 2026
11e5404
refactor(pace-cc): split RunCached by phase (#1728)
christianparpart Oct 28, 2026
d84abb6
fix: clang-tidy findings in the admission sources and tests (#1728)
christianparpart Oct 28, 2026
17d7444
refactor(pace-node): start the admission relay outside WorkerBody (#1…
christianparpart Oct 28, 2026
e75c0a1
refactor(pace-node): start the admission queue outside WorkerBody (#1…
christianparpart Oct 28, 2026
52f2896
fix(admission): a worker grant carries its dial hint and files its le…
christianparpart Oct 28, 2026
74e2876
fix(pace-cc): a launcher that stops requeueing runs the compile charg…
christianparpart Oct 28, 2026
0755949
fix(admission): held admission dials carry TCP keepalive (#1728)
christianparpart Oct 28, 2026
324c005
fix(pace-cc): a launcher asks the queue at its own node, whatever PAC…
christianparpart Oct 28, 2026
26c3002
feat(metrics): the admission wait is a real pair, and each machine's …
christianparpart Oct 28, 2026
9f98e9b
fix(pace-cc): a failed settle is said only where the launcher chose (…
christianparpart Oct 28, 2026
a8092d3
fix(pace-node): a connection's hold is released before it stops count…
christianparpart Oct 28, 2026
1e437fd
test(pace-node): the leader's own budget round charges its own machin…
christianparpart Oct 28, 2026
8a5e28a
docs(rules): the compile queue's grant, lease, endpoint and keepalive…
christianparpart Oct 28, 2026
bb04aab
chore: doc-comment fixes from the #1728 review
christianparpart Oct 28, 2026
2c118ca
docs(deployment): the per-machine admission gauges are the node's, no…
christianparpart Oct 28, 2026
aa9aa2b
fix: clang-tidy findings in the #1728 review fixes
christianparpart Oct 28, 2026
77bed26
docs(rules): link the #1728 follow-ups from Open work (#1728)
christianparpart Oct 28, 2026
827261b
fix(pace-cc): hand the peak-memory helper to its test at run time, no…
christianparpart Oct 28, 2026
2ed9484
test(pace-cc): give the slow-queue progress case room for a loaded ma…
christianparpart Oct 28, 2026
e92e60c
build: core-cpp 0.11.0 (#1736)
christianparpart Oct 28, 2026
91f72c0
feat(pace-node): the leader arms TCP keepalive on a held admission co…
christianparpart Oct 28, 2026
0058595
test(pace-node): pin that only a held admission connection is armed, …
christianparpart Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 107 additions & 4 deletions .agent/rules/distributed-compilation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2178,7 +2178,8 @@ symptom was that they were slow.
and a learner granted none, so only a broken client sends one there.
- **A node running NO consensus follows no leader**, and answers as the `pace-kv` daemon
does: the capacity verbs (`Register`, `Heartbeat`, `Withdraw`, `Lease`, `Release`,
`NodeAnnounce`) `DispatchNotPermitted`, the cluster verbs `NoCluster` — the daemon reaching
`NodeAnnounce`, and the admission verbs `Admit`, `Settle`, `MachineBudget`)
`DispatchNotPermitted`, the cluster verbs `NoCluster` — the daemon reaching
`NodeAnnounce`'s code through `RefusalFor`'s fallback. The codes agree on every scheduling
verb, and `NodeFrameSurface_test` pins that agreement on the wire; the words are each
binary's own.
Expand Down Expand Up @@ -2373,6 +2374,13 @@ Six more about what the tier IS and who gets to see it:
and that is correct rather than the same defect: a node offering no slots has a
capacity of zero, which is a reading rather than an unmade measurement. The two
fields answer different questions and the same word would be wrong in one of them.
**"No worker" is not "no machine" for the fleet's compile queue (#1728, R6)**: every
node, a `--slots=0` one included, states its machine's compile budget
(`MachineBudgetOf`: `--machine-slots`, `--machine-memory-budget`) on the presence loop's
round, which runs everywhere. It is a budget record and not a worker registration -- such
a node still registers nothing and is never sent a dispatched compile -- and its flags are
deliberately NOT worker-column rows, or the generated rule above would refuse them on
exactly the node that needs them.
**The worker is `WorkerTier`, null on such a node** (#1387): no idle pool thread, no
capacity sized to zero, no validator. It owns the `SchedulerLink`, so a worker cannot
exist without one and a start that reaches it with no `--scheduler` is refused by
Expand Down Expand Up @@ -3774,9 +3782,11 @@ cluster's pre-shared key instead; #178 PR 6 made it prove WHICH machine it is, b
its own identity key, and sealed every frame after the proof. Each rule below is what some
plausible simpler design gets wrong.

**No address admits another machine, and the verbs a machine JOINS with need a proof.** They -- `Register`, `NodeAnnounce`, `Heartbeat`, `Withdraw` -- decide where the fleet's work
is sent, and they are `IdentityRequirement::ProvenNodeOnly`, a COLUMN of `OpTable` pinned by
`JoiningVerbsNeedAnIdentity`, refused `NodeIdentityRequired` and counted
**No address admits another machine, and the verbs a machine JOINS with need a proof.** They --
`Register`, `NodeAnnounce`, `Heartbeat`, `Withdraw` -- decide where the fleet's work is sent, and
`MachineBudget` decides how much work a machine is ADMITTED (the memory budget the leader charges
it against, #1728); all five are `IdentityRequirement::ProvenNodeOnly`, a COLUMN of `OpTable`
pinned by `JoiningVerbsNeedAnIdentity`, refused `NodeIdentityRequired` and counted
(`SchedulerRequestsRefusedNodeIdentityRequired`). Loopback is NOT exempt: a process on the
scheduler's own host could otherwise register an endpoint of its choosing and be leased the
fleet's jobs. Every node naming `--scheduler` therefore needs a key to prove -- and every node
Expand Down Expand Up @@ -4034,6 +4044,95 @@ here asserts first, with the addition beside it only to stop a change that satis
half from passing. The in-process fleet asserts it from addresses nobody listed
(`FleetTickets_test`, `FrameEndpoint_test`'s relabelled-peer cases).

## The compile queue (#1728)

<!-- agent-tripwire: A miss is ADMITTED (queued) at the fleet's leader -->

A build with more compiler processes than the machine has memory for was killed by the kernel
rather than slowed. The leader now holds a queue in front of every machine's compile budget.
Each rule below is a shape a plausible simpler design gets wrong, and none of them fails a build.

- **A miss is ADMITTED (queued) at the leader, never refused for capacity.** A refusal sends the
client to a local compile, which is exactly the load the queue exists to bound. Waiting is the
product; the leader pulses (`Status::Progress`, `Admit` being the second verb allowed it) so a
long queue reads as alive against the client's sliding idle bound. A request larger than a whole
machine's budget waits for an idle machine and runs ALONE; it is never starved and never refused.
A `NoRoom` Local grant is an ordinary queue decision, not a refusal, and is not counted as one.
- **The connection is the lease.** A grant is charged to the connection that asked, carries no
token to expire, and is released by `SETTLE` on that connection or by its close. A close is an
abandonment, counted apart (`AdmissionAbandoned`); a `kill -9` of ninja must give the memory back
at once with no timer. The only frame a held connection may send next is its `SETTLE`, or, after
`Settle{WorkerRefused}`, a fresh `ADMIT` that binds to the SAME ticket (Ruling 9).
- **A worker refusal keeps the ticket's place and count.** The leader re-parks the same ticket at
the FRONT; after `MaxWorkerRefusals` the grant is Local. Abandoning it instead would lose the
front position and let a flapping worker starve the head of the queue.
- **A worker FAILURE settles `NotStored`, never abandon**: an
abandon raises the ESTIMATE of a translation unit that merely met a bad worker, and feeding that to the
estimator poisons every later admission of it.
- **Deduplication is woken by STORE, never by a release.** Only a `storable` compile that shares a
cache with its peers waits on another's key, and it is answered `Stored` when the object is
STORED. A release wakes nobody: a compile that failed stored nothing, and the waiters must then
compile.
- **Every node states its machine budget, `--slots=0` included, and that is not a worker
registration.** `MachineBudget` rides the presence loop's round, which runs everywhere; its flags
(`--machine-slots`, `--machine-memory-budget`) are deliberately not worker-column rows.
- **A failover admits nothing on a machine before that machine's budget report arrives**, and the
ledger charges `max(charged, held)`: the new leader knows only what the machine says it holds,
and an admission before that report double-books memory a surviving compile still has.
- **Every non-leader forwards `Admit`/`Settle` inline to the leader, never to itself**: a voting
follower as well as a learner. A voter that answered `NotLeader` would leave a launcher with no
queue on every node but one. The relay holds one upstream connection per client connection.
- **The grant says what it granted**: `workerKey`, `lifetime` and `dialHint` as `LeaseGrant` does
-- the hint from the same `DialHintFor`, so a worker reachable only where it was last seen is
dialled there whichever path granted it -- and a `reason` on a Local grant of a dispatchable
request, so a fleet that never distributes still says why (`DeclineDispatch`) instead of reading
green.
- **An admission's worker lease is filed under its TICKET, never under the cache key** (Ruling 17).
Deduplication is the QUEUE's, and only where a waiter can fetch the result; a lease table keyed by
the cache key refused the second of two machines that missed one key `AlreadyInFlight`, which its
launcher read as "compile here" -- an uncharged compile, the load the queue exists to bound. The
token still signs the OBJECT key, as a `Lease` grant's does: the table's key is bookkeeping the
worker never sees.
- **A launcher asks the queue at its OWN node, `PACE_ADDR`, whatever `PACE_SCHEDULER` names** (Ruling
16, `Cc::AdmissionEndpointOf`). The leader charges a compile to the machine that ASKED, and names
it only by a key the connection PROVED or by loopback: the launcher's node proves its machine's key
when it relays, while a launcher dialling a remote scheduler directly proves nothing and would be
charged to no machine. `PACE_SCHEDULER` keeps one meaning, where a `LEASE` goes when no queue
answers, and a plan is refused for a remembered-unreachable endpoint only when EVERY place it could
be spent was unreached (`Cc::PlanUnreachableAt`).
- **A launcher fails OPEN only where no queue ANSWERED.** An unreachable, refusing or unreadable
node costs the build nothing, and a launcher whose FETCH already found the node unreachable does
not dial it again: on Windows a refused loopback connect can take the whole connect budget. The
one compile with no such evidence is a passthrough whose command line did not parse -- it makes
no FETCH -- so with the node stopped and the reachability memo empty it pays one connect, up to a
second on Windows, and records it; a stopped node costs a build at most one such connect per
fifteen-second memo window, never one per compile. Where the queue DID answer and the launcher
gives up on it anyway -- `MaxWorkerRefusals` worker refusals in a row -- the compile is asked for
again `LocalOnly`, excluding that worker, and runs HERE charged, never in the open.
- **A held admission connection carries TCP keepalive at BOTH ends** (Ruling 18, #1736): the
launcher's dial of its node (`Cc::DefaultAdmitBudget`) and the node's relay to the leader
(`Node::ForwardDialBudget`) arm it per dial, as `wire-and-protocol.md` requires, and the leader arms
it on the ACCEPTED connection once that connection holds a grant, through core-cpp's
`ISocket::armKeepAlive` (v0.11.0). The intervals are core-cpp's `KeepAliveSettings {}`, which every
`KeepAlive::Yes` dial applies -- a dial takes none of its own -- and ONE constant names them for all
three, `Pace::AdmitKeepAliveProbe` (`Pace/Protocol/AdmitKeepAlive.hpp`), pinned by a `static_assert`:
a peer whose host vanished is noticed in `idle + count * interval` = **16 s** on Linux and macOS and
**30 s** on Windows, where the OS fixes the count at 10. That bounds the relay noticing a vanished
LEADER (failing its launcher open, giving back its own held charge), a launcher noticing a vanished
node, and the leader noticing a vanished caller -- whose Worker grant would otherwise hold another
machine's budget until `MaxCompileLeaseLifetime` (one hour) sweeps it, which stays as the backstop.
- **What arms is a COLUMN of what is held**, `IConnectionHold::KeepAlive()`, pure so a new hold
answers rather than inherits a silence; `FrameServer::ArmHeldKeepAlive` asks it on the loop thread
(which `armKeepAlive` requires) once a reply that left a hold is WRITTEN, and a queued request
that holds nothing is not armed. Once per CONNECTION (`ConnectionScope::NoteKeepAliveArmed`), so a
requeue on the same socket is not a second request. A failure to arm is a `Warn`, never a
refusal: the sweep is still there.
- **Every pace socket that wraps an `ISocket` forwards `armKeepAlive`** (`SealedFrameSocket`,
`TotalDeadlineSocket`, and the test fake `RelabelledPeerSocket`), or the call lands on the
interface default, `Unsupported` -- `SetReceiveDeadline`'s defect class, one rule higher. The two
production wrappers each have a case asserting the request reaches the socket under them; the fake's
is the proven-leader forwarding case, whose ledger sits UNDER it.

## Open work

<!-- agent-tripwire: none: deferred work, tracked as GitHub issues; AGENT.md tripwires rules, not residuals -->
Expand Down Expand Up @@ -4082,3 +4181,7 @@ half from passing. The in-process fleet asserts it from addresses nobody listed
it can be spawned and once for its banner, and the first is in a serial loop in
front of the pool built to hide exactly that. `CompilerBanner` knows both facts
and reports neither, so two callers reconstruct what it discarded.
- **[#1730](https://github.com/LASTRADA-Software/pace/issues/1730)** — one cache key is not
deduplicated across machines that share a cache, so the same compile can be admitted twice.
- **[#1731](https://github.com/LASTRADA-Software/pace/issues/1731)** — admission test gaps
deferred by the #1728 reviews.
7 changes: 7 additions & 0 deletions .agent/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -3094,3 +3094,10 @@ passed.
does not judge whether a phrase is the right tripwire. Marked `untriaged:` and not
`none:`, and `ctest -R rulebook-tripwires` prints the count against this issue on
every run.

- **[#1735](https://github.com/LASTRADA-Software/pace/issues/1735)** — a test that uses a link must
verify a link can be followed here, and SKIP otherwise (Windows error 448).
- **[#1733](https://github.com/LASTRADA-Software/pace/issues/1733)** — the FileTrust test hard-codes
the English LOCAL SERVICE account name.
- **[#1734](https://github.com/LASTRADA-Software/pace/issues/1734)** — `python3` resolves to the
Windows Store stub on this host.
11 changes: 8 additions & 3 deletions .agent/rules/wire-and-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ Every rule below has already been a bug.
replay, an oversized or unframed header) closes UNANSWERED, because there the
frame is not known to be the peer's at all.
- **A reply is one frame per request, and `Status::Progress` is the first of two exceptions --
bounded to exactly one verb, carrying nothing, and paid for with a version step.** The
bounded to exactly the verbs a table names (`Compile`, and `Admit` since #1728), carrying
nothing, and paid for with a version step.** The
second is `Status::Push`, below, bounded the same way.
A dispatched compile is bounded by how long a COMPILER runs, so its client's total
deadline is minutes by construction and was therefore also how long a worker whose
Expand All @@ -72,7 +73,9 @@ Every rule below has already been a bug.
"The framing already lets a receiver step over what it does not know" is the
tempting reason not to bump, and it is a fact about REQUESTS.
- **Which verbs may be answered with it is a column of `OpTable`**, and
`ProgressIsCompileOnly()` is `static_assert`ed. Every other verb on this wire is
`ProgressVerbsAre({Compile, Admit})` is `static_assert`ed (`Admit` joined at version 16,
#1728: a queue waits as long as it is long, and a pulse is what tells that from a dead leader).
Every other verb on this wire is
answered from a table in microseconds, so a pulse on one would be a frame no client
could observe and a second reply shape every reader would have to handle for
nothing -- and a mask widened by hand would give it to them by omission.
Expand Down Expand Up @@ -853,7 +856,9 @@ Every rule below has already been a bug.
link is torn down, fleet-wide, for a change nobody asked for. Same rule as SIGPIPE
above, reached from the option side rather than the signal side: per socket, never
process-wide. `DialOptions::keepAlive` says so at the call, `Detail::ArmKeepAlive`
applies it, and `SocketAddress_test` asserts the socket is **still not probing**
applies it, and an ACCEPTED socket has the second door: `ISocket::armKeepAlive`, per socket,
called on the loop thread by the server once it decides the connection is long-lived (#1736);
`SocketAddress_test` asserts the socket is **still not probing**
after `ApplyHotSocketOptions` — the negative half, which is the assertion that
fails when somebody later folds the two together for tidiness
([#247](https://github.com/LASTRADA-Software/pace/issues/247)). Four things
Expand Down
6 changes: 6 additions & 0 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,12 @@ launcher's cache key is made of. Before `apps/pace-cc/`, `CompileCache/`.
- A cordon is the worker PROCESS's state, never replicated and never persisted, so a restart lifts
it. Stop, cordon and full are ONE locked decision with the slot take (`SlotAdmission`), and a
compile's slot rides its reply (`IReplyHold`) until the endpoint has WRITTEN it.
- A miss is ADMITTED (queued) at the fleet's leader, never refused for capacity (#1728): the
connection is the lease (a close abandons, `SETTLE` releases), a worker refusal keeps the ticket's
place, dedup is woken by STORE, and a worker failure settles `NotStored`.
- Every node sends its `MachineBudget`, `--slots=0` included, and that is not a worker registration;
a failover admits nothing on a machine before its report, charging `max(charged, held)`.
- Every non-leader, voter or learner, forwards `Admit`/`Settle` inline to the leader, never to itself.
- A heartbeat age is a duration on a report, never a `TimePoint` on `WorkerInfo` — a raw instant
breaks every `ManualClock` test.
- A CoW store file is claimed exclusively at `Open` and a second opener is refused by name
Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -533,8 +533,8 @@ endif()
CPMAddPackage(
NAME core-cpp
GITHUB_REPOSITORY contour-terminal/core-cpp
GIT_TAG v0.6.0
VERSION 0.6.0
GIT_TAG v0.11.0
VERSION 0.11.0
SYSTEM YES
EXCLUDE_FROM_ALL YES
OPTIONS
Expand Down
Loading
Loading