feat: rt-sim: deterministic simulated AsyncRuntime (draft, #206) - #2099
Open
pchrysostomou wants to merge 3 commits into
Open
pchrysostomou wants to merge 3 commits into
pchrysostomou wants to merge 3 commits into
Conversation
pchrysostomou
force-pushed
the
rt-sim-prototype
branch
2 times, most recently
from
September 20, 2026 00:32
4fe8a50 to
f7c6f04
Compare
pchrysostomou
marked this pull request as ready for review
September 20, 2026 20:03
# Summary Add `openraft-rt-sim`, a standalone, unpublished `AsyncRuntime` that runs every task on one thread against a virtual clock, so a run with the same seed replays the same schedule. # Details Tasks are polled FIFO in the order they became runnable. When the run queue is empty the clock jumps to the earliest timer; timers with equal deadlines fire in registration order. If nothing is runnable and no timer is pending, `block_on` panics instead of hanging. `thread_rng()` draws from the runtime seed, taken from `OPENRAFT_RT_SIM_SEED` (0 if unset). mpsc, oneshot and mutex wrap `tokio::sync`. Watch has its own FIFO waiter list, because `tokio::sync::watch` picks the waker slot at random when tokio's `rt` feature is enabled. Tasks still pending when the runtime drops are dropped with the runtime installed, so their destructors can use the clock. `SimInstant::try_now()` reads the virtual clock from outside a task, for log formatters. With the `sim-log` feature and `OPENRAFT_SIM_LOG=<file>`, `block_on` routes the tracing events of its thread to that file. Each event is stamped with virtual time, thread and span ids are left out, and the wall-clock times that `DisplayInstant` prints are masked. The scheduling trace is recorded only when `OPENRAFT_RT_SIM_TRACE` names a file. The `futures-reseed` feature calls `futures_util::reseed(seed)` at the start of each `block_on`; it needs the futures-util fork that tests-turmoil patches in. `Suite::test_all` passes. `tests/determinism.rs` checks that one seed records the same trace twice. CI runs both in a new `rt-sim` job. Refs databendlabs#206
# Summary Run `t24_append_membership` on `openraft-rt-sim` under a seed, and have CI check that the same seed writes the same full log. # Details `openraft-memstore` gets an `rt-sim` feature that declares its `TypeConfig` with `AsyncRuntime = SimRuntime`. The fixtures draw the network send delay from `thread_rng()` instead of `rand::random()`, so under rt-sim it follows the seed. Tokio runs are unaffected. `tests-sim` is an excluded crate that patches in the same futures-util fork as tests-turmoil and enables rt-sim's `futures-reseed`, so the `select!` shuffle restarts from the seed on every run. It includes the fixtures and `t24_append_membership.rs` by path; the test file is included three times so one process can rerun the scenario. The `tests-sim` CI job runs one test twice with the same seed, in separate processes, and diffs the two logs. Refs databendlabs#206
pchrysostomou
force-pushed
the
rt-sim-prototype
branch
from
September 21, 2026 19:28
4f16e78 to
9a77d9e
Compare
pchrysostomou
marked this pull request as draft
September 21, 2026 19:28
pchrysostomou
marked this pull request as ready for review
September 23, 2026 00:16
Member
|
This runtime alone cannot make OpenRaft fully deterministic. OpenRaft uses I’ll keep this PR open for now and explore making branch selection part of the async runtime abstraction. |
This branch has not been deployed
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.
Draft for #206: a standalone
rt-simruntime, andt24_append_membershiprunning on it by seed with a repeatable full OpenRaft log.Seed demo: #2059 replayed
With #2059's wait reverted locally (
git show 4eb14581 | git apply -R, not part of this PR) andOPENRAFT_NETWORK_SEND_DELAY=30, 24 of 300 seeds failfollower_answers_forward_to_leaderthe way CI did:Seed 9 fails in 5 of 5 fresh processes with a byte-identical 3,467-line DEBUG log. With the wait restored, all 24 seeds pass. Seed 9's passing log matches the failing one for the first 3,431 lines. At virtual 0.143 s
new_cluster()returns while follower 1 still reportslast_log:5: the failing run asserts there, and the fixed run waits forlast_log:6at 0.147 s.CI runs this twice in a new
tests-simjob and diffs the two logs.select!: with and without the reseedtests-simpatches in your futures-util fork (same tag as tests-turmoil), and rt-sim callsfutures_util::reseed(seed)at the start of everyblock_on. It includes t24 three times, asrun1..run3, so one process runs the scenario back to back. Seed 9, 30 ms delay:reseed on: the three runs write identical 3,558-line logs, sequentially and on 3 parallel threads, and match
run1alone in a fresh process.reseed off (
--no-default-features): three different logs. They diverge at line 1,396, in the four-branchselect!inreplication/stream_state.rs:154. rt-sim's scheduling trace stays identical across all three: the shuffle changes which branch body runs, not which task is polled.The runs share one log file; the
membership::runNspan target tells them apart.Changes
rt-sim/, excluded,publish = false:thread_rng()seeded fromOPENRAFT_RT_SIM_SEED;tokio::sync; its own FIFO watch, sincetokio::sync::watchpicks a random waker slot when tokio'srtfeature is on;sim-logfeature: withOPENRAFT_SIM_LOG,block_onroutes its thread's tracing events to a file, stamped with virtual time, without thread and span ids, withDisplayInstant's wall-clock times masked;Suite::test_allpasses, in a newrt-simCI job.openraft-memstore: anrt-simfeature with a second, cfg-gateddeclare_raft_types!that setsAsyncRuntime = SimRuntime.thread_rng()instead ofrand::random(); tokio behaviour is unchanged.tests-sim/, excluded: the futures[patch], and t24 included by path. It is the only way to run a test on rt-sim;testsitself has no new feature or dependency.Easy to cut
sim-log. Without it seeds still reproduce pass/fail and the scheduling trace, but theselect!drift above only shows in this log.tests-sim, which exists only for the in-process check.Limits
t23_custom_payload) or namesTokioInstant(t61,t14,t15) can't switch runtimes as is.RUST_LOG=offrt-sim is faster.cargo package -p openraft-memstorefails as is: its optional dependency on rt-sim is a path without a version.single-threaded; seeds other than from env vars; masking beyondDisplayInstant's two formats.make verifypasses, apart from two things this machine lacks:cargo-expandfor thetest_expandandtest_sincemacrotest targets, and Python 3.10 forcheck-doc-links.py(its 129 links resolve under 3.9 with postponed annotations).One question for you. Cargo won't package memstore with an unversioned optional dependency, so keeping the
rt-simfeature onopenraft-memstoremeans publishingopenraft-rt-simalongside it, the wayrt-tokiois. The alternative is to leave rt-sim unpublished and move the runtime switch out of memstore. Which do you prefer?Checklist
This change is