Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ members = [
"crates/primitives",
"crates/query",
"crates/runtime",
"crates/runtime-core",
"crates/sats",
"crates/schema",
"crates/smoketests",
Expand Down Expand Up @@ -152,6 +153,7 @@ spacetimedb-primitives = { path = "crates/primitives", version = "=2.7.1" }
spacetimedb-query = { path = "crates/query", version = "=2.7.1" }
spacetimedb-query-builder = { path = "crates/query-builder", version = "=2.7.1" }
spacetimedb-runtime = { path = "crates/runtime", version = "=2.7.1" }
spacetimedb-runtime-core = { path = "crates/runtime-core", version = "=2.7.1" }
spacetimedb-sats = { path = "crates/sats", version = "=2.7.1" }
spacetimedb-schema = { path = "crates/schema", version = "=2.7.1" }
spacetimedb-snapshot = { path = "crates/snapshot", version = "=2.7.1" }
Expand Down
18 changes: 18 additions & 0 deletions crates/runtime-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "spacetimedb-runtime-core"
version.workspace = true
edition.workspace = true
license-file = "LICENSE"
description = "Portable no_std runtime core utilities for SpacetimeDB"
rust-version.workspace = true

[lints]
workspace = true

[features]
default = []
sim = ["dep:async-task", "dep:spin"]

[dependencies]
async-task = { version = "4.4", default-features = false, optional = true }
spin = { version = "0.9", default-features = false, features = ["mutex", "spin_mutex"], optional = true }
1 change: 1 addition & 0 deletions crates/runtime-core/LICENSE
9 changes: 9 additions & 0 deletions crates/runtime-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#![no_std]

#[cfg(feature = "sim")]
extern crate alloc;
#[cfg(test)]
extern crate std;

#[cfg(feature = "sim")]
pub mod sim;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::sim::Runtime;
use super::Runtime;

/// Probabilistic fault-injection helpers for simulation code.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use core::{

use spin::Mutex;

use crate::sim::{time::TimeHandle, Rng};
use super::{time::TimeHandle, Rng};

mod task;
use task::Abortable;
Expand Down Expand Up @@ -220,22 +220,26 @@ impl Runtime {
}

#[allow(dead_code)]
pub(crate) fn enable_determinism_log(&self) {
#[doc(hidden)]
pub fn enable_determinism_log(&self) {
self.executor.rng.enable_determinism_log();
}

#[allow(dead_code)]
pub(crate) fn enable_determinism_check(&self, log: crate::sim::DeterminismLog) {
#[doc(hidden)]
pub fn enable_determinism_check(&self, log: super::DeterminismLog) {
self.executor.rng.enable_determinism_check(log);
}

#[allow(dead_code)]
pub(crate) fn take_determinism_log(&self) -> Option<crate::sim::DeterminismLog> {
#[doc(hidden)]
pub fn take_determinism_log(&self) -> Option<super::DeterminismLog> {
self.executor.rng.take_determinism_log()
}

#[allow(dead_code)]
pub(crate) fn finish_determinism_check(&self) -> Result<(), alloc::string::String> {
#[doc(hidden)]
pub fn finish_determinism_check(&self) -> Result<(), alloc::string::String> {
self.executor.rng.finish_determinism_check()
}
}
Expand Down Expand Up @@ -306,7 +310,7 @@ impl Handle {
}

/// Create a future that becomes ready after `duration` of virtual time.
pub fn sleep(&self, duration: Duration) -> crate::sim::time::Sleep {
pub fn sleep(&self, duration: Duration) -> super::time::Sleep {
self.executor.time.sleep(duration)
}

Expand All @@ -315,7 +319,7 @@ impl Handle {
&self,
duration: Duration,
future: impl Future<Output = T>,
) -> Result<T, crate::sim::time::TimeoutElapsed> {
) -> Result<T, super::time::TimeoutElapsed> {
self.executor.time.timeout(duration, future).await
}

Expand Down Expand Up @@ -645,7 +649,7 @@ impl Receiver {
#[cfg(test)]
mod tests {
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
atomic::{AtomicUsize, Ordering},
Arc,
};

Expand Down Expand Up @@ -734,13 +738,12 @@ mod tests {
assert_eq!(err, JoinError);
}

#[cfg(feature = "simulation")]
#[test]
fn sim_std_block_on_can_spawn_local_task_with_explicit_handle() {
fn block_on_can_spawn_local_task_with_explicit_handle() {
let mut runtime = Runtime::new(5);
let handle = runtime.handle();
let node = handle.create_node().name("local").build();
let value = crate::sim_std::block_on(&mut runtime, async move {
let value = runtime.block_on(async move {
let captured = std::rc::Rc::new(17);
node.spawn_local(async move {
yield_now().await;
Expand All @@ -763,34 +766,4 @@ mod tests {
assert_eq!(named.name(), Some("replica-1"));
assert_ne!(unnamed.id(), named.id());
}

#[cfg(feature = "simulation")]
#[test]
fn check_determinism_runs_future_twice() {
static CALLS: AtomicUsize = AtomicUsize::new(0);
CALLS.store(0, Ordering::SeqCst);

let value = crate::sim_std::check_determinism(3, || async {
CALLS.fetch_add(1, Ordering::SeqCst);
yield_now().await;
13
});

assert_eq!(value, 13);
assert_eq!(CALLS.load(Ordering::SeqCst), 2);
}

#[cfg(feature = "simulation")]
#[test]
#[should_panic(expected = "non-determinism detected")]
fn check_determinism_rejects_different_scheduler_sequence() {
static FIRST_RUN: AtomicBool = AtomicBool::new(true);
FIRST_RUN.store(true, Ordering::SeqCst);

crate::sim_std::check_determinism(4, || async {
if FIRST_RUN.swap(false, Ordering::SeqCst) {
yield_now().await;
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ impl<T> JoinHandle<T> {
}

/// Poll the underlying async_task::Task for its output.
pub(crate) fn poll_join(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<T, JoinError>> {
#[doc(hidden)]
pub fn poll_join(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<T, JoinError>> {
// async_task::Task implements Future. Polling it drives the wrapped
// Abortable future inside the executor.
Pin::new(&mut self.task).poll(cx)
Expand Down Expand Up @@ -96,8 +97,7 @@ impl fmt::Display for JoinError {
}
}

#[cfg(feature = "simulation")]
impl std::error::Error for JoinError {}
impl core::error::Error for JoinError {}

// Shared state between AbortHandle and Abortable.
struct AbortState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ pub mod time;
pub use executor::{
yield_now, AbortHandle, Handle, JoinError, JoinHandle, Node, NodeBuilder, NodeId, Runtime, RuntimeConfig,
};
pub(crate) use rng::DeterminismLog;
#[doc(hidden)]
pub use rng::DeterminismLog;
pub use rng::{GlobalRng, Rng};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub type Rng = GlobalRng;
/// The simulator owns one runtime-wide RNG handle and uses it for scheduler
/// choices, probabilistic fault injection, and determinism checks. Hosted
/// conveniences such as thread-local current-RNG access and libc random hooks
/// live in `crate::sim_std`, not here.
/// live in the hosted runtime facade, not here.
#[derive(Clone, Debug)]
pub struct GlobalRng {
inner: Arc<Mutex<Inner>>,
Expand Down Expand Up @@ -143,21 +143,24 @@ impl GlobalRng {
}

#[allow(dead_code)]
pub(crate) fn enable_determinism_log(&self) {
#[doc(hidden)]
pub fn enable_determinism_log(&self) {
let mut inner = self.inner.lock();
inner.log = Some(Vec::new());
inner.check = None;
}

#[allow(dead_code)]
pub(crate) fn enable_determinism_check(&self, log: DeterminismLog) {
#[doc(hidden)]
pub fn enable_determinism_check(&self, log: DeterminismLog) {
let mut inner = self.inner.lock();
inner.check = Some((log.0, 0));
inner.log = None;
}

#[allow(dead_code)]
pub(crate) fn take_determinism_log(&self) -> Option<DeterminismLog> {
#[doc(hidden)]
pub fn take_determinism_log(&self) -> Option<DeterminismLog> {
let mut inner = self.inner.lock();
inner
.log
Expand All @@ -167,7 +170,8 @@ impl GlobalRng {
}

#[allow(dead_code)]
pub(crate) fn finish_determinism_check(&self) -> Result<(), String> {
#[doc(hidden)]
pub fn finish_determinism_check(&self) -> Result<(), String> {
let inner = self.inner.lock();
if let Some((log, consumed)) = &inner.check
&& *consumed != log.len()
Expand All @@ -183,7 +187,7 @@ impl GlobalRng {
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct DeterminismLog(Vec<u8>);
pub struct DeterminismLog(Vec<u8>);

fn probability_sample(value: u64, probability: f64) -> bool {
if probability <= 0.0 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,11 @@ impl fmt::Display for TimeoutElapsed {
}
}

impl std::error::Error for TimeoutElapsed {}
impl core::error::Error for TimeoutElapsed {}

#[cfg(test)]
mod tests {
use alloc::{vec, vec::Vec};
use std::{sync::Arc, time::Duration};

use crate::sim;
Expand Down
5 changes: 2 additions & 3 deletions crates/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ workspace = true

[dependencies]
tokio.workspace = true
async-task = { version = "4.4", default-features = false, optional = true }
spin = { version = "0.9", default-features = false, features = ["mutex", "spin_mutex"], optional = true }
spacetimedb-runtime-core = { workspace = true, optional = true }
libc = { version = "0.2", optional = true }

[dev-dependencies]
futures.workspace = true

[features]
simulation = ["dep:async-task", "dep:spin", "dep:libc"]
simulation = ["dep:spacetimedb-runtime-core", "spacetimedb-runtime-core/sim", "dep:libc"]
5 changes: 1 addition & 4 deletions crates/runtime/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
#[cfg(feature = "simulation")]
extern crate alloc;

use core::{
fmt,
future::Future,
Expand All @@ -11,7 +8,7 @@ use core::{
};

#[cfg(feature = "simulation")]
pub mod sim;
pub use spacetimedb_runtime_core::sim;
#[cfg(feature = "simulation")]
pub mod sim_std;

Expand Down
39 changes: 19 additions & 20 deletions crates/runtime/src/sim_std.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
//! Std-hosted entry points for running the deterministic simulator in tests.
//!
//! The portable simulator lives in [`crate::sim`]. This module is deliberately
//! host-specific: it installs thread-local context while a simulation is
//! running, checks determinism by replaying a seed in fresh OS threads, and
//! intercepts a few libc calls so std code cannot silently escape determinism.
//! host-specific: it marks a thread as simulation-owned while hosted tests run,
//! checks determinism by replaying a seed in fresh OS threads, and intercepts a
//! few libc calls so std code cannot silently escape determinism.

#![allow(clippy::disallowed_macros)]

use alloc::boxed::Box;
use core::{cell::Cell, future::Future};
use std::boxed::Box;
use std::sync::OnceLock;

use crate::sim;
Expand All @@ -21,7 +21,7 @@ use crate::sim;
/// tests that execute inside a hosted process. While the future runs, this
/// marks the thread as inside simulation so OS thread spawns can be rejected.
pub fn block_on<F: Future>(runtime: &mut sim::Runtime, future: F) -> F::Output {
let _system_thread_context = enter_simulation_thread();
let _guard = enter();
runtime.block_on(future)
}

Expand Down Expand Up @@ -68,34 +68,33 @@ fn panic_with_seed(seed: u64, payload: Box<dyn core::any::Any + Send>) -> ! {

// Simulation thread context.

// Ambient state used only while `sim_std::block_on` is driving a simulation.
// Ambient hosted state used only while sim_std is driving a simulation runtime.
//
// The simulator itself stays explicit-handle based. This thread-local only
// marks whether the current OS thread is owned by a running simulation so
// host thread creation can be rejected.
// The simulator itself stays explicit-handle based. This flag only marks the
// current OS thread as simulation-owned so host thread creation and randomness
// hooks can reject accidental escapes from deterministic simulation.
thread_local! {
// Marks the current OS thread as simulation-owned so thread creation hooks
// can reject accidental escapes to the host scheduler.
static IN_SIMULATION: Cell<bool> = const { Cell::new(false) };
}

struct SimulationThreadGuard {
previous: bool,
}
#[must_use = "simulation runtime context exits immediately unless the guard is held"]
struct EnterGuard;

fn enter_simulation_thread() -> SimulationThreadGuard {
let previous = IN_SIMULATION.with(|state| state.replace(true));
SimulationThreadGuard { previous }
fn enter() -> EnterGuard {
IN_SIMULATION.with(|current| {
assert!(!current.replace(true), "nested hosted simulation block_on");
});
EnterGuard
}

fn in_simulation() -> bool {
IN_SIMULATION.with(Cell::get)
}

impl Drop for SimulationThreadGuard {
impl Drop for EnterGuard {
fn drop(&mut self) {
IN_SIMULATION.with(|state| {
state.set(self.previous);
IN_SIMULATION.with(|current| {
assert!(current.replace(false), "simulation context guard dropped without enter");
});
}
}
Expand Down
Loading
Loading