Add a deterministic blocking-task pool (limited multi-threading)#76
Open
mystenmark wants to merge 12 commits into
Open
Add a deterministic blocking-task pool (limited multi-threading)#76mystenmark wants to merge 12 commits into
mystenmark wants to merge 12 commits into
Conversation
spawn_blocking closures now run on real OS threads, serialized by a ThreadQuantum: at most one thread (main executor or pool thread) runs at a time, and the executor grants each pool thread a turn between polling rounds in seeded-random order. Blocking code waits for sim progress via task::yield_blocking() instead of blocking the OS thread. Node kill unwinds in-flight blocking tasks at their next yield point via deferred abort flags (kill() may itself run on a pool thread, so it cannot perform a wake handshake).
…add tests - block_on now tolerates quiet wake rounds (a parked task can progress purely by being re-woken), declaring deadlock only after 10k consecutive stalled rounds with no other events. - spawn_blocking no longer requires a runtime context, matching spawn(); the pool handle is set at runtime construction and cleared on shutdown to break the Arc cycle. - shutdown() leaves the panic hook alone when already unwinding (std forbids modifying it from a panicking thread, which turned any failing test with parked blocking tasks into a SIGABRT).
Pool threads exit with TLS destructors still to run; an intercepted clock call there (intercepts enabled, context gone) panicked inside an extern "C" fn, which cannot unwind and aborts the process. Observed as a flaky SIGABRT in sui e2e simtests under parallel load. - disable intercepts before pool thread teardown - give gettimeofday the same no-context bypass as clock_gettime and mach_absolute_time
Pool thread startup runs concurrently with the main sim thread, so the info-level log (with a nondeterministic ThreadId) lands at a racy position in otherwise-deterministic log output.
A PanicWrapper thrown on a pool thread kills the task's own node, so the result-channel wrapper task is dead and cannot deliver the payload - the requested restart would be lost. Pool threads now record such payloads in the quantum; each wake round hands them to the main loop, which schedules restarts via the same handling as run_all_ready (now factored into Executor::handle_task_panic).
Tasks parked at yield points occupy pool threads; the pool must be larger than the maximum number of concurrent waiters (e.g. sui's execution driver allows 100 in-flight executions) or the task that would unblock them starves. Idle threads are skipped by wake rounds, so a large pool costs little.
- select candidate threads (aborts, parked tasks, idle threads up to queue length) under a single lock, and shuffle only those - per-thread condvars + a main condvar: waking exactly the target thread replaces a notify_all thundering herd across the whole pool (2 x pool-size wakeups per turn, ~4ms/turn at 128 threads)
Lets shared sync code choose between quantum yields (on pool threads) and regular blocking behavior.
Test frameworks may keep per-test state in process globals (needed now that a simulation spans multiple threads), so a process must host at most one simulation. Setting either env var now fails loudly. Replacements: an external seed-search runner (one process per seed) for MSIM_TEST_NUM; running a test twice in separate processes and comparing log output for the determinism check. The check_determinism attribute is accepted and ignored so existing annotations keep compiling.
- Remove the pool->runtime Arc cycle entirely rather than weakening it: the pool no longer stores the runtime Handle. Threads are spawned lazily from wake_round (on the main executor thread, where the Handle is already in context), so there is nothing to break on shutdown. - Default pool size 128 -> 32 (still overridable via MSIM_BLOCKING_THREADS). - Move the intercept-disable-on-exit guard into the intercept module as enable_intercepts_scoped()/InterceptsGuard. - Drop is_blocking_pool_thread(): its only consumer was the Sui MutexTable integration, which is deferred; will reintroduce with a cleaner design if still needed. - Clarify why every kill_current_node() panic in a wake round is handled: each is an independent per-node restart, and two tasks of the same node cannot both reach it in one round (sequential turns + run_one's is_killed() drop). - Remove accidentally-committed GIT_CHECKPOINT.txt.
mystenmark
force-pushed
the
mlogan-msim-yield
branch
from
July 24, 2026 18:57
d6bf734 to
74ba07f
Compare
mystenmark
marked this pull request as ready for review
July 24, 2026 20:35
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
Adds limited multi-threading to the simulator:
spawn_blockingclosures now run on real OS threads, serialized by aThreadQuantumso that at most one thread (the main executor or one pool thread) runs at any instant. The executor grants each pool thread a turn between polling rounds in seeded-random order, so execution stays deterministic without intercepting synchronization primitives.Blocking code that must wait for progress made elsewhere in the simulation calls
msim::task::yield_blocking()in its wait loop instead of blocking the OS thread — under the quantum an OS-level block would never end the thread's turn and would hang the simulator.Node kill/restart, panic propagation, and
kill_current_node()are all handled on pool threads: killed tasks unwind at their next yield point (via a deferred abort delivered as anAbortturn), andPanicWrapperrestarts are routed back to the main loop through the pool.Why
Enables simulating code that genuinely needs to yield to another thread — e.g. synchronous execution that blocks mid-transaction waiting on a condition another task must satisfy. Previously such code could only be modeled by running it inline on the single sim thread, where any real wait deadlocks the simulator.
Design sketch
The original high-level design. The implementation refines several details — a
Turnenum instead ofOption<u32>sokill()can request a deferred abort at a yield point, routingkill_current_node()panics/restarts back to the main loop, per-thread condvars, lazy thread startup, and joining the pool on shutdown — but the core idea (one thread runs at a time, woken in deterministic order) is unchanged.The runtime spawns a fixed number of extra "blocking pool" threads for code that can yield execution to other threads. Normally no code needs to yield — we poll() ready futures one at a time and they always make progress immediately, because there is no other thread that could be holding a lock they require.
spawn_blocking()sends tasks to a blocking_queue; the run loop enforces that only one thing happens at a time, and wakes the blocking threads in deterministic order:Notes
thread_local!s (fail-point registries, injection overrides) must become process-global, which requires at most one simulation per process. AccordinglyMSIM_TEST_NUMandMSIM_TEST_CHECK_DETERMINISMare retired in thesim_testmacro (both now fail loudly; thecheck_determinismattribute is accepted and ignored). Replacements: an external seed-search runner (one process per seed) and running a test twice in separate processes comparing log output.MSIM_BLOCKING_THREADS); it must exceed the max number of concurrently-parked blocking tasks in the workload.Test plan
RUSTFLAGS="--cfg msim" cargo test -p msim --lib— 35 tests incl. 10 new pool tests: result delivery, determinism across a fixed seed, yield-based cross-task waits, nestedspawn_blocking, panic propagation, kill-unwinds-parked-task (Drop impls run), clean shutdown with a parked task,kill_current_noderestart, and deadlock detection.LOCAL_MSIM_PATH:sui-e2e-testscheckpoint/reconfiguration/address-balance suites (78 tests) andtest_simulated_load_reconfig_with_crashes_and_delayspass, and repeated concurrent runs produce byte-identical simulation logs (determinism preserved).