Harden scheduler stealing and runtime fault containment - #29
Merged
Conversation
List the concrete problems that still prevent the stable native collector from reaching production throughput: cross-workload regressions, detached payload storage, global serialization, repeated layout construction, barrier overhead, ABI 1 relocation limits, and incomplete concurrent integration. Define observable completion targets for retained-object performance without allowing allocation churn, P99, memory, or correctness regressions.
Document the scheduler decisions drawn from BEAM, Go, Tokio, and the work-stealing and deterministic-testing literature. Specify bounded queues, logical scheduler ownership, fairness, blocking isolation, GC transitions, and record/replay gates before implementation.
Implement the synchronized M:N correctness scheduler with bounded local and injection queues, cooperative polling and cancellation, wake coalescing, work stealing, panic containment, deterministic record/replay, typed collector transitions, and an isolated bounded blocking pool. Exercise capacity, fairness, wake races, migration refusal, shutdown, and worker survival after task and blocking-operation failures.
Add bounded host and virtual timers, exact external-event delivery, atomic task batches, owner-compatible injection selection, and deterministic resource cleanup. Keep migration disabled until a runtime transition hook explicitly approves it. Harden parking and stealing against affinity wake loss, park-gate self-deadlock, admission contention, stale queue accounting, concurrent wake/cancellation races, panicking work, and dormant-source shutdown.
Derive alternative decision prefixes from recorded enabled sets so concurrency failures can be explored and replayed without ambient timing or randomness. Reject invalid and unbounded exploration requests before executing them.
Measure the synchronized reference scheduler with bounded, checksum-validated workloads for task control, ready work, injection, stealing, suspension, timers, events, and blocking work. Label every result with its schema, runtime stage, host profile, and exact latency scope so local evidence cannot be mistaken for a production GC or portable performance claim.
Exercise repeated task failures across four logical schedulers, release every retained terminal record, and require affine continuation work to complete on each worker. This guards against a panic storm shrinking or poisoning the normal scheduler pool.
Track exact current and high-water ready and blocking queues, bounded steal search outcomes, batch sizes, and worker lifecycle events. Publish the facts through the versioned benchmark and a final shutdown snapshot. Keep wake and cancellation readiness accounting inside queue publication so a fast worker cannot leave a phantom ready task after termination.
Require every non-running task frame to retain one precise collector-visible root container, including ready frames waiting in queues. Bind normal workers to detached mutator registrations, restore relocated root slots before dispatch, and make scheduler selection atomic with each native runtime operation.
Make SchedulerId a backend-neutral runtime-interface identity instead of a collector-owned type. Add a distinct TaskFrameRootId so retained coroutine frames can cross the scheduler and collector boundary without string or integer-based resolution.
Keep every queued or suspended task frame alive through a bounded collector-owned container. Preserve exact stack-map shape, follow minor relocation through private strong handles, reject foreign ownership, and restore or release roots exactly once through both moving and native stable collector profiles.
Renumber the later native runtime decisions after the accepted ADR sequence grew through 0069, and update references by semantic owner. Keep one typed Actor and Cluster catalog entry so the public library inventory remains unique and consistent with ADR 0068.
Keep retained task-frame roots live while a compiler-generated frame adapter installs relocated slots. Release the collector container only after explicit completion so a rejected installation can retry without losing the last valid root set.
Require every scheduler task to publish an exact frame map before admission and after every nonterminal poll. Restore collector-updated slots before dispatch so queued and suspended tasks remain precise managed roots. Make admission, migration, restoration, and shutdown failure-atomic. Expose root lifecycle telemetry and keep the deterministic scheduler on the same explicit contract. Add forced-collection, rejection, rollback, cleanup, and repeated concurrency coverage.
Register every normal worker as a detached mutator for its logical scheduler. Enter managed state only around task polling and route native ABI operations through the exact thread-local scheduler binding while holding the serialized runtime lock. Make managed safe points acknowledge each active epoch at most once, detach before publishing a task state, and unregister on all worker exit paths. Preserve precise task roots when dispatch fails and add allocation ownership, epoch, lifecycle, and failure cleanup coverage.
Convert the bounded churn-test index with a checked conversion instead of a truncating cast. This keeps strict all-target runtime lint clean and makes the safe-point identity assumption explicit.
Give every task dispatch a nonzero deterministic work budget and require compiler or trusted-runtime progress to consume explicit units. Reject zero-unit charges and zero-budget configurations before scheduling. Requeue an exhausted nonterminal task at the ready tail in both native and deterministic schedulers. Count every exhaustion and test that a hot task cannot suspend or run again before an already-ready peer progresses.
Record each collector-approved scheduler ownership transfer separately from queue steals and GC-delayed migration attempts. This makes migration observability exact for injection and steal paths without treating a refused transfer as success.
Record one semantic work-unit delay sample for every successful task\ndispatch. Use a fixed logarithmic histogram so percentile telemetry\nremains bounded even under long-running scheduler workloads.\n\nKeep deterministic and native schedulers on the same transition-based\nmeasurement contract, and exclude rejected dispatches from the sample\ncount.
Count deterministic and native event/timer readiness inspections and\nmeasure handoff delay on the shared semantic work clock. Start event\ndelay at accepted signal publication and timer delay when the driver\nfirst observes an expired deadline.\n\nStore delivery observations in fixed histograms and keep cancelled or\nrejected sources out of the sample population.
Measure the accepted blocking-work drain and worker-join interval on the\nscheduler observation clock. Record exactly one bounded sample and make\nsubsequent drop cleanup idempotent.\n\nAdvance the clock for blocking completions so shutdown telemetry exposes\nwork drained after admission closes, then mark the scheduler observability\nroadmap item complete.
Advance the checksum-validated benchmark schema to v3 and emit bounded\nscheduler work-delay distributions, budget and migration counters, and\nlabelled operating-system resource observations.\n\nRead Linux memory and context-switch facts from procfs. Report an explicit\nunavailable source with zero values on unsupported or unreadable targets\ninstead of implying measurements that were not collected.
Add checksum-validated local and foreign wake, atomic-turn ping-pong,\nshort-task steal storm, continuous event fairness, and scheduler/GC\ninteraction workloads. Sample process memory while minimal task frames\nare suspended so the explicit million-frame profile measures retained\nstate rather than only its post-release footprint.\n\nKeep logical operations distinct from retry polls in the ping-pong\nprofile and verify every workload completes without stale or duplicated\nwork.
Document the checksum-validated local scale run for one million\nsuspended minimal frames, including elapsed time, memory, context\nswitches, and stale-entry result.\n\nKeep the production benchmark gate open because the accepted ABI 2\nwritable-root transition is still required before collector-coupled\nperformance evidence can be claimed.
Keep ABI 1.11 bootstrap safe points distinct from the ABI 2.0\nwritable-root entry. Define fixed capability negotiation, failure-atomic\nslot writeback, and mandatory LLVM post-safe-point alias rewriting.\n\nRequire emitted control-flow verification and forced relocation before\nLLVM or the native facade can advertise production relocation.
Publish distinct immutable ABI 1.11 and ABI 2.0 descriptors, add fixed\ncapability negotiation, and implement the failure-atomic writable root\nsafe-point entry.\n\nKeep the stable native facade advertising ABI 1.11 only. This permits\nABI 2 writeback tests without falsely enabling moving execution before\nLLVM reload proof and a production collector composition exist.
Select the distinct ABI 2 safe-point entry for production-profile\nlowering. Spill exact roots before the poll branch, reject failed runtime\npublications, and reload successful writable slots into new SSA values.\n\nCarry relocated values through later instructions, terminators, control-flow\narguments, repeated safe points, and loop backedges while leaving ABI 1\nlowering unchanged.
Make the backend poll interval a typed lowering option so conformance\ntests can force every safe point without changing production defaults.\n\nLink optimized ABI 2 output against a deterministic native test runtime\nthat changes each published token and aborts on stale uses. Mutate the\nemitted retain path back to the old SSA token to prove the negative check\nactually fails. Keep the relocation capability disabled until the remaining\ncontrol-flow-wide verifier and transition proofs land.
Track every writable managed root in a backend-private function-local\ncell. Initialize cells at definitions and block arguments, publish current\nvalues at safe points, store relocated values on success, and reload uses\nbefore instructions and control-flow edges. This lets LLVM form the needed\nphis without exposing relocation operations in MIR.\n\nReject direct old-token operands during lowering. Add optimized positive and\nnegative execution tests for divergent merges and loop backedges while\nkeeping the production capability disabled for the remaining transition\nproofs.
Emit an exact pop_rt_supports_abi(2, 0) check in production-profile\nentry wrappers before argument decoding or user code. Route rejection through\nthe closed native trap path and leave ABI 1 entry wrappers unchanged.\n\nExtend the relocation conformance runtime to advertise descriptor 2.0 and\nprove that the stable ABI 1 archive rejects the same executable before normal\nentry.
Implement async function and await syntax, exact Task<T> typing, and\nbackend-neutral HIR/MIR suspension state. Preserve async identity in\nreference metadata and verify cold task creation, cancellation cleanup,\nprecise live frames, root maps, and coroutine state identities.\n\nReject async MIR explicitly in backends that do not yet implement task\nexecution so no synchronous or dynamic fallback can change semantics.
Derive cold task object maps from verified dispatch, argument, and completion types. Verifying these maps prevents runtime values from redefining precise tracing and keeps retained task completions visible to GC.
Model created, running, and retained terminal task states without a synchronous fallback. Execute direct and typed indirect task bodies on await, preserve exact live frames, route cancellation and panic exits, and publish managed completions through the verified task object map.
Remove the obsolete proposed-syntax disclaimer after ADR 0068 accepted async and await spelling. Keep the catalog regression aligned by requiring the typed async task-group closure.
Replace fixed-neighbor steal searches with deterministic per-worker rounds. Each round visits every peer exactly once from a mixed starting offset, reducing repeated victim contention without ambient randomness or weakening bounded search admission.
Prevent a movable task from migrating more than once during one ready\npublication. Repeated batch re-stealing could otherwise amplify scheduler\nmigrations before useful polling occurred.\n\nReset migration eligibility only when the task is republished as ready, and\nmake both the benchmark contract and optimized steal-storm workload reject\nmigration amplification or repeated peer scans.
Keep recoverable migration refusal distinct from collector transition\nfailure. The scheduler previously collapsed both outcomes, and worker startup\nerrors could leave the remaining pool open for admission.\n\nPropagate transition failures through injection and stealing, close every\nworker on lifecycle failure, and convert panics in trusted transition hooks\nto the typed collector-state incident. Ordinary task panics remain isolated\ninside their task boundary.
Publish concurrent task batches only after every normal worker is\nobservably parked. This exercises the lost-wakeup boundary repeatedly with\nfixed replay seeds and reports the seed, round, and telemetry on failure.\n\nVerify exact completion, queue drainage, retained terminal release, and zero\nstale ready entries after every round.
A merge retained the task ABI assertions but dropped their explicit imports, causing the runtime ABI test target to fail during compilation. Restore the five exported task symbols to the test module scope.
The branch merge combined duplicate async syntax and type nodes with two incompatible MIR designs. This left repeated enum variants, partial call signatures, and a direct Await instruction alongside the accepted cold-task state machine.\n\nKeep the typed TaskCreate and explicit Suspend terminator pipeline, remove the duplicate and premature async-cleanup fragments, and update runtime-contract coverage to follow task creation.
Assign the eBPF backend decision identity 0071 and remove duplicate shifted copies introduced by the branch merge. Move the later scheduler and native ABI decisions to unused identities and update every semantic reference so the accepted ADR inventory remains unique and all links resolve.
Include the accepted POP7000 backend range in the catalog contract and keep style-warning assertions limited to the POP6400 entries. This keeps the diagnostic phase partition aligned with ADR 0071 and the integrated diagnostics architecture.
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.
Summary
This PR completes a resilience pass over Pop Lang’s bounded synchronized M:N
scheduler.
The scheduler is the runtime foundation for typed tasks, async functions,
coroutines, actors, supervision, timers, external events, and future VM
execution. This work strengthens that foundation without introducing a new
source-level scheduling contract or hiding unfinished production-GC work.
The scheduler already provides:
Ready,Running,Suspended,Completed,Cancelled, andPanickedtask states;This PR closes three concrete scheduler resilience gaps.
Deterministic steal-victim selection
Steal searches previously started with a fixed neighboring scheduler. That
could repeatedly direct idle workers toward the same queue and create avoidable
contention.
Each logical scheduler now maintains a deterministic steal-search round.
Victim selection:
The existing bound on simultaneous searching workers remains active.
Bounded migration during steal storms
A ready task could previously migrate repeatedly between queues before any
worker polled it. In an optimized 1,024-task steal-storm run this produced
between 1,881 and 2,048 migrations for only 1,024 ready publications.
A task may now migrate at most once during one ready publication. Migration
eligibility resets only when the task makes real progress and is published as
ready again, including:
This prevents queue-to-queue ping-pong without permanently pinning a movable
task. A task can still migrate again during a later ready episode.
The optimized benchmark contract now rejects:
After the change, three optimized 1,024-task samples recorded 1,012–1,017
migrations, compared with 1,881–2,048 before the bound.
Fail-closed runtime-transition handling
Recoverable migration refusal and collector/runtime failure are now distinct.
A normal migration refusal:
A runtime-transition failure:
Worker registration or
WorkerStartedfailure now closes the completescheduler as well. Previously a startup transition could terminate only one
worker while leaving the scheduler open to admit work for that dead logical
scheduler.
Panics from trusted runtime-transition hooks are caught at the boundary and
converted into the same typed collector-state incident. They are not confused
with an ordinary task panic or recoverable migration refusal.
This preserves the intended failure hierarchy:
instead of leaving a partially live runtime.
Reproducible park/publication stress
A new seeded stress test exercises the lost-wakeup boundary.
For each fixed replay seed, the test:
Failures report the seed, round, task identity, and scheduler telemetry. This
makes the workload reproducible instead of reporting an unexplained intermittent
timeout.
The complete scheduler suite also passed 25 consecutive stress iterations.
Research basis
The implementation follows the accepted scheduler research rather than copying
another runtime’s public semantics:
observable scheduler state, failure isolation, and separate dirty execution
domains.
operating-system threads, local/global queues, periodic global polling,
batch stealing, and throttled searching workers.
randomized victim starts, half-queue stealing, and protection against
thundering-herd search behavior.
stealing, explicit linearization requirements, and deterministic replay.
The implementation deliberately retains synchronized bounded queues. Replacing
them with a lock-free Chase–Lev-style deque remains an optimization gate
requiring a written linearization/reclamation proof, weak-memory tests, stress
coverage, and benchmark evidence.
Architecture traceability
architecture/23.1-scheduler-runtime-implementation.mdarchitecture/19-architecture-conformance-and-regression-policy.mdordinary migration refusal.
ROADMAP.mdrecords deterministic victim selection, bounded migration,fail-closed transition handling, and seeded park/publication stress.
remain unchanged.
Verification
cargo fmt --all -- --checkcargo check --workspace --all-targetscargo test --workspace --all-targetscargo clippy --workspace --all-targets -- -D warningsAdditional checks run successfully:
cargo test --workspacecargo test -p pop-runtime-nativecargo test -p pop-runtime-collector --test task_frame_rootscargo test -p pop-architecture-testssteal_stormbenchmarkgit diff --checkThe native scheduler suite includes coverage for:
If a check was not run, explain why:
cargo check --workspace --all-targets, the exact--all-targetsworkspace testcommand, and workspace Clippy were not run in this tranche. The full ordinary
workspace test suite, focused runtime/collector suites, architecture suite,
formatter, repeated scheduler stress, and optimized scheduler benchmark passed.
Review notes
Known limitations and deliberately open gates:
production concurrent collector is selectable.
unwind, coroutine, and FFI relocation proofs.
reclamation, active-stack watermarks, and declared supported-target latency
gates remain open.
rather than emitting a synchronous or dynamic fallback.
complete scheduler/frame/GC contract is implemented.
popbook/directory is not part of this PR.