Skip to main content

commonware_runtime/tokio/
runtime.rs

1#[cfg(feature = "external")]
2use crate::Pacer;
3#[cfg(not(feature = "iouring-network"))]
4use crate::network::tokio::{Config as TokioNetworkConfig, Network as TokioNetwork};
5#[cfg(feature = "iouring-storage")]
6use crate::storage::iouring::{Config as IoUringConfig, Storage as IoUringStorage};
7#[cfg(not(feature = "iouring-storage"))]
8use crate::storage::tokio::{Config as TokioStorageConfig, Storage as TokioStorage};
9use crate::{
10    BlobLayout, BlobVersion, BufferPool, BufferPoolConfig, Clock, Error, Execution, Handle,
11    METRICS_PREFIX, Name, SinkOf, StreamOf, child_label,
12    network::metered::Network as MeteredNetwork,
13    prefixed_name,
14    process::metered::Metrics as MeteredProcess,
15    signal::Signal,
16    storage::metered::Storage as MeteredStorage,
17    telemetry::metrics::{
18        CounterFamily, GaugeFamily, Metric, Register, Registered, Registry, add_attribute, raw,
19        task::Label, validate_label,
20    },
21    utils::{self, Panicker, signal::Stopper, supervision::Tree},
22};
23#[cfg(feature = "iouring-network")]
24use crate::{
25    iouring,
26    network::iouring::{Config as IoUringNetworkConfig, Network as IoUringNetwork},
27};
28use commonware_macros::{select, stability};
29#[stability(BETA)]
30use commonware_parallel::Rayon;
31use commonware_utils::{NZUsize, sync::Mutex, sys_rng};
32use governor::clock::{Clock as GClock, ReasonablyRealtime};
33use rand_core::{Rng, TryCryptoRng, TryRng};
34#[stability(BETA)]
35use rayon::ThreadPoolBuilder;
36use std::{
37    convert::Infallible,
38    env,
39    future::Future,
40    net::{IpAddr, SocketAddr},
41    num::NonZeroUsize,
42    ops::RangeInclusive,
43    panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
44    path::PathBuf,
45    sync::Arc,
46    time::{Duration, SystemTime},
47};
48use tokio::{
49    runtime::{Builder, Handle as RuntimeHandle},
50    sync::Notify,
51};
52
53#[cfg(feature = "iouring-network")]
54cfg_if::cfg_if! {
55    if #[cfg(test)] {
56        // Use a smaller ring in tests to reduce `io_uring_setup` failures
57        // under parallel test load due to mlock/resource limits.
58        const IOURING_NETWORK_SIZE: u32 = 128;
59    } else {
60        const IOURING_NETWORK_SIZE: u32 = 1024;
61    }
62}
63
64#[derive(Debug)]
65struct Metrics {
66    tasks_spawned: CounterFamily<Label>,
67    tasks_running: GaugeFamily<Label>,
68}
69
70impl Metrics {
71    pub fn init(registry: &mut impl Register) -> Self {
72        Self {
73            tasks_spawned: registry.register(
74                "tasks_spawned",
75                "Total number of tasks spawned",
76                raw::Family::default(),
77            ),
78            tasks_running: registry.register(
79                "tasks_running",
80                "Number of tasks currently running",
81                raw::Family::default(),
82            ),
83        }
84    }
85}
86
87#[derive(Clone, Debug)]
88pub struct NetworkConfig {
89    /// If Some, explicitly sets TCP_NODELAY on the socket.
90    /// Otherwise uses system default.
91    ///
92    /// Defaults to `Some(true)`.
93    tcp_nodelay: Option<bool>,
94
95    /// Whether to set `SO_LINGER` to zero on the socket.
96    ///
97    /// When enabled, causes an immediate RST on close, avoiding
98    /// `TIME_WAIT` state. This is useful in adversarial environments to
99    /// reclaim socket resources immediately when closing connections to
100    /// misbehaving peers.
101    ///
102    /// Defaults to `true`.
103    zero_linger: bool,
104
105    /// Timeout for establishing an outbound TCP connection.
106    ///
107    /// Defaults to 10 seconds.
108    connect_timeout: Duration,
109
110    /// Read/write timeout for network operations.
111    ///
112    /// Bounds the full `Sink::send` and `Stream::recv` calls rather than each
113    /// individual socket syscall. Larger batched writes may therefore require a
114    /// larger timeout.
115    ///
116    /// Defaults to 60 seconds.
117    read_write_timeout: Duration,
118}
119
120impl Default for NetworkConfig {
121    fn default() -> Self {
122        Self {
123            tcp_nodelay: Some(true),
124            zero_linger: true,
125            connect_timeout: Duration::from_secs(10),
126            read_write_timeout: Duration::from_secs(60),
127        }
128    }
129}
130
131/// Configuration for the `tokio` runtime.
132#[derive(Clone)]
133pub struct Config {
134    /// Number of threads to use for handling async tasks.
135    ///
136    /// Worker threads are always active (waiting for work).
137    ///
138    /// Tokio sets the default value to the number of logical CPUs.
139    worker_threads: usize,
140
141    /// Number of scheduler ticks between global queue polls.
142    ///
143    /// When unset, Tokio uses its default behavior for the multi-thread
144    /// scheduler. Smaller values reduce the delay before tasks woken from
145    /// outside a worker, such as io_uring completion notifications, are polled
146    /// from the global queue again.
147    global_queue_interval: Option<u32>,
148
149    /// Maximum number of threads to use for blocking tasks.
150    ///
151    /// Unlike worker threads, blocking threads are created as needed and
152    /// exit if left idle for too long.
153    ///
154    /// Tokio sets the default value to 512 to avoid hanging on lower-level
155    /// operations that require blocking (like `fs` and writing to `Stdout`).
156    max_blocking_threads: usize,
157
158    /// Stack size to use for runtime-owned threads.
159    ///
160    /// Defaults to the system stack size when the current platform exposes it,
161    /// and otherwise falls back to Rust's default spawned-thread stack size.
162    ///
163    /// See [utils::thread::system_thread_stack_size].
164    thread_stack_size: usize,
165
166    /// Whether or not to catch panics.
167    catch_panics: bool,
168
169    /// Base directory for all storage operations, created at start if missing
170    /// and held for the run.
171    storage_directory: PathBuf,
172
173    /// Blob layouts accepted by storage.
174    ///
175    /// Defaults to [BlobLayout::ALL] and must be non-empty. New blobs use the latest layout in
176    /// the range. Existing blobs outside it fail to open with [Error::BlobLayoutMismatch], so
177    /// restrict the range to what a rollback target can read before the upgraded binary first
178    /// opens storage.
179    storage_blob_layouts: RangeInclusive<BlobLayout>,
180
181    /// Network configuration.
182    network_cfg: NetworkConfig,
183
184    /// Explicit buffer pool configuration for network I/O, if provided.
185    network_buffer_pool_cfg: Option<BufferPoolConfig>,
186
187    /// Explicit buffer pool configuration for storage I/O, if provided.
188    storage_buffer_pool_cfg: Option<BufferPoolConfig>,
189}
190
191impl Config {
192    /// Returns a new [Config] with default values.
193    pub fn new() -> Self {
194        let rng = sys_rng().next_u64();
195        let storage_directory = env::temp_dir().join(format!("commonware_tokio_runtime_{rng}"));
196        Self {
197            worker_threads: 2,
198            global_queue_interval: None,
199            max_blocking_threads: 512,
200            thread_stack_size: utils::thread::system_thread_stack_size(),
201            catch_panics: false,
202            storage_directory,
203            storage_blob_layouts: BlobLayout::ALL,
204            network_cfg: NetworkConfig::default(),
205            network_buffer_pool_cfg: None,
206            storage_buffer_pool_cfg: None,
207        }
208    }
209
210    // Setters
211    /// See [Config]
212    pub const fn with_worker_threads(mut self, n: usize) -> Self {
213        self.worker_threads = n;
214        self
215    }
216    /// See [Config]
217    pub const fn with_global_queue_interval(mut self, n: u32) -> Self {
218        self.global_queue_interval = Some(n);
219        self
220    }
221    /// See [Config]
222    pub const fn with_max_blocking_threads(mut self, n: usize) -> Self {
223        self.max_blocking_threads = n;
224        self
225    }
226    /// See [Config]
227    pub const fn with_thread_stack_size(mut self, n: usize) -> Self {
228        self.thread_stack_size = n;
229        self
230    }
231    /// See [Config]
232    pub const fn with_catch_panics(mut self, b: bool) -> Self {
233        self.catch_panics = b;
234        self
235    }
236    /// See [Config]
237    pub const fn with_connect_timeout(mut self, timeout: Duration) -> Self {
238        self.network_cfg.connect_timeout = timeout;
239        self
240    }
241    /// See [Config]
242    pub const fn with_read_write_timeout(mut self, d: Duration) -> Self {
243        self.network_cfg.read_write_timeout = d;
244        self
245    }
246    /// See [Config]
247    pub const fn with_tcp_nodelay(mut self, n: Option<bool>) -> Self {
248        self.network_cfg.tcp_nodelay = n;
249        self
250    }
251    /// See [Config]
252    pub const fn with_zero_linger(mut self, l: bool) -> Self {
253        self.network_cfg.zero_linger = l;
254        self
255    }
256    /// See [Config]
257    pub fn with_storage_directory(mut self, p: impl Into<PathBuf>) -> Self {
258        self.storage_directory = p.into();
259        self
260    }
261    /// See [Config]
262    ///
263    /// # Panics
264    ///
265    /// Panics if `layouts` is empty.
266    pub fn with_storage_blob_layouts(mut self, layouts: RangeInclusive<BlobLayout>) -> Self {
267        assert!(
268            !layouts.is_empty(),
269            "storage blob layouts must be non-empty"
270        );
271        self.storage_blob_layouts = layouts;
272        self
273    }
274    /// See [Config]
275    pub fn with_network_buffer_pool_config(mut self, cfg: BufferPoolConfig) -> Self {
276        self.network_buffer_pool_cfg = Some(cfg);
277        self
278    }
279    /// See [Config]
280    pub fn with_storage_buffer_pool_config(mut self, cfg: BufferPoolConfig) -> Self {
281        self.storage_buffer_pool_cfg = Some(cfg);
282        self
283    }
284
285    // Getters
286    /// See [Config]
287    pub const fn worker_threads(&self) -> usize {
288        self.worker_threads
289    }
290    /// See [Config]
291    pub const fn global_queue_interval(&self) -> Option<u32> {
292        self.global_queue_interval
293    }
294    /// See [Config]
295    pub const fn max_blocking_threads(&self) -> usize {
296        self.max_blocking_threads
297    }
298    /// See [Config]
299    pub const fn thread_stack_size(&self) -> usize {
300        self.thread_stack_size
301    }
302    /// See [Config]
303    pub const fn catch_panics(&self) -> bool {
304        self.catch_panics
305    }
306    /// See [Config]
307    pub const fn connect_timeout(&self) -> Duration {
308        self.network_cfg.connect_timeout
309    }
310    /// See [Config]
311    pub const fn read_write_timeout(&self) -> Duration {
312        self.network_cfg.read_write_timeout
313    }
314    /// See [Config]
315    pub const fn tcp_nodelay(&self) -> Option<bool> {
316        self.network_cfg.tcp_nodelay
317    }
318    /// See [Config]
319    pub const fn zero_linger(&self) -> bool {
320        self.network_cfg.zero_linger
321    }
322    /// See [Config]
323    pub const fn storage_directory(&self) -> &PathBuf {
324        &self.storage_directory
325    }
326    /// See [Config]
327    pub const fn storage_blob_layouts(&self) -> &RangeInclusive<BlobLayout> {
328        &self.storage_blob_layouts
329    }
330
331    /// Returns the network buffer pool config, deriving pool parallelism from
332    /// `worker_threads` if not explicitly configured.
333    fn resolved_network_buffer_pool_config(&self) -> BufferPoolConfig {
334        self.network_buffer_pool_cfg.clone().unwrap_or_else(|| {
335            BufferPoolConfig::for_network().with_parallelism(NZUsize!(self.worker_threads))
336        })
337    }
338
339    /// Returns the storage buffer pool config, deriving pool parallelism from
340    /// `worker_threads` if not explicitly configured.
341    fn resolved_storage_buffer_pool_config(&self) -> BufferPoolConfig {
342        self.storage_buffer_pool_cfg.clone().unwrap_or_else(|| {
343            BufferPoolConfig::for_storage().with_parallelism(NZUsize!(self.worker_threads))
344        })
345    }
346}
347
348impl Default for Config {
349    fn default() -> Self {
350        Self::new()
351    }
352}
353
354/// Runtime based on [Tokio](https://tokio.rs).
355pub struct Executor {
356    registry: Registry,
357    metrics: Arc<Metrics>,
358    runtime: RuntimeHandle,
359    tasks: Arc<TaskTracker>,
360    shutdown: Mutex<Stopper>,
361    panicker: Panicker,
362    thread_stack_size: usize,
363}
364
365/// Closes task admission and tracks wrappers through user-future drop and descendant cleanup.
366#[derive(Default)]
367struct TaskTracker {
368    state: Mutex<TaskTrackerState>,
369    idle: Notify,
370}
371
372#[derive(Default)]
373struct TaskTrackerState {
374    active: usize,
375    closed: bool,
376}
377
378impl TaskTracker {
379    fn admit(self: &Arc<Self>) -> Option<TaskGuard> {
380        let mut state = self.state.lock();
381        if state.closed {
382            return None;
383        }
384        state.active = state.active.checked_add(1).expect("active task overflow");
385        Some(TaskGuard(Arc::clone(self)))
386    }
387
388    fn close(&self) {
389        self.state.lock().closed = true;
390    }
391
392    async fn wait(&self) {
393        loop {
394            // Subscribe before checking so the final task cannot notify between the check and wait.
395            let idle = self.idle.notified();
396            if self.state.lock().active == 0 {
397                return;
398            }
399            idle.await;
400        }
401    }
402}
403
404struct TaskGuard(Arc<TaskTracker>);
405
406impl Drop for TaskGuard {
407    fn drop(&mut self) {
408        let mut state = self.0.state.lock();
409        state.active = state.active.checked_sub(1).expect("active task underflow");
410        if state.active == 0 {
411            drop(state);
412            self.0.idle.notify_one();
413        }
414    }
415}
416
417/// Implementation of [crate::Runner] for the `tokio` runtime.
418pub struct Runner {
419    cfg: Config,
420}
421
422impl Default for Runner {
423    fn default() -> Self {
424        Self::new(Config::default())
425    }
426}
427
428impl Runner {
429    /// Initialize a new `tokio` runtime with the given number of threads.
430    pub const fn new(cfg: Config) -> Self {
431        Self { cfg }
432    }
433}
434
435impl crate::Runner for Runner {
436    type Context = Context;
437
438    fn start<F, Fut>(self, f: F) -> Fut::Output
439    where
440        F: FnOnce(Self::Context) -> Fut,
441        Fut: Future,
442    {
443        // Create a new registry
444        let mut registry = Registry::new();
445        let mut runtime_registry = registry.sub_registry(METRICS_PREFIX);
446
447        // Initialize runtime
448        let metrics = Arc::new(Metrics::init(&mut runtime_registry));
449        let mut builder = Builder::new_multi_thread();
450        builder
451            .worker_threads(self.cfg.worker_threads)
452            .max_blocking_threads(self.cfg.max_blocking_threads)
453            .thread_stack_size(self.cfg.thread_stack_size)
454            .enable_all();
455        if let Some(global_queue_interval) = self.cfg.global_queue_interval {
456            builder.global_queue_interval(global_queue_interval);
457        }
458        let runtime = builder.build().expect("failed to create Tokio runtime");
459
460        // Initialize panicker
461        let (panicker, panicked) = Panicker::new(self.cfg.catch_panics);
462
463        // Collect process metrics.
464        //
465        // We prefer to collect process metrics outside of `Context` because
466        // we are using `runtime_registry` rather than the one provided by `Context`.
467        let process = MeteredProcess::init(&mut runtime_registry);
468        runtime.spawn(process.collect(tokio::time::sleep));
469
470        // Initialize buffer pools
471        let network_buffer_pool = BufferPool::new(
472            self.cfg.resolved_network_buffer_pool_config(),
473            &mut runtime_registry.sub_registry("network_buffer_pool"),
474        );
475        let storage_buffer_pool = BufferPool::new(
476            self.cfg.resolved_storage_buffer_pool_config(),
477            &mut runtime_registry.sub_registry("storage_buffer_pool"),
478        );
479
480        // Initialize storage
481        cfg_if::cfg_if! {
482            if #[cfg(feature = "iouring-storage")] {
483                let mut iouring_registry = runtime_registry.sub_registry("iouring_storage");
484                let storage = MeteredStorage::new(
485                    IoUringStorage::start(
486                        IoUringConfig {
487                            storage_directory: self.cfg.storage_directory.clone(),
488                            blob_layouts: self.cfg.storage_blob_layouts.clone(),
489                            iouring_config: Default::default(),
490                            thread_stack_size: self.cfg.thread_stack_size,
491                        },
492                        &mut iouring_registry,
493                        storage_buffer_pool.clone(),
494                    ),
495                    &mut runtime_registry,
496                );
497            } else {
498                let storage = MeteredStorage::new(
499                    TokioStorage::new(
500                        TokioStorageConfig::new(
501                            self.cfg.storage_directory.clone(),
502                            self.cfg.storage_blob_layouts.clone(),
503                        ),
504                        storage_buffer_pool.clone(),
505                    ),
506                    &mut runtime_registry,
507                );
508            }
509        }
510
511        // Make any storage a prior process left in the page cache crash-durable before we open it,
512        // so the data read during init is durable. This runs under the hold, after any straggling
513        // writes from a previous run have landed, so the flush covers them too.
514        if let Err(e) = crate::storage::sync(&self.cfg.storage_directory) {
515            panic!(
516                "failed to sync storage filesystem at startup ({}): {e}",
517                self.cfg.storage_directory.display()
518            );
519        }
520
521        // Initialize network
522        cfg_if::cfg_if! {
523            if #[cfg(feature = "iouring-network")] {
524                let mut iouring_registry = runtime_registry.sub_registry("iouring_network");
525                let config = IoUringNetworkConfig {
526                    tcp_nodelay: self.cfg.network_cfg.tcp_nodelay,
527                    zero_linger: self.cfg.network_cfg.zero_linger,
528                    connect_timeout: self.cfg.network_cfg.connect_timeout,
529                    read_write_timeout: self.cfg.network_cfg.read_write_timeout,
530                    iouring_config: iouring::Config {
531                        // TODO (#1045): make `IOURING_NETWORK_SIZE` configurable
532                        size: IOURING_NETWORK_SIZE,
533                        max_request_timeout: self.cfg.network_cfg.read_write_timeout,
534                        shutdown_timeout: Some(self.cfg.network_cfg.read_write_timeout),
535                        ..Default::default()
536                    },
537                    thread_stack_size: self.cfg.thread_stack_size,
538                    ..Default::default()
539                };
540                let network = MeteredNetwork::new(
541                    IoUringNetwork::start(
542                        config,
543                        &mut iouring_registry,
544                        network_buffer_pool.clone(),
545                    )
546                    .unwrap(),
547                    &mut runtime_registry,
548                );
549            } else {
550                let config = TokioNetworkConfig::default()
551                    .with_connect_timeout(self.cfg.network_cfg.connect_timeout)
552                    .with_read_timeout(self.cfg.network_cfg.read_write_timeout)
553                    .with_write_timeout(self.cfg.network_cfg.read_write_timeout)
554                    .with_tcp_nodelay(self.cfg.network_cfg.tcp_nodelay)
555                    .with_zero_linger(self.cfg.network_cfg.zero_linger);
556                let network = MeteredNetwork::new(
557                    TokioNetwork::new(config, network_buffer_pool.clone()),
558                    &mut runtime_registry,
559                );
560            }
561        }
562
563        // Initialize executor
564        let executor = Arc::new(Executor {
565            registry,
566            metrics,
567            runtime: runtime.handle().clone(),
568            tasks: Arc::new(TaskTracker::default()),
569            shutdown: Mutex::new(Stopper::default()),
570            panicker,
571            thread_stack_size: self.cfg.thread_stack_size,
572        });
573
574        // Get metrics
575        let label = Label::root();
576        executor.metrics.tasks_spawned.get_or_create(&label).inc();
577        let gauge = executor.metrics.tasks_running.get_or_create(&label).clone();
578
579        // Run the future
580        let tree = Tree::root();
581        let context = Context {
582            storage,
583            name: label.name(),
584            attributes: Vec::new(),
585            executor: executor.clone(),
586            network,
587            network_buffer_pool,
588            storage_buffer_pool,
589            tree: Arc::clone(&tree),
590            execution: Execution::default(),
591        };
592        let output = catch_unwind(AssertUnwindSafe(|| {
593            runtime.block_on(panicked.interrupt(f(context)))
594        }));
595        executor.tasks.close();
596        tree.abort();
597        runtime.block_on(executor.tasks.wait());
598        gauge.dec();
599
600        match output {
601            Ok(output) => output,
602            Err(panic) => resume_unwind(panic),
603        }
604    }
605}
606
607cfg_if::cfg_if! {
608    if #[cfg(feature = "iouring-storage")] {
609        type Storage = MeteredStorage<IoUringStorage>;
610    } else {
611        type Storage = MeteredStorage<TokioStorage>;
612    }
613}
614
615cfg_if::cfg_if! {
616    if #[cfg(feature = "iouring-network")] {
617        type Network = MeteredNetwork<IoUringNetwork>;
618    } else {
619        type Network = MeteredNetwork<TokioNetwork>;
620    }
621}
622
623/// Implementation of [crate::Spawner], [crate::Clock],
624/// [crate::Network], and [crate::Storage] for the `tokio`
625/// runtime.
626pub struct Context {
627    name: String,
628    attributes: Vec<(String, String)>,
629    executor: Arc<Executor>,
630    storage: Storage,
631    network: Network,
632    network_buffer_pool: BufferPool,
633    storage_buffer_pool: BufferPool,
634    tree: Arc<Tree>,
635    execution: Execution,
636}
637
638impl Context {
639    /// Access the [Metrics] of the runtime.
640    fn metrics(&self) -> &Metrics {
641        &self.executor.metrics
642    }
643}
644
645impl crate::Spawner for Context {
646    fn dedicated(mut self) -> Self {
647        self.execution = Execution::Dedicated;
648        self
649    }
650
651    fn shared(mut self, blocking: bool) -> Self {
652        self.execution = Execution::Shared(blocking);
653        self
654    }
655
656    fn spawn<F, Fut, T>(mut self, f: F) -> Handle<T>
657    where
658        F: FnOnce(Self) -> Fut + Send + 'static,
659        Fut: Future<Output = T> + Send + 'static,
660        T: Send + 'static,
661    {
662        // Get metrics
663        let (_, metric) = spawn_metrics!(self);
664
665        // Track supervision before resetting configuration
666        let parent = Arc::clone(&self.tree);
667        let past = self.execution;
668        self.execution = Execution::default();
669        let (child, aborted) = Tree::child(&parent);
670        if aborted {
671            return Handle::closed(metric);
672        }
673        self.tree = child;
674
675        // Spawn the task
676        let executor = self.executor.clone();
677        let Some(task_guard) = executor.tasks.admit() else {
678            return Handle::closed(metric);
679        };
680        let future = f(self);
681        let (f, handle) = Handle::init(
682            future,
683            metric,
684            executor.panicker.clone(),
685            Arc::clone(&parent),
686        );
687        let f = async move {
688            let _task_guard = task_guard;
689            f.await;
690        };
691
692        if matches!(past, Execution::Dedicated) {
693            utils::thread::spawn(executor.thread_stack_size, {
694                // Ensure the task can access the tokio runtime
695                let handle = executor.runtime.clone();
696                move || {
697                    handle.block_on(f);
698                }
699            });
700        } else if matches!(past, Execution::Shared(true)) {
701            executor.runtime.spawn_blocking({
702                // Ensure the task can access the tokio runtime
703                let handle = executor.runtime.clone();
704                move || {
705                    handle.block_on(f);
706                }
707            });
708        } else {
709            executor.runtime.spawn(f);
710        }
711
712        // Register the task on the parent
713        if let Some(aborter) = handle.aborter() {
714            parent.register(aborter);
715        }
716
717        handle
718    }
719
720    async fn stop(self, value: i32, timeout: Option<Duration>) -> Result<(), Error> {
721        let stop_resolved = {
722            let mut shutdown = self.executor.shutdown.lock();
723            shutdown.stop(value)
724        };
725
726        // Wait for all tasks to complete or the timeout to fire
727        let timeout_future = timeout.map_or_else(
728            || futures::future::Either::Right(futures::future::pending()),
729            |duration| futures::future::Either::Left(self.sleep(duration)),
730        );
731        select! {
732            result = stop_resolved => {
733                result.map_err(|_| Error::Closed)?;
734                Ok(())
735            },
736            _ = timeout_future => Err(Error::Timeout),
737        }
738    }
739
740    fn stopped(&self) -> Signal {
741        self.executor.shutdown.lock().stopped()
742    }
743}
744
745#[stability(BETA)]
746impl crate::Strategizer for Context {
747    fn strategy(&self, parallelism: NonZeroUsize) -> Rayon {
748        let pool = ThreadPoolBuilder::new()
749            .num_threads(parallelism.get())
750            .stack_size(self.executor.thread_stack_size)
751            .build()
752            .expect("failed to create Tokio Rayon thread pool");
753        Rayon::with_pool(Arc::new(pool))
754    }
755}
756
757impl crate::Supervisor for Context {
758    fn child(&self, label: &'static str) -> Self {
759        let (tree, _) = Tree::child(&self.tree);
760        Self {
761            name: child_label(&self.name, label),
762            attributes: self.attributes.clone(),
763            executor: self.executor.clone(),
764            storage: self.storage.clone(),
765            network: self.network.clone(),
766            network_buffer_pool: self.network_buffer_pool.clone(),
767            storage_buffer_pool: self.storage_buffer_pool.clone(),
768            tree,
769            execution: Execution::default(),
770        }
771    }
772
773    fn with_attribute(mut self, key: &'static str, value: impl std::fmt::Display) -> Self {
774        // Validate label format (must match [a-zA-Z][a-zA-Z0-9_]*)
775        validate_label(key);
776
777        // Add the attribute to the list of attributes
778        add_attribute(&mut self.attributes, key, value);
779        self
780    }
781
782    fn name(&self) -> Name {
783        Name {
784            label: self.name.clone(),
785            attributes: self.attributes.clone(),
786        }
787    }
788}
789
790impl crate::Metrics for Context {
791    fn register<N: Into<String>, H: Into<String>, M: Metric>(
792        &self,
793        name: N,
794        help: H,
795        metric: M,
796    ) -> Registered<M> {
797        let name = name.into();
798        let help = help.into();
799        let metric = Arc::new(metric);
800        self.executor.registry.register(
801            prefixed_name(&self.name, &name),
802            help,
803            self.attributes.clone(),
804            metric,
805        )
806    }
807
808    fn encode(&self) -> String {
809        self.executor.registry.encode()
810    }
811}
812
813impl Clock for Context {
814    fn current(&self) -> SystemTime {
815        SystemTime::now()
816    }
817
818    fn sleep(&self, duration: Duration) -> impl Future<Output = ()> + Send + 'static {
819        tokio::time::sleep(duration)
820    }
821
822    fn sleep_until(&self, deadline: SystemTime) -> impl Future<Output = ()> + Send + 'static {
823        let duration_until_deadline = deadline.duration_since(self.current()).unwrap_or_default();
824        tokio::time::sleep(duration_until_deadline)
825    }
826}
827
828#[cfg(feature = "external")]
829impl Pacer for Context {
830    fn pace<'a, F, T>(
831        &'a self,
832        _latency: Duration,
833        future: F,
834    ) -> impl Future<Output = T> + Send + 'a
835    where
836        F: Future<Output = T> + Send + 'a,
837        T: Send + 'a,
838    {
839        // Execute the future immediately
840        future
841    }
842}
843
844impl GClock for Context {
845    type Instant = SystemTime;
846
847    fn now(&self) -> Self::Instant {
848        self.current()
849    }
850}
851
852impl ReasonablyRealtime for Context {}
853
854impl crate::Network for Context {
855    type Listener = <Network as crate::Network>::Listener;
856
857    async fn bind(&self, socket: SocketAddr) -> Result<Self::Listener, Error> {
858        self.network.bind(socket).await
859    }
860
861    async fn dial(&self, socket: SocketAddr) -> Result<(SinkOf<Self>, StreamOf<Self>), Error> {
862        self.network.dial(socket).await
863    }
864}
865
866impl crate::Resolver for Context {
867    async fn resolve(&self, host: &str) -> Result<Vec<IpAddr>, Error> {
868        // Uses the host's DNS configuration (e.g. /etc/resolv.conf). This delegates to the
869        // system's libc resolver.
870        //
871        // The `:0` port is required by lookup_host's API but is not used
872        // for DNS resolution.
873        let addrs = tokio::net::lookup_host(format!("{host}:0"))
874            .await
875            .map_err(|e| Error::ResolveFailed(e.to_string()))?;
876        Ok(addrs.map(|addr| addr.ip()).collect())
877    }
878}
879
880impl TryRng for Context {
881    type Error = Infallible;
882
883    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
884        Ok(sys_rng().next_u32())
885    }
886
887    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
888        Ok(sys_rng().next_u64())
889    }
890
891    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
892        sys_rng().fill_bytes(dest);
893        Ok(())
894    }
895}
896
897impl TryCryptoRng for Context {}
898
899impl crate::Storage for Context {
900    type Blob = <Storage as crate::Storage>::Blob;
901
902    async fn open_versioned(
903        &self,
904        partition: &str,
905        name: &[u8],
906        versions: std::ops::RangeInclusive<BlobVersion>,
907    ) -> Result<(Self::Blob, u64, BlobVersion), Error> {
908        self.storage.open_versioned(partition, name, versions).await
909    }
910
911    async fn remove(&self, partition: &str, name: Option<&[u8]>) -> Result<(), Error> {
912        self.storage.remove(partition, name).await
913    }
914
915    async fn scan(&self, partition: &str) -> Result<Vec<Vec<u8>>, Error> {
916        self.storage.scan(partition).await
917    }
918}
919
920impl crate::BufferPooler for Context {
921    fn network_buffer_pool(&self) -> &BufferPool {
922        &self.network_buffer_pool
923    }
924
925    fn storage_buffer_pool(&self) -> &BufferPool {
926        &self.storage_buffer_pool
927    }
928}
929
930#[cfg(test)]
931#[allow(deprecated)]
932mod tests {
933    use super::*;
934    use crate::{
935        Blob as _, Metrics, Network, Resolver, Runner as _, Sink, Spawner as _, Storage as _,
936        Strategizer as _, Stream, Supervisor as _, telemetry::metrics::raw::Counter,
937        tokio::telemetry,
938    };
939    use bytes::Bytes;
940    use commonware_parallel::Strategy as _;
941    use std::{
942        self,
943        collections::HashMap,
944        net::{IpAddr, Ipv4Addr, Ipv6Addr},
945        str::FromStr,
946    };
947    use tracing::{Level, error};
948
949    struct TaskDropGate {
950        entered: std::sync::mpsc::Sender<()>,
951        release: std::sync::mpsc::Receiver<()>,
952    }
953
954    impl Drop for TaskDropGate {
955        fn drop(&mut self) {
956            let _ = self.entered.send(());
957            let _ = self.release.recv();
958        }
959    }
960
961    #[derive(Clone, Copy, Debug)]
962    enum RootExit {
963        Return,
964        FuturePanic,
965        ConstructorPanic,
966    }
967
968    fn spawn_drop_gated_task(
969        context: Context,
970        execution: Execution,
971        drop_gate: TaskDropGate,
972        ready: Option<commonware_utils::channel::oneshot::Sender<()>>,
973    ) {
974        let child = match execution {
975            Execution::Dedicated => context.dedicated(),
976            Execution::Shared(blocking) => context.shared(blocking),
977        };
978        child.spawn(move |context| async move {
979            let _context = context;
980            let _drop_gate = drop_gate;
981            if let Some(ready) = ready {
982                ready.send(()).unwrap();
983            }
984            futures::future::pending::<()>().await;
985        });
986    }
987
988    fn assert_runner_drains_spawned_task(execution: Execution, root_exit: RootExit) {
989        let cfg = Config::new();
990        let storage_directory = cfg.storage_directory().clone();
991        let (ready_tx, ready_rx) = commonware_utils::channel::oneshot::channel();
992        let (drop_entered_tx, drop_entered_rx) = std::sync::mpsc::channel();
993        let (drop_release_tx, drop_release_rx) = std::sync::mpsc::channel();
994        let (runner_done_tx, runner_done_rx) = std::sync::mpsc::channel();
995        let runner = std::thread::spawn(move || {
996            let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
997                let drop_gate = TaskDropGate {
998                    entered: drop_entered_tx,
999                    release: drop_release_rx,
1000                };
1001                match root_exit {
1002                    RootExit::ConstructorPanic => {
1003                        Runner::new(cfg).start(move |context| -> futures::future::Pending<()> {
1004                            spawn_drop_gated_task(context, execution, drop_gate, None);
1005                            panic!("root constructor panic after spawning child");
1006                        })
1007                    }
1008                    RootExit::Return | RootExit::FuturePanic => {
1009                        Runner::new(cfg).start(move |context| async move {
1010                            spawn_drop_gated_task(context, execution, drop_gate, Some(ready_tx));
1011                            ready_rx.await.unwrap();
1012                            assert!(
1013                                matches!(root_exit, RootExit::Return),
1014                                "root future panic after spawning child"
1015                            );
1016                        })
1017                    }
1018                }
1019            }));
1020            runner_done_tx.send(result.is_err()).unwrap();
1021        });
1022
1023        drop_entered_rx
1024            .recv_timeout(Duration::from_secs(5))
1025            .expect("spawned task was not canceled after the root returned");
1026        let early = runner_done_rx.recv_timeout(Duration::from_millis(250));
1027        let returned_before_cleanup = match early {
1028            Ok(panicked) => Some(panicked),
1029            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => None,
1030            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
1031                panic!("Runner::start exited without reporting its result")
1032            }
1033        };
1034        drop_release_tx.send(()).unwrap();
1035        let panicked = returned_before_cleanup.unwrap_or_else(|| {
1036            runner_done_rx
1037                .recv_timeout(Duration::from_secs(5))
1038                .expect("Runner::start did not return after task cleanup completed")
1039        });
1040        runner.join().unwrap();
1041        let _ = std::fs::remove_dir_all(storage_directory);
1042        assert!(
1043            returned_before_cleanup.is_none(),
1044            "Runner::start returned before {execution:?} task cleanup completed"
1045        );
1046        assert_eq!(panicked, !matches!(root_exit, RootExit::Return));
1047    }
1048
1049    fn run_with_returned_strategy(retain: bool) -> Option<Rayon> {
1050        let cfg = Config::new();
1051        let storage_directory = cfg.storage_directory().clone();
1052        let (strategy_tx, strategy_rx) = std::sync::mpsc::channel();
1053        let runner = std::thread::spawn(move || {
1054            let strategy = Runner::new(cfg).start(move |context| async move {
1055                let strategy = context.strategy(NZUsize!(2));
1056                strategy.spawn(1, |_| ()).await;
1057                retain.then_some(strategy)
1058            });
1059            strategy_tx.send(strategy).unwrap();
1060        });
1061
1062        let strategy = strategy_rx
1063            .recv_timeout(Duration::from_secs(5))
1064            .expect("Runner::start did not return after the strategy completed work");
1065        runner.join().unwrap();
1066        let _ = std::fs::remove_dir_all(storage_directory);
1067        strategy
1068    }
1069
1070    #[test]
1071    fn test_storage_blob_layout_restriction() {
1072        // The default policy accepts legacy V0 blobs while selecting V1 for new blobs.
1073        let cfg = Config::new();
1074        let storage_directory = cfg.storage_directory().clone();
1075        assert_eq!(cfg.storage_blob_layouts(), &BlobLayout::ALL);
1076
1077        let partition = "layout_restriction";
1078        let v0_name = b"v0";
1079        let partition_directory = storage_directory.join(partition);
1080        std::fs::create_dir_all(&partition_directory).unwrap();
1081        let v0_path = partition_directory.join(commonware_formatting::hex(v0_name));
1082        let v0_bytes = crate::storage::tests::v0_blob_bytes(0, b"payload");
1083        std::fs::write(&v0_path, &v0_bytes).unwrap();
1084
1085        Runner::new(cfg).start(|context| async move {
1086            let (blob, size) = context.open(partition, v0_name).await.unwrap();
1087            assert_eq!(size, 7);
1088            let payload = blob
1089                .read_at(0, 7, crate::ReadOptions::default())
1090                .await
1091                .unwrap();
1092            assert_eq!(payload.coalesce(), b"payload".as_slice());
1093        });
1094
1095        // A V1-only policy rejects V0 without modifying it and creates new blobs as V1.
1096        let cfg = Config::new()
1097            .with_storage_directory(storage_directory.clone())
1098            .with_storage_blob_layouts(BlobLayout::V1..=BlobLayout::V1);
1099        Runner::new(cfg).start(|context| async move {
1100            let result = context.open(partition, v0_name).await;
1101            assert!(matches!(
1102                result,
1103                Err(Error::BlobLayoutMismatch { expected, found })
1104                    if expected == (BlobLayout::V1..=BlobLayout::V1)
1105                        && found == BlobLayout::V0
1106            ));
1107
1108            context.open(partition, b"v1").await.unwrap();
1109        });
1110
1111        assert_eq!(std::fs::read(&v0_path).unwrap(), v0_bytes);
1112        let v1_path = partition_directory.join(commonware_formatting::hex(b"v1"));
1113        let v1 = std::fs::read(&v1_path).unwrap();
1114        assert_eq!(&v1[..4], &BlobLayout::V1.magic());
1115
1116        // A V0-only policy rejects the intact header-only V1 blob without healing it and
1117        // creates new blobs the rollback target can read and write.
1118        let cfg = Config::new()
1119            .with_storage_directory(storage_directory.clone())
1120            .with_storage_blob_layouts(BlobLayout::V0..=BlobLayout::V0);
1121        Runner::new(cfg).start(|context| async move {
1122            let result = context.open(partition, b"v1").await;
1123            assert!(matches!(
1124                result,
1125                Err(Error::BlobLayoutMismatch { expected, found })
1126                    if expected == (BlobLayout::V0..=BlobLayout::V0)
1127                        && found == BlobLayout::V1
1128            ));
1129
1130            let (blob, size) = context.open(partition, b"rollback").await.unwrap();
1131            assert_eq!(size, 0);
1132            blob.write_at(0, b"rollback!".as_slice(), crate::WriteOptions::SYNC)
1133                .await
1134                .unwrap();
1135            drop(blob);
1136            let (blob, size) = context.open(partition, b"rollback").await.unwrap();
1137            assert_eq!(size, 9);
1138            let payload = blob
1139                .read_at(0, 9, crate::ReadOptions::default())
1140                .await
1141                .unwrap();
1142            assert_eq!(payload.coalesce(), b"rollback!".as_slice());
1143        });
1144
1145        assert_eq!(std::fs::read(&v1_path).unwrap(), v1);
1146        let rollback_path = partition_directory.join(commonware_formatting::hex(b"rollback"));
1147        let rollback = std::fs::read(rollback_path).unwrap();
1148        assert_eq!(&rollback[..4], &BlobLayout::V0.magic());
1149        assert_eq!(&rollback[8..], b"rollback!");
1150        let _ = std::fs::remove_dir_all(storage_directory);
1151    }
1152
1153    #[test]
1154    #[should_panic(expected = "non-empty")]
1155    fn test_storage_blob_layout_restriction_rejects_empty_range() {
1156        let _ = Config::new().with_storage_blob_layouts(BlobLayout::V1..=BlobLayout::V0);
1157    }
1158
1159    #[test]
1160    fn test_worker_threads_updates_default_buffer_pool_parallelism() {
1161        let cfg = Config::new().with_worker_threads(8);
1162
1163        assert_eq!(cfg.worker_threads, 8);
1164        let network = cfg.resolved_network_buffer_pool_config();
1165        assert_eq!(network.parallelism(), NZUsize!(8));
1166        assert_eq!(
1167            network.thread_cache_config,
1168            BufferPoolConfig::for_network().thread_cache_config
1169        );
1170
1171        let storage = cfg.resolved_storage_buffer_pool_config();
1172        assert_eq!(storage.parallelism(), NZUsize!(8));
1173        assert_eq!(
1174            storage.thread_cache_config,
1175            BufferPoolConfig::for_storage().thread_cache_config
1176        );
1177    }
1178
1179    #[test]
1180    fn test_default_thread_stack_size_uses_system_default() {
1181        let cfg = Config::new();
1182        assert_eq!(
1183            cfg.thread_stack_size(),
1184            utils::thread::system_thread_stack_size()
1185        );
1186    }
1187
1188    #[test]
1189    fn test_runner_waits_for_spawned_task_cancellation() {
1190        for execution in [
1191            Execution::Shared(false),
1192            Execution::Shared(true),
1193            Execution::Dedicated,
1194        ] {
1195            for root_exit in [
1196                RootExit::Return,
1197                RootExit::FuturePanic,
1198                RootExit::ConstructorPanic,
1199            ] {
1200                assert_runner_drains_spawned_task(execution, root_exit);
1201            }
1202        }
1203    }
1204
1205    #[test]
1206    fn test_runner_start_waits_for_previous_run() {
1207        let cfg = Config::new();
1208        let storage_directory = cfg.storage_directory().clone();
1209
1210        // The first run keeps its context, and with it the storage directory,
1211        // until it is released.
1212        let (started, first_started) = std::sync::mpsc::channel();
1213        let (release, released) = futures::channel::oneshot::channel();
1214        let first_cfg = cfg.clone();
1215        let first = std::thread::spawn(move || {
1216            Runner::new(first_cfg).start(|context| async move {
1217                started.send(()).unwrap();
1218                released.await.unwrap();
1219                drop(context);
1220            });
1221        });
1222        first_started.recv_timeout(Duration::from_secs(10)).unwrap();
1223
1224        // A second run on the same directory cannot start until the first has
1225        // returned.
1226        let (started, second_started) = std::sync::mpsc::channel();
1227        let second = std::thread::spawn(move || {
1228            Runner::new(cfg).start(|_| async move {
1229                started.send(()).unwrap();
1230            });
1231        });
1232        match second_started.recv_timeout(Duration::from_millis(200)) {
1233            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
1234            other => panic!("second run started while the first held the directory: {other:?}"),
1235        }
1236        release.send(()).unwrap();
1237        first.join().unwrap();
1238        second_started
1239            .recv_timeout(Duration::from_secs(10))
1240            .expect("second run did not start after the first returned");
1241        second.join().unwrap();
1242        let _ = std::fs::remove_dir_all(storage_directory);
1243    }
1244
1245    #[test]
1246    fn test_runner_owns_runtime_when_context_escapes() {
1247        let cfg = Config::new();
1248        let storage_directory = cfg.storage_directory().clone();
1249        let (ready_tx, ready_rx) = commonware_utils::channel::oneshot::channel();
1250        let (drop_entered_tx, drop_entered_rx) = std::sync::mpsc::channel();
1251        let (drop_release_tx, drop_release_rx) = std::sync::mpsc::channel();
1252        let (runner_returned_tx, runner_returned_rx) = std::sync::mpsc::channel();
1253        let (context_release_tx, context_release_rx) = std::sync::mpsc::channel();
1254        let runner = std::thread::spawn(move || {
1255            let context = Runner::new(cfg).start(move |context| async move {
1256                context.executor.runtime.spawn(async move {
1257                    let _drop_gate = TaskDropGate {
1258                        entered: drop_entered_tx,
1259                        release: drop_release_rx,
1260                    };
1261                    ready_tx.send(()).unwrap();
1262                    futures::future::pending::<()>().await;
1263                });
1264                ready_rx.await.unwrap();
1265                context
1266            });
1267            runner_returned_tx.send(()).unwrap();
1268            context_release_rx.recv().unwrap();
1269            drop(context);
1270        });
1271
1272        let returned_early = runner_returned_rx
1273            .recv_timeout(Duration::from_millis(500))
1274            .is_ok();
1275        if returned_early {
1276            drop_release_tx.send(()).unwrap();
1277            context_release_tx.send(()).unwrap();
1278            drop_entered_rx
1279                .recv_timeout(Duration::from_secs(5))
1280                .expect("escaped Context did not retain the raw runtime task");
1281        } else {
1282            drop_entered_rx
1283                .recv_timeout(Duration::from_secs(5))
1284                .expect("Runner did not cancel its raw runtime task");
1285            drop_release_tx.send(()).unwrap();
1286            runner_returned_rx
1287                .recv_timeout(Duration::from_secs(5))
1288                .expect("Runner did not return after raw task cleanup");
1289            context_release_tx.send(()).unwrap();
1290        }
1291        runner.join().unwrap();
1292        let _ = std::fs::remove_dir_all(storage_directory);
1293        assert!(
1294            !returned_early,
1295            "a returned Context kept the Tokio runtime alive after Runner::start"
1296        );
1297    }
1298
1299    #[test]
1300    fn test_runner_returns_strategy_after_pool_work() {
1301        assert!(run_with_returned_strategy(false).is_none());
1302
1303        let strategy = run_with_returned_strategy(true).unwrap();
1304        assert_eq!(futures::executor::block_on(strategy.spawn(1, |_| 42)), 42);
1305    }
1306
1307    #[test]
1308    fn test_runner_resumes_strategy_panic_payload_after_pool_work() {
1309        let cfg = Config::new();
1310        let storage_directory = cfg.storage_directory().clone();
1311        let (strategy_tx, strategy_rx) = std::sync::mpsc::channel();
1312        let runner = std::thread::spawn(move || {
1313            let result: std::thread::Result<()> =
1314                std::panic::catch_unwind(AssertUnwindSafe(|| {
1315                    Runner::new(cfg).start(move |context| async move {
1316                        let strategy = context.strategy(NZUsize!(2));
1317                        strategy.spawn(1, |_| ()).await;
1318                        std::panic::panic_any(strategy);
1319                    });
1320                }));
1321            let strategy = result
1322                .expect_err("Runner::start did not resume the root panic")
1323                .downcast::<Rayon>()
1324                .expect("Runner::start changed the root panic payload");
1325            strategy_tx.send(*strategy).unwrap();
1326        });
1327
1328        let strategy = strategy_rx
1329            .recv_timeout(Duration::from_secs(5))
1330            .expect("Runner::start did not resume the strategy panic payload");
1331        runner.join().unwrap();
1332        let _ = std::fs::remove_dir_all(storage_directory);
1333        assert_eq!(futures::executor::block_on(strategy.spawn(1, |_| 42)), 42);
1334    }
1335
1336    #[test]
1337    fn test_thread_stack_size_override() {
1338        let cfg = Config::new().with_thread_stack_size(4 * 1024 * 1024);
1339        assert_eq!(cfg.thread_stack_size(), 4 * 1024 * 1024);
1340    }
1341
1342    #[test]
1343    fn test_explicit_buffer_pool_configs_override_worker_threads() {
1344        // Order does not matter -- explicit configs always win.
1345        let cfg = Config::new()
1346            .with_network_buffer_pool_config(
1347                BufferPoolConfig::for_network().with_parallelism(NZUsize!(2)),
1348            )
1349            .with_worker_threads(8)
1350            .with_storage_buffer_pool_config(
1351                BufferPoolConfig::for_storage().with_thread_cache_disabled(),
1352            );
1353
1354        let network = cfg.resolved_network_buffer_pool_config();
1355        assert_eq!(network.parallelism(), NZUsize!(2));
1356        assert_eq!(
1357            network.thread_cache_config,
1358            BufferPoolConfig::for_network().thread_cache_config
1359        );
1360
1361        let storage = cfg.resolved_storage_buffer_pool_config();
1362        assert_eq!(storage.parallelism(), NZUsize!(1));
1363        assert_eq!(
1364            storage.thread_cache_config,
1365            BufferPoolConfig::for_storage()
1366                .with_thread_cache_disabled()
1367                .thread_cache_config
1368        );
1369    }
1370
1371    #[test]
1372    fn test_process_rss_metric() {
1373        let executor = Runner::default();
1374        executor.start(|context| async move {
1375            loop {
1376                // Wait for RSS metric to be available
1377                let metrics = context.encode();
1378                if !metrics.contains("runtime_process_rss") {
1379                    context.sleep(Duration::from_millis(100)).await;
1380                    continue;
1381                }
1382
1383                // Verify the RSS value is eventually populated (greater than 0)
1384                for line in metrics.lines() {
1385                    if line.starts_with("runtime_process_rss")
1386                        && !line.starts_with("runtime_process_rss{")
1387                    {
1388                        let parts: Vec<&str> = line.split_whitespace().collect();
1389                        if parts.len() >= 2 {
1390                            let rss_value: i64 =
1391                                parts[1].parse().expect("Failed to parse RSS value");
1392                            if rss_value > 0 {
1393                                return;
1394                            }
1395                        }
1396                    }
1397                }
1398            }
1399        });
1400    }
1401
1402    #[test]
1403    fn test_telemetry() {
1404        let executor = Runner::default();
1405        executor.start(|context| async move {
1406            // Define the server address
1407            let address = SocketAddr::from_str("127.0.0.1:8000").unwrap();
1408
1409            // Configure telemetry
1410            telemetry::init(
1411                context.child("metrics"),
1412                telemetry::Logs {
1413                    level: Level::INFO,
1414                    json: false,
1415                },
1416                Some(address),
1417                None,
1418            );
1419
1420            // Register a test metric
1421            let counter: Counter<u64> = Counter::default();
1422            let _registered = context.register("test_counter", "Test counter", counter.clone());
1423            counter.inc();
1424
1425            // Helper functions to parse HTTP response
1426            async fn read_line<St: Stream>(stream: &mut St) -> Result<String, Error> {
1427                let mut line = Vec::new();
1428                loop {
1429                    let received = stream.recv(1).await?;
1430                    let byte = received.coalesce().as_ref()[0];
1431                    if byte == b'\n' {
1432                        if line.last() == Some(&b'\r') {
1433                            line.pop(); // Remove trailing \r
1434                        }
1435                        break;
1436                    }
1437                    line.push(byte);
1438                }
1439                String::from_utf8(line).map_err(|_| Error::ReadFailed)
1440            }
1441
1442            async fn read_headers<St: Stream>(
1443                stream: &mut St,
1444            ) -> Result<HashMap<String, String>, Error> {
1445                let mut headers = HashMap::new();
1446                loop {
1447                    let line = read_line(stream).await?;
1448                    if line.is_empty() {
1449                        break;
1450                    }
1451                    let parts: Vec<&str> = line.splitn(2, ": ").collect();
1452                    if parts.len() == 2 {
1453                        headers.insert(parts[0].to_string(), parts[1].to_string());
1454                    }
1455                }
1456                Ok(headers)
1457            }
1458
1459            async fn read_body<St: Stream>(
1460                stream: &mut St,
1461                content_length: usize,
1462            ) -> Result<String, Error> {
1463                let received = stream.recv(content_length).await?;
1464                String::from_utf8(received.coalesce().into()).map_err(|_| Error::ReadFailed)
1465            }
1466
1467            // Simulate a client connecting to the server
1468            let client_handle = context.child("client").spawn(move |context| async move {
1469                let (mut sink, mut stream) = loop {
1470                    match context.dial(address).await {
1471                        Ok((sink, stream)) => break (sink, stream),
1472                        Err(e) => {
1473                            // The client may be polled before the server is ready, that's alright!
1474                            error!(err =?e, "failed to connect");
1475                            context.sleep(Duration::from_millis(10)).await;
1476                        }
1477                    }
1478                };
1479
1480                // Send a GET request to the server
1481                let request = format!(
1482                    "GET /metrics HTTP/1.1\r\nHost: {address}\r\nConnection: close\r\n\r\n"
1483                );
1484                sink.send(Bytes::from(request)).await.unwrap();
1485
1486                // Read and verify the HTTP status line
1487                let status_line = read_line(&mut stream).await.unwrap();
1488                assert_eq!(status_line, "HTTP/1.1 200 OK");
1489
1490                // Read and parse headers
1491                let headers = read_headers(&mut stream).await.unwrap();
1492                println!("Headers: {headers:?}");
1493                let content_length = headers
1494                    .get("content-length")
1495                    .unwrap()
1496                    .parse::<usize>()
1497                    .unwrap();
1498
1499                // Read and verify the body
1500                let body = read_body(&mut stream, content_length).await.unwrap();
1501                assert!(body.contains("test_counter_total 1"));
1502            });
1503
1504            // Wait for the client task to complete
1505            client_handle.await.unwrap();
1506        });
1507    }
1508
1509    #[test]
1510    fn test_resolver() {
1511        let executor = Runner::default();
1512        executor.start(|context| async move {
1513            let addrs = context.resolve("localhost").await.unwrap();
1514            assert!(!addrs.is_empty());
1515            for addr in addrs {
1516                assert!(
1517                    addr == IpAddr::V4(Ipv4Addr::LOCALHOST)
1518                        || addr == IpAddr::V6(Ipv6Addr::LOCALHOST)
1519                );
1520            }
1521        });
1522    }
1523}