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 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 tcp_nodelay: Option<bool>,
94
95 zero_linger: bool,
104
105 connect_timeout: Duration,
109
110 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#[derive(Clone)]
133pub struct Config {
134 worker_threads: usize,
140
141 global_queue_interval: Option<u32>,
148
149 max_blocking_threads: usize,
157
158 thread_stack_size: usize,
165
166 catch_panics: bool,
168
169 storage_directory: PathBuf,
172
173 storage_blob_layouts: RangeInclusive<BlobLayout>,
180
181 network_cfg: NetworkConfig,
183
184 network_buffer_pool_cfg: Option<BufferPoolConfig>,
186
187 storage_buffer_pool_cfg: Option<BufferPoolConfig>,
189}
190
191impl Config {
192 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 pub const fn with_worker_threads(mut self, n: usize) -> Self {
213 self.worker_threads = n;
214 self
215 }
216 pub const fn with_global_queue_interval(mut self, n: u32) -> Self {
218 self.global_queue_interval = Some(n);
219 self
220 }
221 pub const fn with_max_blocking_threads(mut self, n: usize) -> Self {
223 self.max_blocking_threads = n;
224 self
225 }
226 pub const fn with_thread_stack_size(mut self, n: usize) -> Self {
228 self.thread_stack_size = n;
229 self
230 }
231 pub const fn with_catch_panics(mut self, b: bool) -> Self {
233 self.catch_panics = b;
234 self
235 }
236 pub const fn with_connect_timeout(mut self, timeout: Duration) -> Self {
238 self.network_cfg.connect_timeout = timeout;
239 self
240 }
241 pub const fn with_read_write_timeout(mut self, d: Duration) -> Self {
243 self.network_cfg.read_write_timeout = d;
244 self
245 }
246 pub const fn with_tcp_nodelay(mut self, n: Option<bool>) -> Self {
248 self.network_cfg.tcp_nodelay = n;
249 self
250 }
251 pub const fn with_zero_linger(mut self, l: bool) -> Self {
253 self.network_cfg.zero_linger = l;
254 self
255 }
256 pub fn with_storage_directory(mut self, p: impl Into<PathBuf>) -> Self {
258 self.storage_directory = p.into();
259 self
260 }
261 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 pub fn with_network_buffer_pool_config(mut self, cfg: BufferPoolConfig) -> Self {
276 self.network_buffer_pool_cfg = Some(cfg);
277 self
278 }
279 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 pub const fn worker_threads(&self) -> usize {
288 self.worker_threads
289 }
290 pub const fn global_queue_interval(&self) -> Option<u32> {
292 self.global_queue_interval
293 }
294 pub const fn max_blocking_threads(&self) -> usize {
296 self.max_blocking_threads
297 }
298 pub const fn thread_stack_size(&self) -> usize {
300 self.thread_stack_size
301 }
302 pub const fn catch_panics(&self) -> bool {
304 self.catch_panics
305 }
306 pub const fn connect_timeout(&self) -> Duration {
308 self.network_cfg.connect_timeout
309 }
310 pub const fn read_write_timeout(&self) -> Duration {
312 self.network_cfg.read_write_timeout
313 }
314 pub const fn tcp_nodelay(&self) -> Option<bool> {
316 self.network_cfg.tcp_nodelay
317 }
318 pub const fn zero_linger(&self) -> bool {
320 self.network_cfg.zero_linger
321 }
322 pub const fn storage_directory(&self) -> &PathBuf {
324 &self.storage_directory
325 }
326 pub const fn storage_blob_layouts(&self) -> &RangeInclusive<BlobLayout> {
328 &self.storage_blob_layouts
329 }
330
331 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 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
354pub 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#[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 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
417pub 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 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 let mut registry = Registry::new();
445 let mut runtime_registry = registry.sub_registry(METRICS_PREFIX);
446
447 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 let (panicker, panicked) = Panicker::new(self.cfg.catch_panics);
462
463 let process = MeteredProcess::init(&mut runtime_registry);
468 runtime.spawn(process.collect(tokio::time::sleep));
469
470 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 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 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 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 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 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 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 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
623pub 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 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 let (_, metric) = spawn_metrics!(self);
664
665 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 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 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 let handle = executor.runtime.clone();
704 move || {
705 handle.block_on(f);
706 }
707 });
708 } else {
709 executor.runtime.spawn(f);
710 }
711
712 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 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(key);
776
777 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 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 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 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 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 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 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 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 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 let metrics = context.encode();
1378 if !metrics.contains("runtime_process_rss") {
1379 context.sleep(Duration::from_millis(100)).await;
1380 continue;
1381 }
1382
1383 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 let address = SocketAddr::from_str("127.0.0.1:8000").unwrap();
1408
1409 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 let counter: Counter<u64> = Counter::default();
1422 let _registered = context.register("test_counter", "Test counter", counter.clone());
1423 counter.inc();
1424
1425 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(); }
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 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 error!(err =?e, "failed to connect");
1475 context.sleep(Duration::from_millis(10)).await;
1476 }
1477 }
1478 };
1479
1480 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 let status_line = read_line(&mut stream).await.unwrap();
1488 assert_eq!(status_line, "HTTP/1.1 200 OK");
1489
1490 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 let body = read_body(&mut stream, content_length).await.unwrap();
1501 assert!(body.contains("test_counter_total 1"));
1502 });
1503
1504 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}