Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
dcef6f1
WIP: I/O API + simulator
kim Aug 3, 2026
11dc4c7
Use SimulatorIO as the "I/O driver" in the executor
kim Aug 4, 2026
f47352a
Make it clearer that we're clearing
kim Aug 4, 2026
243930b
Expose ways for the runtime to inject failures.
kim Aug 6, 2026
40faf30
Encapsulate SimulatorIO in an I/O "driver" that can inject failures.
kim Aug 7, 2026
c60c637
Remove TODO
kim Aug 7, 2026
51b1f6e
Fix optional dependencies
kim Aug 7, 2026
36ca1f4
Fix windows
kim Aug 7, 2026
4536207
Fix fix windows
kim Aug 8, 2026
d4da868
Add a length function for files
kim Aug 8, 2026
be161e4
Fix editor fuckup, satisfy error trait bound
kim Aug 18, 2026
d498da1
Unify completion future
kim Aug 21, 2026
a375c7f
WIP: simulator redesign
kim Aug 27, 2026
059a6ac
Adjust queuing / overflow behavior to match io-uring more closely
kim Sep 2, 2026
31baac5
Slight cleanup
kim Sep 2, 2026
80f2787
Model file durability
kim Sep 2, 2026
77b3a0b
Prepare `SqeId` for export
kim Sep 3, 2026
c5c7963
WIP: fault injection API
kim Sep 3, 2026
e133798
Merge branch 'master' into kim/sim-io
kim Sep 4, 2026
f25388d
Consider actual size written/read for Write/Read completions
kim Sep 4, 2026
6c23090
Turns out we can do without the unsafe, ownership-erased `ErasedPtr`
kim Sep 7, 2026
08397f6
Fix tests: file reads must actually use the volatile state
kim Sep 7, 2026
ade9e51
Fix buf offset
kim Sep 7, 2026
6a7679c
Fix file length ops
kim Sep 7, 2026
9178efd
Tests
kim Sep 7, 2026
bebc3dc
Move I/O to separate runtime-io crate
kim Sep 7, 2026
9074d72
Remove unused `Instant` alias
kim Sep 7, 2026
e5a2178
Replace oneshot channel with custom, no-alloc future
kim Sep 9, 2026
a84015a
Make cancellation impossible, matching io-uring semantics
kim Sep 14, 2026
a3335ea
Avoid allocations for results and ops
kim Sep 14, 2026
1db074b
Simplify types a bit
kim Sep 14, 2026
34a23f3
Merge origin/master into kim/sim-io
kim Sep 14, 2026
7dbb5c5
Require io buffers to point to stable memory, i.e. Box
kim Sep 14, 2026
311b7f0
Avoid internal allocation for paths
kim Sep 14, 2026
e4221e1
Put file behind a single Arc, and perform copy-on-write when a page is
kim Sep 14, 2026
a61e77b
Re-use btree map allocation in page map on power-loss
kim Sep 14, 2026
d87139f
Fix task selection + use vec
kim Sep 15, 2026
32fda57
Preserve `executing` allocation on `restart`
kim Sep 15, 2026
ee1c623
Pending capacity, options constructor, Rc instead of Arc (not Sync
kim Sep 15, 2026
7b7da4e
Bound executing queue
kim Sep 15, 2026
f0e302e
Avoid alloc when trimming file
kim Sep 15, 2026
108ac4c
Track blocked linked in-flight tasks in the same queue, and make that
kim Sep 15, 2026
036f554
Feed `executing` incrementally
kim Sep 16, 2026
685a485
Make pending completions bounded
kim Sep 16, 2026
01cbcf1
Make the completion future `Send`
kim Sep 16, 2026
9d8183e
Undo all changes in runtime-core
kim Sep 16, 2026
cc4c39b
Redo fs
kim Sep 16, 2026
5dc8ace
Cancel and complete CQEs on restart
kim Sep 17, 2026
fd2bc88
Revisit task selection and document fault injection.
kim Sep 18, 2026
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
16 changes: 16 additions & 0 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 @@ -29,6 +29,7 @@ members = [
"crates/query",
"crates/runtime",
"crates/runtime-core",
"crates/runtime-io",
"crates/sats",
"crates/schema",
"crates/smoketests",
Expand Down Expand Up @@ -176,6 +177,7 @@ spacetimedb-query = { path = "crates/query", version = "=2.10.1" }
spacetimedb-query-builder = { path = "crates/query-builder", version = "=2.10.1" }
spacetimedb-runtime = { path = "crates/runtime", version = "=2.10.1" }
spacetimedb-runtime-core = { path = "crates/runtime-core", version = "=2.10.1" }
spacetimedb-runtime-io = { path = "crates/runtime-io", version = "=2.10.1" }
spacetimedb-sats = { path = "crates/sats", version = "=2.10.1" }
spacetimedb-schema = { path = "crates/schema", version = "=2.10.1" }
spacetimedb-snapshot = { path = "crates/snapshot", version = "=2.10.1" }
Expand Down
22 changes: 22 additions & 0 deletions crates/runtime-io/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "spacetimedb-runtime-io"
version.workspace = true
edition.workspace = true
rust-version.workspace = true

[lints]
workspace = true

[features]
sim = ["dep:slab", "dep:spin"]

[dependencies]
slab = { version = "0.4", default-features = false, optional = true }
spin = { version = "0.9", default-features = false, features = ["mutex", "spin_mutex"], optional = true }
thiserror = { version = "2.0", default-features = false }
zerocopy = "0.8"

[dev-dependencies]
spacetimedb-runtime-core = { workspace = true, features = ["sim"] }
spacetimedb-runtime-io = { path = ".", features = ["sim"] }
tokio.workspace = true
163 changes: 163 additions & 0 deletions crates/runtime-io/src/buf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
use alloc::boxed::Box;
use core::{alloc::Layout, any::TypeId, ptr::NonNull};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};

use crate::SECTOR_SIZE;

/// Types that can be safely converted to and from sector-aligned byte slices.
pub trait AlignedBytes: Sized {
/// Assert that the type' size is a multiple of [SECTOR_SIZE] and has the
/// right alignment.
///
/// The type must also not rely on drop glue, i.e. `!core::mem::needs_drop()`.
///
/// NOTE: Associated constants are evaluated lazily -- add a free
///
/// `const _: () = <T as AlignedBytes>::ASSERT_VALID_LAYOUT;`
///
/// for each `T` that is supposed to be used as an `AlignedBytes`.
const ASSERT_VALID_LAYOUT: () = {
assert!(align_of::<Self>() == SECTOR_SIZE);
assert!(size_of::<Self>().is_multiple_of(SECTOR_SIZE));
assert!(!core::mem::needs_drop::<Self>());
};

/// Reinterpret `self` as a byte slice.
///
/// The returned slice will be of length `size_of::<Self>()`.
fn as_bytes(&self) -> &[u8];

/// Reinterpret `self` as a mutable byte slice.
///
/// The returned slice will be of length `size_of::<Self>()`.
fn as_bytes_mut(&mut self) -> &mut [u8];

/// Reinterpret a byte slice as `Self`.
///
/// The slice must be of length `size_of::<Self>()`.
///
/// NOTE: Any slice of the right size, but consisting of only `0` (zero)
/// bytes can be converted to `Self`. It is the caller's responsibility to
/// validate the returned type as per the application's invariants.
///
/// # Panics
///
/// Panics if `b.len() != size_of::<Self>()`.
fn from_bytes(b: &[u8]) -> Self;
}

impl<T: FromBytes + IntoBytes + KnownLayout + Immutable> AlignedBytes for T {
fn as_bytes(&self) -> &[u8] {
<T as IntoBytes>::as_bytes(self)
}

fn as_bytes_mut(&mut self) -> &mut [u8] {
<T as IntoBytes>::as_mut_bytes(self)
}

fn from_bytes(b: &[u8]) -> Self {
Self::read_from_bytes(b).unwrap()
}
}

/// A type-erased [AlignedBytes] heap allocation.
#[derive(Debug)]
pub struct ErasedBox {
ptr: NonNull<u8>,
len: usize,
layout: Layout,
ty: TypeId,
}

impl ErasedBox {
/// Create an [ErasedBox] from boxed [AlignedBytes]..
pub fn from_aligned<B: AlignedBytes + Send + 'static>(b: Box<B>) -> Self {
let () = B::ASSERT_VALID_LAYOUT;

let ptr = Box::into_raw(b);
Self {
ptr: NonNull::new(ptr.cast()).unwrap(),
len: size_of::<B>(),
layout: Layout::from_size_align(size_of::<B>(), align_of::<B>()).unwrap(),
ty: TypeId::of::<B>(),
}
}

/// Reify `B` via casting.
pub fn into_aligned<B: AlignedBytes + Send + 'static>(self) -> Box<B> {
assert_eq!(self.len, size_of::<B>());
assert_eq!(self.ty, TypeId::of::<B>());

let boxed = unsafe { Box::from_raw(self.ptr.as_ptr().cast::<B>()) };
// Prevent drop, which would deallocate.
core::mem::forget(self);

boxed
}

pub fn as_mut_ptr(&self) -> *mut u8 {
self.ptr.as_ptr()
}

pub fn len(&self) -> usize {
self.len
}

pub fn is_empty(&self) -> bool {
self.len == 0
}

pub fn as_bytes(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
}

pub fn as_bytes_mut(&mut self) -> &mut [u8] {
unsafe { core::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
}
}

impl Drop for ErasedBox {
fn drop(&mut self) {
unsafe { alloc::alloc::dealloc(self.ptr.as_ptr(), self.layout) }
}
}

// SAFETY: [ErasedBox] is `Send` because it can only be constructed from a
// `Send` [AlignedBuffer].
unsafe impl Send for ErasedBox {}

#[cfg(test)]
mod tests {
use super::*;

#[repr(C, align(4096))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Trivial([u8; 4096]);

impl AlignedBytes for Trivial {
fn as_bytes(&self) -> &[u8] {
&self.0
}

fn as_bytes_mut(&mut self) -> &mut [u8] {
&mut self.0
}

fn from_bytes(b: &[u8]) -> Self {
assert_eq!(b.len(), size_of::<Self>());
let mut a = [0; 4096];
a.copy_from_slice(b);
Self(a)
}
}

#[test]
fn roundtrip_preserves_value() {
let t = Trivial([32; 4096]);

let erased = ErasedBox::from_aligned(Box::new(t));
let reified = erased.into_aligned::<Trivial>();

assert_eq!(reified, Box::new(t));
}
}
43 changes: 43 additions & 0 deletions crates/runtime-io/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/// An error `E`, along with auxiliary data `T`.
///
/// `T` is usually a buffer of type [AlignedBytes], whose ownership is
/// transferred back to the caller when an error occurs.
///
/// As this type signifies an error condition, the contents of `T` are
/// unspecified.
///
/// [AlignedBytes]: crate::io::buf::AlignedBytes
#[derive(Debug)]
pub struct ErrorWith<E, T> {
pub error: E,
pub with: T,
}

impl<E, T> ErrorWith<E, T> {
/// Map a type-changing function over `self.error`.
pub fn map_err<F>(self, f: impl FnOnce(E) -> F) -> ErrorWith<F, T> {
ErrorWith {
error: f(self.error),
with: self.with,
}
}

/// Map a type-changing function over `self.with`.
pub fn map_with<U>(self, f: impl FnOnce(T) -> U) -> ErrorWith<E, U> {
ErrorWith {
error: self.error,
with: f(self.with),
}
}

/// Extract `self.error`, discarding `self.with`.
pub fn into_err(self) -> E {
self.error
}

/// Convert from `&ErrorWith<E, T>` to `ErrorWith<&E, &T>`.
pub fn as_ref(&self) -> ErrorWith<&E, &T> {
let Self { ref error, ref with } = *self;
ErrorWith { error, with }
}
}
Loading
Loading