Skip to main content

commonware_runtime/
lib.rs

1//! Execute asynchronous tasks with a configurable scheduler.
2//!
3//! This crate provides a collection of runtimes that can be used to execute
4//! asynchronous tasks in a variety of ways. For production use, the `tokio`
5//! module provides a runtime backed by [Tokio](https://tokio.rs). On Linux, the
6//! `iouring` module provides a runtime backed by io_uring. Each worker in its
7//! pool drives its tasks and I/O together on its own ring, and the first runs
8//! on the calling thread. The `mimalloc` feature exports mimalloc's allocator
9//! statistics as runtime metrics when the process's global allocator is
10//! `commonware_allocator::mimalloc::MiMalloc`. For testing and simulation, the
11//! `deterministic` module provides a runtime that allows for deterministic
12//! execution of tasks (given a fixed seed).
13//!
14//! # Terminology
15//!
16//! Each runtime is typically composed of an `Executor` and a `Context`. The `Executor` implements the
17//! `Runner` trait and drives execution of a runtime. The `Context` implements any number of the
18//! other traits to provide core functionality.
19//!
20//! # Status
21//!
22//! Stability varies by primitive. See [README](https://github.com/commonwarexyz/monorepo#stability) for details.
23
24#![doc(
25    html_logo_url = "https://commonware.xyz/imgs/rustdoc_logo.svg",
26    html_favicon_url = "https://commonware.xyz/favicon.ico"
27)]
28
29use commonware_macros::stability_scope;
30
31#[macro_use]
32mod macros;
33
34mod network;
35mod process;
36mod storage;
37
38stability_scope!(ALPHA {
39    #[cfg(feature = "arbitrary")]
40    pub mod conformance;
41    pub mod deterministic;
42    pub mod mocks;
43});
44stability_scope!(ALPHA, cfg(not(target_arch = "wasm32")) {
45    pub mod benchmarks;
46});
47stability_scope!(ALPHA, cfg(target_os = "linux") {
48    pub mod iouring;
49});
50stability_scope!(BETA, cfg(not(target_arch = "wasm32")) {
51    pub mod tokio;
52});
53stability_scope!(BETA {
54    /// Re-export of `Buf` and `BufMut` traits for usage with [I/O buffers](iobuf).
55    pub use bytes::{Buf, BufMut};
56    use commonware_macros::select;
57    use commonware_parallel::Rayon;
58    use futures::FutureExt as _;
59    /// Re-export of [governor::Quota] for rate limiting configuration.
60    pub use governor::Quota;
61    use std::{
62        future::Future,
63        io::Error as IoError,
64        net::SocketAddr,
65        num::NonZeroUsize,
66        sync::Arc,
67        time::{Duration, SystemTime},
68    };
69    pub(crate) use telemetry::metrics::{METRICS_PREFIX, child_label, prefixed_name};
70    use thiserror::Error;
71
72    pub mod iobuf;
73    pub use iobuf::{Builder as IoBufsBuilder, IoBuf, IoBufMut, IoBufs, IoBufsMut};
74
75    pub mod utils;
76    pub use utils::*;
77
78    pub mod telemetry;
79
80    /// Runtime-owned layout of a [`Blob`].
81    ///
82    /// This determines the container format, including its header and data offset, not the
83    /// application-owned contents version passed to [`Storage::open_versioned`].
84    ///
85    /// Restricting the layouts a runtime accepts can be used to orchestrate rollback-safe
86    /// upgrades.
87    #[repr(u16)]
88    #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
89    pub enum BlobLayout {
90        /// An 8-byte header, with data beginning immediately after it.
91        ///
92        /// A V0 header has no checksum, so an interrupted creation is not reliably
93        /// recognized: an image whose magic and layout version are durable parses as a
94        /// complete header and reopens as blob version 0 or fails as a version mismatch
95        /// until the blob is removed.
96        #[deprecated(note = "unaligned pages can degrade performance")]
97        V0 = 0,
98        /// A header padded to one 4096-byte page, so data begins on an aligned boundary.
99        V1 = 1,
100    }
101
102    /// Latest supported [`BlobLayout`], used to create new [`Blob`]s unless the runtime
103    /// restricts layouts to an older range.
104    pub const DEFAULT_BLOB_LAYOUT: BlobLayout = BlobLayout::V1;
105
106    impl BlobLayout {
107        /// All blob layouts supported by this runtime.
108        #[allow(deprecated)]
109        pub const ALL: std::ops::RangeInclusive<Self> = Self::V0..=DEFAULT_BLOB_LAYOUT;
110    }
111
112    /// Size in bytes of a blob page, the smallest unit in which storage is expected to read and
113    /// persist [`Blob`] data.
114    ///
115    /// Blobs created with [`DEFAULT_BLOB_LAYOUT`] begin their data on a blob-page boundary, so blob
116    /// offset `n` lies in page `n / BLOB_PAGE_SIZE`.
117    ///
118    /// Writing part of a page costs about as much as writing all of it, so writes that share a
119    /// page are cheaper combined into one. Data read together is likewise cheaper when it stays
120    /// within one page.
121    ///
122    /// A host may use a larger power-of-two unit, such as a bigger memory page or filesystem block.
123    /// Each such unit holds whole blob pages, so data within one page still shares a unit, but a
124    /// structure larger than one page may straddle two of them.
125    ///
126    /// Page alignment affects performance only: a write within one page can still tear (see
127    /// [`Blob`]). The size is a fixed power of two, independent of the host's memory page
128    /// size, so layouts derived from it are the same on every host.
129    pub const BLOB_PAGE_SIZE: u32 = 4096;
130
131    // Blob pages are a power of two, and the default layout begins blob data on a page boundary.
132    const _: () = assert!(
133        BLOB_PAGE_SIZE.is_power_of_two()
134            && DEFAULT_BLOB_LAYOUT
135                .data_offset()
136                .is_multiple_of(BLOB_PAGE_SIZE as u64)
137    );
138
139    /// Application-owned version of a [`Blob`]'s contents.
140    ///
141    /// This is independent of the runtime-owned [`BlobLayout`].
142    #[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
143    #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
144    pub struct BlobVersion(u16);
145
146    impl BlobVersion {
147        /// Creates a blob version from a `u16`.
148        pub const fn new(version: u16) -> Self {
149            Self(version)
150        }
151
152        /// Returns the underlying `u16`.
153        pub const fn get(self) -> u16 {
154            self.0
155        }
156    }
157
158    impl std::fmt::Display for BlobVersion {
159        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160            write!(f, "{}", self.0)
161        }
162    }
163
164    impl std::fmt::Debug for BlobVersion {
165        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166            write!(f, "{}", self.0)
167        }
168    }
169
170    /// Default application-owned [`Blob`] contents version used by [`Storage::open`].
171    pub const DEFAULT_BLOB_VERSION: BlobVersion = BlobVersion::new(0);
172
173    /// Errors that can occur when interacting with the runtime.
174    #[derive(Error, Debug, Clone)]
175    pub enum Error {
176        #[error("exited")]
177        Exited,
178        #[error("closed")]
179        Closed,
180        #[error("aborted")]
181        Aborted,
182        #[error("timeout")]
183        Timeout,
184        #[error("bind failed")]
185        BindFailed,
186        #[error("connection failed")]
187        ConnectionFailed,
188        #[error("write failed")]
189        WriteFailed,
190        #[error("read failed")]
191        ReadFailed,
192        #[error("send failed")]
193        SendFailed,
194        #[error("recv failed")]
195        RecvFailed,
196        #[error("dns resolution failed: {0}")]
197        ResolveFailed(String),
198        #[error(
199            "partition name invalid, must only contain alphanumeric, dash ('-'), or underscore ('_') characters: {0}"
200        )]
201        PartitionNameInvalid(String),
202        #[error("partition creation failed: {0}")]
203        PartitionCreationFailed(String),
204        #[error("partition missing: {0}")]
205        PartitionMissing(String),
206        #[error("partition corrupt: {0}")]
207        PartitionCorrupt(String),
208        #[error("blob already open: {0}/{1}")]
209        BlobAlreadyOpen(String, String),
210        #[error("blob open failed: {0}/{1} error: {2}")]
211        BlobOpenFailed(String, String, Arc<IoError>),
212        #[error("blob missing: {0}/{1}")]
213        BlobMissing(String, String),
214        #[error("blob resize failed: {0}/{1} error: {2}")]
215        BlobResizeFailed(String, String, Arc<IoError>),
216        #[error("blob sync failed: {0}/{1} error: {2}")]
217        BlobSyncFailed(String, String, Arc<IoError>),
218        #[error("blob insufficient length")]
219        BlobInsufficientLength,
220        #[error("blob corrupt: {0}/{1} reason: {2}")]
221        BlobCorrupt(String, String, String),
222        #[error("blob layout mismatch: expected one of {expected:?}, found {found:?}")]
223        BlobLayoutMismatch {
224            expected: std::ops::RangeInclusive<BlobLayout>,
225            found: BlobLayout,
226        },
227        #[error("blob version mismatch: expected one of {expected:?}, found {found}")]
228        BlobVersionMismatch {
229            expected: std::ops::RangeInclusive<BlobVersion>,
230            found: BlobVersion,
231        },
232        #[error("invalid or missing checksum")]
233        InvalidChecksum,
234        #[error("offset overflow")]
235        OffsetOverflow,
236        #[error("io error: {0}")]
237        Io(Arc<IoError>),
238    }
239
240    impl From<IoError> for Error {
241        fn from(err: IoError) -> Self {
242            Self::Io(Arc::new(err))
243        }
244    }
245
246    /// Interface that any task scheduler must implement to start
247    /// running tasks.
248    pub trait Runner {
249        /// Context defines the environment available to tasks.
250        type Context;
251
252        /// Start running a root task.
253        ///
254        /// When this function returns, all spawned tasks will be canceled. If clean
255        /// shutdown cannot be implemented via `Drop`, consider using [Spawner::stop] and
256        /// [Spawner::stopped] to coordinate clean shutdown.
257        fn start<F, Fut>(self, f: F) -> Fut::Output
258        where
259            F: FnOnce(Self::Context) -> Fut,
260            Fut: Future;
261    }
262
263    /// The full identity of a [`Supervisor`] handle.
264    #[derive(Clone, Debug, Default)]
265    pub struct Name {
266        /// Label prefix built by successive [`Supervisor::child`] calls.
267        pub label: String,
268        /// Attributes attached via [`Supervisor::with_attribute`].
269        pub attributes: Vec<(String, String)>,
270    }
271
272    /// Interface to track task hierarchy and identity.
273    pub trait Supervisor: Send + Sync + 'static {
274        /// Return the current label prefix and attributes.
275        fn name(&self) -> Name;
276
277        /// Create a named child context with a new supervision-tree node.
278        ///
279        /// This appends `label` to the current metric prefix and creates a
280        /// child in the supervision tree. Use static role names like
281        /// `"engine"`, `"worker"`, or `"resolver"`. Dynamic values belong in
282        /// [`Supervisor::with_attribute`] so metric names remain bounded.
283        ///
284        /// Labels must start with `[a-zA-Z]` and contain only
285        /// `[a-zA-Z0-9_]`. Runtime-reserved metric prefixes must not be used.
286        #[must_use]
287        fn child(&self, label: &'static str) -> Self;
288
289        /// Add a key-value attribute to this context's identity.
290        ///
291        /// Attributes are attached to metrics registered in this context and
292        /// any child contexts. Unlike [`Supervisor::child`], attributes do not
293        /// affect metric names and do not create a supervision-tree edge. This
294        /// makes them the right place for dynamic values like epochs, rounds,
295        /// shards, or peer identifiers.
296        ///
297        /// Keys must start with `[a-zA-Z]` and contain only `[a-zA-Z0-9_]`.
298        /// Values can be any string. If the key already exists, its value is
299        /// replaced.
300        ///
301        /// ```text
302        /// context
303        ///   |-- child("orchestrator")
304        ///         |-- with_attribute("epoch", "5")
305        ///               |-- counter: votes      -> orchestrator_votes{epoch="5"}
306        ///               |-- counter: proposals  -> orchestrator_proposals{epoch="5"}
307        ///               |-- child("engine")
308        ///                     |-- gauge: height -> orchestrator_engine_height{epoch="5"}
309        /// ```
310        ///
311        /// This pattern avoids wrapping every metric in a `Family` and avoids
312        /// putting dynamic values in metric names like
313        /// `orchestrator_epoch_5_votes`.
314        ///
315        /// Attributes do not reduce cardinality. N epochs still means N time
316        /// series. They just make metrics easier to query, filter, and
317        /// aggregate.
318        ///
319        /// # Family Label Conflicts
320        ///
321        /// When using `Family` metrics, avoid attribute keys that match the
322        /// family's label field names. A conflict produces duplicate labels in
323        /// the encoded output, which is invalid Prometheus format.
324        ///
325        /// ```ignore
326        /// #[derive(EncodeLabelSet)]
327        /// struct Labels { env: String }
328        ///
329        /// // Bad: attribute "env" conflicts with Family field "env".
330        /// let ctx = context.child("api").with_attribute("env", "prod");
331        /// let family: Family<Labels, Counter> = Family::default();
332        /// ctx.register("requests", "help", family);
333        ///
334        /// // Good: use distinct names.
335        /// let ctx = context.child("api").with_attribute("region", "us_east");
336        /// ```
337        ///
338        /// # Querying The Latest Attribute
339        ///
340        /// To query the latest attribute value dynamically, create a gauge to
341        /// track the current value:
342        ///
343        /// ```ignore
344        /// let latest_epoch = context
345        ///     .child("orchestrator")
346        ///     .register("latest_epoch", "current epoch", Gauge::default());
347        /// latest_epoch.set(current_epoch);
348        /// ```
349        ///
350        /// A dashboard can then query `max(orchestrator_latest_epoch)` and use
351        /// the result as a variable in queries such as
352        /// `consensus_engine_votes_total{epoch="$latest_epoch"}`.
353        #[must_use]
354        fn with_attribute(self, key: &'static str, value: impl std::fmt::Display) -> Self;
355    }
356
357    /// Interface that any task scheduler must implement to spawn tasks.
358    pub trait Spawner: Supervisor {
359        /// Return a [`Spawner`] that schedules the next task onto the runtime's shared executor.
360        ///
361        /// Set `blocking` to `true` when the task may hold the thread for a short, blocking operation.
362        /// Runtimes can use this hint to move the work to a blocking-friendly pool so asynchronous
363        /// tasks on a work-stealing executor are not starved. For long-lived, blocking work, use
364        /// [`Spawner::dedicated`] instead.
365        ///
366        /// The shared executor with `blocking == false` is the default spawn mode.
367        #[must_use]
368        fn shared(self, blocking: bool) -> Self;
369
370        /// Return a [`Spawner`] that runs the next task on a dedicated thread when the runtime supports it.
371        ///
372        /// Reserve this for long-lived or prioritized tasks that should not compete for resources in the
373        /// shared executor.
374        ///
375        /// This is not the default behavior. See [`Spawner::shared`] for more information.
376        #[must_use]
377        fn dedicated(self) -> Self;
378
379        /// Spawn a task with the current context.
380        ///
381        /// Unlike directly awaiting a future, the task starts running immediately even if the caller
382        /// never awaits the returned [`Handle`].
383        ///
384        /// # Mandatory Supervision
385        ///
386        /// All tasks are supervised. When a parent task finishes or is aborted, all its descendants are aborted.
387        ///
388        /// Spawn consumes the current context and runs the task at a new child node. Additional supervised
389        /// children are created explicitly with [`Supervisor::child`].
390        ///
391        /// ```txt
392        /// ctx_a
393        ///   |
394        ///   +-- child("worker") ---> ctx_c
395        ///   |                  |
396        ///   |                  +-- spawn() ---> Task C (ctx_d)
397        ///   |
398        ///   +-- spawn() ---> Task A (ctx_b)
399        ///                              |
400        ///                              +-- spawn() ---> Task B (ctx_e)
401        ///
402        /// Task A finishes or aborts --> Task B and Task C are aborted
403        /// ```
404        ///
405        /// # Spawn Configuration
406        ///
407        /// [`Spawner::dedicated`] and [`Spawner::shared`] only affect the
408        /// handle they return. [`Supervisor::child`] and [`Spawner::spawn`]
409        /// both start child task contexts from a clean spawn configuration.
410        ///
411        /// Child tasks should assume they start from a clean configuration without needing to inspect how their
412        /// parent was configured.
413        fn spawn<F, Fut, T>(self, f: F) -> Handle<T>
414        where
415            Self: Sized,
416            F: FnOnce(Self) -> Fut + Send + 'static,
417            Fut: Future<Output = T> + Send + 'static,
418            T: Send + 'static;
419
420        /// Signals the runtime to stop execution and waits for all outstanding tasks
421        /// to perform any required cleanup and exit.
422        ///
423        /// This method does not actually kill any tasks but rather signals to them, using
424        /// the [signal::Signal] returned by [Spawner::stopped], that they should exit.
425        /// It then waits for all [signal::Signal] references to be dropped before returning.
426        ///
427        /// ## Multiple Stop Calls
428        ///
429        /// This method is idempotent and safe to call multiple times concurrently (on
430        /// different instances of the same context since it consumes `self`). The first
431        /// call initiates shutdown with the provided `value`, and all subsequent calls
432        /// will wait for the same completion regardless of their `value` parameter, i.e.
433        /// the original `value` from the first call is preserved.
434        ///
435        /// ## Timeout
436        ///
437        /// If a timeout is provided, the method will return an error if all [signal::Signal]
438        /// references have not been dropped within the specified duration.
439        fn stop(
440            self,
441            value: i32,
442            timeout: Option<Duration>,
443        ) -> impl Future<Output = Result<(), Error>> + Send;
444
445        /// Returns an instance of a [signal::Signal] that resolves when [Spawner::stop] is called by
446        /// any task.
447        ///
448        /// If [Spawner::stop] has already been called, the [signal::Signal] returned will resolve
449        /// immediately. The [signal::Signal] returned will always resolve to the value of the
450        /// first [Spawner::stop] call.
451        fn stopped(&self) -> signal::Signal;
452    }
453
454    /// Interface that runtimes implement to provide parallel execution strategies.
455    pub trait Strategizer: Spawner {
456        /// Returns a new [Rayon] strategy with the requested parallelism.
457        ///
458        /// # Arguments
459        /// - `parallelism`: The number of tasks to execute concurrently in the pool.
460        ///
461        /// # Panics
462        ///
463        /// Panics if the runtime cannot initialize the strategy's backing Rayon thread pool.
464        fn strategy(&self, parallelism: NonZeroUsize) -> Rayon;
465    }
466
467    /// Interface to register and encode metrics.
468    pub trait Metrics: Supervisor {
469        /// Register a metric with the runtime.
470        ///
471        /// Any registered metric includes the current context label as its name
472        /// prefix and the current context attributes as Prometheus labels. See
473        /// [`Supervisor`] for the identity model and examples.
474        ///
475        /// The returned [`telemetry::metrics::Registered`] value must be retained for as long as the
476        /// metric should remain exposed. Dropping the returned handle unregisters
477        /// the metric immediately.
478        ///
479        /// Re-registering the same metric key (same prefixed name and attributes)
480        /// returns another handle to the existing metric when the concrete metric
481        /// type matches. Registering the same key with a different metric type
482        /// panics.
483        ///
484        /// Names must start with `[a-zA-Z]` and contain only `[a-zA-Z0-9_]`.
485        fn register<N: Into<String>, H: Into<String>, M: telemetry::metrics::Metric>(
486            &self,
487            name: N,
488            help: H,
489            metric: M,
490        ) -> telemetry::metrics::Registered<M>;
491
492        /// Encode all metrics into a buffer.
493        fn encode(&self) -> String;
494    }
495
496    /// A direct (non-keyed) rate limiter using the provided [governor::clock::Clock] `C`.
497    ///
498    /// This is a convenience type alias for creating single-entity rate limiters.
499    /// For per-key rate limiting, use [KeyedRateLimiter].
500    pub type RateLimiter<C> = governor::RateLimiter<
501        governor::state::NotKeyed,
502        governor::state::InMemoryState,
503        C,
504        governor::middleware::NoOpMiddleware<<C as governor::clock::Clock>::Instant>,
505    >;
506
507    /// A rate limiter keyed by `K` using the provided [governor::clock::Clock] `C`.
508    ///
509    /// This is a convenience type alias for creating per-peer rate limiters
510    /// using governor's [HashMapStateStore].
511    ///
512    /// [HashMapStateStore]: governor::state::keyed::HashMapStateStore
513    pub type KeyedRateLimiter<K, C> = governor::RateLimiter<
514        K,
515        governor::state::keyed::HashMapStateStore<K>,
516        C,
517        governor::middleware::NoOpMiddleware<<C as governor::clock::Clock>::Instant>,
518    >;
519
520    /// Interface that any task scheduler must implement to provide
521    /// time-based operations.
522    ///
523    /// It is necessary to mock time to provide deterministic execution
524    /// of arbitrary tasks.
525    pub trait Clock:
526        governor::clock::Clock<Instant = SystemTime>
527        + governor::clock::ReasonablyRealtime
528        + Send
529        + Sync
530        + 'static
531    {
532        /// Returns the current time.
533        fn current(&self) -> SystemTime;
534
535        /// Sleep for the given duration.
536        fn sleep(&self, duration: Duration) -> impl Future<Output = ()> + Send + 'static + use<Self>;
537
538        /// Sleep until the given deadline.
539        fn sleep_until(&self, deadline: SystemTime) -> impl Future<Output = ()> + Send + 'static + use<Self>;
540
541        /// Await a future with a timeout, returning `Error::Timeout` if it expires.
542        ///
543        /// # Examples
544        ///
545        /// ```
546        /// use std::time::Duration;
547        /// use commonware_runtime::{deterministic, Error, Runner, Clock};
548        ///
549        /// let executor = deterministic::Runner::default();
550        /// executor.start(|context| async move {
551        ///     match context
552        ///         .timeout(Duration::from_millis(100), async { 42 })
553        ///         .await
554        ///     {
555        ///         Ok(value) => assert_eq!(value, 42),
556        ///         Err(Error::Timeout) => panic!("should not timeout"),
557        ///         Err(e) => panic!("unexpected error: {:?}", e),
558        ///     }
559        /// });
560        /// ```
561        fn timeout<F, T>(
562            &self,
563            duration: Duration,
564            future: F,
565        ) -> impl Future<Output = Result<T, Error>> + Send + '_
566        where
567            F: Future<Output = T> + Send + 'static,
568            T: Send + 'static,
569        {
570            async move {
571                select! {
572                    result = future => Ok(result),
573                    _ = self.sleep(duration) => Err(Error::Timeout),
574                }
575            }
576        }
577    }
578
579    /// Syntactic sugar for the type of [Sink] used by a given [Network] N.
580    pub type SinkOf<N> = <<N as Network>::Listener as Listener>::Sink;
581
582    /// Syntactic sugar for the type of [Stream] used by a given [Network] N.
583    pub type StreamOf<N> = <<N as Network>::Listener as Listener>::Stream;
584
585    /// Syntactic sugar for the type of [Listener] used by a given [Network] N.
586    pub type ListenerOf<N> = <N as crate::Network>::Listener;
587
588    /// Interface that any runtime must implement to create
589    /// network connections.
590    pub trait Network: Send + Sync + 'static {
591        /// The type of [Listener] that's returned when binding to a socket.
592        /// Accepting a connection returns a [Sink] and [Stream] which are defined
593        /// by the [Listener] and used to send and receive data over the connection.
594        type Listener: Listener;
595
596        /// Bind to the given socket address.
597        fn bind(
598            &self,
599            socket: SocketAddr,
600        ) -> impl Future<Output = Result<Self::Listener, Error>> + Send;
601
602        /// Dial the given socket address.
603        fn dial(
604            &self,
605            socket: SocketAddr,
606        ) -> impl Future<Output = Result<(SinkOf<Self>, StreamOf<Self>), Error>> + Send;
607    }
608
609    /// Interface for DNS resolution.
610    pub trait Resolver: Send + Sync + 'static {
611        /// Resolve a hostname to IP addresses.
612        ///
613        /// Returns a list of IP addresses that the hostname resolves to.
614        fn resolve(
615            &self,
616            host: &str,
617        ) -> impl Future<Output = Result<Vec<std::net::IpAddr>, Error>> + Send;
618    }
619
620    /// Interface that any runtime must implement to handle
621    /// incoming network connections.
622    pub trait Listener: Sync + Send + 'static {
623        /// The type of [Sink] that's returned when accepting a connection.
624        /// This is used to send data to the remote connection.
625        type Sink: Sink;
626        /// The type of [Stream] that's returned when accepting a connection.
627        /// This is used to receive data from the remote connection.
628        type Stream: Stream;
629
630        /// Accept an incoming connection.
631        fn accept(
632            &mut self,
633        ) -> impl Future<Output = Result<(SocketAddr, Self::Sink, Self::Stream), Error>> + Send;
634
635        /// Returns the local address of the listener.
636        fn local_addr(&self) -> Result<SocketAddr, std::io::Error>;
637    }
638
639    /// Interface that any runtime must implement to send
640    /// messages over a network connection.
641    pub trait Sink: Sync + Send + 'static {
642        /// Send a message to the sink.
643        ///
644        /// # Warning
645        ///
646        /// If the sink returns an error, part of the message may still be delivered.
647        /// After any error, the sink is no longer reusable and subsequent sends will
648        /// return [`Error::Closed`].
649        ///
650        /// Dropping the future (e.g. via `select!`) also poisons the sink, since a
651        /// partial write may have occurred.
652        fn send(
653            &mut self,
654            bufs: impl Into<IoBufs> + Send,
655        ) -> impl Future<Output = Result<(), Error>> + Send;
656    }
657
658    /// Interface that any runtime must implement to receive
659    /// messages over a network connection.
660    pub trait Stream: Sync + Send + 'static {
661        /// Receive exactly `len` bytes from the stream.
662        ///
663        /// The runtime allocates the buffer and returns it as `IoBufs`.
664        ///
665        /// # Warning
666        ///
667        /// If the stream returns an error, partially read data may be discarded.
668        /// After any error, the stream is no longer reusable and subsequent receives
669        /// will return [`Error::Closed`].
670        ///
671        /// Dropping the future (e.g. via `select!`) also poisons the stream, since
672        /// partially read data may be lost.
673        fn recv(&mut self, len: usize) -> impl Future<Output = Result<IoBufs, Error>> + Send;
674
675        /// Peek at buffered data without consuming.
676        ///
677        /// Returns up to `max_len` bytes from the internal buffer, or an empty slice
678        /// if no data is currently buffered. This does not perform any I/O or block.
679        ///
680        /// This is useful e.g. for parsing length prefixes without committing to a read
681        /// or paying the cost of async.
682        fn peek(&self, max_len: usize) -> &[u8];
683    }
684
685    /// Interface to interact with storage.
686    ///
687    /// Blobs can be read and written concurrently. The Concurrency section of [Blob] describes
688    /// what such reads observe.
689    ///
690    /// Storage can be backed by a local filesystem, cloud storage, etc.
691    ///
692    /// # Durability
693    ///
694    /// Runtimes must ensure that any data readable when user code starts
695    /// executing is crash-durable. Storage structures may rely on this during
696    /// recovery: data read at initialization can be assumed to survive a
697    /// subsequent crash without an explicit [`Blob::sync`].
698    ///
699    /// A blob reopened within a run has the same durability guarantee. Opening
700    /// waits for outstanding operations from the previous open to finish and
701    /// ensures that any data readable when it returns is crash-durable.
702    ///
703    /// # Cancellation
704    ///
705    /// Dropping an operation's future does not guarantee cancellation: the
706    /// operation may still complete, and later operations may observe its
707    /// effect.
708    ///
709    /// Runtimes must ensure that no operation issued by a previous run against
710    /// the same storage is still in flight when a new run begins.
711    ///
712    /// # Partition Names
713    ///
714    /// Partition names must be non-empty and contain only ASCII alphanumeric
715    /// characters, dashes (`-`), or underscores (`_`). Names containing other
716    /// characters (e.g., `/`, `.`, spaces) will return an error. On case-insensitive
717    /// filesystems, partition names that differ only by letter case must not both
718    /// be used.
719    pub trait Storage: Send + Sync + 'static {
720        /// The readable/writeable storage buffer that can be opened by this Storage.
721        type Blob: Blob;
722
723        /// [`Storage::open_versioned`] with [`DEFAULT_BLOB_VERSION`] as the only value
724        /// in the versions range. The blob version is omitted from the return value.
725        fn open(
726            &self,
727            partition: &str,
728            name: &[u8],
729        ) -> impl Future<Output = Result<(Self::Blob, u64), Error>> + Send {
730            async move {
731                let (blob, size, _) = self
732                    .open_versioned(partition, name, DEFAULT_BLOB_VERSION..=DEFAULT_BLOB_VERSION)
733                    .await?;
734                Ok((blob, size))
735            }
736        }
737
738        /// Open an existing blob in a given partition or create a new one, returning
739        /// the blob and its length.
740        ///
741        /// # Uniqueness
742        ///
743        /// Only one handle can be opened for a blob at a time. Use an [Arc] to share the
744        /// handle, and drop all owners before reopening the blob.
745        ///
746        /// An otherwise valid open returns [`Error::BlobAlreadyOpen`] if a handle from an earlier
747        /// open of the blob is still alive and the blob has not been removed since.
748        ///
749        /// # Durability
750        ///
751        /// An Ok result indicates the blob is durably created (or already exists).
752        ///
753        /// A runtime may retain a durability failure across logical opens. Later opens
754        /// can return that failure even after every handle has been dropped.
755        ///
756        /// # Versions
757        ///
758        /// Blobs are versioned. If the blob's version is not in `versions`, returns
759        /// [Error::BlobVersionMismatch]. `versions` must be non-empty.
760        ///
761        /// # Layout
762        ///
763        /// New blobs are created with the latest layout allowed by the runtime. Reopening an
764        /// existing blob honors the layout recorded in its header when the runtime's
765        /// configured layout range allows it, and returns [Error::BlobLayoutMismatch]
766        /// otherwise.
767        ///
768        /// # Returns
769        ///
770        /// A tuple of (blob, logical_size, blob_version).
771        fn open_versioned(
772            &self,
773            partition: &str,
774            name: &[u8],
775            versions: std::ops::RangeInclusive<BlobVersion>,
776        ) -> impl Future<Output = Result<(Self::Blob, u64, BlobVersion), Error>> + Send;
777
778        /// Remove a blob from a given partition.
779        ///
780        /// If no `name` is provided, the entire partition is removed.
781        ///
782        /// An Ok result indicates the blob is durably removed.
783        ///
784        /// # Read-after-remove
785        ///
786        /// Removal unlinks the blob's name but does not invalidate previously opened handles:
787        /// they remain fully readable until dropped, whether the blob was removed by name or by
788        /// removing its entire partition. This includes bytes written but never synced. Physical
789        /// resources are reclaimed once the last handle is dropped.
790        ///
791        /// Re-opening a removed blob's name creates a new, independent blob; handles opened
792        /// before the removal continue to observe the removed blob's contents.
793        ///
794        /// Mutating a removed blob (e.g. via [`Blob::write_at`], [`Blob::resize`], or
795        /// [`Blob::sync`]) is unspecified: implementations may succeed or return an error.
796        fn remove(
797            &self,
798            partition: &str,
799            name: Option<&[u8]>,
800        ) -> impl Future<Output = Result<(), Error>> + Send;
801
802        /// Return all blobs in a given partition.
803        fn scan(&self, partition: &str)
804        -> impl Future<Output = Result<Vec<Vec<u8>>, Error>> + Send;
805    }
806
807    /// Options that alter one [`Blob::read_at`], [`Blob::read_at_buf`], or [`Blob::read_many`]
808    /// operation.
809    ///
810    /// [`ReadOptions::default`] applies no options.
811    #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
812    pub struct ReadOptions(u8);
813
814    impl ReadOptions {
815        /// Advise that data brought in by this read need not remain in the OS page cache.
816        ///
817        /// This is a best-effort performance hint for callers that retain the data or do not
818        /// expect to read it again soon. Implementations may ignore it, and it does not
819        /// guarantee that the range is absent from the OS page cache. Honoring it can make
820        /// reads of cached or recently written data slower, and the returned buffers may be
821        /// views of a larger block-aligned allocation.
822        pub const DONT_CACHE: Self = Self(1 << 0);
823
824        /// Return whether all of `options` are set.
825        #[must_use]
826        pub const fn contains(self, options: Self) -> bool {
827            self.0 & options.0 == options.0
828        }
829
830        /// Return these options with `options` cleared.
831        #[must_use]
832        pub const fn without(self, options: Self) -> Self {
833            Self(self.0 & !options.0)
834        }
835    }
836
837    impl std::ops::BitOr for ReadOptions {
838        type Output = Self;
839
840        fn bitor(self, rhs: Self) -> Self::Output {
841            Self(self.0 | rhs.0)
842        }
843    }
844
845    impl std::ops::BitOrAssign for ReadOptions {
846        fn bitor_assign(&mut self, rhs: Self) {
847            self.0 |= rhs.0;
848        }
849    }
850
851    /// Options that alter one [`Blob::write_at`] operation.
852    ///
853    /// [`WriteOptions::default`] applies no options.
854    /// Combine options with `|`, such as `WriteOptions::SYNC | WriteOptions::DONT_CACHE`.
855    #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
856    pub struct WriteOptions(u8);
857
858    impl WriteOptions {
859        /// Durably persist the submitted bytes before returning.
860        ///
861        /// This is not a durability barrier for earlier writes without
862        /// [`WriteOptions::SYNC`] or earlier [`Blob::resize`] calls.
863        pub const SYNC: Self = Self(1 << 0);
864
865        /// Advise that the submitted bytes need not remain in the OS page cache.
866        ///
867        /// This is a best-effort performance hint for callers that maintain their own cache.
868        /// Implementations may ignore it. It does not change visibility or durability, or
869        /// guarantee that the range is absent from the OS page cache.
870        pub const DONT_CACHE: Self = Self(1 << 1);
871
872        /// Return whether all of `options` are set.
873        #[must_use]
874        pub const fn contains(self, options: Self) -> bool {
875            self.0 & options.0 == options.0
876        }
877
878        /// Return these options with `options` cleared.
879        #[must_use]
880        pub const fn without(self, options: Self) -> Self {
881            Self(self.0 & !options.0)
882        }
883    }
884
885    impl std::ops::BitOr for WriteOptions {
886        type Output = Self;
887
888        fn bitor(self, rhs: Self) -> Self::Output {
889            Self(self.0 | rhs.0)
890        }
891    }
892
893    impl std::ops::BitOrAssign for WriteOptions {
894        fn bitor_assign(&mut self, rhs: Self) {
895            self.0 |= rhs.0;
896        }
897    }
898
899    /// Interface to read and write to a blob.
900    ///
901    /// A blob represents one open. Opening it again while it is alive fails unless the
902    /// blob was removed since, see [`Storage::open_versioned`]. Wrap the blob in an [Arc]
903    /// to share access to that open.
904    ///
905    /// When a blob is dropped, any unsynced changes may be discarded. Dropping
906    /// does not synchronize the blob. Call [`Blob::sync`] before dropping to
907    /// ensure all changes are durably persisted.
908    ///
909    /// # Durability
910    ///
911    /// After a crash, a write not covered by a completed [`Blob::sync`] may be torn: any
912    /// subset of its bytes may be durable. Bytes outside the written range remain
913    /// unchanged.
914    ///
915    /// # Concurrency
916    ///
917    /// A read observes every write and resize that completed before the read began. A read
918    /// that overlaps a write or resize is not an atomic snapshot and may observe different
919    /// stages of that mutation. The ranges of one batched read are not a snapshot either.
920    /// Callers that need a stable view must synchronize mutations with their reads or read
921    /// only immutable ranges.
922    pub trait Blob: Send + Sync + 'static {
923        /// Read exactly `len` bytes at `offset` into caller-provided buffers.
924        ///
925        /// Returns the same buffers with their chunk layout preserved.
926        ///
927        /// # Panics
928        ///
929        /// Panics if `len` exceeds the total capacity of `bufs`.
930        fn read_at_buf(
931            &self,
932            offset: u64,
933            len: usize,
934            bufs: impl Into<IoBufsMut> + Send,
935            options: ReadOptions,
936        ) -> impl Future<Output = Result<IoBufsMut, Error>> + Send;
937
938        /// Read exactly `len` bytes at `offset`.
939        ///
940        /// To reuse a buffer(s), use [`Blob::read_at_buf`].
941        fn read_at(
942            &self,
943            offset: u64,
944            len: usize,
945            options: ReadOptions,
946        ) -> impl Future<Output = Result<IoBufsMut, Error>> + Send;
947
948        /// Read every `(offset, len)` range in `ranges`, yielding `(range index, buffer)` as each
949        /// read completes, in any order. Each buffer holds the `len` bytes at `offset`.
950        ///
951        /// Without an error, every range is yielded exactly once. An error need not identify the
952        /// ranges it affects, and those ranges may never be yielded, so treat any error as failing
953        /// the call. Reads may begin before the stream is polled and continue after it is dropped.
954        fn read_many(
955            &self,
956            ranges: &[(u64, usize)],
957            options: ReadOptions,
958        ) -> impl futures::Stream<Item = Result<(usize, IoBufsMut), Error>> + Send {
959            read_each(self, ranges, options)
960        }
961
962        /// Request the `len` bytes at `offset` of every `(offset, len)` range in `ranges`.
963        ///
964        /// Awaiting this future waits until the runtime accepts responsibility for the reads.
965        /// They then proceed whether or not the returned [`Handle`] is polled. Dropping the
966        /// handle abandons them, and unlike a sync, an abandoned read may be cancelled or left to
967        /// finish. Awaiting the handle yields the bytes of every range, in range order. Any error
968        /// fails the whole read, as it does for [`Blob::read_many`].
969        fn start_read_many(
970            &self,
971            ranges: &[(u64, usize)],
972            options: ReadOptions,
973        ) -> impl Future<Output = Handle<Vec<IoBufsMut>>> + Send {
974            read_many_now(self, ranges, options)
975        }
976
977        /// Write every remaining byte in `bufs` to the blob at `offset`.
978        ///
979        /// The buffers are treated as one logical byte sequence in chunk order.
980        fn write_at(
981            &self,
982            offset: u64,
983            bufs: impl Into<IoBufs> + Send,
984            options: WriteOptions,
985        ) -> impl Future<Output = Result<(), Error>> + Send;
986
987        /// Resize the blob to the given length.
988        ///
989        /// If the length is greater than the current length, the blob is extended with zeros.
990        /// If the length is less than the current length, the blob is resized.
991        fn resize(&self, len: u64) -> impl Future<Output = Result<(), Error>> + Send;
992
993        /// Make every write and resize that completed before this call durable.
994        fn sync(&self) -> impl Future<Output = Result<(), Error>> + Send;
995
996        /// Request that every write and resize that completed before this call is durable.
997        ///
998        /// Awaiting this future waits until the runtime accepts responsibility for
999        /// the sync. It continues even if the returned [`Handle`] is dropped.
1000        /// Awaiting that handle waits for the same durability guarantee as [`Blob::sync`].
1001        fn start_sync(&self) -> impl Future<Output = Handle<()>> + Send;
1002    }
1003
1004    impl<B: Blob> Blob for Arc<B> {
1005        fn read_at_buf(
1006            &self,
1007            offset: u64,
1008            len: usize,
1009            bufs: impl Into<IoBufsMut> + Send,
1010            options: ReadOptions,
1011        ) -> impl Future<Output = Result<IoBufsMut, Error>> + Send {
1012            self.as_ref().read_at_buf(offset, len, bufs, options)
1013        }
1014
1015        fn read_at(
1016            &self,
1017            offset: u64,
1018            len: usize,
1019            options: ReadOptions,
1020        ) -> impl Future<Output = Result<IoBufsMut, Error>> + Send {
1021            self.as_ref().read_at(offset, len, options)
1022        }
1023
1024        fn read_many(
1025            &self,
1026            ranges: &[(u64, usize)],
1027            options: ReadOptions,
1028        ) -> impl futures::Stream<Item = Result<(usize, IoBufsMut), Error>> + Send {
1029            self.as_ref().read_many(ranges, options)
1030        }
1031
1032        fn start_read_many(
1033            &self,
1034            ranges: &[(u64, usize)],
1035            options: ReadOptions,
1036        ) -> impl Future<Output = Handle<Vec<IoBufsMut>>> + Send {
1037            self.as_ref().start_read_many(ranges, options)
1038        }
1039
1040        fn write_at(
1041            &self,
1042            offset: u64,
1043            bufs: impl Into<IoBufs> + Send,
1044            options: WriteOptions,
1045        ) -> impl Future<Output = Result<(), Error>> + Send {
1046            self.as_ref().write_at(offset, bufs, options)
1047        }
1048
1049        fn resize(&self, len: u64) -> impl Future<Output = Result<(), Error>> + Send {
1050            self.as_ref().resize(len)
1051        }
1052
1053        fn sync(&self) -> impl Future<Output = Result<(), Error>> + Send {
1054            self.as_ref().sync()
1055        }
1056
1057        fn start_sync(&self) -> impl Future<Output = Handle<()>> + Send {
1058            self.as_ref().start_sync()
1059        }
1060    }
1061
1062    /// Serve `ranges` with one [`Blob::read_at`] per range, all in flight at once: the
1063    /// [`Blob::read_many`] of a blob that cannot batch the reads.
1064    pub(crate) fn read_each<'a, B: Blob + ?Sized>(
1065        blob: &'a B,
1066        ranges: &[(u64, usize)],
1067        options: ReadOptions,
1068    ) -> impl futures::Stream<Item = Result<(usize, IoBufsMut), Error>> + Send + 'a {
1069        ranges
1070            .iter()
1071            .enumerate()
1072            .map(|(index, &(offset, len))| {
1073                blob.read_at(offset, len, options)
1074                    .map(move |result| result.map(|bufs| (index, bufs)))
1075            })
1076            .collect::<futures::stream::FuturesUnordered<_>>()
1077    }
1078
1079    /// Read `ranges` through [`Blob::read_many`] before handing back their buffers: the
1080    /// [`Blob::start_read_many`] of a blob that cannot keep reads in flight on its own.
1081    pub(crate) async fn read_many_now<B: Blob + ?Sized>(
1082        blob: &B,
1083        ranges: &[(u64, usize)],
1084        options: ReadOptions,
1085    ) -> Handle<Vec<IoBufsMut>> {
1086        Handle::ready(collect_reads(blob.read_many(ranges, options), ranges.len()).await)
1087    }
1088
1089    /// Collect a [`Blob::read_many`] stream of `ranges` ranges into its buffers in range order.
1090    pub(crate) async fn collect_reads<S>(reads: S, ranges: usize) -> Result<Vec<IoBufsMut>, Error>
1091    where
1092        S: futures::Stream<Item = Result<(usize, IoBufsMut), Error>>,
1093    {
1094        let mut bufs: Vec<Option<IoBufsMut>> = (0..ranges).map(|_| None).collect();
1095        let mut reads = std::pin::pin!(reads);
1096        while let Some(read) = futures::StreamExt::next(&mut reads).await {
1097            let (index, buf) = read?;
1098            *bufs.get_mut(index).ok_or(Error::ReadFailed)? = Some(buf);
1099        }
1100        bufs.into_iter().map(|buf| buf.ok_or(Error::ReadFailed)).collect()
1101    }
1102
1103    /// Join reads started one range at a time into one [`Blob::start_read_many`] handle that
1104    /// awaits them in range order.
1105    #[cfg(not(target_arch = "wasm32"))]
1106    pub(crate) fn join_reads<F>(reads: Vec<F>) -> Handle<Vec<IoBufsMut>>
1107    where
1108        F: Future<Output = Result<IoBufsMut, Error>> + Send + 'static,
1109    {
1110        Handle::from_future(async move {
1111            let mut bufs = Vec::with_capacity(reads.len());
1112            for read in reads {
1113                bufs.push(read.await?);
1114            }
1115            Ok(bufs)
1116        })
1117    }
1118});
1119stability_scope!(BETA, cfg(feature = "external") {
1120    /// Interface that runtimes can implement to constrain the execution latency of a future.
1121    pub trait Pacer: Clock + Send + Sync + 'static {
1122        /// Defer completion of a future until a specified `latency` has elapsed. If the future is
1123        /// not yet ready at the desired time of completion, the runtime will block until the future
1124        /// is ready.
1125        ///
1126        /// In [crate::deterministic], this is used to ensure interactions with external systems can
1127        /// be interacted with deterministically. In [crate::tokio], this is a no-op (allows
1128        /// multiple runtimes to be tested with no code changes).
1129        ///
1130        /// # Setting Latency
1131        ///
1132        /// `pace` is not meant to be a time penalty applied to awaited futures and should be set to
1133        /// the expected resolution latency of the future. To better explore the possible behavior of an
1134        /// application, users can set latency to a randomly chosen value in the range of
1135        /// `[expected latency / 2, expected latency * 2]`.
1136        ///
1137        /// # Warning
1138        ///
1139        /// Because `pace` blocks if the future is not ready, it is important that the future's completion
1140        /// doesn't require anything in the current thread to complete (or else it will deadlock).
1141        fn pace<'a, F, T>(
1142            &'a self,
1143            latency: Duration,
1144            future: F,
1145        ) -> impl Future<Output = T> + Send + 'a
1146        where
1147            F: Future<Output = T> + Send + 'a,
1148            T: Send + 'a;
1149    }
1150
1151    /// Extension trait that makes it more ergonomic to use [Pacer].
1152    ///
1153    /// This inverts the call-site of [`Pacer::pace`] by letting the future itself request how the
1154    /// runtime should delay completion relative to the clock.
1155    pub trait FutureExt: Future + Send + Sized {
1156        /// Delay completion of the future until a specified `latency` on `pacer`.
1157        fn pace<'a, E>(
1158            self,
1159            pacer: &'a E,
1160            latency: Duration,
1161        ) -> impl Future<Output = Self::Output> + Send + 'a
1162        where
1163            E: Pacer + 'a,
1164            Self: Send + 'a,
1165            Self::Output: Send + 'a,
1166        {
1167            pacer.pace(latency, self)
1168        }
1169    }
1170
1171    impl<F> FutureExt for F where F: Future + Send {}
1172});
1173
1174#[cfg(test)]
1175mod tests {
1176    use super::*;
1177    use crate::telemetry::metrics::{
1178        EncodeLabelKey, EncodeLabelSetTrait as EncodeLabelSet,
1179        EncodeLabelValueTrait as EncodeLabelValue, LabelSetEncoder, count_running_tasks,
1180        raw::{Counter, Family},
1181    };
1182    use commonware_macros::select;
1183    use commonware_parallel::Strategy as _;
1184    use commonware_utils::{
1185        NZUsize, SystemTimeExt,
1186        channel::{mpsc, oneshot},
1187        futures::Pool as FuturesPool,
1188        sync::Mutex,
1189    };
1190    use futures::{
1191        FutureExt,
1192        future::{pending, ready},
1193        join, pin_mut,
1194    };
1195    use rstest::rstest;
1196    use std::{
1197        panic::{AssertUnwindSafe, catch_unwind},
1198        pin::Pin,
1199        sync::{
1200            Arc,
1201            atomic::{AtomicU32, Ordering},
1202        },
1203        task::{Context as TContext, Poll, Waker},
1204    };
1205    use utils::reschedule;
1206
1207    #[test]
1208    fn test_blob_version() {
1209        let version = BlobVersion::new(7);
1210        assert_eq!(version.get(), 7);
1211        assert_eq!(version.to_string(), "7");
1212        assert_eq!(format!("{version:?}"), "7");
1213        assert_eq!(BlobVersion::default(), DEFAULT_BLOB_VERSION);
1214        assert!((BlobVersion::new(3)..=BlobVersion::new(7)).contains(&version));
1215    }
1216
1217    #[test]
1218    fn test_read_options_compose() {
1219        // The flag must compose, assign, remove, and remain absent from the default.
1220        let options = ReadOptions::default() | ReadOptions::DONT_CACHE;
1221        let mut assigned = ReadOptions::default();
1222        assigned |= ReadOptions::DONT_CACHE;
1223        assert!(options.contains(ReadOptions::DONT_CACHE));
1224        assert_eq!(assigned, options);
1225        assert_eq!(
1226            options.without(ReadOptions::DONT_CACHE),
1227            ReadOptions::default()
1228        );
1229        assert!(!ReadOptions::default().contains(ReadOptions::DONT_CACHE));
1230    }
1231
1232    #[test]
1233    fn test_write_options_compose() {
1234        let options = WriteOptions::SYNC | WriteOptions::DONT_CACHE;
1235        let mut assigned = WriteOptions::SYNC;
1236        assigned |= WriteOptions::DONT_CACHE;
1237        assert!(options.contains(WriteOptions::SYNC));
1238        assert!(options.contains(WriteOptions::DONT_CACHE));
1239        assert_eq!(assigned, options);
1240        assert_eq!(
1241            options.without(WriteOptions::SYNC),
1242            WriteOptions::DONT_CACHE
1243        );
1244        let default = WriteOptions::default();
1245        assert!(!default.contains(WriteOptions::SYNC));
1246        assert!(!default.contains(WriteOptions::DONT_CACHE));
1247    }
1248
1249    #[rstest]
1250    #[case::deterministic(deterministic::Runner::default())]
1251    #[case::tokio(tokio::Runner::default())]
1252    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1253    fn test_error_future<R: Runner>(#[case] runner: R) {
1254        #[allow(clippy::unused_async)]
1255        async fn error_future() -> Result<&'static str, &'static str> {
1256            Err("An error occurred")
1257        }
1258        let result = runner.start(|_| error_future());
1259        assert_eq!(result, Err("An error occurred"));
1260    }
1261
1262    #[rstest]
1263    #[case::deterministic(deterministic::Runner::default())]
1264    #[case::tokio(tokio::Runner::default())]
1265    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1266    fn test_handle_can_use_futures_pool<R: Runner>(#[case] runner: R) {
1267        runner.start(|_| async move {
1268            let mut pool = FuturesPool::<Result<(), Error>>::default();
1269            pool.push(Handle::ready(Ok(())));
1270            assert!(pool.next_completed().await.is_ok());
1271        });
1272    }
1273
1274    #[rstest]
1275    #[case::deterministic(deterministic::Runner::default())]
1276    #[case::tokio(tokio::Runner::default())]
1277    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1278    fn test_clock_sleep<R: Runner>(#[case] runner: R)
1279    where
1280        R::Context: Spawner + Clock,
1281    {
1282        runner.start(|context| async move {
1283            // Capture initial time
1284            let start = context.current();
1285            let sleep_duration = Duration::from_millis(10);
1286            context.sleep(sleep_duration).await;
1287
1288            // After run, time should have advanced
1289            let end = context.current();
1290            assert!(end.duration_since(start).unwrap() >= sleep_duration);
1291        });
1292    }
1293
1294    #[rstest]
1295    #[case::deterministic(deterministic::Runner::default())]
1296    #[case::tokio(tokio::Runner::default())]
1297    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1298    fn test_clock_sleep_until<R: Runner>(#[case] runner: R)
1299    where
1300        R::Context: Spawner + Clock + Metrics,
1301    {
1302        runner.start(|context| async move {
1303            // Trigger sleep
1304            let now = context.current();
1305            context.sleep_until(now + Duration::from_millis(100)).await;
1306
1307            // Ensure slept duration has elapsed
1308            let elapsed = now.elapsed().unwrap();
1309            assert!(elapsed >= Duration::from_millis(100));
1310        });
1311    }
1312
1313    #[rstest]
1314    #[case::deterministic(deterministic::Runner::default())]
1315    #[case::tokio(tokio::Runner::default())]
1316    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1317    fn test_clock_sleep_until_far_future<R: Runner>(#[case] runner: R)
1318    where
1319        R::Context: Spawner + Clock,
1320    {
1321        runner.start(|context| async move {
1322            let sleep = context.sleep_until(SystemTime::limit());
1323            let result = context.timeout(Duration::from_millis(1), sleep).await;
1324            assert!(matches!(result, Err(Error::Timeout)));
1325        });
1326    }
1327
1328    #[rstest]
1329    #[case::deterministic(deterministic::Runner::default())]
1330    #[case::tokio(tokio::Runner::default())]
1331    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1332    fn test_clock_timeout<R: Runner>(#[case] runner: R)
1333    where
1334        R::Context: Spawner + Clock,
1335    {
1336        runner.start(|context| async move {
1337            // Future completes before timeout
1338            let result = context
1339                .timeout(Duration::from_millis(100), async { "success" })
1340                .await;
1341            assert_eq!(result.unwrap(), "success");
1342
1343            // Future exceeds timeout duration
1344            let result = context
1345                .timeout(Duration::from_millis(50), pending::<()>())
1346                .await;
1347            assert!(matches!(result, Err(Error::Timeout)));
1348
1349            // Future completes within timeout
1350            let result = context
1351                .timeout(
1352                    Duration::from_millis(100),
1353                    context.sleep(Duration::from_millis(50)),
1354                )
1355                .await;
1356            assert!(result.is_ok());
1357        });
1358    }
1359
1360    #[rstest]
1361    #[case::deterministic(deterministic::Runner::default())]
1362    #[case::tokio(tokio::Runner::default())]
1363    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1364    fn test_clock_futures_move_between_tasks<R: Runner>(#[case] runner: R)
1365    where
1366        R::Context: Spawner + Clock,
1367    {
1368        runner.start(|context| async move {
1369            for kind in ["sleep", "sleep_until", "timeout", "stop"] {
1370                // Holding a shutdown observer keeps the stop case pending until its timeout.
1371                let clock = context.child("clock");
1372                let stopped = (kind == "stop").then(|| context.stopped());
1373                let duration = Duration::from_millis(50);
1374                let deadline = context.current() + duration;
1375
1376                // Cover direct sleeps and operations that wait on a timer.
1377                let mut future = async move {
1378                    match kind {
1379                        "sleep" => clock.sleep(duration).await,
1380                        "sleep_until" => clock.sleep_until(deadline).await,
1381                        "timeout" => assert!(matches!(
1382                            clock.timeout(duration, pending::<()>()).await,
1383                            Err(Error::Timeout)
1384                        )),
1385                        _ => assert!(matches!(
1386                            clock.stop(7, Some(duration)).await,
1387                            Err(Error::Timeout)
1388                        )),
1389                    }
1390                }
1391                .boxed();
1392
1393                // Register this task's waker before transferring the pending future.
1394                // The destination must receive the eventual wakeup.
1395                assert!(futures::poll!(&mut future).is_pending());
1396                let moved = context.child("moved").dedicated().spawn(move |_| future);
1397
1398                // The source task's independent timeout detects a missing destination wakeup.
1399                select! {
1400                    result = moved => result.unwrap(),
1401                    _ = context.sleep(Duration::from_secs(2)) => panic!("moved {kind} did not complete"),
1402                }
1403
1404                // Changing tasks must preserve the operation's deadline.
1405                assert!(context.current() >= deadline);
1406                drop(stopped);
1407            }
1408        });
1409    }
1410
1411    #[rstest]
1412    #[case::deterministic(deterministic::Runner::default())]
1413    #[case::tokio(tokio::Runner::default())]
1414    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1415    fn test_root_finishes<R: Runner>(#[case] runner: R)
1416    where
1417        R::Context: Spawner,
1418    {
1419        runner.start(|context| async move {
1420            context.spawn(|_| async move {
1421                loop {
1422                    reschedule().await;
1423                }
1424            });
1425        });
1426    }
1427
1428    #[rstest]
1429    #[case::deterministic(deterministic::Runner::default())]
1430    #[case::tokio(tokio::Runner::default())]
1431    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1432    fn test_spawn_after_abort<R>(#[case] runner: R)
1433    where
1434        R: Runner,
1435        R::Context: Spawner,
1436    {
1437        runner.start(|context| async move {
1438            // Create a child context
1439            let child = context.child("child");
1440
1441            // Spawn parent and abort
1442            let parent_handle = context.spawn(move |_| async move {
1443                pending::<()>().await;
1444            });
1445            parent_handle.abort();
1446
1447            // Spawn child and ensure it aborts
1448            let child_handle = child.spawn(move |_| async move {
1449                pending::<()>().await;
1450            });
1451            assert!(matches!(child_handle.await, Err(Error::Closed)));
1452        });
1453    }
1454
1455    #[rstest]
1456    #[case::deterministic(deterministic::Runner::default())]
1457    #[case::tokio(tokio::Runner::default())]
1458    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1459    fn test_spawn_abort<R: Runner>(
1460        #[case] runner: R,
1461        #[values(
1462            Execution::Shared(false),
1463            Execution::Shared(true),
1464            Execution::Dedicated
1465        )]
1466        execution: Execution,
1467    ) where
1468        R::Context: Spawner,
1469    {
1470        runner.start(|context| async move {
1471            let context = match execution {
1472                Execution::Dedicated => context.dedicated(),
1473                Execution::Shared(blocking) => context.shared(blocking),
1474            };
1475
1476            let handle = context.spawn(|_| async move {
1477                loop {
1478                    reschedule().await;
1479                }
1480            });
1481            handle.abort();
1482            assert!(matches!(handle.await, Err(Error::Closed)));
1483        });
1484    }
1485
1486    #[rstest]
1487    #[case::deterministic(deterministic::Runner::default())]
1488    #[case::deterministic_caught(deterministic::Runner::new(
1489        deterministic::Config::default().with_catch_panics(true)
1490    ))]
1491    #[case::tokio(tokio::Runner::default())]
1492    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1493    #[case::tokio_caught(tokio::Runner::new(tokio::Config::default().with_catch_panics(true)))]
1494    #[cfg_attr(target_os = "linux", case::iouring_caught(iouring::Runner::new(iouring::Config::default().with_catch_panics(true))))]
1495    #[should_panic(expected = "blah")]
1496    fn test_panic_aborts_root<R: Runner>(#[case] runner: R) {
1497        let result: Result<(), Error> = runner.start(|_| async move {
1498            panic!("blah");
1499        });
1500        result.unwrap_err();
1501    }
1502
1503    #[rstest]
1504    #[case::deterministic(deterministic::Runner::default())]
1505    #[case::tokio(tokio::Runner::default())]
1506    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1507    #[should_panic(expected = "blah")]
1508    fn test_panic_aborts_spawn<R: Runner>(#[case] runner: R)
1509    where
1510        R::Context: Spawner + Clock,
1511    {
1512        runner.start(|context| async move {
1513            context.child("panic").spawn(|_| async move {
1514                panic!("blah");
1515            });
1516
1517            // Loop until panic
1518            loop {
1519                context.sleep(Duration::from_millis(100)).await;
1520            }
1521        });
1522    }
1523
1524    #[rstest]
1525    #[case::deterministic(deterministic::Runner::new(
1526        deterministic::Config::default().with_catch_panics(true)
1527    ))]
1528    #[case::tokio(tokio::Runner::new(tokio::Config::default().with_catch_panics(true)))]
1529    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::new(iouring::Config::default().with_catch_panics(true))))]
1530    fn test_panic_aborts_spawn_caught<R: Runner>(#[case] runner: R)
1531    where
1532        R::Context: Spawner + Clock,
1533    {
1534        let result: Result<(), Error> = runner.start(|context| async move {
1535            let result = context.child("panic").spawn(|_| async move {
1536                panic!("blah");
1537            });
1538            result.await
1539        });
1540        assert!(matches!(result, Err(Error::Exited)));
1541    }
1542
1543    #[rstest]
1544    #[case::deterministic(deterministic::Runner::default())]
1545    #[case::tokio(tokio::Runner::default())]
1546    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1547    #[should_panic(expected = "boom")]
1548    fn test_multiple_panics<R: Runner>(#[case] runner: R)
1549    where
1550        R::Context: Spawner + Clock,
1551    {
1552        runner.start(|context| async move {
1553            context.child("panic").spawn(|_| async move {
1554                panic!("boom 1");
1555            });
1556            context.child("panic").spawn(|_| async move {
1557                panic!("boom 2");
1558            });
1559            context.child("panic").spawn(|_| async move {
1560                panic!("boom 3");
1561            });
1562
1563            // Loop until panic
1564            loop {
1565                context.sleep(Duration::from_millis(100)).await;
1566            }
1567        });
1568    }
1569
1570    #[rstest]
1571    #[case::deterministic(deterministic::Runner::new(
1572        deterministic::Config::default().with_catch_panics(true)
1573    ))]
1574    #[case::tokio(tokio::Runner::new(tokio::Config::default().with_catch_panics(true)))]
1575    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::new(iouring::Config::default().with_catch_panics(true))))]
1576    fn test_multiple_panics_caught<R: Runner>(#[case] runner: R)
1577    where
1578        R::Context: Spawner + Clock,
1579    {
1580        let (res1, res2, res3) = runner.start(|context| async move {
1581            let handle1 = context.child("panic").spawn(|_| async move {
1582                panic!("boom 1");
1583            });
1584            let handle2 = context.child("panic").spawn(|_| async move {
1585                panic!("boom 2");
1586            });
1587            let handle3 = context.child("panic").spawn(|_| async move {
1588                panic!("boom 3");
1589            });
1590
1591            join!(handle1, handle2, handle3)
1592        });
1593        assert!(matches!(res1, Err(Error::Exited)));
1594        assert!(matches!(res2, Err(Error::Exited)));
1595        assert!(matches!(res3, Err(Error::Exited)));
1596    }
1597
1598    #[rstest]
1599    #[case::deterministic(deterministic::Runner::default())]
1600    #[case::tokio(tokio::Runner::default())]
1601    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1602    fn test_select<R: Runner>(#[case] runner: R) {
1603        runner.start(|_| async move {
1604            // Test first branch
1605            let output = Mutex::new(0);
1606            select! {
1607                v1 = ready(1) => {
1608                    *output.lock() = v1;
1609                },
1610                v2 = ready(2) => {
1611                    *output.lock() = v2;
1612                },
1613            };
1614            assert_eq!(*output.lock(), 1);
1615
1616            // Test second branch
1617            select! {
1618                v1 = std::future::pending::<i32>() => {
1619                    *output.lock() = v1;
1620                },
1621                v2 = ready(2) => {
1622                    *output.lock() = v2;
1623                },
1624            };
1625            assert_eq!(*output.lock(), 2);
1626        });
1627    }
1628
1629    /// Ensure future fusing works as expected.
1630    #[rstest]
1631    #[case::deterministic(deterministic::Runner::default())]
1632    #[case::tokio(tokio::Runner::default())]
1633    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1634    fn test_select_loop<R: Runner>(#[case] runner: R)
1635    where
1636        R::Context: Clock,
1637    {
1638        runner.start(|context| async move {
1639            // Should hit timeout
1640            let (sender, mut receiver) = mpsc::unbounded_channel();
1641            for _ in 0..2 {
1642                select! {
1643                    v = receiver.recv() => {
1644                        panic!("unexpected value: {v:?}");
1645                    },
1646                    _ = context.sleep(Duration::from_millis(100)) => {
1647                        continue;
1648                    },
1649                };
1650            }
1651
1652            // Populate channel
1653            sender.send(0).unwrap();
1654            sender.send(1).unwrap();
1655
1656            // Prefer not reading channel without losing messages
1657            select! {
1658                _ = async {} => {
1659                    // Skip reading from channel even though populated
1660                },
1661                v = receiver.recv() => {
1662                    panic!("unexpected value: {v:?}");
1663                },
1664            };
1665
1666            // Process messages
1667            for i in 0..2 {
1668                select! {
1669                    _ = context.sleep(Duration::from_millis(100)) => {
1670                        panic!("timeout");
1671                    },
1672                    v = receiver.recv() => {
1673                        assert_eq!(v.unwrap(), i);
1674                    },
1675                };
1676            }
1677        });
1678    }
1679
1680    #[rstest]
1681    #[case::deterministic(deterministic::Runner::default())]
1682    #[case::tokio(tokio::Runner::default())]
1683    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1684    fn test_duplicate_open_returns_error<R: Runner>(#[case] runner: R)
1685    where
1686        R::Context: Storage,
1687    {
1688        runner.start(|context| async move {
1689            // Cloning the Arc shares one open. A second open fails while that open is alive.
1690            let partition = "duplicate_open";
1691            let name = b"blob";
1692            let (first, _) = context.open(partition, name).await.unwrap();
1693            let first = Arc::new(first);
1694            first
1695                .write_at(0, b"old", WriteOptions::default())
1696                .await
1697                .unwrap();
1698            let retained = Arc::clone(&first);
1699            assert!(matches!(
1700                context.open(partition, name).await,
1701                Err(Error::BlobAlreadyOpen(p, n)) if p == partition && n == "626c6f62"
1702            ));
1703
1704            // Dropping the original owner leaves the clone holding the open. `open_versioned`
1705            // still fails. The clone reads the unsynced write and syncs new contents.
1706            drop(first);
1707            assert!(matches!(
1708                context.open_versioned(
1709                    partition, name, DEFAULT_BLOB_VERSION..=DEFAULT_BLOB_VERSION
1710                ).await,
1711                Err(Error::BlobAlreadyOpen(p, n)) if p == partition && n == "626c6f62"
1712            ));
1713            assert_eq!(
1714                retained
1715                    .read_at(0, 3, ReadOptions::default())
1716                    .await
1717                    .unwrap()
1718                    .coalesce(),
1719                b"old".as_slice()
1720            );
1721            retained
1722                .write_at(0, b"new", WriteOptions::default())
1723                .await
1724                .unwrap();
1725            retained.sync().await.unwrap();
1726            drop(retained);
1727
1728            // The final owner's drop permits reopening the synchronized contents.
1729            let (old, size) = context.open(partition, name).await.unwrap();
1730            assert_eq!(size, 3);
1731            assert_eq!(
1732                old.read_at(0, 3, ReadOptions::default())
1733                    .await
1734                    .unwrap()
1735                    .coalesce(),
1736                b"new".as_slice()
1737            );
1738
1739            // Removal permits a replacement while the old handle remains alive. Dropping that
1740            // old handle must not release the replacement's registration.
1741            context.remove(partition, Some(name)).await.unwrap();
1742            let (current, size) = context.open(partition, name).await.unwrap();
1743            assert_eq!(size, 0);
1744            drop(old);
1745            assert!(matches!(
1746                context.open(partition, name).await,
1747                Err(Error::BlobAlreadyOpen(p, n)) if p == partition && n == "626c6f62"
1748            ));
1749            current
1750                .write_at(0, b"replacement", WriteOptions::default())
1751                .await
1752                .unwrap();
1753            current.sync().await.unwrap();
1754            drop(current);
1755
1756            // Reopening the replacement must return its own durable contents.
1757            let (reopened, size) = context.open(partition, name).await.unwrap();
1758            assert_eq!(size, 11);
1759            assert_eq!(
1760                reopened
1761                    .read_at(0, 11, ReadOptions::default())
1762                    .await
1763                    .unwrap()
1764                    .coalesce(),
1765                b"replacement".as_slice()
1766            );
1767        });
1768    }
1769
1770    #[rstest]
1771    #[case::deterministic(deterministic::Runner::default())]
1772    #[case::tokio(tokio::Runner::default())]
1773    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1774    fn test_storage_operations<R: Runner>(#[case] runner: R)
1775    where
1776        R::Context: Storage,
1777    {
1778        runner.start(|context| async move {
1779            let partition = "test_partition";
1780            let name = b"test_blob";
1781
1782            // Open a new blob
1783            let (blob, size) = context
1784                .open(partition, name)
1785                .await
1786                .expect("Failed to open blob");
1787            assert_eq!(size, 0, "new blob should have size 0");
1788
1789            // Write data to the blob
1790            let data = b"Hello, Storage!";
1791            blob.write_at(0, data, WriteOptions::default())
1792                .await
1793                .expect("Failed to write to blob");
1794
1795            // Sync the blob
1796            blob.sync().await.expect("Failed to sync blob");
1797
1798            // Read data from the blob
1799            let read = blob
1800                .read_at(0, data.len(), ReadOptions::default())
1801                .await
1802                .expect("Failed to read from blob");
1803            assert_eq!(read.coalesce(), data);
1804
1805            // Sync the blob
1806            blob.sync().await.expect("Failed to sync blob");
1807
1808            // Scan blobs in the partition
1809            let blobs = context
1810                .scan(partition)
1811                .await
1812                .expect("Failed to scan partition");
1813            assert!(blobs.contains(&name.to_vec()));
1814
1815            // Reopen the blob
1816            drop(blob);
1817            let (blob, len) = context
1818                .open(partition, name)
1819                .await
1820                .expect("Failed to reopen blob");
1821            assert_eq!(len, data.len() as u64);
1822
1823            // Read data part of message back
1824            let read = blob
1825                .read_at(7, 7, ReadOptions::default())
1826                .await
1827                .expect("Failed to read data");
1828            assert_eq!(read.coalesce(), b"Storage");
1829
1830            // Sync the blob
1831            blob.sync().await.expect("Failed to sync blob");
1832
1833            // Remove the blob
1834            context
1835                .remove(partition, Some(name))
1836                .await
1837                .expect("Failed to remove blob");
1838
1839            // Ensure the blob is removed
1840            let blobs = context
1841                .scan(partition)
1842                .await
1843                .expect("Failed to scan partition");
1844            assert!(!blobs.contains(&name.to_vec()));
1845
1846            // Remove the partition
1847            context
1848                .remove(partition, None)
1849                .await
1850                .expect("Failed to remove partition");
1851
1852            // Scan the partition
1853            let result = context.scan(partition).await;
1854            assert!(matches!(result, Err(Error::PartitionMissing(_))));
1855        });
1856    }
1857
1858    #[rstest]
1859    #[case::deterministic(deterministic::Runner::default())]
1860    #[case::tokio(tokio::Runner::default())]
1861    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1862    fn test_blob_read_write<R: Runner>(#[case] runner: R)
1863    where
1864        R::Context: Storage,
1865    {
1866        runner.start(|context| async move {
1867            let partition = "test_partition";
1868            let name = b"test_blob_rw";
1869
1870            // Open a new blob
1871            let (blob, _) = context
1872                .open(partition, name)
1873                .await
1874                .expect("Failed to open blob");
1875
1876            // Write data at different offsets
1877            let data1 = b"Hello";
1878            let data2 = b"World";
1879            blob.write_at(0, data1, WriteOptions::default())
1880                .await
1881                .expect("Failed to write data1");
1882            blob.write_at(5, data2, WriteOptions::default())
1883                .await
1884                .expect("Failed to write data2");
1885
1886            // Read data back
1887            let read = blob
1888                .read_at(0, 10, ReadOptions::default())
1889                .await
1890                .expect("Failed to read data");
1891            let read = read.coalesce();
1892            assert_eq!(&read.as_ref()[..5], data1);
1893            assert_eq!(&read.as_ref()[5..], data2);
1894
1895            // Read past end of blob
1896            let result = blob.read_at(10, 10, ReadOptions::default()).await;
1897            assert!(result.is_err());
1898
1899            // Rewrite data without affecting length
1900            let data3 = b"Store";
1901            blob.write_at(5, data3, WriteOptions::default())
1902                .await
1903                .expect("Failed to write data3");
1904
1905            // Read data back
1906            let read = blob
1907                .read_at(0, 10, ReadOptions::default())
1908                .await
1909                .expect("Failed to read data");
1910            let read = read.coalesce();
1911            assert_eq!(&read.as_ref()[..5], data1);
1912            assert_eq!(&read.as_ref()[5..], data3);
1913
1914            // Read past end of blob
1915            let result = blob.read_at(10, 10, ReadOptions::default()).await;
1916            assert!(result.is_err());
1917        });
1918    }
1919
1920    #[rstest]
1921    #[case::deterministic(deterministic::Runner::default())]
1922    #[case::tokio(tokio::Runner::default())]
1923    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1924    fn test_blob_resize<R: Runner>(#[case] runner: R)
1925    where
1926        R::Context: Storage,
1927    {
1928        runner.start(|context| async move {
1929            let partition = "test_partition_resize";
1930            let name = b"test_blob_resize";
1931
1932            // Open and write to a new blob
1933            let (blob, _) = context
1934                .open(partition, name)
1935                .await
1936                .expect("Failed to open blob");
1937
1938            let data = b"some data";
1939            blob.write_at(0, data.to_vec(), WriteOptions::default())
1940                .await
1941                .expect("Failed to write");
1942            blob.sync().await.expect("Failed to sync after write");
1943
1944            // Re-open and check length
1945            drop(blob);
1946            let (blob, len) = context.open(partition, name).await.unwrap();
1947            assert_eq!(len, data.len() as u64);
1948
1949            // Resize to extend the file
1950            let new_len = (data.len() as u64) * 2;
1951            blob.resize(new_len)
1952                .await
1953                .expect("Failed to resize to extend");
1954            blob.sync().await.expect("Failed to sync after resize");
1955
1956            // Re-open and check length again
1957            drop(blob);
1958            let (blob, len) = context.open(partition, name).await.unwrap();
1959            assert_eq!(len, new_len);
1960
1961            // Read original data
1962            let read_buf = blob
1963                .read_at(0, data.len(), ReadOptions::default())
1964                .await
1965                .unwrap();
1966            assert_eq!(read_buf.coalesce(), data);
1967
1968            // Read extended part (should be zeros)
1969            let extended_part = blob
1970                .read_at(data.len() as u64, data.len(), ReadOptions::default())
1971                .await
1972                .unwrap();
1973            assert_eq!(extended_part.coalesce(), vec![0; data.len()].as_slice());
1974
1975            // Truncate the blob
1976            blob.resize(data.len() as u64).await.unwrap();
1977            blob.sync().await.unwrap();
1978
1979            // Reopen to check truncation
1980            drop(blob);
1981            let (blob, size) = context.open(partition, name).await.unwrap();
1982            assert_eq!(size, data.len() as u64);
1983
1984            // Read truncated data
1985            let read_buf = blob
1986                .read_at(0, data.len(), ReadOptions::default())
1987                .await
1988                .unwrap();
1989            assert_eq!(read_buf.coalesce(), data);
1990            blob.sync().await.unwrap();
1991        });
1992    }
1993
1994    #[rstest]
1995    #[case::deterministic(deterministic::Runner::default())]
1996    #[case::tokio(tokio::Runner::default())]
1997    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1998    fn test_many_partition_read_write<R: Runner>(#[case] runner: R)
1999    where
2000        R::Context: Storage,
2001    {
2002        runner.start(|context| async move {
2003            let partitions = ["partition1", "partition2", "partition3"];
2004            let name = b"test_blob_rw";
2005            let data1 = b"Hello";
2006            let data2 = b"World";
2007
2008            for (additional, partition) in partitions.iter().enumerate() {
2009                // Open a new blob
2010                let (blob, _) = context
2011                    .open(partition, name)
2012                    .await
2013                    .expect("Failed to open blob");
2014
2015                // Write data at different offsets
2016                blob.write_at(0, data1, WriteOptions::default())
2017                    .await
2018                    .expect("Failed to write data1");
2019                blob.write_at(5 + additional as u64, data2, WriteOptions::default())
2020                    .await
2021                    .expect("Failed to write data2");
2022
2023                // Sync the blob
2024                blob.sync().await.expect("Failed to sync blob");
2025            }
2026
2027            for (additional, partition) in partitions.iter().enumerate() {
2028                // Open a new blob
2029                let (blob, len) = context
2030                    .open(partition, name)
2031                    .await
2032                    .expect("Failed to open blob");
2033                assert_eq!(len, (data1.len() + data2.len() + additional) as u64);
2034
2035                // Read data back
2036                let read = blob
2037                    .read_at(0, 10 + additional, ReadOptions::default())
2038                    .await
2039                    .expect("Failed to read data");
2040                let read = read.coalesce();
2041                assert_eq!(&read.as_ref()[..5], b"Hello");
2042                assert_eq!(&read.as_ref()[5 + additional..], b"World");
2043            }
2044        });
2045    }
2046
2047    #[rstest]
2048    #[case::deterministic(deterministic::Runner::default())]
2049    #[case::tokio(tokio::Runner::default())]
2050    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2051    fn test_blob_read_past_length<R: Runner>(#[case] runner: R)
2052    where
2053        R::Context: Storage,
2054    {
2055        runner.start(|context| async move {
2056            let partition = "test_partition";
2057            let name = b"test_blob_rw";
2058
2059            // Open a new blob
2060            let (blob, _) = context
2061                .open(partition, name)
2062                .await
2063                .expect("Failed to open blob");
2064
2065            // Read data past file length (empty file)
2066            let result = blob.read_at(0, 10, ReadOptions::default()).await;
2067            assert!(result.is_err());
2068
2069            // Write data to the blob
2070            let data = b"Hello, Storage!".to_vec();
2071            blob.write_at(0, data, WriteOptions::default())
2072                .await
2073                .expect("Failed to write to blob");
2074
2075            // Read data past file length (non-empty file)
2076            let result = blob.read_at(0, 20, ReadOptions::default()).await;
2077            assert!(result.is_err());
2078        })
2079    }
2080
2081    #[rstest]
2082    #[case::deterministic(deterministic::Runner::default())]
2083    #[case::tokio(tokio::Runner::default())]
2084    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2085    fn test_blob_shared_concurrent_read<R: Runner>(#[case] runner: R)
2086    where
2087        R::Context: Spawner + Storage + Metrics,
2088    {
2089        runner.start(|context| async move {
2090            let partition = "test_partition";
2091            let name = b"test_blob_rw";
2092
2093            // Open a new blob
2094            let (blob, _) = context
2095                .open(partition, name)
2096                .await
2097                .expect("Failed to open blob");
2098            let blob = Arc::new(blob);
2099
2100            // Write data to the blob
2101            let data = b"Hello, Storage!";
2102            blob.write_at(0, data, WriteOptions::default())
2103                .await
2104                .expect("Failed to write to blob");
2105
2106            // Sync the blob
2107            blob.sync().await.expect("Failed to sync blob");
2108
2109            // Shared owners issue concurrent reads through the same open.
2110            let check1 = context.child("check1").spawn({
2111                let blob = blob.clone();
2112                let data_len = data.len();
2113                move |_| async move {
2114                    let read = blob
2115                        .read_at(0, data_len, ReadOptions::default())
2116                        .await
2117                        .expect("Failed to read from blob");
2118                    assert_eq!(read.coalesce(), data);
2119                }
2120            });
2121            let check2 = context.child("check2").spawn({
2122                let blob = blob.clone();
2123                let data_len = data.len();
2124                move |_| async move {
2125                    let read = blob
2126                        .read_at(0, data_len, ReadOptions::default())
2127                        .await
2128                        .expect("Failed to read from blob");
2129                    assert_eq!(read.coalesce(), data);
2130                }
2131            });
2132
2133            // Wait for both reads to complete
2134            let result = join!(check1, check2);
2135            assert!(result.0.is_ok());
2136            assert!(result.1.is_ok());
2137
2138            // Read data from the blob
2139            let read = blob
2140                .read_at(0, data.len(), ReadOptions::default())
2141                .await
2142                .expect("Failed to read from blob");
2143            assert_eq!(read.coalesce(), data);
2144
2145            // Drop the blob
2146            drop(blob);
2147
2148            // Ensure no blobs still open
2149            let buffer = context.encode();
2150            assert!(buffer.contains("open_blobs 0"));
2151        });
2152    }
2153
2154    #[rstest]
2155    #[case::deterministic(deterministic::Runner::default())]
2156    #[case::tokio(tokio::Runner::default())]
2157    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2158    fn test_shutdown<R: Runner>(#[case] runner: R)
2159    where
2160        R::Context: Spawner + Metrics + Clock,
2161    {
2162        let kill = 9;
2163        runner.start(|context| async move {
2164            // Spawn a task that waits for signal
2165            let before = context.child("before").spawn(move |context| async move {
2166                let mut signal = context.stopped();
2167                let value = (&mut signal).await.unwrap();
2168                assert_eq!(value, kill);
2169                drop(signal);
2170            });
2171
2172            // Signal the tasks and wait for them to stop
2173            let result = context.child("stop").stop(kill, None).await;
2174            assert!(result.is_ok());
2175
2176            // Spawn a task after stop is called
2177            let after = context.child("after").spawn(move |context| async move {
2178                // A call to `stopped()` after `stop()` resolves immediately
2179                let value = context.stopped().await.unwrap();
2180                assert_eq!(value, kill);
2181            });
2182
2183            // Ensure both tasks complete
2184            let result = join!(before, after);
2185            assert!(result.0.is_ok());
2186            assert!(result.1.is_ok());
2187        });
2188    }
2189
2190    #[rstest]
2191    #[case::deterministic(deterministic::Runner::default())]
2192    #[case::tokio(tokio::Runner::default())]
2193    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2194    fn test_shutdown_multiple_signals<R: Runner>(#[case] runner: R)
2195    where
2196        R::Context: Spawner + Metrics + Clock,
2197    {
2198        let kill = 42;
2199        runner.start(|context| async move {
2200            let (started_tx, mut started_rx) = mpsc::channel(3);
2201            let counter = Arc::new(AtomicU32::new(0));
2202
2203            // Spawn 3 tasks that do cleanup work after receiving stop signal
2204            // and increment a shared counter
2205            let task = |context: R::Context, cleanup_duration: Duration| {
2206                let counter = counter.clone();
2207                let started_tx = started_tx.clone();
2208                context.spawn(move |context| async move {
2209                    // Wait for signal to be acquired
2210                    let mut signal = context.stopped();
2211                    started_tx.send(()).await.unwrap();
2212
2213                    // Increment once killed
2214                    let value = (&mut signal).await.unwrap();
2215                    assert_eq!(value, kill);
2216                    context.sleep(cleanup_duration).await;
2217                    counter.fetch_add(1, Ordering::SeqCst);
2218
2219                    // Wait to drop signal until work has been done
2220                    drop(signal);
2221                })
2222            };
2223
2224            let task1 = task(context.child("cleanup"), Duration::from_millis(10));
2225            let task2 = task(context.child("cleanup"), Duration::from_millis(20));
2226            let task3 = task(context.child("cleanup"), Duration::from_millis(30));
2227
2228            // Give tasks time to start
2229            for _ in 0..3 {
2230                started_rx.recv().await.unwrap();
2231            }
2232
2233            // Stop and verify all cleanup completed
2234            context.stop(kill, None).await.unwrap();
2235            assert_eq!(counter.load(Ordering::SeqCst), 3);
2236
2237            // Ensure all tasks completed
2238            let result = join!(task1, task2, task3);
2239            assert!(result.0.is_ok());
2240            assert!(result.1.is_ok());
2241            assert!(result.2.is_ok());
2242        });
2243    }
2244
2245    #[rstest]
2246    #[case::deterministic(deterministic::Runner::default())]
2247    #[case::tokio(tokio::Runner::default())]
2248    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2249    fn test_shutdown_timeout<R: Runner>(#[case] runner: R)
2250    where
2251        R::Context: Spawner + Metrics + Clock,
2252    {
2253        let kill = 42;
2254        runner.start(|context| async move {
2255            // Setup startup coordinator
2256            let (started_tx, started_rx) = oneshot::channel();
2257
2258            // Spawn a task that never completes its cleanup
2259            context.child("signal").spawn(move |context| async move {
2260                let signal = context.stopped();
2261                started_tx.send(()).unwrap();
2262                pending::<()>().await;
2263                signal.await.unwrap();
2264            });
2265
2266            // Try to stop with a timeout
2267            started_rx.await.unwrap();
2268            let result = context.stop(kill, Some(Duration::from_millis(100))).await;
2269
2270            // Assert that we got a timeout error
2271            assert!(matches!(result, Err(Error::Timeout)));
2272        });
2273    }
2274
2275    #[rstest]
2276    #[case::deterministic(deterministic::Runner::default())]
2277    #[case::tokio(tokio::Runner::default())]
2278    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2279    fn test_shutdown_multiple_stop_calls<R: Runner>(#[case] runner: R)
2280    where
2281        R::Context: Spawner + Metrics + Clock,
2282    {
2283        let kill1 = 42;
2284        let kill2 = 43;
2285
2286        runner.start(|context| async move {
2287            let (started_tx, started_rx) = oneshot::channel();
2288            let counter = Arc::new(AtomicU32::new(0));
2289
2290            // Spawn a task that delays completion to test timing
2291            let task = context.child("blocking_task").spawn({
2292                let counter = counter.clone();
2293                move |context| async move {
2294                    // Wait for signal to be acquired
2295                    let mut signal = context.stopped();
2296                    started_tx.send(()).unwrap();
2297
2298                    // Wait for signal to be resolved
2299                    let value = (&mut signal).await.unwrap();
2300                    assert_eq!(value, kill1);
2301                    context.sleep(Duration::from_millis(50)).await;
2302
2303                    // Increment counter
2304                    counter.fetch_add(1, Ordering::SeqCst);
2305                    drop(signal);
2306                }
2307            });
2308
2309            // Give task time to start
2310            started_rx.await.unwrap();
2311
2312            // Issue two separate stop calls
2313            // The second stop call uses a different stop value that should be ignored
2314            let stop_task1 = context.child("stop").stop(kill1, None);
2315            pin_mut!(stop_task1);
2316            let stop_task2 = context.child("stop").stop(kill2, None);
2317            pin_mut!(stop_task2);
2318
2319            // Both of them should be awaiting completion
2320            assert!(stop_task1.as_mut().now_or_never().is_none());
2321            assert!(stop_task2.as_mut().now_or_never().is_none());
2322
2323            // Wait for both stop calls to complete
2324            assert!(stop_task1.await.is_ok());
2325            assert!(stop_task2.await.is_ok());
2326
2327            // Verify first stop value wins
2328            let sig = context.stopped().await;
2329            assert_eq!(sig.unwrap(), kill1);
2330
2331            // Wait for blocking task to complete
2332            let result = task.await;
2333            assert!(result.is_ok());
2334            assert_eq!(counter.load(Ordering::SeqCst), 1);
2335
2336            // Post-completion stop should return immediately
2337            assert!(context.stop(kill2, None).now_or_never().unwrap().is_ok());
2338        });
2339    }
2340
2341    #[rstest]
2342    #[case::deterministic(deterministic::Runner::default())]
2343    #[case::tokio(tokio::Runner::default())]
2344    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2345    fn test_unfulfilled_shutdown<R: Runner>(#[case] runner: R)
2346    where
2347        R::Context: Spawner + Metrics,
2348    {
2349        runner.start(|context| async move {
2350            // Spawn a task that waits for signal
2351            context.child("before").spawn(move |context| async move {
2352                let mut signal = context.stopped();
2353                let value = (&mut signal).await.unwrap();
2354
2355                // We should never reach this point
2356                assert_eq!(value, 42);
2357                drop(signal);
2358            });
2359
2360            // Ensure waker is registered
2361            reschedule().await;
2362        });
2363    }
2364
2365    #[rstest]
2366    #[case::deterministic(deterministic::Runner::default())]
2367    #[case::tokio(tokio::Runner::default())]
2368    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2369    fn test_spawn_dedicated<R: Runner>(#[case] runner: R)
2370    where
2371        R::Context: Spawner,
2372    {
2373        runner.start(|context| async move {
2374            let handle = context.dedicated().spawn(|_| async move { 42 });
2375            assert!(matches!(handle.await, Ok(42)));
2376        });
2377    }
2378
2379    #[rstest]
2380    #[case::deterministic(deterministic::Runner::default())]
2381    #[case::tokio(tokio::Runner::default())]
2382    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2383    fn test_spawn<R: Runner>(#[case] runner: R)
2384    where
2385        R::Context: Spawner + Clock,
2386    {
2387        runner.start(|context| async move {
2388            let child_handle = Arc::new(Mutex::new(None));
2389            let child_handle2 = child_handle.clone();
2390
2391            let (parent_initialized_tx, parent_initialized_rx) = oneshot::channel();
2392            let (parent_complete_tx, parent_complete_rx) = oneshot::channel();
2393            let parent_handle = context.spawn(move |context| async move {
2394                // Spawn child that completes immediately
2395                let handle = context.spawn(|_| async {});
2396
2397                // Store child handle so we can test it later
2398                *child_handle2.lock() = Some(handle);
2399
2400                parent_initialized_tx.send(()).unwrap();
2401
2402                // Parent task completes
2403                parent_complete_rx.await.unwrap();
2404            });
2405
2406            // Wait for parent task to spawn the children
2407            parent_initialized_rx.await.unwrap();
2408
2409            // Child task completes successfully
2410            let child_handle = child_handle.lock().take().unwrap();
2411            assert!(child_handle.await.is_ok());
2412
2413            // Complete the parent task
2414            parent_complete_tx.send(()).unwrap();
2415
2416            // Wait for parent task to complete successfully
2417            assert!(parent_handle.await.is_ok());
2418        });
2419    }
2420
2421    #[rstest]
2422    #[case::deterministic(deterministic::Runner::default())]
2423    #[case::tokio(tokio::Runner::default())]
2424    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2425    fn test_spawn_abort_on_parent_abort<R: Runner>(#[case] runner: R)
2426    where
2427        R::Context: Spawner + Clock,
2428    {
2429        runner.start(|context| async move {
2430            let child_handle = Arc::new(Mutex::new(None));
2431            let child_handle2 = child_handle.clone();
2432
2433            let (parent_initialized_tx, parent_initialized_rx) = oneshot::channel();
2434            let parent_handle = context.spawn(move |context| async move {
2435                // Spawn child task that hangs forever, should be aborted when parent aborts
2436                let handle = context.spawn(|_| pending::<()>());
2437
2438                // Store child task handle so we can test it later
2439                *child_handle2.lock() = Some(handle);
2440
2441                parent_initialized_tx.send(()).unwrap();
2442
2443                // Parent task runs until aborted
2444                pending::<()>().await
2445            });
2446
2447            // Wait for parent task to spawn the children
2448            parent_initialized_rx.await.unwrap();
2449
2450            // Abort parent task
2451            parent_handle.abort();
2452            assert!(matches!(parent_handle.await, Err(Error::Closed)));
2453
2454            // Child task should also resolve with error since its parent aborted
2455            let child_handle = child_handle.lock().take().unwrap();
2456            assert!(matches!(child_handle.await, Err(Error::Closed)));
2457        });
2458    }
2459
2460    #[rstest]
2461    #[case::deterministic(deterministic::Runner::default())]
2462    #[case::tokio(tokio::Runner::default())]
2463    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2464    fn test_spawn_abort_on_parent_completion<R: Runner>(#[case] runner: R)
2465    where
2466        R::Context: Spawner + Clock,
2467    {
2468        runner.start(|context| async move {
2469            let child_handle = Arc::new(Mutex::new(None));
2470            let child_handle2 = child_handle.clone();
2471
2472            let (parent_complete_tx, parent_complete_rx) = oneshot::channel();
2473            let parent_handle = context.spawn(move |context| async move {
2474                // Spawn child task that hangs forever, should be aborted when parent completes
2475                let handle = context.spawn(|_| pending::<()>());
2476
2477                // Store child task handle so we can test it later
2478                *child_handle2.lock() = Some(handle);
2479
2480                // Parent task completes
2481                parent_complete_rx.await.unwrap();
2482            });
2483
2484            // Fire parent completion
2485            parent_complete_tx.send(()).unwrap();
2486
2487            // Wait for parent task to complete
2488            assert!(parent_handle.await.is_ok());
2489
2490            // Child task should resolve with error since its parent has completed
2491            let child_handle = child_handle.lock().take().unwrap();
2492            assert!(matches!(child_handle.await, Err(Error::Closed)));
2493        });
2494    }
2495
2496    #[rstest]
2497    #[case::deterministic(deterministic::Runner::default())]
2498    #[case::tokio(tokio::Runner::default())]
2499    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2500    fn test_spawn_cascading_abort<R: Runner>(#[case] runner: R)
2501    where
2502        R::Context: Spawner + Clock,
2503    {
2504        runner.start(|context| async move {
2505            // We create the following tree of tasks. All tasks will run
2506            // indefinitely (until aborted).
2507            //
2508            //          root
2509            //     /     |     \
2510            //    /      |      \
2511            //   c0      c1      c2
2512            //  /  \    /  \    /  \
2513            // g0  g1  g2  g3  g4  g5
2514            let c0 = context.child("c0");
2515            let g0 = c0.child("g0");
2516            let g1 = c0.child("g1");
2517            let c1 = context.child("c1");
2518            let g2 = c1.child("g2");
2519            let g3 = c1.child("g3");
2520            let c2 = context.child("c2");
2521            let g4 = c2.child("g4");
2522            let g5 = c2.child("g5");
2523
2524            // Spawn tasks
2525            let handles = Arc::new(Mutex::new(Vec::new()));
2526            let (initialized_tx, mut initialized_rx) = mpsc::channel(9);
2527            let root_task = context.spawn({
2528                let handles = handles.clone();
2529                move |_| async move {
2530                    for (context, grandchildren) in [(c0, [g0, g1]), (c1, [g2, g3]), (c2, [g4, g5])]
2531                    {
2532                        let handle = context.spawn({
2533                            let handles = handles.clone();
2534                            let initialized_tx = initialized_tx.clone();
2535                            move |_| async move {
2536                                for grandchild in grandchildren {
2537                                    let handle = grandchild.spawn(|_| async {
2538                                        pending::<()>().await;
2539                                    });
2540                                    handles.lock().push(handle);
2541                                    initialized_tx.send(()).await.unwrap();
2542                                }
2543
2544                                pending::<()>().await;
2545                            }
2546                        });
2547                        handles.lock().push(handle);
2548                        initialized_tx.send(()).await.unwrap();
2549                    }
2550
2551                    pending::<()>().await;
2552                }
2553            });
2554
2555            // Wait for tasks to initialize
2556            for _ in 0..9 {
2557                initialized_rx.recv().await.unwrap();
2558            }
2559
2560            // Verify we have all 9 handles (3 children + 6 grandchildren)
2561            assert_eq!(handles.lock().len(), 9);
2562
2563            // Abort root task
2564            root_task.abort();
2565            assert!(matches!(root_task.await, Err(Error::Closed)));
2566
2567            // All handles should resolve with error due to cascading abort
2568            let handles = handles.lock().drain(..).collect::<Vec<_>>();
2569            for handle in handles {
2570                assert!(matches!(handle.await, Err(Error::Closed)));
2571            }
2572        });
2573    }
2574
2575    #[rstest]
2576    #[case::deterministic(deterministic::Runner::default())]
2577    #[case::tokio(tokio::Runner::default())]
2578    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2579    fn test_child_survives_sibling_completion<R: Runner>(#[case] runner: R)
2580    where
2581        R::Context: Spawner + Clock,
2582    {
2583        runner.start(|context| async move {
2584            let (child_started_tx, child_started_rx) = oneshot::channel();
2585            let (child_complete_tx, child_complete_rx) = oneshot::channel();
2586            let (child_handle_tx, child_handle_rx) = oneshot::channel();
2587            let (sibling_started_tx, sibling_started_rx) = oneshot::channel();
2588            let (sibling_complete_tx, sibling_complete_rx) = oneshot::channel();
2589            let (sibling_handle_tx, sibling_handle_rx) = oneshot::channel();
2590            let (parent_complete_tx, parent_complete_rx) = oneshot::channel();
2591
2592            let parent = context.spawn(move |context| async move {
2593                // Spawn a child task
2594                let child_handle = context.child("child").spawn(|_| async move {
2595                    child_started_tx.send(()).unwrap();
2596                    // Wait for signal to complete
2597                    child_complete_rx.await.unwrap();
2598                });
2599                assert!(
2600                    child_handle_tx.send(child_handle).is_ok(),
2601                    "child handle receiver dropped"
2602                );
2603
2604                // Spawn an independent sibling task
2605                let sibling_handle = context.child("sibling").spawn(move |_| async move {
2606                    sibling_started_tx.send(()).unwrap();
2607                    // Wait for signal to complete
2608                    sibling_complete_rx.await.unwrap();
2609                });
2610                assert!(
2611                    sibling_handle_tx.send(sibling_handle).is_ok(),
2612                    "sibling handle receiver dropped"
2613                );
2614
2615                // Wait for signal to complete
2616                parent_complete_rx.await.unwrap();
2617            });
2618
2619            // Wait for both to start
2620            child_started_rx.await.unwrap();
2621            sibling_started_rx.await.unwrap();
2622
2623            // Kill the sibling
2624            sibling_complete_tx.send(()).unwrap();
2625            assert!(sibling_handle_rx.await.is_ok());
2626
2627            // The child task should still be alive
2628            child_complete_tx.send(()).unwrap();
2629            assert!(child_handle_rx.await.is_ok());
2630
2631            // As well as the parent
2632            parent_complete_tx.send(()).unwrap();
2633            assert!(parent.await.is_ok());
2634        });
2635    }
2636
2637    #[rstest]
2638    #[case::deterministic(deterministic::Runner::default())]
2639    #[case::tokio(tokio::Runner::default())]
2640    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2641    fn test_spawn_clone_chain<R: Runner>(#[case] runner: R)
2642    where
2643        R::Context: Spawner + Clock,
2644    {
2645        runner.start(|context| async move {
2646            let (parent_started_tx, parent_started_rx) = oneshot::channel();
2647            let (child_started_tx, child_started_rx) = oneshot::channel();
2648            let (grandchild_started_tx, grandchild_started_rx) = oneshot::channel();
2649            let (child_handle_tx, child_handle_rx) = oneshot::channel();
2650            let (grandchild_handle_tx, grandchild_handle_rx) = oneshot::channel();
2651
2652            let parent = context.child("parent").spawn({
2653                move |context| async move {
2654                    let child = context.child("child").spawn({
2655                        move |context| async move {
2656                            let grandchild = context.child("grandchild").spawn({
2657                                move |_| async move {
2658                                    grandchild_started_tx.send(()).unwrap();
2659                                    pending::<()>().await;
2660                                }
2661                            });
2662                            assert!(
2663                                grandchild_handle_tx.send(grandchild).is_ok(),
2664                                "grandchild handle receiver dropped"
2665                            );
2666                            child_started_tx.send(()).unwrap();
2667                            pending::<()>().await;
2668                        }
2669                    });
2670                    assert!(
2671                        child_handle_tx.send(child).is_ok(),
2672                        "child handle receiver dropped"
2673                    );
2674                    parent_started_tx.send(()).unwrap();
2675                    pending::<()>().await;
2676                }
2677            });
2678
2679            parent_started_rx.await.unwrap();
2680            child_started_rx.await.unwrap();
2681            grandchild_started_rx.await.unwrap();
2682
2683            let child_handle = child_handle_rx.await.unwrap();
2684            let grandchild_handle = grandchild_handle_rx.await.unwrap();
2685
2686            parent.abort();
2687            assert!(parent.await.is_err());
2688
2689            assert!(child_handle.await.is_err());
2690            assert!(grandchild_handle.await.is_err());
2691        });
2692    }
2693
2694    #[rstest]
2695    #[case::deterministic(deterministic::Runner::default())]
2696    #[case::tokio(tokio::Runner::default())]
2697    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2698    fn test_spawn_sparse_clone_chain<R: Runner>(#[case] runner: R)
2699    where
2700        R::Context: Spawner + Clock,
2701    {
2702        runner.start(|context| async move {
2703            let (leaf_started_tx, leaf_started_rx) = oneshot::channel();
2704            let (leaf_handle_tx, leaf_handle_rx) = oneshot::channel();
2705
2706            let parent = context.child("parent").spawn({
2707                move |context| async move {
2708                    let clone1 = context.child("clone1");
2709                    let clone2 = clone1.child("clone2");
2710                    let clone3 = clone2.child("clone3");
2711
2712                    let leaf = clone3.spawn({
2713                        move |_| async move {
2714                            leaf_started_tx.send(()).unwrap();
2715                            pending::<()>().await;
2716                        }
2717                    });
2718
2719                    leaf_handle_tx
2720                        .send(leaf)
2721                        .unwrap_or_else(|_| panic!("leaf handle receiver dropped"));
2722                    pending::<()>().await;
2723                }
2724            });
2725
2726            leaf_started_rx.await.unwrap();
2727            let leaf_handle = leaf_handle_rx.await.unwrap();
2728
2729            parent.abort();
2730            assert!(parent.await.is_err());
2731            assert!(leaf_handle.await.is_err());
2732        });
2733    }
2734
2735    #[rstest]
2736    #[case::deterministic(deterministic::Runner::default())]
2737    #[case::tokio(tokio::Runner::default())]
2738    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2739    fn test_spawn_blocking<R: Runner>(
2740        #[case] runner: R,
2741        #[values(Execution::Shared(true), Execution::Dedicated)] execution: Execution,
2742    ) where
2743        R::Context: Spawner,
2744    {
2745        runner.start(|context| async move {
2746            let context = match execution {
2747                Execution::Dedicated => context.dedicated(),
2748                Execution::Shared(blocking) => context.shared(blocking),
2749            };
2750
2751            let handle = context.spawn(|_| async move { 42 });
2752            let result = handle.await;
2753            assert!(matches!(result, Ok(42)));
2754        });
2755    }
2756
2757    #[rstest]
2758    #[case::deterministic(deterministic::Runner::default())]
2759    #[case::tokio(tokio::Runner::default())]
2760    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2761    #[should_panic(expected = "blocking task panicked")]
2762    fn test_spawn_blocking_panic<R: Runner>(
2763        #[case] runner: R,
2764        #[values(Execution::Shared(true), Execution::Dedicated)] execution: Execution,
2765    ) where
2766        R::Context: Spawner + Clock,
2767    {
2768        runner.start(|context| async move {
2769            let spawner = match execution {
2770                Execution::Dedicated => context.child("blocking").dedicated(),
2771                Execution::Shared(blocking) => context.child("blocking").shared(blocking),
2772            };
2773            spawner.spawn(|_| async move {
2774                panic!("blocking task panicked");
2775            });
2776
2777            // Loop until panic
2778            loop {
2779                context.sleep(Duration::from_millis(100)).await;
2780            }
2781        });
2782    }
2783
2784    #[rstest]
2785    #[case::deterministic(deterministic::Runner::new(
2786        deterministic::Config::default().with_catch_panics(true)
2787    ))]
2788    #[case::tokio(tokio::Runner::new(tokio::Config::default().with_catch_panics(true)))]
2789    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::new(iouring::Config::default().with_catch_panics(true))))]
2790    fn test_spawn_blocking_panic_caught<R: Runner>(
2791        #[case] runner: R,
2792        #[values(Execution::Shared(true), Execution::Dedicated)] execution: Execution,
2793    ) where
2794        R::Context: Spawner + Clock,
2795    {
2796        let result: Result<(), Error> = runner.start(|context| async move {
2797            let spawner = match execution {
2798                Execution::Dedicated => context.child("blocking").dedicated(),
2799                Execution::Shared(blocking) => context.child("blocking").shared(blocking),
2800            };
2801            let handle = spawner.spawn(|_| async move {
2802                panic!("blocking task panicked");
2803            });
2804            handle.await
2805        });
2806        assert!(matches!(result, Err(Error::Exited)));
2807    }
2808
2809    #[rstest]
2810    #[case::deterministic(deterministic::Runner::default())]
2811    #[case::tokio(tokio::Runner::default())]
2812    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2813    fn test_circular_reference_prevents_cleanup<R: Runner>(#[case] runner: R) {
2814        runner.start(|_| async move {
2815            // Setup tracked resource
2816            let dropper = Arc::new(());
2817            let executor = deterministic::Runner::default();
2818            executor.start({
2819                let dropper = dropper.clone();
2820                move |context| async move {
2821                    // Create tasks with circular dependencies through channels
2822                    let (setup_tx, mut setup_rx) = mpsc::unbounded_channel::<()>();
2823                    let (tx1, mut rx1) = mpsc::unbounded_channel::<()>();
2824                    let (tx2, mut rx2) = mpsc::unbounded_channel::<()>();
2825
2826                    // Task 1 holds tx2 and waits on rx1
2827                    context.child("task1").spawn({
2828                        let setup_tx = setup_tx.clone();
2829                        let dropper = dropper.clone();
2830                        move |_| async move {
2831                            // Setup deadlock and mark ready
2832                            tx2.send(()).unwrap();
2833                            rx1.recv().await.unwrap();
2834                            setup_tx.send(()).unwrap();
2835
2836                            // Wait forever
2837                            while rx1.recv().await.is_some() {}
2838                            drop(tx2);
2839                            drop(dropper);
2840                        }
2841                    });
2842
2843                    // Task 2 holds tx1 and waits on rx2
2844                    context.child("task2").spawn(move |_| async move {
2845                        // Setup deadlock and mark ready
2846                        tx1.send(()).unwrap();
2847                        rx2.recv().await.unwrap();
2848                        setup_tx.send(()).unwrap();
2849
2850                        // Wait forever
2851                        while rx2.recv().await.is_some() {}
2852                        drop(tx1);
2853                        drop(dropper);
2854                    });
2855
2856                    // Wait for tasks to start
2857                    setup_rx.recv().await.unwrap();
2858                    setup_rx.recv().await.unwrap();
2859                }
2860            });
2861
2862            // After runtime drop, both tasks should be cleaned up
2863            Arc::try_unwrap(dropper).expect("references remaining");
2864        });
2865    }
2866
2867    #[rstest]
2868    #[case::deterministic(deterministic::Runner::default())]
2869    #[case::tokio(tokio::Runner::default())]
2870    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2871    fn test_late_waker<R: Runner>(#[case] runner: R)
2872    where
2873        R::Context: Metrics + Spawner,
2874    {
2875        // A future that captures its waker and sends it to the caller, then
2876        // stays pending forever.
2877        struct CaptureWaker {
2878            tx: Option<oneshot::Sender<Waker>>,
2879            sent: bool,
2880        }
2881        impl Future for CaptureWaker {
2882            type Output = ();
2883            fn poll(mut self: Pin<&mut Self>, cx: &mut TContext<'_>) -> Poll<Self::Output> {
2884                if !self.sent {
2885                    if let Some(tx) = self.tx.take() {
2886                        // Send a clone of the current task's waker to the root
2887                        let _ = tx.send(cx.waker().clone());
2888                    }
2889                    self.sent = true;
2890                }
2891                Poll::Pending
2892            }
2893        }
2894
2895        // A guard that wakes the captured waker on drop.
2896        struct WakeOnDrop(Option<Waker>);
2897        impl Drop for WakeOnDrop {
2898            fn drop(&mut self) {
2899                if let Some(w) = self.0.take() {
2900                    w.wake_by_ref();
2901                }
2902            }
2903        }
2904
2905        // Run the executor to completion
2906        let holder = runner.start(|context| async move {
2907            // Wire a oneshot to receive the task waker.
2908            let (tx, rx) = oneshot::channel::<Waker>();
2909
2910            // Spawn a task that registers its waker and then stays pending.
2911            context.child("capture_waker").spawn(move |_| async move {
2912                CaptureWaker {
2913                    tx: Some(tx),
2914                    sent: false,
2915                }
2916                .await;
2917            });
2918
2919            // Ensure the spawned task runs and registers its waker.
2920            utils::reschedule().await;
2921
2922            // Receive the waker from the spawned task.
2923            let waker = rx.await.expect("waker not received");
2924
2925            // Return a guard that will wake after the runtime has dropped.
2926            WakeOnDrop(Some(waker))
2927        });
2928
2929        // Dropping the guard after the runtime has torn down will trigger a wake on
2930        // a task whose executor has been dropped.
2931        drop(holder);
2932    }
2933
2934    #[rstest]
2935    #[case::deterministic(deterministic::Runner::default())]
2936    #[case::tokio(tokio::Runner::default())]
2937    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2938    fn test_factory_panic_finishes_running_metric<R: Runner>(#[case] runner: R)
2939    where
2940        R::Context: Spawner + Metrics,
2941    {
2942        // A task factory that unwinds must finish the running gauge and close its
2943        // supervision node, even though the execution wrapper never received them.
2944        runner.start(|context| async move {
2945            let child = context.child("panicking_factory");
2946            let panic = catch_unwind(AssertUnwindSafe(|| {
2947                child.spawn(|_| -> std::future::Ready<()> { panic!("factory failed") })
2948            }))
2949            .err()
2950            .expect("factory panic must reach its caller");
2951            assert_eq!(panic.downcast_ref::<&str>(), Some(&"factory failed"));
2952
2953            // The attempted spawn stays counted, but nothing is left running.
2954            assert_eq!(count_running_tasks(&context, "panicking_factory"), 0);
2955            let buffer = context.encode();
2956            assert!(buffer.contains(
2957                "runtime_tasks_spawned_total{name=\"panicking_factory\",kind=\"Task\",execution=\"Shared\"} 1"
2958            ));
2959        });
2960    }
2961
2962    #[rstest]
2963    #[case::deterministic(deterministic::Runner::default())]
2964    #[case::tokio(tokio::Runner::default())]
2965    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2966    fn test_metrics<R: Runner>(#[case] runner: R)
2967    where
2968        R::Context: Metrics,
2969    {
2970        runner.start(|context| async move {
2971            // Assert label
2972            assert_eq!(context.name().label, "");
2973
2974            // Register a metric
2975            let counter = Counter::<u64>::default();
2976            let _registered = context.register("test", "test", counter.clone());
2977
2978            // Increment the counter
2979            counter.inc();
2980
2981            // Encode metrics
2982            let buffer = context.encode();
2983            assert!(buffer.contains("test_total 1"));
2984
2985            // Nested context
2986            let context = context.child("nested");
2987            let nested_counter = Counter::<u64>::default();
2988            let _nested_registered = context.register("test", "test", nested_counter.clone());
2989
2990            // Increment the counter
2991            nested_counter.inc();
2992
2993            // Encode metrics
2994            let buffer = context.encode();
2995            assert!(buffer.contains("nested_test_total 1"));
2996            assert!(buffer.contains("test_total 1"));
2997        });
2998    }
2999
3000    #[rstest]
3001    #[case::deterministic(deterministic::Runner::default())]
3002    #[case::tokio(tokio::Runner::default())]
3003    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3004    fn test_metrics_with_attribute<R: Runner>(#[case] runner: R)
3005    where
3006        R::Context: Metrics,
3007    {
3008        runner.start(|context| async move {
3009            // Create context with a attribute
3010            let ctx_epoch5 = context.child("consensus").with_attribute("epoch", "e5");
3011
3012            // Register a metric with the attribute
3013            let counter = Counter::<u64>::default();
3014            let _epoch5 = ctx_epoch5.register("votes", "vote count", counter.clone());
3015            counter.inc();
3016
3017            // Encode and verify the attribute appears as a label
3018            let buffer = context.encode();
3019            assert!(
3020                buffer.contains("consensus_votes_total{epoch=\"e5\"} 1"),
3021                "Expected metric with epoch attribute, got: {}",
3022                buffer
3023            );
3024
3025            // Create context with different epoch attribute (same metric name)
3026            let ctx_epoch6 = context.child("consensus").with_attribute("epoch", "e6");
3027            let counter2 = Counter::<u64>::default();
3028            let _epoch6 = ctx_epoch6.register("votes", "vote count", counter2.clone());
3029            counter2.inc();
3030            counter2.inc();
3031
3032            // Both should appear in encoded output with canonical format (single HELP/TYPE)
3033            let buffer = context.encode();
3034            assert!(
3035                buffer.contains("consensus_votes_total{epoch=\"e5\"} 1"),
3036                "Expected metric with epoch=e5, got: {}",
3037                buffer
3038            );
3039            assert!(
3040                buffer.contains("consensus_votes_total{epoch=\"e6\"} 2"),
3041                "Expected metric with epoch=e6, got: {}",
3042                buffer
3043            );
3044
3045            // Verify canonical format: HELP and TYPE should appear exactly once
3046            assert_eq!(
3047                buffer.matches("# HELP consensus_votes").count(),
3048                1,
3049                "HELP should appear exactly once, got: {}",
3050                buffer
3051            );
3052            assert_eq!(
3053                buffer.matches("# TYPE consensus_votes").count(),
3054                1,
3055                "TYPE should appear exactly once, got: {}",
3056                buffer
3057            );
3058
3059            // Multiple attributes
3060            let ctx_multi = context
3061                .child("engine")
3062                .with_attribute("region", "us")
3063                .with_attribute("instance", "i1");
3064            let counter3 = Counter::<u64>::default();
3065            let _multi = ctx_multi.register("requests", "request count", counter3.clone());
3066            counter3.inc();
3067
3068            let buffer = context.encode();
3069            assert!(
3070                buffer.contains("engine_requests_total{instance=\"i1\",region=\"us\"} 1"),
3071                "Expected metric with sorted attributes, got: {}",
3072                buffer
3073            );
3074        });
3075    }
3076
3077    #[rstest]
3078    #[case::deterministic(deterministic::Runner::default())]
3079    #[case::tokio(tokio::Runner::default())]
3080    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3081    fn test_metrics_attribute_with_nested_label<R: Runner>(#[case] runner: R)
3082    where
3083        R::Context: Metrics,
3084    {
3085        runner.start(|context| async move {
3086            // Create context with attribute, then nest a label
3087            let ctx = context
3088                .child("orchestrator")
3089                .with_attribute("epoch", "e5")
3090                .child("engine");
3091
3092            // Register a metric
3093            let counter = Counter::<u64>::default();
3094            let _registered = ctx.register("votes", "vote count", counter.clone());
3095            counter.inc();
3096
3097            // Verify the attribute is preserved through the nested label
3098            let buffer = context.encode();
3099            assert!(
3100                buffer.contains("orchestrator_engine_votes_total{epoch=\"e5\"} 1"),
3101                "Expected metric with preserved epoch attribute, got: {}",
3102                buffer
3103            );
3104
3105            // Multiple levels of nesting with attributes at different levels
3106            let ctx2 = context
3107                .child("outer")
3108                .with_attribute("region", "us")
3109                .child("middle")
3110                .with_attribute("az", "east")
3111                .child("inner");
3112
3113            let counter2 = Counter::<u64>::default();
3114            let _registered2 = ctx2.register("requests", "request count", counter2.clone());
3115            counter2.inc();
3116            counter2.inc();
3117
3118            let buffer = context.encode();
3119            assert!(
3120                buffer.contains("outer_middle_inner_requests_total{az=\"east\",region=\"us\"} 2"),
3121                "Expected metric with all attributes preserved and sorted, got: {}",
3122                buffer
3123            );
3124        });
3125    }
3126
3127    #[rstest]
3128    #[case::deterministic(deterministic::Runner::default())]
3129    #[case::tokio(tokio::Runner::default())]
3130    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3131    fn test_metrics_attributes_isolated_between_contexts<R: Runner>(#[case] runner: R)
3132    where
3133        R::Context: Metrics,
3134    {
3135        runner.start(|context| async move {
3136            // Create two separate sub-contexts, each with their own attribute
3137            let ctx_a = context.child("component_a").with_attribute("epoch", 1);
3138            let ctx_b = context.child("component_b").with_attribute("epoch", 2);
3139
3140            // Register metrics in ctx_a
3141            let c1 = Counter::<u64>::default();
3142            let _ctx_a_requests = ctx_a.register("requests", "help", c1);
3143
3144            // Register metrics in ctx_b
3145            let c2 = Counter::<u64>::default();
3146            let _ctx_b_requests = ctx_b.register("requests", "help", c2);
3147
3148            // Register another metric in ctx_a AFTER ctx_b was used
3149            let c3 = Counter::<u64>::default();
3150            let _ctx_a_errors = ctx_a.register("errors", "help", c3);
3151
3152            let output = context.encode();
3153
3154            // ctx_a metrics should only have epoch=1
3155            assert!(
3156                output.contains("component_a_requests_total{epoch=\"1\"} 0"),
3157                "ctx_a requests should have epoch=1: {output}"
3158            );
3159            assert!(
3160                output.contains("component_a_errors_total{epoch=\"1\"} 0"),
3161                "ctx_a errors should have epoch=1: {output}"
3162            );
3163            assert!(
3164                !output.contains("component_a_requests_total{epoch=\"2\"}"),
3165                "ctx_a requests should not have epoch=2: {output}"
3166            );
3167
3168            // ctx_b metrics should only have epoch=2
3169            assert!(
3170                output.contains("component_b_requests_total{epoch=\"2\"} 0"),
3171                "ctx_b should have epoch=2: {output}"
3172            );
3173            assert!(
3174                !output.contains("component_b_requests_total{epoch=\"1\"}"),
3175                "ctx_b should not have epoch=1: {output}"
3176            );
3177        });
3178    }
3179
3180    /// Regression test for https://github.com/commonwarexyz/monorepo/issues/3485.
3181    ///
3182    /// Verifies the documented guarantee that runtime task metrics ignore context
3183    /// attributes: spawning with a varying `with_attribute` (as the marshaled
3184    /// consensus code does for each round) must not create per-value entries in
3185    /// `runtime_tasks_spawned` / `runtime_tasks_running`.
3186    #[rstest]
3187    #[case::deterministic(deterministic::Runner::default())]
3188    #[case::tokio(tokio::Runner::default())]
3189    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3190    fn test_metrics_spawn_attribute_cardinality<R: Runner>(#[case] runner: R)
3191    where
3192        R::Context: Spawner + Metrics + Clock,
3193    {
3194        runner.start(|context| async move {
3195            const ROUNDS: u64 = 128;
3196
3197            let mut handles = Vec::with_capacity(ROUNDS as usize);
3198            for round in 0..ROUNDS {
3199                let handle = context
3200                    .child("deferred_verify")
3201                    .with_attribute("round", round)
3202                    .spawn(move |_| async move { round });
3203                handles.push(handle);
3204            }
3205            for (expected, handle) in handles.into_iter().enumerate() {
3206                assert_eq!(handle.await.expect("task failed"), expected as u64);
3207            }
3208
3209            // handle.await resolves when the task's output is ready, but
3210            // the running-gauge decrement fires on task-struct drop which
3211            // may lag slightly. Yield to the executor so it can run cleanup.
3212            while count_running_tasks(&context, "deferred_verify") > 0 {
3213                context.sleep(Duration::from_millis(10)).await;
3214            }
3215            let buffer = context.encode();
3216
3217            // Count occurrences of each runtime task metric for our label. If
3218            // attributes were incorrectly folded into the task family key, we
3219            // would see ROUNDS distinct time series instead of one.
3220            let spawned_lines = buffer
3221                .lines()
3222                .filter(|line| {
3223                    line.starts_with("runtime_tasks_spawned_total{")
3224                        && line.contains("name=\"deferred_verify\"")
3225                })
3226                .count();
3227            let running_lines = buffer
3228                .lines()
3229                .filter(|line| {
3230                    line.starts_with("runtime_tasks_running{")
3231                        && line.contains("name=\"deferred_verify\"")
3232                })
3233                .count();
3234            assert_eq!(
3235                spawned_lines, 1,
3236                "expected exactly 1 runtime_tasks_spawned entry for deferred_verify, got {spawned_lines}: {buffer}",
3237            );
3238            assert_eq!(
3239                running_lines, 1,
3240                "expected exactly 1 runtime_tasks_running entry for deferred_verify, got {running_lines}: {buffer}",
3241            );
3242
3243            // The single spawned-counter entry should reflect every round.
3244            let spawned_value = format!(
3245                "runtime_tasks_spawned_total{{name=\"deferred_verify\",kind=\"Task\",execution=\"Shared\"}} {ROUNDS}"
3246            );
3247            assert!(
3248                buffer.contains(&spawned_value),
3249                "expected accumulated spawned counter `{spawned_value}`, got: {buffer}",
3250            );
3251            let running_value = "runtime_tasks_running{name=\"deferred_verify\",kind=\"Task\",execution=\"Shared\"} 0";
3252            assert!(
3253                buffer.contains(running_value),
3254                "expected running gauge to return to 0, got: {buffer}",
3255            );
3256
3257            // The per-round attribute must not surface on task metrics (the
3258            // task `Label` does not include context attributes).
3259            assert!(
3260                !buffer
3261                    .lines()
3262                    .any(|line| line.starts_with("runtime_tasks_")
3263                        && line.contains("round=")),
3264                "task metrics must not carry `round` attribute: {buffer}",
3265            );
3266        });
3267    }
3268
3269    #[rstest]
3270    #[case::deterministic(deterministic::Runner::default())]
3271    #[case::tokio(tokio::Runner::default())]
3272    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3273    fn test_metrics_attributes_sorted_deterministically<R: Runner>(#[case] runner: R)
3274    where
3275        R::Context: Metrics,
3276    {
3277        runner.start(|context| async move {
3278            // Create two contexts with same attributes but different order
3279            let ctx_ab = context
3280                .child("service")
3281                .with_attribute("region", "us")
3282                .with_attribute("env", "prod");
3283
3284            let ctx_ba = context
3285                .child("service")
3286                .with_attribute("env", "prod")
3287                .with_attribute("region", "us");
3288
3289            // Register via first context
3290            let c1 = Counter::<u64>::default();
3291            let _requests = ctx_ab.register("requests", "help", c1.clone());
3292            c1.inc();
3293
3294            // Register via second context: same attributes, different metric
3295            let c2 = Counter::<u64>::default();
3296            let _errors = ctx_ba.register("errors", "help", c2.clone());
3297            c2.inc();
3298            c2.inc();
3299
3300            let output = context.encode();
3301
3302            // Both should have the same label order (alphabetically sorted: env, region)
3303            assert!(
3304                output.contains("service_requests_total{env=\"prod\",region=\"us\"} 1"),
3305                "requests should have sorted labels: {output}"
3306            );
3307            assert!(
3308                output.contains("service_errors_total{env=\"prod\",region=\"us\"} 2"),
3309                "errors should have sorted labels: {output}"
3310            );
3311
3312            // Should NOT have reverse order
3313            assert!(
3314                !output.contains("region=\"us\",env=\"prod\""),
3315                "should not have unsorted label order: {output}"
3316            );
3317        });
3318    }
3319
3320    #[rstest]
3321    #[case::deterministic(deterministic::Runner::default())]
3322    #[case::tokio(tokio::Runner::default())]
3323    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3324    fn test_metrics_nested_labels_with_attributes<R: Runner>(#[case] runner: R)
3325    where
3326        R::Context: Metrics,
3327    {
3328        runner.start(|context| async move {
3329            // Service A: plain, no nested labels
3330            let svc_a = context.child("service_a");
3331
3332            // Service A with attribute (same top-level label, different context)
3333            let svc_a_v2 = context.child("service_a").with_attribute("version", 2);
3334
3335            // Service B with nested label: service_b_worker
3336            let svc_b_worker = context.child("service_b").child("worker");
3337
3338            // Service B with nested label AND attribute
3339            let svc_b_worker_shard = context
3340                .child("service_b")
3341                .child("worker")
3342                .with_attribute("shard", 99);
3343
3344            // Service B different nested label: service_b_manager
3345            let svc_b_manager = context.child("service_b").child("manager");
3346
3347            // Service C: plain, proves no cross-service contamination
3348            let svc_c = context.child("service_c");
3349
3350            // Register metrics in all contexts
3351            let c1 = Counter::<u64>::default();
3352            let _svc_a = svc_a.register("requests", "help", c1);
3353
3354            let c2 = Counter::<u64>::default();
3355            let _svc_a_v2 = svc_a_v2.register("requests", "help", c2);
3356
3357            let c3 = Counter::<u64>::default();
3358            let _svc_b_worker = svc_b_worker.register("tasks", "help", c3);
3359
3360            let c4 = Counter::<u64>::default();
3361            let _svc_b_worker_shard = svc_b_worker_shard.register("tasks", "help", c4);
3362
3363            let c5 = Counter::<u64>::default();
3364            let _svc_b_manager = svc_b_manager.register("decisions", "help", c5);
3365
3366            let c6 = Counter::<u64>::default();
3367            let _svc_c = svc_c.register("requests", "help", c6);
3368
3369            let output = context.encode();
3370
3371            // Service A plain and attributed both exist independently
3372            assert!(
3373                output.contains("service_a_requests_total 0"),
3374                "svc_a plain should exist: {output}"
3375            );
3376            assert!(
3377                output.contains("service_a_requests_total{version=\"2\"} 0"),
3378                "svc_a_v2 should have version=2: {output}"
3379            );
3380
3381            // Service B worker: plain and attributed versions
3382            assert!(
3383                output.contains("service_b_worker_tasks_total 0"),
3384                "svc_b_worker plain should exist: {output}"
3385            );
3386            assert!(
3387                output.contains("service_b_worker_tasks_total{shard=\"99\"} 0"),
3388                "svc_b_worker_shard should have shard=99: {output}"
3389            );
3390
3391            // Service B manager: no attributes
3392            assert!(
3393                output.contains("service_b_manager_decisions_total 0"),
3394                "svc_b_manager should have no attributes: {output}"
3395            );
3396            assert!(
3397                !output.contains("service_b_manager_decisions_total{"),
3398                "svc_b_manager should have no attributes at all: {output}"
3399            );
3400
3401            // Service C: no attributes, no contamination
3402            assert!(
3403                output.contains("service_c_requests_total 0"),
3404                "svc_c should have no attributes: {output}"
3405            );
3406            assert!(
3407                !output.contains("service_c_requests_total{"),
3408                "svc_c should have no attributes at all: {output}"
3409            );
3410
3411            // Cross-contamination checks
3412            assert!(
3413                !output.contains("service_b_manager_decisions_total{shard="),
3414                "svc_b_manager should not have shard: {output}"
3415            );
3416            assert!(
3417                !output.contains("service_a_requests_total{shard="),
3418                "svc_a should not have shard: {output}"
3419            );
3420            assert!(
3421                !output.contains("service_c_requests_total{version="),
3422                "svc_c should not have version: {output}"
3423            );
3424        });
3425    }
3426
3427    #[rstest]
3428    #[case::deterministic(deterministic::Runner::default())]
3429    #[case::tokio(tokio::Runner::default())]
3430    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3431    fn test_metrics_family_with_attributes<R: Runner>(#[case] runner: R)
3432    where
3433        R::Context: Metrics,
3434    {
3435        runner.start(|context| async move {
3436            #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
3437            struct RequestLabels {
3438                method: String,
3439                status: u16,
3440            }
3441
3442            // Create context with attribute
3443            let ctx = context
3444                .child("api")
3445                .with_attribute("region", "us_east")
3446                .with_attribute("env", "prod");
3447
3448            // Register a Family metric
3449            let requests: Family<RequestLabels, Counter<u64>> = Family::default();
3450            let _requests = ctx.register("requests", "HTTP requests", requests.clone());
3451
3452            // Increment counters for different label combinations
3453            requests
3454                .get_or_create(&RequestLabels {
3455                    method: "GET".to_string(),
3456                    status: 200,
3457                })
3458                .inc();
3459            requests
3460                .get_or_create(&RequestLabels {
3461                    method: "POST".to_string(),
3462                    status: 201,
3463                })
3464                .inc();
3465            requests
3466                .get_or_create(&RequestLabels {
3467                    method: "GET".to_string(),
3468                    status: 404,
3469                })
3470                .inc();
3471
3472            let output = context.encode();
3473
3474            // Context attributes appear first (alphabetically sorted), then Family labels
3475            // Context attributes: env="prod", region="us_east"
3476            // Family labels: method, status
3477            assert!(
3478                output.contains(
3479                    "api_requests_total{env=\"prod\",region=\"us_east\",method=\"GET\",status=\"200\"} 1"
3480                ),
3481                "GET 200 should have merged labels: {output}"
3482            );
3483            assert!(
3484                output.contains(
3485                    "api_requests_total{env=\"prod\",region=\"us_east\",method=\"POST\",status=\"201\"} 1"
3486                ),
3487                "POST 201 should have merged labels: {output}"
3488            );
3489            assert!(
3490                output.contains(
3491                    "api_requests_total{env=\"prod\",region=\"us_east\",method=\"GET\",status=\"404\"} 1"
3492                ),
3493                "GET 404 should have merged labels: {output}"
3494            );
3495
3496            // Create another context WITHOUT attributes to verify isolation
3497            let ctx_plain = context.child("api_plain");
3498            let plain_requests: Family<RequestLabels, Counter<u64>> = Family::default();
3499            let _plain_requests =
3500                ctx_plain.register("requests", "HTTP requests", plain_requests.clone());
3501
3502            plain_requests
3503                .get_or_create(&RequestLabels {
3504                    method: "DELETE".to_string(),
3505                    status: 204,
3506                })
3507                .inc();
3508
3509            let output = context.encode();
3510
3511            // Plain context should have Family labels but no context attributes
3512            assert!(
3513                output.contains("api_plain_requests_total{method=\"DELETE\",status=\"204\"} 1"),
3514                "plain DELETE should have only family labels: {output}"
3515            );
3516            assert!(
3517                !output.contains("api_plain_requests_total{env="),
3518                "plain should not have env attribute: {output}"
3519            );
3520            assert!(
3521                !output.contains("api_plain_requests_total{region="),
3522                "plain should not have region attribute: {output}"
3523            );
3524        });
3525    }
3526
3527    #[rstest]
3528    #[case::deterministic(deterministic::Runner::default())]
3529    #[case::tokio(tokio::Runner::default())]
3530    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3531    fn test_register_and_encode<R: Runner>(#[case] runner: R)
3532    where
3533        R::Context: Metrics,
3534    {
3535        runner.start(|context| async move {
3536            let counter =
3537                context
3538                    .child("engine")
3539                    .register("votes", "vote count", Counter::<u64>::default());
3540            counter.inc();
3541
3542            let buffer = context.encode();
3543            assert!(
3544                buffer.contains("engine_votes_total 1"),
3545                "registered metric should appear in encode: {buffer}"
3546            );
3547        });
3548    }
3549
3550    #[rstest]
3551    #[case::deterministic(deterministic::Runner::default())]
3552    #[case::tokio(tokio::Runner::default())]
3553    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3554    fn test_register_drop_removes_metrics<R: Runner>(#[case] runner: R)
3555    where
3556        R::Context: Metrics,
3557    {
3558        runner.start(|context| async move {
3559            let permanent = context.child("permanent").register(
3560                "counter",
3561                "permanent counter",
3562                Counter::<u64>::default(),
3563            );
3564            permanent.inc();
3565
3566            let counter =
3567                context
3568                    .child("engine")
3569                    .register("votes", "vote count", Counter::<u64>::default());
3570            counter.inc();
3571
3572            let buffer = context.encode();
3573            assert!(buffer.contains("permanent_counter_total 1"));
3574            assert!(buffer.contains("engine_votes_total 1"));
3575
3576            drop(counter);
3577
3578            let buffer = context.encode();
3579            assert!(
3580                buffer.contains("permanent_counter_total 1"),
3581                "other registered metrics should survive handle drop: {buffer}"
3582            );
3583            assert!(
3584                !buffer.contains("engine_votes"),
3585                "metric should be removed after handle drop: {buffer}"
3586            );
3587        });
3588    }
3589
3590    #[rstest]
3591    #[case::deterministic(deterministic::Runner::default())]
3592    #[case::tokio(tokio::Runner::default())]
3593    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3594    fn test_register_with_attributes<R: Runner>(#[case] runner: R)
3595    where
3596        R::Context: Metrics,
3597    {
3598        runner.start(|context| async move {
3599            let epoch1 = context.child("engine").with_attribute("epoch", 1).register(
3600                "votes",
3601                "vote count",
3602                Counter::<u64>::default(),
3603            );
3604            epoch1.inc();
3605
3606            let epoch2 = context.child("engine").with_attribute("epoch", 2).register(
3607                "votes",
3608                "vote count",
3609                Counter::<u64>::default(),
3610            );
3611            epoch2.inc();
3612            epoch2.inc();
3613
3614            let buffer = context.encode();
3615            assert!(buffer.contains("engine_votes_total{epoch=\"1\"} 1"));
3616            assert!(buffer.contains("engine_votes_total{epoch=\"2\"} 2"));
3617
3618            assert_eq!(
3619                buffer.matches("# HELP engine_votes").count(),
3620                1,
3621                "HELP should appear once: {buffer}"
3622            );
3623            assert_eq!(
3624                buffer.matches("# TYPE engine_votes").count(),
3625                1,
3626                "TYPE should appear once: {buffer}"
3627            );
3628
3629            drop(epoch1);
3630            let buffer = context.encode();
3631            assert!(
3632                !buffer.contains("epoch=\"1\""),
3633                "epoch 1 should be gone: {buffer}"
3634            );
3635            assert!(buffer.contains("engine_votes_total{epoch=\"2\"} 2"));
3636
3637            drop(epoch2);
3638            let buffer = context.encode();
3639            assert!(
3640                !buffer.contains("engine_votes"),
3641                "all epoch metrics should be gone: {buffer}"
3642            );
3643        });
3644    }
3645
3646    #[rstest]
3647    #[case::deterministic(deterministic::Runner::default())]
3648    #[case::tokio(tokio::Runner::default())]
3649    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3650    fn test_reregister_after_drop<R: Runner>(#[case] runner: R)
3651    where
3652        R::Context: Metrics,
3653    {
3654        runner.start(|context| async move {
3655            let votes = context.child("engine").with_attribute("epoch", 1).register(
3656                "votes",
3657                "vote count",
3658                Counter::<u64>::default(),
3659            );
3660            drop(votes);
3661
3662            let replacement = context.child("engine").with_attribute("epoch", 1).register(
3663                "votes",
3664                "vote count",
3665                Counter::<u64>::default(),
3666            );
3667            drop(replacement);
3668        });
3669    }
3670
3671    #[rstest]
3672    #[case::deterministic(deterministic::Runner::default())]
3673    #[case::tokio(tokio::Runner::default())]
3674    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3675    fn test_register_clone_keeps_metric_alive<R: Runner>(#[case] runner: R)
3676    where
3677        R::Context: Metrics,
3678    {
3679        runner.start(|context| async move {
3680            let registered =
3681                context
3682                    .child("engine")
3683                    .register("votes", "vote count", Counter::<u64>::default());
3684            registered.inc();
3685            let clone = registered.clone();
3686
3687            let buffer = context.encode();
3688            assert!(
3689                buffer.contains("engine_votes_total 1"),
3690                "metric should remain registered while any handle exists: {buffer}"
3691            );
3692
3693            drop(registered);
3694            let buffer = context.encode();
3695            assert!(
3696                buffer.contains("engine_votes_total 1"),
3697                "metric should survive while clone is retained: {buffer}"
3698            );
3699
3700            drop(clone);
3701            let buffer = context.encode();
3702            assert!(
3703                !buffer.contains("engine_votes"),
3704                "metric should be removed when all handle clones are dropped: {buffer}"
3705            );
3706        });
3707    }
3708
3709    #[rstest]
3710    #[case::deterministic(deterministic::Runner::default())]
3711    #[case::tokio(tokio::Runner::default())]
3712    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3713    fn test_encode_single_eof<R: Runner>(#[case] runner: R)
3714    where
3715        R::Context: Metrics,
3716    {
3717        runner.start(|context| async move {
3718            let root_counter = context.register("root", "root metric", Counter::<u64>::default());
3719            root_counter.inc();
3720
3721            let child =
3722                context
3723                    .child("engine")
3724                    .register("ops", "child metric", Counter::<u64>::default());
3725            child.inc();
3726
3727            let buffer = context.encode();
3728            assert!(
3729                buffer.contains("root_total 1"),
3730                "root metric missing: {buffer}"
3731            );
3732            assert!(
3733                buffer.contains("engine_ops_total 1"),
3734                "child metric missing: {buffer}"
3735            );
3736            assert_eq!(
3737                buffer.matches("# EOF").count(),
3738                1,
3739                "expected exactly one EOF marker: {buffer}"
3740            );
3741            assert!(
3742                buffer.ends_with("# EOF\n"),
3743                "EOF must be the last line: {buffer}"
3744            );
3745        });
3746    }
3747
3748    #[rstest]
3749    #[case::deterministic(deterministic::Runner::default())]
3750    #[case::tokio(tokio::Runner::default())]
3751    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3752    fn test_family_with_attributes<R: Runner>(#[case] runner: R)
3753    where
3754        R::Context: Metrics,
3755    {
3756        #[derive(Clone, Debug, Hash, PartialEq, Eq)]
3757        struct Peer {
3758            name: String,
3759        }
3760        impl EncodeLabelSet for Peer {
3761            fn encode(&self, encoder: &mut LabelSetEncoder<'_>) -> Result<(), std::fmt::Error> {
3762                let mut label = encoder.encode_label();
3763                let mut key = label.encode_label_key()?;
3764                EncodeLabelKey::encode(&"peer", &mut key)?;
3765                let mut value = key.encode_label_value()?;
3766                EncodeLabelValue::encode(&self.name.as_str(), &mut value)?;
3767                value.finish()
3768            }
3769        }
3770
3771        runner.start(|context| async move {
3772            let family = context
3773                .child("batcher")
3774                .with_attribute("epoch", 1)
3775                .register(
3776                    "votes",
3777                    "votes per peer",
3778                    Family::<Peer, Counter>::default(),
3779                );
3780            family
3781                .get_or_create(&Peer {
3782                    name: "alice".into(),
3783                })
3784                .inc();
3785            family.get_or_create(&Peer { name: "bob".into() }).inc();
3786
3787            let buffer = context.encode();
3788            assert!(
3789                buffer.contains("batcher_votes_total{epoch=\"1\",peer=\"alice\"} 1"),
3790                "family with attributes should combine labels: {buffer}"
3791            );
3792            assert!(
3793                buffer.contains("batcher_votes_total{epoch=\"1\",peer=\"bob\"} 1"),
3794                "family with attributes should combine labels: {buffer}"
3795            );
3796
3797            drop(family);
3798            let buffer = context.encode();
3799            assert!(
3800                !buffer.contains("batcher_votes"),
3801                "family metrics should be removed: {buffer}"
3802            );
3803        });
3804    }
3805
3806    #[rstest]
3807    #[case::deterministic(deterministic::Runner::default())]
3808    #[case::tokio(tokio::Runner::default())]
3809    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3810    fn test_strategy<R: Runner>(#[case] runner: R)
3811    where
3812        R::Context: Strategizer + Metrics,
3813    {
3814        runner.start(|context| async move {
3815            // Create a strategy with a parallelism of 4.
3816            let strategy = context.child("pool").strategy(NZUsize!(4));
3817
3818            // Use the strategy to sum a vector of numbers.
3819            let sum = strategy.fold(0..10000, || 0i32, |acc, n| acc + n, |a, b| a + b);
3820            assert_eq!(sum, 10000 * 9999 / 2);
3821        });
3822    }
3823
3824    #[rstest]
3825    #[case::deterministic(deterministic::Runner::default())]
3826    #[case::tokio(tokio::Runner::default())]
3827    #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3828    fn test_nested_strategy_runs_inline<R: Runner>(#[case] runner: R)
3829    where
3830        R::Context: Strategizer + Metrics,
3831    {
3832        runner.start(|context| async move {
3833            let strategy = context.child("pool").strategy(NZUsize!(1));
3834
3835            let output = strategy
3836                .spawn(2, |strategy| strategy.map_collect_vec(0..2, |i| i + 1))
3837                .await;
3838
3839            assert_eq!(output, vec![1, 2]);
3840        });
3841    }
3842}