Skip to content

all: fix the 0.3.0 review findings and bump to v0.3.1 - #10

Merged
karalabe merged 4 commits into
dark-bio:mainfrom
karalabe:loom-models
Sep 26, 2026
Merged

karalabe merged 4 commits into
dark-bio:mainfrom
karalabe:loom-models

Conversation

@karalabe

@karalabe karalabe commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Fixes the findings of a second review of 0.3.0, and model-checks the wake protocol that #7 introduced.

A notify_one ends at most one wait, one already waiting when it was sent. Waits on one condvar used to share a notification count. So once a notify_one had gone to one wait, every other wait on that condvar left at its next wakeup of any kind. On a test clock, that included an advance reaching another wait on the condvar, and the wait that left then dropped out of next_deadline() until it waited again. In the review's probe, a driver stepping through next_deadline() over timed pool workers stopped early in 537 of 3,000 runs. Each notify_one now queues a numbered notification that only a wait started before it can take, oldest first, and a notify_all ends every wait started before it. An advance still wakes the other waits on a reached wait's condvar, but they find nothing and keep waiting, counted and listed. Notifications are still not buffered, and a notification the wait takes still wins over a reached deadline.

next_deadline() reports a deadline that an advance already passed as the current time, since its wait stays listed until its thread runs. advance_to(next_deadline()) used to panic with "clock cannot go backwards" when a thread the driver does not await, such as a heartbeat, was reached but had not run yet.

An advance wakes the waits it reaches in deadline order, and timers due at the same instant fire in the order they were armed, both through a per-clock sequence number. Both used to follow heap addresses. A waiting select_biased! over timers due at the same instant then took an arbitrary arm, and loom's replays diverged whenever one advance reached two waits, so no model could check that case. The signal registry only served that address lookup, so it is gone, and each listed deadline holds its own signal.

The real Clock::sleep sleeps in slices of at most a day, as sleep_until does, since some Windows versions never return from a thread::sleep of about 49.7 days or more.

The first commit adds three loom models: a partial advance leaves a sleep listed, reaching one deadline wait keeps a later one on the same condvar listed, and a notification survives an advance racing it. The second adds six more. They cover a consumed notify_one leaving the other wait listed through an advance, a later wait never taking an earlier wait's notification, and a broadcast followed at once by a notify_one. They also cover one advance reaching two sleeps, at different and at equal deadlines, and two condvars, which fail on replay against 0.3.0. Unit tests cover the same notification cases deterministically, plus oldest-first taking, one notification against two reached waits, next_deadline() at the current time, delivery in deadline and arming order through a blocked select_biased!, and deadline registrations. Each fails when the rule it checks is broken, 32 mutations in all.

The unit tests also pass with every untimed wait returning spuriously every 20 µs, in 60 runs of the whole suite. Locally, every loom model passes at preemption bounds 5 and 6, and all but one at 7. In release mode, the suite ran 25,280 times on Linux and 2,000 times on macOS without a failure, all on this library code. The review's probes run clean: no early stop or overshoot in 24,000 driver runs, and no panic in 22,000 heartbeat and overshoot runs.

On macOS, real-clock hand-offs stay within the noise of 0.3.0, and a wait whose deadline has already passed takes about 48 ns instead of 37 ns. Locally, the loom suite takes about 90 s at CI's bound, against 12 s before the first commit.

The third commit bumps the version to 0.3.1, since the public API is unchanged. The fourth rewords three comments that ended a claim with "and no other" or a similar exclusion. Once this is merged, tagging v0.3.1 publishes the release through the trusted publishing workflow.

Left as they are: the real-clock condvar keeps its extra lock, recv_deadline keeps its retry after an advance, and after and recv_timeout keep their own overflow rule. The real sleep's slicing has no test, as sleep_until's has none.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Astra <noreply@openai.com>
@karalabe karalabe changed the title tests: model-check that advances wake only the waits they reach all: fix the 0.3.0 review findings and model-check the wake protocol Sep 26, 2026
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Astra <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@karalabe karalabe changed the title all: fix the 0.3.0 review findings and model-check the wake protocol all: fix the 0.3.0 review findings and bump to v0.3.1 Sep 26, 2026
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@karalabe
karalabe merged commit 1ac0f3c into dark-bio:main Sep 26, 2026
11 checks passed
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