[draft/foundation] Executor-drain settle(): non-starvable quiescence (coupled to expect follow-up)#23
Closed
mansbernhardt wants to merge 2 commits into
Closed
[draft/foundation] Executor-drain settle(): non-starvable quiescence (coupled to expect follow-up)#23mansbernhardt wants to merge 2 commits into
mansbernhardt wants to merge 2 commits into
Conversation
`settle()` (and the settle phase of `expect { … }`) resolves on the model's
executor-drain FIXPOINT instead of a `.deferential`/`.background`-QoS quiet-check.
Under heavy parallel load macOS starves `.background` indefinitely, so the
quiet-check never fired and settle reported a false `settle() timed out: model
still has active tasks` (empty task list) at ANY budget — the years-old flake the
serial-CI fallback and SWIFT_MODEL_TIMEOUT_SCALE were working around. The drain
signal is non-starvable (a job-count + GTS, never `.background`) and
dependency-free, so settle waits as long as necessary under load and resolves the
instant the model is genuinely quiescent.
How it works:
- `_DrainTestExecutor`: one shared concurrent GCD queue backs every per-test
executor (avoids per-test thread-pool explosion); each keeps its own
outstanding-job count + event-driven `waitUntilIdleOrDeadline`.
- Model task bodies (`node.task`/`forEach`) adopt it via `executorPreference`
under `.modelTesting`; the trait installs a per-test executor box.
- `_driveToStableFixpoint`: quiescent = executor idle + per-test bg-idle + no
pending-start task, persisted for a short NON-STARVABLE grace that debounces
against ALL activity (every `_noteActivity` + executor enqueue) so a clock-
parked task's resume resets it. `mainCall` excluded (process-global).
- `waitUntilSettled` resolves on the fixpoint; a generous watchdog only catches
a true deadlock.
OPT-IN via SWIFT_MODEL_EXPERIMENTAL_DRAIN=1 — inert by default (executor box is
nil → every wait keeps its current path), so the suite is unchanged unless
enabled. Validated: flag ON, settle suites green incl. a 60-iteration
load-stressed child-task settle test; flag OFF, broad regression green
(unchanged). Custom task executors need Swift 6 runtime (macOS 15+); older
OS/WASM stay on the existing path.
`expect`/`waitUntil` drive-primary migration is deliberately NOT in this PR (the
fixpoint-as-fail judgment has open scaling/race work) — follow-up. Full design
arc in docs/test-determinism-executor-drain.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ve-primary (executor routing regresses expect+clock tests under load) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
settle()(and the settle phase ofexpect { … }) resolves on the model's executor-drain fixpoint — a non-starvable quiescence signal — instead of the.deferential→DispatchQueue.global(qos: .background)quiet-check that macOS starves under parallel load. The settle mechanism is validated standalone: flag-on, the 72 falsesettle() timed outfailures disappear; flag-off, the suite is unchanged.Coupling — why it can't be on by default yet
Making
settleuse the drive requires the per-test executor to be on, which routes all model tasks through one shared GCD queue. That queue is slightly slower than the cooperative pool under parallel load — enough to tripexpect's wall-clock budget in latency-sensitive clock tests. Verified: flipping the executor on by default regresseschildTasksCompleteBeforeTeardownandtestImmediateClock(both green onmainat normal parallel). So enabling the drive is coupled to makingexpect/waitUntildrive-primary too (so they don't depend on a wall-clock budget). Until that lands, this stays opt-in.How (settle slice)
_DrainTestExecutor: one shared concurrent GCD queue backs every per-test executor; each keeps its own outstanding-job count + event-drivenwaitUntilIdleOrDeadline.executorPreferenceunder.modelTesting; the trait installs a per-test executor box._driveToStableFixpoint: executor idle + per-test bg-idle + no pending-start, persisted for a short non-starvable grace (debounces a clock-parked task's resume).mainCallexcluded (process-global).waitUntilSettledresolves on the fixpoint; a generous watchdog catches a true deadlock.Validation
main).Follow-up (the coupled remainder)
expect/waitUntildrive-primary — seedocs/test-determinism-executor-drain.md(Updates 7–10) for the open problems (scaling, the fast-fail-vs-delayed-resume race, the long tail). Once done, the executor flips on by default and this becomes the real fix.CHANGELOG: deliberately not added until the on-by-default fix is complete.
🤖 Generated with Claude Code