Repository navigation
buildlock: one polled wait with a ceiling, and a would-be maker tries - #1145
Conversation
Review r1 of #1145 at dce7a48Architectural assessment
Is polling justified? Yes, as a workaround for a host defect. I looked for an event-based mechanism and found none that is simpler and correct:
So the macOS defect is what forces the poll. The lack of a timed wait does not force it by itself, which means the commit message's "a wait with a failure timeout over it is a poll anyway" overstates the case. Under root's no-silent-debt rule the compromise needs a record (B2). Starvation. There is no FIFO within a mode. A new waiter tries at once, then after 1, 2, 4 ms and so on, so it beats long waiters that sleep 50 ms for roughly its first 100 ms. Among long waiters the winner depends only on where each one's sleep falls. Being overtaken without limit is possible, and only the ceiling bounds it. This matters most on the artifact lock: the incident log shows five waiters there for over 100 s. Writer preference still holds, because Ceiling. Crash. std's lock is flock(2) on Unix, scoped to the open file description, so a dead holder still releases it, and Hosts. On Linux the semantics are unchanged. Linux admits all compatible waiters, so the poll costs only handoff latency there. The build system does not compile on Windows (an existing issue). This change makes Windows no worse, and the FFI it removes was Unix-only. Probe. It can be reproduced from the evidence: a stand-alone program with its source, manifest, Tests and controls. Each of the three new tests has a control that builds and fails, so each can expose its defect. The control snapshot differs from the head only in two test lines (I checked with diff). RequiredB1.
B2. The polling compromise is not recorded in
B3. Two comments are now false.
B4.
The PR closes the forty-minutes issue and leaves this one open with a cause that may now be wrong. Do one of these:
Suggestions
SEND BACK |
A 12-wide `cargo test --test toyos-build` waited 2611 s on a sysroot key
lock its own process had finished with. Two defects, both reproduced:
1. macOS lets blocked shared flock waiters in one at a time. Measured on
Darwin 27.0.0, across threads and across processes alike: of four
shared waiters queued behind an exclusive holder, one is granted when
it lets go, and each other only once the shared holder before it lets
go, while a fresh non-blocking shared try succeeds at once. A build
holds its uses (of a sysroot, of the worktree lock) for its whole
length, so a waiter there waits on another build's whole length, and
forever on one held to the end of the run. In the incident one waiter
was admitted after the sysroot build, five more after 17:45:06, when
the failing build ahead of them dropped its use, and the seventh was
left behind a use never dropped.
2. `keyed_made` queued exclusively to make a key it had just found
unmade. When another waiter made it meanwhile and its users kept
their uses, the queued maker waited out every one of them.
Every acquisition now polls `File::try_lock{,_shared}` with a pause
doubling from 1 ms to 50 ms, and fails after six hours, past the 350
minutes the toolchain workflow allows, naming the holder. The macOS
admission is what forces the poll: on a host that admits every
compatible waiter, a blocking wait with a timed join would do.
This replaces the blocking `flock` FFI and the heartbeat thread: the
poll loop says "still waiting" itself. `keyed_building` is a try, and
`keyed_made` goes back to waiting for a use when it fails.
Tests: `waiters_on_a_failed_build_all_get_in` (a thread's make fails
with eight waiting threads; all eight get and hold their use),
`a_would_be_maker_does_not_wait_out_another_use` and
`a_wait_past_its_deadline_names_the_holder`. Negative controls: the base
code with the first test, and this change with kernel-blocking waits,
both let in 2 of 8; the try reverted to a wait fails the second; the
deadline check removed fails the third.
Closes issues/a-test-build-waited-forty-minutes-on-its-own-processs-sysroot-lock.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`keyed_made`'s retry was a second wait loop: its own deadline and backoff, and silent while another's use kept its make out. It is now one attempt, "use it if whole, else try to make it", polled by the same `poll` as every other acquisition. So there is one deadline, one "waiting"/"still waiting"/"acquired" voice, and one ceiling, which `a_wait_past_its_deadline_names_the_holder` now tests through `keyed_made`. The scenario test now stages both outcomes of the first make: success with eight waiters (the incident: the sysroot was built), and failure. Under kernel-blocking uses, success admits 1 of 8. The poll is a workaround for macOS's admission of blocked shared waiters. `flock` having no timed wait would not force it alone: a blocking wait on a helper thread with a timed join would do on a host that admits every compatible waiter. Its costs and exit are recorded in issues/macos-lets-blocked-shared-flock-waiters-in-one-at-a-time.md, and the comment at `poll` names that. Two comments were false: the tests' header said a single-process test proves nothing about flock (threads with their own descriptors contend), and a key's second user was said to wait "not on a timer". issues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md is this defect. Its recurrences have the signature: after the sysroot build, one worker alone on `using sysroot … — the holder left no readable note`, the lock open only in the harness. Its exit condition holds. Sysroot, compiler and freestanding keys are taken with the worktree lock put down (`Held::without_shared`, sysroot.rs, compiler.rs). The LLVM and ToyOS-hosted clang keys are taken inside it by the documented order. Eight guest tests run 12 wide on a head whose sysroot was not yet made finished; that run is the PR's evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dce7a48 to
5ee9f08
Compare
Closes
issues/a-test-build-waited-forty-minutes-on-its-own-processs-sysroot-lock.mdandissues/a-suite-that-builds-its-sysroot-can-wedge-on-its-own-locks.md(both deleted). Records the polling compromise inissues/macos-lets-blocked-shared-flock-waiters-in-one-at-a-time.md.Problem
In a 12-wide
cargo test --test toyos-build, a guest build waited 2611 s on a sysroot key lock that its own process had finished with. I reproduced two causes:flockwaiters in one at a time. Measured on Darwin 27.0.0, across threads and across processes (probe, output). When an exclusive holder releases, macOS admits one of the shared waiters queued behind it. Each of the others is admitted only when the shared holder ahead of it releases, although a new non-blocking shared try succeeds at once. A build holds its uses (of a sysroot, of the worktree lock) for its whole length, so a waiter behind one waits that whole length, and forever behind a use held to the end of a run. That matches the incident log: one waiter was admitted after the sysroot build, five more after 17:45:06 when the failingshm_commitbuild dropped its use, and the seventh stayed behind a use that was never dropped.keyed_madequeued exclusively to make a key it had found unmade. When another waiter made the key in the meantime and its users kept their uses, the queued maker waited until every one of them finished.The wedge issue is the first cause. Its recurrences on #836 r4 and #1052 r2 have the same signature: after the sysroot build, one worker is left alone on
using sysroot … — the holder left no readable note, andlsofshows the lock open only in the harness. Its first case fits too: shared waits on the worktree lock'sstatefile go through the same admission. The ordering inversion it names has no path: sysroot, compiler and freestanding keys are all taken with the worktree lock put down (Held::without_shared, atsysroot.rs:832andcompiler.rs:150; the freestanding make runs inside the sysroot build). The LLVM and ToyOS-hosted clang keys are taken inside it, which is the documented order.Change (
build/buildlock.rs)pollover an attempt:taketries a lock mode;keyed_usingtries a use and dates it;keyed_madeuses the key if whole, or else tries to make it.build lock: gave up waiting for the sysroot lock (using sysroot K) after … — held by pid N (building sysroot K). 6 h is above the 350 minutes.github/workflows/toolchain.ymlallows the whole toolchain.keyed_buildingis a try that returnsOption<Guard>.flockFFI and the blocking take, replaced by std'sFile::try_lock{,_shared}, and the heartbeat thread with its channel.Decisions and costs
intent. A holder that dies still releases its lock.Tests (host,
buildlock::tests)waiters_on_a_build_all_get_in: eight threads wait on another thread's make of a sysroot. All eight must hold their use at once, both when that make succeeds (the incident) and when it fails and a waiter makes the key.a_would_be_maker_does_not_wait_out_another_use: the key is whole and in use by another process, anddefectreports it unmade once.a_wait_past_its_deadline_names_the_holder:keyed_madewith a 500 ms deadline on a key that another holds in the making must fail, namingheld by pid <self> (building sysroot K).Negative controls (patches and logs): each built (exit 0) and its test failed (exit 101).
keyed_try_usingblocking in the kernel admits 1 of 8.pollnever checking its deadline never ends.keyed_buildingwaiting instead of trying waits out another's use.Checks
cargo test --test toyos-buildof seven of the incident's eight tests, 12 wide, on a head whose sysroot was not yet made (log): exit 0, 7 of 7. The eighth,shm_commit_alone, is not on main. To make the sysroot unmade I appendedconst _: () = ();tosdk/libc/src/alarmreq.rs, uncommitted, and reverted it afterwards. Six workers waited onbuilding sysroot b68c8b152facb43cand all six were admitted in the same second, 178 s later.toyos-build --all-targetsat the head, command line in the log: exit 0. The buildlock tests at the head (log): exit 0.cargo run -- --ci host(log): exit 1. Its one red step was a clippyredundant_clonein a new test, since fixed. This round changes onlybuild/buildlock.rsandissues/, so the other steps cannot change.--build-only(log): exit 0.Risks
🤖 Generated with Claude Code