From 6da287d5fe62d6d8433e19be795b9f668a881c38 Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Sun, 28 Jun 2026 15:55:45 +0530 Subject: [PATCH 1/5] make lib and async --- Cargo.lock | 2 +- crates/dst/Cargo.toml | 2 +- crates/dst/README.md | 20 -------- crates/dst/src/engine.rs | 5 +- crates/dst/src/lib.rs | 6 +++ crates/dst/src/main.rs | 91 --------------------------------- crates/dst/src/sim/commitlog.rs | 6 +++ crates/dst/src/traits.rs | 34 +++++++----- 8 files changed, 37 insertions(+), 129 deletions(-) delete mode 100644 crates/dst/README.md create mode 100644 crates/dst/src/lib.rs delete mode 100644 crates/dst/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 1db7622f17f..7fc7b0c9129 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8413,7 +8413,7 @@ dependencies = [ ] [[package]] -name = "spacetimedb-dst" +name = "spacetimedb-dst-lib" version = "2.7.0" dependencies = [ "anyhow", diff --git a/crates/dst/Cargo.toml b/crates/dst/Cargo.toml index 722d293ba19..79a2e02d0b5 100644 --- a/crates/dst/Cargo.toml +++ b/crates/dst/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "spacetimedb-dst" +name = "spacetimedb-dst-lib" version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/dst/README.md b/crates/dst/README.md deleted file mode 100644 index 1a4dfcb2396..00000000000 --- a/crates/dst/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# SpacetimeDB DST - -Deterministic Simulation Testing framework for SpacetimeDB. - -## Test - -```sh -cargo test -p spacetimedb-dst -``` - -## Run - -```sh -cargo run -p spacetimedb-dst -- run --seed 42 --max-interactions 1000 -``` - -Options: - -- `--seed ` — RNG seed (defaults to wall-clock nanos) -- `--max-interactions ` — interaction budget diff --git a/crates/dst/src/engine.rs b/crates/dst/src/engine.rs index 8dcfaab46a7..451b7417e41 100644 --- a/crates/dst/src/engine.rs +++ b/crates/dst/src/engine.rs @@ -274,10 +274,11 @@ impl EngineTarget { impl TargetDriver for EngineTarget { type Observation = Observation; - fn execute(&mut self, interaction: &Interaction) -> Result { + async fn execute<'a>(&'a mut self, interaction: &'a Interaction) -> Result { EngineTarget::execute(self, interaction) } } + pub struct EngineTest; impl TestSuite for EngineTest { @@ -289,7 +290,7 @@ impl TestSuite for EngineTest { type Properties = EngineProperties; - fn build(&self, rng: Rng) -> Result<(Self::Interactions, Self::Target, Self::Properties), anyhow::Error> { + async fn build(&self, rng: Rng) -> Result<(Self::Interactions, Self::Target, Self::Properties), anyhow::Error> { let schema = default_schema(rng.clone()); let runtime_seed = rng.next_u64(); let target = EngineTarget::init(schema.clone(), runtime_seed)?; diff --git a/crates/dst/src/lib.rs b/crates/dst/src/lib.rs new file mode 100644 index 00000000000..8d12c575e4c --- /dev/null +++ b/crates/dst/src/lib.rs @@ -0,0 +1,6 @@ +pub mod engine; +pub mod schema; +pub mod sim; +pub mod traits; + +pub use traits::{Properties, TargetDriver, TestSuite, TestSuiteParts}; diff --git a/crates/dst/src/main.rs b/crates/dst/src/main.rs deleted file mode 100644 index e208f921a83..00000000000 --- a/crates/dst/src/main.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -use clap::{Args, Parser, Subcommand}; -use spacetimedb_runtime::sim::Rng; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - -mod engine; -mod schema; -mod sim; -mod traits; - -use crate::{engine::EngineTest, traits::TestSuite}; - -#[derive(Parser, Debug)] -#[command(name = "spacetimedb-dst")] -#[command(about = "Run deterministic simulation targets")] -struct Cli { - #[command(subcommand)] - command: Command, -} - -#[derive(Subcommand, Debug)] -enum Command { - Run(RunArgs), -} - -#[derive(Args, Debug)] -struct RunArgs { - #[arg(long, help = "Seed for generated choices. Defaults to wall-clock time.")] - seed: Option, - #[arg(long, help = "Deterministic interaction budget.")] - max_interactions: Option, -} - -fn main() -> anyhow::Result<()> { - init_tracing(); - match Cli::parse().command { - Command::Run(args) => run_command(args), - } -} - -fn init_tracing() { - let timer = tracing_subscriber::fmt::time(); - let format = tracing_subscriber::fmt::format::Format::default() - .with_timer(timer) - .with_line_number(true) - .with_file(true) - .with_target(false) - .compact(); - let fmt_layer = tracing_subscriber::fmt::Layer::default() - .event_format(format) - .with_writer(std::io::stderr); - let env_filter_layer = tracing_subscriber::EnvFilter::from_default_env(); - - let _ = tracing_subscriber::Registry::default() - .with(fmt_layer) - .with(env_filter_layer) - .try_init(); -} - -fn run_command(args: RunArgs) -> anyhow::Result<()> { - let seed = resolve_seed(args.seed); - let config = RunConfig { - max_interactions: args.max_interactions, - seed, - }; - - tracing::info!(?config, "initial run config"); - - // Generate schema from seed. - let rng = Rng::new(config.seed); - - let test = EngineTest {}; - test.run(rng, config.max_interactions)?; - Ok(()) -} - -fn resolve_seed(seed: Option) -> u64 { - seed.unwrap_or_else(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("time went backwards") - .as_nanos() as u64 - }) -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct RunConfig { - pub max_interactions: Option, - pub seed: u64, -} diff --git a/crates/dst/src/sim/commitlog.rs b/crates/dst/src/sim/commitlog.rs index dbf6d004a4c..6c1fd2afa2d 100644 --- a/crates/dst/src/sim/commitlog.rs +++ b/crates/dst/src/sim/commitlog.rs @@ -22,6 +22,12 @@ pub struct InMemoryCommitlog { options: Options, } +impl Default for InMemoryCommitlog { + fn default() -> Self { + Self::new() + } +} + impl InMemoryCommitlog { pub fn new() -> Self { Self { diff --git a/crates/dst/src/traits.rs b/crates/dst/src/traits.rs index d84aafa97db..2185e0ec918 100644 --- a/crates/dst/src/traits.rs +++ b/crates/dst/src/traits.rs @@ -5,7 +5,10 @@ use spacetimedb_runtime::sim::Rng; pub trait TargetDriver { type Observation; - fn execute(&mut self, interaction: &I) -> Result; + fn execute<'a>( + &'a mut self, + interaction: &'a I, + ) -> impl std::future::Future> + 'a; } /// Ensures if Output of `TargetDrive` is expected for the input @@ -20,32 +23,35 @@ pub type TestSuiteParts = ( ); pub trait TestSuite { - type Interaction; + type Interaction: std::fmt::Debug; type Interactions: Iterator + std::fmt::Debug; type Target: TargetDriver; type Properties: Properties>::Observation>; - fn build(&self, rng: Rng) -> Result, Error> + fn build(&self, rng: Rng) -> impl std::future::Future, Error>> + '_ where Self: Sized; - fn run(&self, rng: Rng, max_interactions: Option) -> Result<(), Error> + fn run(&self, rng: Rng, max_interactions: usize) -> impl std::future::Future> + '_ where Self: Sized, { - let (mut interactions, mut target, mut properties) = self.build(rng)?; + async move { + let (mut interactions, mut target, mut properties) = self.build(rng).await?; - let result = (|| { - for interaction in interactions.by_ref().take(max_interactions.unwrap_or(usize::MAX)) { - let observation = target.execute(&interaction)?; - properties.observe(&interaction, &observation)?; - } + let result = async { + for interaction in interactions.by_ref().take(max_interactions) { + let observation = target.execute(&interaction).await?; + properties.observe(&interaction, &observation)?; + } - Ok(()) - })(); + Ok(()) + } + .await; - tracing::info!(interaction_counts = ?interactions, "final interaction counts"); + tracing::info!(interaction_counts = ?interactions, "final interaction counts"); - result + result + } } } From 236c9fe2bc309bb8c4f2d0c9efcb22823227787a Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Mon, 6 Jul 2026 15:19:06 +0530 Subject: [PATCH 2/5] cfallocate feature --- crates/dst/Cargo.toml | 4 ++++ crates/dst/src/sim/commitlog.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/crates/dst/Cargo.toml b/crates/dst/Cargo.toml index 79a2e02d0b5..7d47c9a1b47 100644 --- a/crates/dst/Cargo.toml +++ b/crates/dst/Cargo.toml @@ -4,6 +4,10 @@ version.workspace = true edition.workspace = true rust-version.workspace = true +[features] +default = ["fallocate"] +fallocate = ["spacetimedb-commitlog/fallocate"] + [dependencies] anyhow.workspace = true clap.workspace = true diff --git a/crates/dst/src/sim/commitlog.rs b/crates/dst/src/sim/commitlog.rs index 6c1fd2afa2d..f2644d8767a 100644 --- a/crates/dst/src/sim/commitlog.rs +++ b/crates/dst/src/sim/commitlog.rs @@ -400,6 +400,10 @@ impl FileLike for Segment { Ok(()) } + + fn fallocate(&mut self, _size: u64) -> io::Result<()> { + Ok(()) + } } pub struct ReadOnlySegment { From 9f58cece7b94d4b271a1dd84643ffe25f063d758 Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Mon, 6 Jul 2026 20:30:59 +0530 Subject: [PATCH 3/5] simulation runtime task panic handling --- crates/runtime/src/lib.rs | 64 +++++++++++++++++++- crates/runtime/src/sim/executor/mod.rs | 78 +++++++++++++++++++++++-- crates/runtime/src/sim/executor/task.rs | 45 +++++++++++--- crates/runtime/src/sim/mod.rs | 2 +- 4 files changed, 172 insertions(+), 17 deletions(-) diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 9511cf52c50..a5cfefffc2c 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -49,13 +49,32 @@ pub mod sync { pub use tokio::sync::watch; } -#[derive(Clone)] pub enum Handle { Tokio(TokioHandle), #[cfg(feature = "simulation")] Simulation(sim::Handle), } +impl Clone for Handle { + fn clone(&self) -> Self { + match self { + Self::Tokio(handle) => Self::Tokio(handle.clone()), + #[cfg(feature = "simulation")] + Self::Simulation(handle) => Self::Simulation(handle.clone()), + } + } +} + +impl fmt::Debug for Handle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Tokio(_) => f.write_str("Handle::Tokio"), + #[cfg(feature = "simulation")] + Self::Simulation(_) => f.write_str("Handle::Simulation"), + } + } +} + pub struct JoinHandle { inner: JoinHandleInner, } @@ -135,6 +154,16 @@ impl fmt::Display for JoinError { } } +impl JoinError { + pub fn is_panic(&self) -> bool { + match &self.inner { + JoinErrorInner::Tokio(err) => err.is_panic(), + #[cfg(feature = "simulation")] + JoinErrorInner::Simulation(err) => err.is_panic(), + } + } +} + impl std::error::Error for JoinError {} impl JoinHandleInner { @@ -234,6 +263,27 @@ impl Handle { pub fn simulation(handle: sim::Handle) -> Self { Self::Simulation(handle) } + + pub fn on_simulation_node(&self, node: sim::NodeId) -> Self { + match self { + Self::Tokio(_) => panic!("Handle::on_simulation_node requires a simulation runtime"), + Self::Simulation(handle) => Self::Simulation(handle.on_node(node)), + } + } + + pub fn create_simulation_node(&self) -> Option { + match self { + Self::Tokio(_) => panic!("Handle::create_simulation_node requires a simulation runtime"), + Self::Simulation(handle) => Some(handle.create_node()), + } + } + + pub fn drain_simulation_task_panics(&self) -> Vec { + match self { + Self::Tokio(_) => panic!("Handle::drain_simulation_task_panics requires a simulation runtime"), + Self::Simulation(handle) => handle.drain_task_panics(), + } + } } impl Handle { @@ -244,7 +294,7 @@ impl Handle { }, #[cfg(feature = "simulation")] Self::Simulation(handle) => JoinHandle { - inner: JoinHandleInner::Simulation(handle.spawn_on(sim::NodeId::MAIN, future)), + inner: JoinHandleInner::Simulation(handle.spawn(future)), }, } } @@ -268,7 +318,7 @@ impl Handle { // the simulation backend. #[cfg(feature = "simulation")] Self::Simulation(handle) => handle - .spawn_on(sim::NodeId::MAIN, async move { f() }) + .spawn(async move { f() }) .await .expect("simulation spawn_blocking task should not be cancelled"), } @@ -295,6 +345,14 @@ impl Handle { Self::Simulation(handle) => handle.sleep(duration).await, } } + + pub fn block_on(&self, future: F) -> F::Output { + match self { + Self::Tokio(handle) => tokio::task::block_in_place(|| handle.block_on(future)), + #[cfg(feature = "simulation")] + Self::Simulation(handle) => handle.block_on(future), + } + } } #[cfg(test)] diff --git a/crates/runtime/src/sim/executor/mod.rs b/crates/runtime/src/sim/executor/mod.rs index 4a1a8394701..3a42d036628 100644 --- a/crates/runtime/src/sim/executor/mod.rs +++ b/crates/runtime/src/sim/executor/mod.rs @@ -83,6 +83,11 @@ pub struct Node { config: Arc, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TaskPanic { + pub node: NodeId, +} + impl Node { /// Return the stable identifier for this simulated node. pub fn id(&self) -> NodeId { @@ -158,10 +163,15 @@ impl Runtime { self.executor.elapsed() } + pub fn drain_task_panics(&self) -> Vec { + self.executor.drain_task_panics() + } + /// Get a cloneable handle for spawning tasks and accessing runtime services. pub fn handle(&self) -> Handle { Handle { executor: Arc::clone(&self.executor), + node: NodeId::MAIN, } } @@ -244,9 +254,32 @@ impl Runtime { #[derive(Clone)] pub struct Handle { executor: Arc, + node: NodeId, } impl Handle { + /// Return a handle that spawns default work on `node`. + pub fn on_node(&self, node: NodeId) -> Self { + Self { + executor: Arc::clone(&self.executor), + node, + } + } + + /// Return the node this handle uses for default spawned work. + pub fn node(&self) -> NodeId { + self.node + } + + /// Spawn a `Send` future onto this handle's current simulated node. + pub fn spawn(&self, future: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.spawn_on(self.node, future) + } + /// Create a new simulated node owned by this runtime. pub fn create_node(&self) -> NodeBuilder { NodeBuilder { @@ -260,7 +293,7 @@ impl Handle { let config = self.executor.node_config(id); Node { id, - handle: self.clone(), + handle: self.on_node(id), config, } } @@ -319,6 +352,10 @@ impl Handle { self.executor.time.timeout(duration, future).await } + pub fn block_on(&self, future: F) -> F::Output { + self.executor.block_on(future) + } + pub fn enable_buggify(&self) { self.executor.enable_buggify(); } @@ -342,6 +379,10 @@ impl Handle { pub fn buggify_with_prob(&self, probability: f64) -> bool { self.executor.buggify_with_prob(probability) } + + pub fn drain_task_panics(&self) -> Vec { + self.executor.drain_task_panics() + } } /// Core single-threaded scheduler backing a simulation [`Runtime`]. @@ -356,6 +397,7 @@ struct Executor { next_node: AtomicU64, rng: Rng, time: TimeHandle, + task_panics: Arc>>, } impl Executor { @@ -371,6 +413,7 @@ impl Executor { next_node: AtomicU64::new(1), rng: Rng::new(config.seed), time: TimeHandle::new(), + task_panics: Arc::new(Mutex::new(Vec::new())), } } @@ -378,6 +421,10 @@ impl Executor { self.time.now() } + fn drain_task_panics(&self) -> Vec { + core::mem::take(&mut *self.task_panics.lock()) + } + fn enable_buggify(&self) { self.rng.enable_buggify(); } @@ -439,7 +486,7 @@ impl Executor { self.assert_known_node(node); let abort = AbortHandle::new(); - let abortable = Abortable::new(future, abort.clone()); + let abortable = Abortable::new(future, abort.clone(), node, Arc::clone(&self.task_panics)); let sender = self.sender.clone(); let (runnable, task) = async_task::Builder::new() .metadata(node) @@ -458,7 +505,7 @@ impl Executor { self.assert_known_node(node); let abort = AbortHandle::new(); - let abortable = Abortable::new(future, abort.clone()); + let abortable = Abortable::new(future, abort.clone(), node, Arc::clone(&self.task_panics)); let sender = self.sender.clone(); let (runnable, task) = unsafe { async_task::Builder::new() @@ -731,7 +778,30 @@ mod tests { let err = runtime .block_on(task) .expect_err("aborted task should surface JoinError instead of panicking"); - assert_eq!(err, JoinError); + assert_eq!(err, JoinError::Cancelled); + } + + #[test] + fn spawned_task_panic_is_captured_without_stopping_runtime() { + let mut runtime = Runtime::new(10); + let node = runtime.create_node().name("panic").build(); + let node_id = node.id(); + let task = node.spawn(async move { + yield_now().await; + panic!("node boom"); + }); + + runtime.block_on(async { + yield_now().await; + yield_now().await; + }); + + let panics = runtime.drain_task_panics(); + assert_eq!(panics, vec![TaskPanic { node: node_id }]); + let err = runtime + .block_on(task) + .expect_err("panicked task should surface JoinError instead of unwinding"); + assert!(err.is_panic()); } #[cfg(feature = "simulation")] diff --git a/crates/runtime/src/sim/executor/task.rs b/crates/runtime/src/sim/executor/task.rs index bf03a8293d3..d9cc1c94921 100644 --- a/crates/runtime/src/sim/executor/task.rs +++ b/crates/runtime/src/sim/executor/task.rs @@ -1,4 +1,4 @@ -use alloc::sync::Arc; +use alloc::{sync::Arc, vec::Vec}; use core::{ fmt, future::Future, @@ -9,7 +9,7 @@ use core::{ use spin::Mutex; -use super::NodeId; +use super::{NodeId, TaskPanic}; /// A spawned simulated task. /// @@ -87,12 +87,24 @@ impl AbortHandle { } } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct JoinError; +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum JoinError { + Cancelled, + Panicked, +} impl fmt::Display for JoinError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("task was cancelled") + match self { + Self::Cancelled => f.write_str("task was cancelled"), + Self::Panicked => f.write_str("task panicked"), + } + } +} + +impl JoinError { + pub fn is_panic(&self) -> bool { + matches!(self, Self::Panicked) } } @@ -124,11 +136,18 @@ impl AbortState { pub(crate) struct Abortable { future: F, abort: AbortHandle, + node: NodeId, + task_panics: Arc>>, } impl Abortable { - pub(crate) fn new(future: F, abort: AbortHandle) -> Self { - Self { future, abort } + pub(crate) fn new(future: F, abort: AbortHandle, node: NodeId, task_panics: Arc>>) -> Self { + Self { + future, + abort, + node, + task_panics, + } } } @@ -138,7 +157,7 @@ impl Future for Abortable { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { // Check cancellation before doing any work. if self.abort.state.aborted.load(Ordering::Relaxed) { - return Poll::Ready(Err(JoinError)); + return Poll::Ready(Err(JoinError::Cancelled)); } // Register the waker so abort() can wake this task. @@ -153,7 +172,15 @@ impl Future for Abortable { // address of `future` relative to `self`. // 3. The caller guarantees `self` stays pinned for the lifetime of the // future. + let node = self.node; + let task_panics = Arc::clone(&self.task_panics); let mut future = unsafe { self.map_unchecked_mut(|this| &mut this.future) }; - future.as_mut().poll(cx).map(Ok) + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| future.as_mut().poll(cx))) { + Ok(poll) => poll.map(Ok), + Err(_) => { + task_panics.lock().push(TaskPanic { node }); + Poll::Ready(Err(JoinError::Panicked)) + } + } } } diff --git a/crates/runtime/src/sim/mod.rs b/crates/runtime/src/sim/mod.rs index ccdcc104991..b798186d533 100644 --- a/crates/runtime/src/sim/mod.rs +++ b/crates/runtime/src/sim/mod.rs @@ -4,7 +4,7 @@ mod rng; pub mod time; pub use executor::{ - yield_now, AbortHandle, Handle, JoinError, JoinHandle, Node, NodeBuilder, NodeId, Runtime, RuntimeConfig, + yield_now, AbortHandle, Handle, JoinError, JoinHandle, Node, NodeBuilder, NodeId, Runtime, RuntimeConfig, TaskPanic, }; pub(crate) use rng::DeterminismLog; pub use rng::{GlobalRng, Rng}; From 5c9c034ae364e893df869179568587ae935c9d09 Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Tue, 7 Jul 2026 15:29:02 +0530 Subject: [PATCH 4/5] add yeild and now apis --- crates/runtime/src/lib.rs | 21 +++++++++++++++++---- crates/runtime/src/sim/executor/mod.rs | 5 +++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index a5cfefffc2c..97fc18d08e1 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -346,6 +346,22 @@ impl Handle { } } + pub async fn yield_now(&self) { + match self { + Self::Tokio(_) => tokio::task::yield_now().await, + #[cfg(feature = "simulation")] + Self::Simulation(handle) => handle.yield_now().await, + } + } + + pub fn now(&self) -> Duration { + match self { + Self::Tokio(_) => panic!("Handle::now is only supported by the simulation runtime"), + #[cfg(feature = "simulation")] + Self::Simulation(handle) => handle.now(), + } + } + pub fn block_on(&self, future: F) -> F::Output { match self { Self::Tokio(handle) => tokio::task::block_in_place(|| handle.block_on(future)), @@ -381,10 +397,7 @@ mod tests { drop(jh); // Yield so the spawned task gets polled. - handle - .timeout(std::time::Duration::from_millis(50), async {}) - .await - .ok(); + handle.yield_now().await; }); assert!(flag.load(Ordering::Acquire)); diff --git a/crates/runtime/src/sim/executor/mod.rs b/crates/runtime/src/sim/executor/mod.rs index 3a42d036628..ba52c585c3c 100644 --- a/crates/runtime/src/sim/executor/mod.rs +++ b/crates/runtime/src/sim/executor/mod.rs @@ -352,6 +352,11 @@ impl Handle { self.executor.time.timeout(duration, future).await } + /// Yield this task back to the simulation scheduler once. + pub async fn yield_now(&self) { + yield_now().await + } + pub fn block_on(&self, future: F) -> F::Output { self.executor.block_on(future) } From 964602ffbd304ee172de0f22fc4b89c735f8e029 Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Tue, 7 Jul 2026 16:01:25 +0530 Subject: [PATCH 5/5] add comment --- crates/runtime/src/lib.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 97fc18d08e1..faa2ba1ecfc 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -260,6 +260,13 @@ impl Handle { #[cfg(feature = "simulation")] impl Handle { + pub fn now(&self) -> Duration { + match self { + Self::Tokio(_) => panic!("Handle::now requires a simulation runtime"), + Self::Simulation(handle) => handle.now(), + } + } + pub fn simulation(handle: sim::Handle) -> Self { Self::Simulation(handle) } @@ -354,14 +361,13 @@ impl Handle { } } - pub fn now(&self) -> Duration { - match self { - Self::Tokio(_) => panic!("Handle::now is only supported by the simulation runtime"), - #[cfg(feature = "simulation")] - Self::Simulation(handle) => handle.now(), - } - } - + /// Blocks the current thread until `future` completes. + /// + /// For Tokio, this uses `block_in_place` before re-entering the runtime with + /// `Handle::block_on`, so it is supported from multi-thread runtime workers. + /// Do not call this from a current-thread Tokio runtime; Tokio panics on + /// that path. For simulation, this runs on the deterministic executor + /// thread; blocking inside the future blocks simulated progress. pub fn block_on(&self, future: F) -> F::Output { match self { Self::Tokio(handle) => tokio::task::block_in_place(|| handle.block_on(future)),