1#![doc(
25 html_logo_url = "https://commonware.xyz/imgs/rustdoc_logo.svg",
26 html_favicon_url = "https://commonware.xyz/favicon.ico"
27)]
28
29use commonware_macros::stability_scope;
30
31#[macro_use]
32mod macros;
33
34mod network;
35mod process;
36mod storage;
37
38stability_scope!(ALPHA {
39 #[cfg(feature = "arbitrary")]
40 pub mod conformance;
41 pub mod deterministic;
42 pub mod mocks;
43});
44stability_scope!(ALPHA, cfg(not(target_arch = "wasm32")) {
45 pub mod benchmarks;
46});
47stability_scope!(ALPHA, cfg(target_os = "linux") {
48 pub mod iouring;
49});
50stability_scope!(BETA, cfg(not(target_arch = "wasm32")) {
51 pub mod tokio;
52});
53stability_scope!(BETA {
54 pub use bytes::{Buf, BufMut};
56 use commonware_macros::select;
57 use commonware_parallel::Rayon;
58 use futures::FutureExt as _;
59 pub use governor::Quota;
61 use std::{
62 future::Future,
63 io::Error as IoError,
64 net::SocketAddr,
65 num::NonZeroUsize,
66 sync::Arc,
67 time::{Duration, SystemTime},
68 };
69 pub(crate) use telemetry::metrics::{METRICS_PREFIX, child_label, prefixed_name};
70 use thiserror::Error;
71
72 pub mod iobuf;
73 pub use iobuf::{Builder as IoBufsBuilder, IoBuf, IoBufMut, IoBufs, IoBufsMut};
74
75 pub mod utils;
76 pub use utils::*;
77
78 pub mod telemetry;
79
80 #[repr(u16)]
88 #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
89 pub enum BlobLayout {
90 #[deprecated(note = "unaligned pages can degrade performance")]
97 V0 = 0,
98 V1 = 1,
100 }
101
102 pub const DEFAULT_BLOB_LAYOUT: BlobLayout = BlobLayout::V1;
105
106 impl BlobLayout {
107 #[allow(deprecated)]
109 pub const ALL: std::ops::RangeInclusive<Self> = Self::V0..=DEFAULT_BLOB_LAYOUT;
110 }
111
112 pub const BLOB_PAGE_SIZE: u32 = 4096;
130
131 const _: () = assert!(
133 BLOB_PAGE_SIZE.is_power_of_two()
134 && DEFAULT_BLOB_LAYOUT
135 .data_offset()
136 .is_multiple_of(BLOB_PAGE_SIZE as u64)
137 );
138
139 #[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
143 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
144 pub struct BlobVersion(u16);
145
146 impl BlobVersion {
147 pub const fn new(version: u16) -> Self {
149 Self(version)
150 }
151
152 pub const fn get(self) -> u16 {
154 self.0
155 }
156 }
157
158 impl std::fmt::Display for BlobVersion {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 write!(f, "{}", self.0)
161 }
162 }
163
164 impl std::fmt::Debug for BlobVersion {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 write!(f, "{}", self.0)
167 }
168 }
169
170 pub const DEFAULT_BLOB_VERSION: BlobVersion = BlobVersion::new(0);
172
173 #[derive(Error, Debug, Clone)]
175 pub enum Error {
176 #[error("exited")]
177 Exited,
178 #[error("closed")]
179 Closed,
180 #[error("aborted")]
181 Aborted,
182 #[error("timeout")]
183 Timeout,
184 #[error("bind failed")]
185 BindFailed,
186 #[error("connection failed")]
187 ConnectionFailed,
188 #[error("write failed")]
189 WriteFailed,
190 #[error("read failed")]
191 ReadFailed,
192 #[error("send failed")]
193 SendFailed,
194 #[error("recv failed")]
195 RecvFailed,
196 #[error("dns resolution failed: {0}")]
197 ResolveFailed(String),
198 #[error(
199 "partition name invalid, must only contain alphanumeric, dash ('-'), or underscore ('_') characters: {0}"
200 )]
201 PartitionNameInvalid(String),
202 #[error("partition creation failed: {0}")]
203 PartitionCreationFailed(String),
204 #[error("partition missing: {0}")]
205 PartitionMissing(String),
206 #[error("partition corrupt: {0}")]
207 PartitionCorrupt(String),
208 #[error("blob already open: {0}/{1}")]
209 BlobAlreadyOpen(String, String),
210 #[error("blob open failed: {0}/{1} error: {2}")]
211 BlobOpenFailed(String, String, Arc<IoError>),
212 #[error("blob missing: {0}/{1}")]
213 BlobMissing(String, String),
214 #[error("blob resize failed: {0}/{1} error: {2}")]
215 BlobResizeFailed(String, String, Arc<IoError>),
216 #[error("blob sync failed: {0}/{1} error: {2}")]
217 BlobSyncFailed(String, String, Arc<IoError>),
218 #[error("blob insufficient length")]
219 BlobInsufficientLength,
220 #[error("blob corrupt: {0}/{1} reason: {2}")]
221 BlobCorrupt(String, String, String),
222 #[error("blob layout mismatch: expected one of {expected:?}, found {found:?}")]
223 BlobLayoutMismatch {
224 expected: std::ops::RangeInclusive<BlobLayout>,
225 found: BlobLayout,
226 },
227 #[error("blob version mismatch: expected one of {expected:?}, found {found}")]
228 BlobVersionMismatch {
229 expected: std::ops::RangeInclusive<BlobVersion>,
230 found: BlobVersion,
231 },
232 #[error("invalid or missing checksum")]
233 InvalidChecksum,
234 #[error("offset overflow")]
235 OffsetOverflow,
236 #[error("io error: {0}")]
237 Io(Arc<IoError>),
238 }
239
240 impl From<IoError> for Error {
241 fn from(err: IoError) -> Self {
242 Self::Io(Arc::new(err))
243 }
244 }
245
246 pub trait Runner {
249 type Context;
251
252 fn start<F, Fut>(self, f: F) -> Fut::Output
258 where
259 F: FnOnce(Self::Context) -> Fut,
260 Fut: Future;
261 }
262
263 #[derive(Clone, Debug, Default)]
265 pub struct Name {
266 pub label: String,
268 pub attributes: Vec<(String, String)>,
270 }
271
272 pub trait Supervisor: Send + Sync + 'static {
274 fn name(&self) -> Name;
276
277 #[must_use]
287 fn child(&self, label: &'static str) -> Self;
288
289 #[must_use]
354 fn with_attribute(self, key: &'static str, value: impl std::fmt::Display) -> Self;
355 }
356
357 pub trait Spawner: Supervisor {
359 #[must_use]
368 fn shared(self, blocking: bool) -> Self;
369
370 #[must_use]
377 fn dedicated(self) -> Self;
378
379 fn spawn<F, Fut, T>(self, f: F) -> Handle<T>
414 where
415 Self: Sized,
416 F: FnOnce(Self) -> Fut + Send + 'static,
417 Fut: Future<Output = T> + Send + 'static,
418 T: Send + 'static;
419
420 fn stop(
440 self,
441 value: i32,
442 timeout: Option<Duration>,
443 ) -> impl Future<Output = Result<(), Error>> + Send;
444
445 fn stopped(&self) -> signal::Signal;
452 }
453
454 pub trait Strategizer: Spawner {
456 fn strategy(&self, parallelism: NonZeroUsize) -> Rayon;
465 }
466
467 pub trait Metrics: Supervisor {
469 fn register<N: Into<String>, H: Into<String>, M: telemetry::metrics::Metric>(
486 &self,
487 name: N,
488 help: H,
489 metric: M,
490 ) -> telemetry::metrics::Registered<M>;
491
492 fn encode(&self) -> String;
494 }
495
496 pub type RateLimiter<C> = governor::RateLimiter<
501 governor::state::NotKeyed,
502 governor::state::InMemoryState,
503 C,
504 governor::middleware::NoOpMiddleware<<C as governor::clock::Clock>::Instant>,
505 >;
506
507 pub type KeyedRateLimiter<K, C> = governor::RateLimiter<
514 K,
515 governor::state::keyed::HashMapStateStore<K>,
516 C,
517 governor::middleware::NoOpMiddleware<<C as governor::clock::Clock>::Instant>,
518 >;
519
520 pub trait Clock:
526 governor::clock::Clock<Instant = SystemTime>
527 + governor::clock::ReasonablyRealtime
528 + Send
529 + Sync
530 + 'static
531 {
532 fn current(&self) -> SystemTime;
534
535 fn sleep(&self, duration: Duration) -> impl Future<Output = ()> + Send + 'static + use<Self>;
537
538 fn sleep_until(&self, deadline: SystemTime) -> impl Future<Output = ()> + Send + 'static + use<Self>;
540
541 fn timeout<F, T>(
562 &self,
563 duration: Duration,
564 future: F,
565 ) -> impl Future<Output = Result<T, Error>> + Send + '_
566 where
567 F: Future<Output = T> + Send + 'static,
568 T: Send + 'static,
569 {
570 async move {
571 select! {
572 result = future => Ok(result),
573 _ = self.sleep(duration) => Err(Error::Timeout),
574 }
575 }
576 }
577 }
578
579 pub type SinkOf<N> = <<N as Network>::Listener as Listener>::Sink;
581
582 pub type StreamOf<N> = <<N as Network>::Listener as Listener>::Stream;
584
585 pub type ListenerOf<N> = <N as crate::Network>::Listener;
587
588 pub trait Network: Send + Sync + 'static {
591 type Listener: Listener;
595
596 fn bind(
598 &self,
599 socket: SocketAddr,
600 ) -> impl Future<Output = Result<Self::Listener, Error>> + Send;
601
602 fn dial(
604 &self,
605 socket: SocketAddr,
606 ) -> impl Future<Output = Result<(SinkOf<Self>, StreamOf<Self>), Error>> + Send;
607 }
608
609 pub trait Resolver: Send + Sync + 'static {
611 fn resolve(
615 &self,
616 host: &str,
617 ) -> impl Future<Output = Result<Vec<std::net::IpAddr>, Error>> + Send;
618 }
619
620 pub trait Listener: Sync + Send + 'static {
623 type Sink: Sink;
626 type Stream: Stream;
629
630 fn accept(
632 &mut self,
633 ) -> impl Future<Output = Result<(SocketAddr, Self::Sink, Self::Stream), Error>> + Send;
634
635 fn local_addr(&self) -> Result<SocketAddr, std::io::Error>;
637 }
638
639 pub trait Sink: Sync + Send + 'static {
642 fn send(
653 &mut self,
654 bufs: impl Into<IoBufs> + Send,
655 ) -> impl Future<Output = Result<(), Error>> + Send;
656 }
657
658 pub trait Stream: Sync + Send + 'static {
661 fn recv(&mut self, len: usize) -> impl Future<Output = Result<IoBufs, Error>> + Send;
674
675 fn peek(&self, max_len: usize) -> &[u8];
683 }
684
685 pub trait Storage: Send + Sync + 'static {
720 type Blob: Blob;
722
723 fn open(
726 &self,
727 partition: &str,
728 name: &[u8],
729 ) -> impl Future<Output = Result<(Self::Blob, u64), Error>> + Send {
730 async move {
731 let (blob, size, _) = self
732 .open_versioned(partition, name, DEFAULT_BLOB_VERSION..=DEFAULT_BLOB_VERSION)
733 .await?;
734 Ok((blob, size))
735 }
736 }
737
738 fn open_versioned(
772 &self,
773 partition: &str,
774 name: &[u8],
775 versions: std::ops::RangeInclusive<BlobVersion>,
776 ) -> impl Future<Output = Result<(Self::Blob, u64, BlobVersion), Error>> + Send;
777
778 fn remove(
797 &self,
798 partition: &str,
799 name: Option<&[u8]>,
800 ) -> impl Future<Output = Result<(), Error>> + Send;
801
802 fn scan(&self, partition: &str)
804 -> impl Future<Output = Result<Vec<Vec<u8>>, Error>> + Send;
805 }
806
807 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
812 pub struct ReadOptions(u8);
813
814 impl ReadOptions {
815 pub const DONT_CACHE: Self = Self(1 << 0);
823
824 #[must_use]
826 pub const fn contains(self, options: Self) -> bool {
827 self.0 & options.0 == options.0
828 }
829
830 #[must_use]
832 pub const fn without(self, options: Self) -> Self {
833 Self(self.0 & !options.0)
834 }
835 }
836
837 impl std::ops::BitOr for ReadOptions {
838 type Output = Self;
839
840 fn bitor(self, rhs: Self) -> Self::Output {
841 Self(self.0 | rhs.0)
842 }
843 }
844
845 impl std::ops::BitOrAssign for ReadOptions {
846 fn bitor_assign(&mut self, rhs: Self) {
847 self.0 |= rhs.0;
848 }
849 }
850
851 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
856 pub struct WriteOptions(u8);
857
858 impl WriteOptions {
859 pub const SYNC: Self = Self(1 << 0);
864
865 pub const DONT_CACHE: Self = Self(1 << 1);
871
872 #[must_use]
874 pub const fn contains(self, options: Self) -> bool {
875 self.0 & options.0 == options.0
876 }
877
878 #[must_use]
880 pub const fn without(self, options: Self) -> Self {
881 Self(self.0 & !options.0)
882 }
883 }
884
885 impl std::ops::BitOr for WriteOptions {
886 type Output = Self;
887
888 fn bitor(self, rhs: Self) -> Self::Output {
889 Self(self.0 | rhs.0)
890 }
891 }
892
893 impl std::ops::BitOrAssign for WriteOptions {
894 fn bitor_assign(&mut self, rhs: Self) {
895 self.0 |= rhs.0;
896 }
897 }
898
899 pub trait Blob: Send + Sync + 'static {
923 fn read_at_buf(
931 &self,
932 offset: u64,
933 len: usize,
934 bufs: impl Into<IoBufsMut> + Send,
935 options: ReadOptions,
936 ) -> impl Future<Output = Result<IoBufsMut, Error>> + Send;
937
938 fn read_at(
942 &self,
943 offset: u64,
944 len: usize,
945 options: ReadOptions,
946 ) -> impl Future<Output = Result<IoBufsMut, Error>> + Send;
947
948 fn read_many(
955 &self,
956 ranges: &[(u64, usize)],
957 options: ReadOptions,
958 ) -> impl futures::Stream<Item = Result<(usize, IoBufsMut), Error>> + Send {
959 read_each(self, ranges, options)
960 }
961
962 fn start_read_many(
970 &self,
971 ranges: &[(u64, usize)],
972 options: ReadOptions,
973 ) -> impl Future<Output = Handle<Vec<IoBufsMut>>> + Send {
974 read_many_now(self, ranges, options)
975 }
976
977 fn write_at(
981 &self,
982 offset: u64,
983 bufs: impl Into<IoBufs> + Send,
984 options: WriteOptions,
985 ) -> impl Future<Output = Result<(), Error>> + Send;
986
987 fn resize(&self, len: u64) -> impl Future<Output = Result<(), Error>> + Send;
992
993 fn sync(&self) -> impl Future<Output = Result<(), Error>> + Send;
995
996 fn start_sync(&self) -> impl Future<Output = Handle<()>> + Send;
1002 }
1003
1004 impl<B: Blob> Blob for Arc<B> {
1005 fn read_at_buf(
1006 &self,
1007 offset: u64,
1008 len: usize,
1009 bufs: impl Into<IoBufsMut> + Send,
1010 options: ReadOptions,
1011 ) -> impl Future<Output = Result<IoBufsMut, Error>> + Send {
1012 self.as_ref().read_at_buf(offset, len, bufs, options)
1013 }
1014
1015 fn read_at(
1016 &self,
1017 offset: u64,
1018 len: usize,
1019 options: ReadOptions,
1020 ) -> impl Future<Output = Result<IoBufsMut, Error>> + Send {
1021 self.as_ref().read_at(offset, len, options)
1022 }
1023
1024 fn read_many(
1025 &self,
1026 ranges: &[(u64, usize)],
1027 options: ReadOptions,
1028 ) -> impl futures::Stream<Item = Result<(usize, IoBufsMut), Error>> + Send {
1029 self.as_ref().read_many(ranges, options)
1030 }
1031
1032 fn start_read_many(
1033 &self,
1034 ranges: &[(u64, usize)],
1035 options: ReadOptions,
1036 ) -> impl Future<Output = Handle<Vec<IoBufsMut>>> + Send {
1037 self.as_ref().start_read_many(ranges, options)
1038 }
1039
1040 fn write_at(
1041 &self,
1042 offset: u64,
1043 bufs: impl Into<IoBufs> + Send,
1044 options: WriteOptions,
1045 ) -> impl Future<Output = Result<(), Error>> + Send {
1046 self.as_ref().write_at(offset, bufs, options)
1047 }
1048
1049 fn resize(&self, len: u64) -> impl Future<Output = Result<(), Error>> + Send {
1050 self.as_ref().resize(len)
1051 }
1052
1053 fn sync(&self) -> impl Future<Output = Result<(), Error>> + Send {
1054 self.as_ref().sync()
1055 }
1056
1057 fn start_sync(&self) -> impl Future<Output = Handle<()>> + Send {
1058 self.as_ref().start_sync()
1059 }
1060 }
1061
1062 pub(crate) fn read_each<'a, B: Blob + ?Sized>(
1065 blob: &'a B,
1066 ranges: &[(u64, usize)],
1067 options: ReadOptions,
1068 ) -> impl futures::Stream<Item = Result<(usize, IoBufsMut), Error>> + Send + 'a {
1069 ranges
1070 .iter()
1071 .enumerate()
1072 .map(|(index, &(offset, len))| {
1073 blob.read_at(offset, len, options)
1074 .map(move |result| result.map(|bufs| (index, bufs)))
1075 })
1076 .collect::<futures::stream::FuturesUnordered<_>>()
1077 }
1078
1079 pub(crate) async fn read_many_now<B: Blob + ?Sized>(
1082 blob: &B,
1083 ranges: &[(u64, usize)],
1084 options: ReadOptions,
1085 ) -> Handle<Vec<IoBufsMut>> {
1086 Handle::ready(collect_reads(blob.read_many(ranges, options), ranges.len()).await)
1087 }
1088
1089 pub(crate) async fn collect_reads<S>(reads: S, ranges: usize) -> Result<Vec<IoBufsMut>, Error>
1091 where
1092 S: futures::Stream<Item = Result<(usize, IoBufsMut), Error>>,
1093 {
1094 let mut bufs: Vec<Option<IoBufsMut>> = (0..ranges).map(|_| None).collect();
1095 let mut reads = std::pin::pin!(reads);
1096 while let Some(read) = futures::StreamExt::next(&mut reads).await {
1097 let (index, buf) = read?;
1098 *bufs.get_mut(index).ok_or(Error::ReadFailed)? = Some(buf);
1099 }
1100 bufs.into_iter().map(|buf| buf.ok_or(Error::ReadFailed)).collect()
1101 }
1102
1103 #[cfg(not(target_arch = "wasm32"))]
1106 pub(crate) fn join_reads<F>(reads: Vec<F>) -> Handle<Vec<IoBufsMut>>
1107 where
1108 F: Future<Output = Result<IoBufsMut, Error>> + Send + 'static,
1109 {
1110 Handle::from_future(async move {
1111 let mut bufs = Vec::with_capacity(reads.len());
1112 for read in reads {
1113 bufs.push(read.await?);
1114 }
1115 Ok(bufs)
1116 })
1117 }
1118});
1119stability_scope!(BETA, cfg(feature = "external") {
1120 pub trait Pacer: Clock + Send + Sync + 'static {
1122 fn pace<'a, F, T>(
1142 &'a self,
1143 latency: Duration,
1144 future: F,
1145 ) -> impl Future<Output = T> + Send + 'a
1146 where
1147 F: Future<Output = T> + Send + 'a,
1148 T: Send + 'a;
1149 }
1150
1151 pub trait FutureExt: Future + Send + Sized {
1156 fn pace<'a, E>(
1158 self,
1159 pacer: &'a E,
1160 latency: Duration,
1161 ) -> impl Future<Output = Self::Output> + Send + 'a
1162 where
1163 E: Pacer + 'a,
1164 Self: Send + 'a,
1165 Self::Output: Send + 'a,
1166 {
1167 pacer.pace(latency, self)
1168 }
1169 }
1170
1171 impl<F> FutureExt for F where F: Future + Send {}
1172});
1173
1174#[cfg(test)]
1175mod tests {
1176 use super::*;
1177 use crate::telemetry::metrics::{
1178 EncodeLabelKey, EncodeLabelSetTrait as EncodeLabelSet,
1179 EncodeLabelValueTrait as EncodeLabelValue, LabelSetEncoder, count_running_tasks,
1180 raw::{Counter, Family},
1181 };
1182 use commonware_macros::select;
1183 use commonware_parallel::Strategy as _;
1184 use commonware_utils::{
1185 NZUsize, SystemTimeExt,
1186 channel::{mpsc, oneshot},
1187 futures::Pool as FuturesPool,
1188 sync::Mutex,
1189 };
1190 use futures::{
1191 FutureExt,
1192 future::{pending, ready},
1193 join, pin_mut,
1194 };
1195 use rstest::rstest;
1196 use std::{
1197 panic::{AssertUnwindSafe, catch_unwind},
1198 pin::Pin,
1199 sync::{
1200 Arc,
1201 atomic::{AtomicU32, Ordering},
1202 },
1203 task::{Context as TContext, Poll, Waker},
1204 };
1205 use utils::reschedule;
1206
1207 #[test]
1208 fn test_blob_version() {
1209 let version = BlobVersion::new(7);
1210 assert_eq!(version.get(), 7);
1211 assert_eq!(version.to_string(), "7");
1212 assert_eq!(format!("{version:?}"), "7");
1213 assert_eq!(BlobVersion::default(), DEFAULT_BLOB_VERSION);
1214 assert!((BlobVersion::new(3)..=BlobVersion::new(7)).contains(&version));
1215 }
1216
1217 #[test]
1218 fn test_read_options_compose() {
1219 let options = ReadOptions::default() | ReadOptions::DONT_CACHE;
1221 let mut assigned = ReadOptions::default();
1222 assigned |= ReadOptions::DONT_CACHE;
1223 assert!(options.contains(ReadOptions::DONT_CACHE));
1224 assert_eq!(assigned, options);
1225 assert_eq!(
1226 options.without(ReadOptions::DONT_CACHE),
1227 ReadOptions::default()
1228 );
1229 assert!(!ReadOptions::default().contains(ReadOptions::DONT_CACHE));
1230 }
1231
1232 #[test]
1233 fn test_write_options_compose() {
1234 let options = WriteOptions::SYNC | WriteOptions::DONT_CACHE;
1235 let mut assigned = WriteOptions::SYNC;
1236 assigned |= WriteOptions::DONT_CACHE;
1237 assert!(options.contains(WriteOptions::SYNC));
1238 assert!(options.contains(WriteOptions::DONT_CACHE));
1239 assert_eq!(assigned, options);
1240 assert_eq!(
1241 options.without(WriteOptions::SYNC),
1242 WriteOptions::DONT_CACHE
1243 );
1244 let default = WriteOptions::default();
1245 assert!(!default.contains(WriteOptions::SYNC));
1246 assert!(!default.contains(WriteOptions::DONT_CACHE));
1247 }
1248
1249 #[rstest]
1250 #[case::deterministic(deterministic::Runner::default())]
1251 #[case::tokio(tokio::Runner::default())]
1252 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1253 fn test_error_future<R: Runner>(#[case] runner: R) {
1254 #[allow(clippy::unused_async)]
1255 async fn error_future() -> Result<&'static str, &'static str> {
1256 Err("An error occurred")
1257 }
1258 let result = runner.start(|_| error_future());
1259 assert_eq!(result, Err("An error occurred"));
1260 }
1261
1262 #[rstest]
1263 #[case::deterministic(deterministic::Runner::default())]
1264 #[case::tokio(tokio::Runner::default())]
1265 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1266 fn test_handle_can_use_futures_pool<R: Runner>(#[case] runner: R) {
1267 runner.start(|_| async move {
1268 let mut pool = FuturesPool::<Result<(), Error>>::default();
1269 pool.push(Handle::ready(Ok(())));
1270 assert!(pool.next_completed().await.is_ok());
1271 });
1272 }
1273
1274 #[rstest]
1275 #[case::deterministic(deterministic::Runner::default())]
1276 #[case::tokio(tokio::Runner::default())]
1277 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1278 fn test_clock_sleep<R: Runner>(#[case] runner: R)
1279 where
1280 R::Context: Spawner + Clock,
1281 {
1282 runner.start(|context| async move {
1283 let start = context.current();
1285 let sleep_duration = Duration::from_millis(10);
1286 context.sleep(sleep_duration).await;
1287
1288 let end = context.current();
1290 assert!(end.duration_since(start).unwrap() >= sleep_duration);
1291 });
1292 }
1293
1294 #[rstest]
1295 #[case::deterministic(deterministic::Runner::default())]
1296 #[case::tokio(tokio::Runner::default())]
1297 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1298 fn test_clock_sleep_until<R: Runner>(#[case] runner: R)
1299 where
1300 R::Context: Spawner + Clock + Metrics,
1301 {
1302 runner.start(|context| async move {
1303 let now = context.current();
1305 context.sleep_until(now + Duration::from_millis(100)).await;
1306
1307 let elapsed = now.elapsed().unwrap();
1309 assert!(elapsed >= Duration::from_millis(100));
1310 });
1311 }
1312
1313 #[rstest]
1314 #[case::deterministic(deterministic::Runner::default())]
1315 #[case::tokio(tokio::Runner::default())]
1316 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1317 fn test_clock_sleep_until_far_future<R: Runner>(#[case] runner: R)
1318 where
1319 R::Context: Spawner + Clock,
1320 {
1321 runner.start(|context| async move {
1322 let sleep = context.sleep_until(SystemTime::limit());
1323 let result = context.timeout(Duration::from_millis(1), sleep).await;
1324 assert!(matches!(result, Err(Error::Timeout)));
1325 });
1326 }
1327
1328 #[rstest]
1329 #[case::deterministic(deterministic::Runner::default())]
1330 #[case::tokio(tokio::Runner::default())]
1331 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1332 fn test_clock_timeout<R: Runner>(#[case] runner: R)
1333 where
1334 R::Context: Spawner + Clock,
1335 {
1336 runner.start(|context| async move {
1337 let result = context
1339 .timeout(Duration::from_millis(100), async { "success" })
1340 .await;
1341 assert_eq!(result.unwrap(), "success");
1342
1343 let result = context
1345 .timeout(Duration::from_millis(50), pending::<()>())
1346 .await;
1347 assert!(matches!(result, Err(Error::Timeout)));
1348
1349 let result = context
1351 .timeout(
1352 Duration::from_millis(100),
1353 context.sleep(Duration::from_millis(50)),
1354 )
1355 .await;
1356 assert!(result.is_ok());
1357 });
1358 }
1359
1360 #[rstest]
1361 #[case::deterministic(deterministic::Runner::default())]
1362 #[case::tokio(tokio::Runner::default())]
1363 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1364 fn test_clock_futures_move_between_tasks<R: Runner>(#[case] runner: R)
1365 where
1366 R::Context: Spawner + Clock,
1367 {
1368 runner.start(|context| async move {
1369 for kind in ["sleep", "sleep_until", "timeout", "stop"] {
1370 let clock = context.child("clock");
1372 let stopped = (kind == "stop").then(|| context.stopped());
1373 let duration = Duration::from_millis(50);
1374 let deadline = context.current() + duration;
1375
1376 let mut future = async move {
1378 match kind {
1379 "sleep" => clock.sleep(duration).await,
1380 "sleep_until" => clock.sleep_until(deadline).await,
1381 "timeout" => assert!(matches!(
1382 clock.timeout(duration, pending::<()>()).await,
1383 Err(Error::Timeout)
1384 )),
1385 _ => assert!(matches!(
1386 clock.stop(7, Some(duration)).await,
1387 Err(Error::Timeout)
1388 )),
1389 }
1390 }
1391 .boxed();
1392
1393 assert!(futures::poll!(&mut future).is_pending());
1396 let moved = context.child("moved").dedicated().spawn(move |_| future);
1397
1398 select! {
1400 result = moved => result.unwrap(),
1401 _ = context.sleep(Duration::from_secs(2)) => panic!("moved {kind} did not complete"),
1402 }
1403
1404 assert!(context.current() >= deadline);
1406 drop(stopped);
1407 }
1408 });
1409 }
1410
1411 #[rstest]
1412 #[case::deterministic(deterministic::Runner::default())]
1413 #[case::tokio(tokio::Runner::default())]
1414 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1415 fn test_root_finishes<R: Runner>(#[case] runner: R)
1416 where
1417 R::Context: Spawner,
1418 {
1419 runner.start(|context| async move {
1420 context.spawn(|_| async move {
1421 loop {
1422 reschedule().await;
1423 }
1424 });
1425 });
1426 }
1427
1428 #[rstest]
1429 #[case::deterministic(deterministic::Runner::default())]
1430 #[case::tokio(tokio::Runner::default())]
1431 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1432 fn test_spawn_after_abort<R>(#[case] runner: R)
1433 where
1434 R: Runner,
1435 R::Context: Spawner,
1436 {
1437 runner.start(|context| async move {
1438 let child = context.child("child");
1440
1441 let parent_handle = context.spawn(move |_| async move {
1443 pending::<()>().await;
1444 });
1445 parent_handle.abort();
1446
1447 let child_handle = child.spawn(move |_| async move {
1449 pending::<()>().await;
1450 });
1451 assert!(matches!(child_handle.await, Err(Error::Closed)));
1452 });
1453 }
1454
1455 #[rstest]
1456 #[case::deterministic(deterministic::Runner::default())]
1457 #[case::tokio(tokio::Runner::default())]
1458 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1459 fn test_spawn_abort<R: Runner>(
1460 #[case] runner: R,
1461 #[values(
1462 Execution::Shared(false),
1463 Execution::Shared(true),
1464 Execution::Dedicated
1465 )]
1466 execution: Execution,
1467 ) where
1468 R::Context: Spawner,
1469 {
1470 runner.start(|context| async move {
1471 let context = match execution {
1472 Execution::Dedicated => context.dedicated(),
1473 Execution::Shared(blocking) => context.shared(blocking),
1474 };
1475
1476 let handle = context.spawn(|_| async move {
1477 loop {
1478 reschedule().await;
1479 }
1480 });
1481 handle.abort();
1482 assert!(matches!(handle.await, Err(Error::Closed)));
1483 });
1484 }
1485
1486 #[rstest]
1487 #[case::deterministic(deterministic::Runner::default())]
1488 #[case::deterministic_caught(deterministic::Runner::new(
1489 deterministic::Config::default().with_catch_panics(true)
1490 ))]
1491 #[case::tokio(tokio::Runner::default())]
1492 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1493 #[case::tokio_caught(tokio::Runner::new(tokio::Config::default().with_catch_panics(true)))]
1494 #[cfg_attr(target_os = "linux", case::iouring_caught(iouring::Runner::new(iouring::Config::default().with_catch_panics(true))))]
1495 #[should_panic(expected = "blah")]
1496 fn test_panic_aborts_root<R: Runner>(#[case] runner: R) {
1497 let result: Result<(), Error> = runner.start(|_| async move {
1498 panic!("blah");
1499 });
1500 result.unwrap_err();
1501 }
1502
1503 #[rstest]
1504 #[case::deterministic(deterministic::Runner::default())]
1505 #[case::tokio(tokio::Runner::default())]
1506 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1507 #[should_panic(expected = "blah")]
1508 fn test_panic_aborts_spawn<R: Runner>(#[case] runner: R)
1509 where
1510 R::Context: Spawner + Clock,
1511 {
1512 runner.start(|context| async move {
1513 context.child("panic").spawn(|_| async move {
1514 panic!("blah");
1515 });
1516
1517 loop {
1519 context.sleep(Duration::from_millis(100)).await;
1520 }
1521 });
1522 }
1523
1524 #[rstest]
1525 #[case::deterministic(deterministic::Runner::new(
1526 deterministic::Config::default().with_catch_panics(true)
1527 ))]
1528 #[case::tokio(tokio::Runner::new(tokio::Config::default().with_catch_panics(true)))]
1529 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::new(iouring::Config::default().with_catch_panics(true))))]
1530 fn test_panic_aborts_spawn_caught<R: Runner>(#[case] runner: R)
1531 where
1532 R::Context: Spawner + Clock,
1533 {
1534 let result: Result<(), Error> = runner.start(|context| async move {
1535 let result = context.child("panic").spawn(|_| async move {
1536 panic!("blah");
1537 });
1538 result.await
1539 });
1540 assert!(matches!(result, Err(Error::Exited)));
1541 }
1542
1543 #[rstest]
1544 #[case::deterministic(deterministic::Runner::default())]
1545 #[case::tokio(tokio::Runner::default())]
1546 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1547 #[should_panic(expected = "boom")]
1548 fn test_multiple_panics<R: Runner>(#[case] runner: R)
1549 where
1550 R::Context: Spawner + Clock,
1551 {
1552 runner.start(|context| async move {
1553 context.child("panic").spawn(|_| async move {
1554 panic!("boom 1");
1555 });
1556 context.child("panic").spawn(|_| async move {
1557 panic!("boom 2");
1558 });
1559 context.child("panic").spawn(|_| async move {
1560 panic!("boom 3");
1561 });
1562
1563 loop {
1565 context.sleep(Duration::from_millis(100)).await;
1566 }
1567 });
1568 }
1569
1570 #[rstest]
1571 #[case::deterministic(deterministic::Runner::new(
1572 deterministic::Config::default().with_catch_panics(true)
1573 ))]
1574 #[case::tokio(tokio::Runner::new(tokio::Config::default().with_catch_panics(true)))]
1575 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::new(iouring::Config::default().with_catch_panics(true))))]
1576 fn test_multiple_panics_caught<R: Runner>(#[case] runner: R)
1577 where
1578 R::Context: Spawner + Clock,
1579 {
1580 let (res1, res2, res3) = runner.start(|context| async move {
1581 let handle1 = context.child("panic").spawn(|_| async move {
1582 panic!("boom 1");
1583 });
1584 let handle2 = context.child("panic").spawn(|_| async move {
1585 panic!("boom 2");
1586 });
1587 let handle3 = context.child("panic").spawn(|_| async move {
1588 panic!("boom 3");
1589 });
1590
1591 join!(handle1, handle2, handle3)
1592 });
1593 assert!(matches!(res1, Err(Error::Exited)));
1594 assert!(matches!(res2, Err(Error::Exited)));
1595 assert!(matches!(res3, Err(Error::Exited)));
1596 }
1597
1598 #[rstest]
1599 #[case::deterministic(deterministic::Runner::default())]
1600 #[case::tokio(tokio::Runner::default())]
1601 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1602 fn test_select<R: Runner>(#[case] runner: R) {
1603 runner.start(|_| async move {
1604 let output = Mutex::new(0);
1606 select! {
1607 v1 = ready(1) => {
1608 *output.lock() = v1;
1609 },
1610 v2 = ready(2) => {
1611 *output.lock() = v2;
1612 },
1613 };
1614 assert_eq!(*output.lock(), 1);
1615
1616 select! {
1618 v1 = std::future::pending::<i32>() => {
1619 *output.lock() = v1;
1620 },
1621 v2 = ready(2) => {
1622 *output.lock() = v2;
1623 },
1624 };
1625 assert_eq!(*output.lock(), 2);
1626 });
1627 }
1628
1629 #[rstest]
1631 #[case::deterministic(deterministic::Runner::default())]
1632 #[case::tokio(tokio::Runner::default())]
1633 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1634 fn test_select_loop<R: Runner>(#[case] runner: R)
1635 where
1636 R::Context: Clock,
1637 {
1638 runner.start(|context| async move {
1639 let (sender, mut receiver) = mpsc::unbounded_channel();
1641 for _ in 0..2 {
1642 select! {
1643 v = receiver.recv() => {
1644 panic!("unexpected value: {v:?}");
1645 },
1646 _ = context.sleep(Duration::from_millis(100)) => {
1647 continue;
1648 },
1649 };
1650 }
1651
1652 sender.send(0).unwrap();
1654 sender.send(1).unwrap();
1655
1656 select! {
1658 _ = async {} => {
1659 },
1661 v = receiver.recv() => {
1662 panic!("unexpected value: {v:?}");
1663 },
1664 };
1665
1666 for i in 0..2 {
1668 select! {
1669 _ = context.sleep(Duration::from_millis(100)) => {
1670 panic!("timeout");
1671 },
1672 v = receiver.recv() => {
1673 assert_eq!(v.unwrap(), i);
1674 },
1675 };
1676 }
1677 });
1678 }
1679
1680 #[rstest]
1681 #[case::deterministic(deterministic::Runner::default())]
1682 #[case::tokio(tokio::Runner::default())]
1683 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1684 fn test_duplicate_open_returns_error<R: Runner>(#[case] runner: R)
1685 where
1686 R::Context: Storage,
1687 {
1688 runner.start(|context| async move {
1689 let partition = "duplicate_open";
1691 let name = b"blob";
1692 let (first, _) = context.open(partition, name).await.unwrap();
1693 let first = Arc::new(first);
1694 first
1695 .write_at(0, b"old", WriteOptions::default())
1696 .await
1697 .unwrap();
1698 let retained = Arc::clone(&first);
1699 assert!(matches!(
1700 context.open(partition, name).await,
1701 Err(Error::BlobAlreadyOpen(p, n)) if p == partition && n == "626c6f62"
1702 ));
1703
1704 drop(first);
1707 assert!(matches!(
1708 context.open_versioned(
1709 partition, name, DEFAULT_BLOB_VERSION..=DEFAULT_BLOB_VERSION
1710 ).await,
1711 Err(Error::BlobAlreadyOpen(p, n)) if p == partition && n == "626c6f62"
1712 ));
1713 assert_eq!(
1714 retained
1715 .read_at(0, 3, ReadOptions::default())
1716 .await
1717 .unwrap()
1718 .coalesce(),
1719 b"old".as_slice()
1720 );
1721 retained
1722 .write_at(0, b"new", WriteOptions::default())
1723 .await
1724 .unwrap();
1725 retained.sync().await.unwrap();
1726 drop(retained);
1727
1728 let (old, size) = context.open(partition, name).await.unwrap();
1730 assert_eq!(size, 3);
1731 assert_eq!(
1732 old.read_at(0, 3, ReadOptions::default())
1733 .await
1734 .unwrap()
1735 .coalesce(),
1736 b"new".as_slice()
1737 );
1738
1739 context.remove(partition, Some(name)).await.unwrap();
1742 let (current, size) = context.open(partition, name).await.unwrap();
1743 assert_eq!(size, 0);
1744 drop(old);
1745 assert!(matches!(
1746 context.open(partition, name).await,
1747 Err(Error::BlobAlreadyOpen(p, n)) if p == partition && n == "626c6f62"
1748 ));
1749 current
1750 .write_at(0, b"replacement", WriteOptions::default())
1751 .await
1752 .unwrap();
1753 current.sync().await.unwrap();
1754 drop(current);
1755
1756 let (reopened, size) = context.open(partition, name).await.unwrap();
1758 assert_eq!(size, 11);
1759 assert_eq!(
1760 reopened
1761 .read_at(0, 11, ReadOptions::default())
1762 .await
1763 .unwrap()
1764 .coalesce(),
1765 b"replacement".as_slice()
1766 );
1767 });
1768 }
1769
1770 #[rstest]
1771 #[case::deterministic(deterministic::Runner::default())]
1772 #[case::tokio(tokio::Runner::default())]
1773 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1774 fn test_storage_operations<R: Runner>(#[case] runner: R)
1775 where
1776 R::Context: Storage,
1777 {
1778 runner.start(|context| async move {
1779 let partition = "test_partition";
1780 let name = b"test_blob";
1781
1782 let (blob, size) = context
1784 .open(partition, name)
1785 .await
1786 .expect("Failed to open blob");
1787 assert_eq!(size, 0, "new blob should have size 0");
1788
1789 let data = b"Hello, Storage!";
1791 blob.write_at(0, data, WriteOptions::default())
1792 .await
1793 .expect("Failed to write to blob");
1794
1795 blob.sync().await.expect("Failed to sync blob");
1797
1798 let read = blob
1800 .read_at(0, data.len(), ReadOptions::default())
1801 .await
1802 .expect("Failed to read from blob");
1803 assert_eq!(read.coalesce(), data);
1804
1805 blob.sync().await.expect("Failed to sync blob");
1807
1808 let blobs = context
1810 .scan(partition)
1811 .await
1812 .expect("Failed to scan partition");
1813 assert!(blobs.contains(&name.to_vec()));
1814
1815 drop(blob);
1817 let (blob, len) = context
1818 .open(partition, name)
1819 .await
1820 .expect("Failed to reopen blob");
1821 assert_eq!(len, data.len() as u64);
1822
1823 let read = blob
1825 .read_at(7, 7, ReadOptions::default())
1826 .await
1827 .expect("Failed to read data");
1828 assert_eq!(read.coalesce(), b"Storage");
1829
1830 blob.sync().await.expect("Failed to sync blob");
1832
1833 context
1835 .remove(partition, Some(name))
1836 .await
1837 .expect("Failed to remove blob");
1838
1839 let blobs = context
1841 .scan(partition)
1842 .await
1843 .expect("Failed to scan partition");
1844 assert!(!blobs.contains(&name.to_vec()));
1845
1846 context
1848 .remove(partition, None)
1849 .await
1850 .expect("Failed to remove partition");
1851
1852 let result = context.scan(partition).await;
1854 assert!(matches!(result, Err(Error::PartitionMissing(_))));
1855 });
1856 }
1857
1858 #[rstest]
1859 #[case::deterministic(deterministic::Runner::default())]
1860 #[case::tokio(tokio::Runner::default())]
1861 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1862 fn test_blob_read_write<R: Runner>(#[case] runner: R)
1863 where
1864 R::Context: Storage,
1865 {
1866 runner.start(|context| async move {
1867 let partition = "test_partition";
1868 let name = b"test_blob_rw";
1869
1870 let (blob, _) = context
1872 .open(partition, name)
1873 .await
1874 .expect("Failed to open blob");
1875
1876 let data1 = b"Hello";
1878 let data2 = b"World";
1879 blob.write_at(0, data1, WriteOptions::default())
1880 .await
1881 .expect("Failed to write data1");
1882 blob.write_at(5, data2, WriteOptions::default())
1883 .await
1884 .expect("Failed to write data2");
1885
1886 let read = blob
1888 .read_at(0, 10, ReadOptions::default())
1889 .await
1890 .expect("Failed to read data");
1891 let read = read.coalesce();
1892 assert_eq!(&read.as_ref()[..5], data1);
1893 assert_eq!(&read.as_ref()[5..], data2);
1894
1895 let result = blob.read_at(10, 10, ReadOptions::default()).await;
1897 assert!(result.is_err());
1898
1899 let data3 = b"Store";
1901 blob.write_at(5, data3, WriteOptions::default())
1902 .await
1903 .expect("Failed to write data3");
1904
1905 let read = blob
1907 .read_at(0, 10, ReadOptions::default())
1908 .await
1909 .expect("Failed to read data");
1910 let read = read.coalesce();
1911 assert_eq!(&read.as_ref()[..5], data1);
1912 assert_eq!(&read.as_ref()[5..], data3);
1913
1914 let result = blob.read_at(10, 10, ReadOptions::default()).await;
1916 assert!(result.is_err());
1917 });
1918 }
1919
1920 #[rstest]
1921 #[case::deterministic(deterministic::Runner::default())]
1922 #[case::tokio(tokio::Runner::default())]
1923 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1924 fn test_blob_resize<R: Runner>(#[case] runner: R)
1925 where
1926 R::Context: Storage,
1927 {
1928 runner.start(|context| async move {
1929 let partition = "test_partition_resize";
1930 let name = b"test_blob_resize";
1931
1932 let (blob, _) = context
1934 .open(partition, name)
1935 .await
1936 .expect("Failed to open blob");
1937
1938 let data = b"some data";
1939 blob.write_at(0, data.to_vec(), WriteOptions::default())
1940 .await
1941 .expect("Failed to write");
1942 blob.sync().await.expect("Failed to sync after write");
1943
1944 drop(blob);
1946 let (blob, len) = context.open(partition, name).await.unwrap();
1947 assert_eq!(len, data.len() as u64);
1948
1949 let new_len = (data.len() as u64) * 2;
1951 blob.resize(new_len)
1952 .await
1953 .expect("Failed to resize to extend");
1954 blob.sync().await.expect("Failed to sync after resize");
1955
1956 drop(blob);
1958 let (blob, len) = context.open(partition, name).await.unwrap();
1959 assert_eq!(len, new_len);
1960
1961 let read_buf = blob
1963 .read_at(0, data.len(), ReadOptions::default())
1964 .await
1965 .unwrap();
1966 assert_eq!(read_buf.coalesce(), data);
1967
1968 let extended_part = blob
1970 .read_at(data.len() as u64, data.len(), ReadOptions::default())
1971 .await
1972 .unwrap();
1973 assert_eq!(extended_part.coalesce(), vec![0; data.len()].as_slice());
1974
1975 blob.resize(data.len() as u64).await.unwrap();
1977 blob.sync().await.unwrap();
1978
1979 drop(blob);
1981 let (blob, size) = context.open(partition, name).await.unwrap();
1982 assert_eq!(size, data.len() as u64);
1983
1984 let read_buf = blob
1986 .read_at(0, data.len(), ReadOptions::default())
1987 .await
1988 .unwrap();
1989 assert_eq!(read_buf.coalesce(), data);
1990 blob.sync().await.unwrap();
1991 });
1992 }
1993
1994 #[rstest]
1995 #[case::deterministic(deterministic::Runner::default())]
1996 #[case::tokio(tokio::Runner::default())]
1997 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
1998 fn test_many_partition_read_write<R: Runner>(#[case] runner: R)
1999 where
2000 R::Context: Storage,
2001 {
2002 runner.start(|context| async move {
2003 let partitions = ["partition1", "partition2", "partition3"];
2004 let name = b"test_blob_rw";
2005 let data1 = b"Hello";
2006 let data2 = b"World";
2007
2008 for (additional, partition) in partitions.iter().enumerate() {
2009 let (blob, _) = context
2011 .open(partition, name)
2012 .await
2013 .expect("Failed to open blob");
2014
2015 blob.write_at(0, data1, WriteOptions::default())
2017 .await
2018 .expect("Failed to write data1");
2019 blob.write_at(5 + additional as u64, data2, WriteOptions::default())
2020 .await
2021 .expect("Failed to write data2");
2022
2023 blob.sync().await.expect("Failed to sync blob");
2025 }
2026
2027 for (additional, partition) in partitions.iter().enumerate() {
2028 let (blob, len) = context
2030 .open(partition, name)
2031 .await
2032 .expect("Failed to open blob");
2033 assert_eq!(len, (data1.len() + data2.len() + additional) as u64);
2034
2035 let read = blob
2037 .read_at(0, 10 + additional, ReadOptions::default())
2038 .await
2039 .expect("Failed to read data");
2040 let read = read.coalesce();
2041 assert_eq!(&read.as_ref()[..5], b"Hello");
2042 assert_eq!(&read.as_ref()[5 + additional..], b"World");
2043 }
2044 });
2045 }
2046
2047 #[rstest]
2048 #[case::deterministic(deterministic::Runner::default())]
2049 #[case::tokio(tokio::Runner::default())]
2050 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2051 fn test_blob_read_past_length<R: Runner>(#[case] runner: R)
2052 where
2053 R::Context: Storage,
2054 {
2055 runner.start(|context| async move {
2056 let partition = "test_partition";
2057 let name = b"test_blob_rw";
2058
2059 let (blob, _) = context
2061 .open(partition, name)
2062 .await
2063 .expect("Failed to open blob");
2064
2065 let result = blob.read_at(0, 10, ReadOptions::default()).await;
2067 assert!(result.is_err());
2068
2069 let data = b"Hello, Storage!".to_vec();
2071 blob.write_at(0, data, WriteOptions::default())
2072 .await
2073 .expect("Failed to write to blob");
2074
2075 let result = blob.read_at(0, 20, ReadOptions::default()).await;
2077 assert!(result.is_err());
2078 })
2079 }
2080
2081 #[rstest]
2082 #[case::deterministic(deterministic::Runner::default())]
2083 #[case::tokio(tokio::Runner::default())]
2084 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2085 fn test_blob_shared_concurrent_read<R: Runner>(#[case] runner: R)
2086 where
2087 R::Context: Spawner + Storage + Metrics,
2088 {
2089 runner.start(|context| async move {
2090 let partition = "test_partition";
2091 let name = b"test_blob_rw";
2092
2093 let (blob, _) = context
2095 .open(partition, name)
2096 .await
2097 .expect("Failed to open blob");
2098 let blob = Arc::new(blob);
2099
2100 let data = b"Hello, Storage!";
2102 blob.write_at(0, data, WriteOptions::default())
2103 .await
2104 .expect("Failed to write to blob");
2105
2106 blob.sync().await.expect("Failed to sync blob");
2108
2109 let check1 = context.child("check1").spawn({
2111 let blob = blob.clone();
2112 let data_len = data.len();
2113 move |_| async move {
2114 let read = blob
2115 .read_at(0, data_len, ReadOptions::default())
2116 .await
2117 .expect("Failed to read from blob");
2118 assert_eq!(read.coalesce(), data);
2119 }
2120 });
2121 let check2 = context.child("check2").spawn({
2122 let blob = blob.clone();
2123 let data_len = data.len();
2124 move |_| async move {
2125 let read = blob
2126 .read_at(0, data_len, ReadOptions::default())
2127 .await
2128 .expect("Failed to read from blob");
2129 assert_eq!(read.coalesce(), data);
2130 }
2131 });
2132
2133 let result = join!(check1, check2);
2135 assert!(result.0.is_ok());
2136 assert!(result.1.is_ok());
2137
2138 let read = blob
2140 .read_at(0, data.len(), ReadOptions::default())
2141 .await
2142 .expect("Failed to read from blob");
2143 assert_eq!(read.coalesce(), data);
2144
2145 drop(blob);
2147
2148 let buffer = context.encode();
2150 assert!(buffer.contains("open_blobs 0"));
2151 });
2152 }
2153
2154 #[rstest]
2155 #[case::deterministic(deterministic::Runner::default())]
2156 #[case::tokio(tokio::Runner::default())]
2157 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2158 fn test_shutdown<R: Runner>(#[case] runner: R)
2159 where
2160 R::Context: Spawner + Metrics + Clock,
2161 {
2162 let kill = 9;
2163 runner.start(|context| async move {
2164 let before = context.child("before").spawn(move |context| async move {
2166 let mut signal = context.stopped();
2167 let value = (&mut signal).await.unwrap();
2168 assert_eq!(value, kill);
2169 drop(signal);
2170 });
2171
2172 let result = context.child("stop").stop(kill, None).await;
2174 assert!(result.is_ok());
2175
2176 let after = context.child("after").spawn(move |context| async move {
2178 let value = context.stopped().await.unwrap();
2180 assert_eq!(value, kill);
2181 });
2182
2183 let result = join!(before, after);
2185 assert!(result.0.is_ok());
2186 assert!(result.1.is_ok());
2187 });
2188 }
2189
2190 #[rstest]
2191 #[case::deterministic(deterministic::Runner::default())]
2192 #[case::tokio(tokio::Runner::default())]
2193 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2194 fn test_shutdown_multiple_signals<R: Runner>(#[case] runner: R)
2195 where
2196 R::Context: Spawner + Metrics + Clock,
2197 {
2198 let kill = 42;
2199 runner.start(|context| async move {
2200 let (started_tx, mut started_rx) = mpsc::channel(3);
2201 let counter = Arc::new(AtomicU32::new(0));
2202
2203 let task = |context: R::Context, cleanup_duration: Duration| {
2206 let counter = counter.clone();
2207 let started_tx = started_tx.clone();
2208 context.spawn(move |context| async move {
2209 let mut signal = context.stopped();
2211 started_tx.send(()).await.unwrap();
2212
2213 let value = (&mut signal).await.unwrap();
2215 assert_eq!(value, kill);
2216 context.sleep(cleanup_duration).await;
2217 counter.fetch_add(1, Ordering::SeqCst);
2218
2219 drop(signal);
2221 })
2222 };
2223
2224 let task1 = task(context.child("cleanup"), Duration::from_millis(10));
2225 let task2 = task(context.child("cleanup"), Duration::from_millis(20));
2226 let task3 = task(context.child("cleanup"), Duration::from_millis(30));
2227
2228 for _ in 0..3 {
2230 started_rx.recv().await.unwrap();
2231 }
2232
2233 context.stop(kill, None).await.unwrap();
2235 assert_eq!(counter.load(Ordering::SeqCst), 3);
2236
2237 let result = join!(task1, task2, task3);
2239 assert!(result.0.is_ok());
2240 assert!(result.1.is_ok());
2241 assert!(result.2.is_ok());
2242 });
2243 }
2244
2245 #[rstest]
2246 #[case::deterministic(deterministic::Runner::default())]
2247 #[case::tokio(tokio::Runner::default())]
2248 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2249 fn test_shutdown_timeout<R: Runner>(#[case] runner: R)
2250 where
2251 R::Context: Spawner + Metrics + Clock,
2252 {
2253 let kill = 42;
2254 runner.start(|context| async move {
2255 let (started_tx, started_rx) = oneshot::channel();
2257
2258 context.child("signal").spawn(move |context| async move {
2260 let signal = context.stopped();
2261 started_tx.send(()).unwrap();
2262 pending::<()>().await;
2263 signal.await.unwrap();
2264 });
2265
2266 started_rx.await.unwrap();
2268 let result = context.stop(kill, Some(Duration::from_millis(100))).await;
2269
2270 assert!(matches!(result, Err(Error::Timeout)));
2272 });
2273 }
2274
2275 #[rstest]
2276 #[case::deterministic(deterministic::Runner::default())]
2277 #[case::tokio(tokio::Runner::default())]
2278 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2279 fn test_shutdown_multiple_stop_calls<R: Runner>(#[case] runner: R)
2280 where
2281 R::Context: Spawner + Metrics + Clock,
2282 {
2283 let kill1 = 42;
2284 let kill2 = 43;
2285
2286 runner.start(|context| async move {
2287 let (started_tx, started_rx) = oneshot::channel();
2288 let counter = Arc::new(AtomicU32::new(0));
2289
2290 let task = context.child("blocking_task").spawn({
2292 let counter = counter.clone();
2293 move |context| async move {
2294 let mut signal = context.stopped();
2296 started_tx.send(()).unwrap();
2297
2298 let value = (&mut signal).await.unwrap();
2300 assert_eq!(value, kill1);
2301 context.sleep(Duration::from_millis(50)).await;
2302
2303 counter.fetch_add(1, Ordering::SeqCst);
2305 drop(signal);
2306 }
2307 });
2308
2309 started_rx.await.unwrap();
2311
2312 let stop_task1 = context.child("stop").stop(kill1, None);
2315 pin_mut!(stop_task1);
2316 let stop_task2 = context.child("stop").stop(kill2, None);
2317 pin_mut!(stop_task2);
2318
2319 assert!(stop_task1.as_mut().now_or_never().is_none());
2321 assert!(stop_task2.as_mut().now_or_never().is_none());
2322
2323 assert!(stop_task1.await.is_ok());
2325 assert!(stop_task2.await.is_ok());
2326
2327 let sig = context.stopped().await;
2329 assert_eq!(sig.unwrap(), kill1);
2330
2331 let result = task.await;
2333 assert!(result.is_ok());
2334 assert_eq!(counter.load(Ordering::SeqCst), 1);
2335
2336 assert!(context.stop(kill2, None).now_or_never().unwrap().is_ok());
2338 });
2339 }
2340
2341 #[rstest]
2342 #[case::deterministic(deterministic::Runner::default())]
2343 #[case::tokio(tokio::Runner::default())]
2344 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2345 fn test_unfulfilled_shutdown<R: Runner>(#[case] runner: R)
2346 where
2347 R::Context: Spawner + Metrics,
2348 {
2349 runner.start(|context| async move {
2350 context.child("before").spawn(move |context| async move {
2352 let mut signal = context.stopped();
2353 let value = (&mut signal).await.unwrap();
2354
2355 assert_eq!(value, 42);
2357 drop(signal);
2358 });
2359
2360 reschedule().await;
2362 });
2363 }
2364
2365 #[rstest]
2366 #[case::deterministic(deterministic::Runner::default())]
2367 #[case::tokio(tokio::Runner::default())]
2368 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2369 fn test_spawn_dedicated<R: Runner>(#[case] runner: R)
2370 where
2371 R::Context: Spawner,
2372 {
2373 runner.start(|context| async move {
2374 let handle = context.dedicated().spawn(|_| async move { 42 });
2375 assert!(matches!(handle.await, Ok(42)));
2376 });
2377 }
2378
2379 #[rstest]
2380 #[case::deterministic(deterministic::Runner::default())]
2381 #[case::tokio(tokio::Runner::default())]
2382 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2383 fn test_spawn<R: Runner>(#[case] runner: R)
2384 where
2385 R::Context: Spawner + Clock,
2386 {
2387 runner.start(|context| async move {
2388 let child_handle = Arc::new(Mutex::new(None));
2389 let child_handle2 = child_handle.clone();
2390
2391 let (parent_initialized_tx, parent_initialized_rx) = oneshot::channel();
2392 let (parent_complete_tx, parent_complete_rx) = oneshot::channel();
2393 let parent_handle = context.spawn(move |context| async move {
2394 let handle = context.spawn(|_| async {});
2396
2397 *child_handle2.lock() = Some(handle);
2399
2400 parent_initialized_tx.send(()).unwrap();
2401
2402 parent_complete_rx.await.unwrap();
2404 });
2405
2406 parent_initialized_rx.await.unwrap();
2408
2409 let child_handle = child_handle.lock().take().unwrap();
2411 assert!(child_handle.await.is_ok());
2412
2413 parent_complete_tx.send(()).unwrap();
2415
2416 assert!(parent_handle.await.is_ok());
2418 });
2419 }
2420
2421 #[rstest]
2422 #[case::deterministic(deterministic::Runner::default())]
2423 #[case::tokio(tokio::Runner::default())]
2424 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2425 fn test_spawn_abort_on_parent_abort<R: Runner>(#[case] runner: R)
2426 where
2427 R::Context: Spawner + Clock,
2428 {
2429 runner.start(|context| async move {
2430 let child_handle = Arc::new(Mutex::new(None));
2431 let child_handle2 = child_handle.clone();
2432
2433 let (parent_initialized_tx, parent_initialized_rx) = oneshot::channel();
2434 let parent_handle = context.spawn(move |context| async move {
2435 let handle = context.spawn(|_| pending::<()>());
2437
2438 *child_handle2.lock() = Some(handle);
2440
2441 parent_initialized_tx.send(()).unwrap();
2442
2443 pending::<()>().await
2445 });
2446
2447 parent_initialized_rx.await.unwrap();
2449
2450 parent_handle.abort();
2452 assert!(matches!(parent_handle.await, Err(Error::Closed)));
2453
2454 let child_handle = child_handle.lock().take().unwrap();
2456 assert!(matches!(child_handle.await, Err(Error::Closed)));
2457 });
2458 }
2459
2460 #[rstest]
2461 #[case::deterministic(deterministic::Runner::default())]
2462 #[case::tokio(tokio::Runner::default())]
2463 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2464 fn test_spawn_abort_on_parent_completion<R: Runner>(#[case] runner: R)
2465 where
2466 R::Context: Spawner + Clock,
2467 {
2468 runner.start(|context| async move {
2469 let child_handle = Arc::new(Mutex::new(None));
2470 let child_handle2 = child_handle.clone();
2471
2472 let (parent_complete_tx, parent_complete_rx) = oneshot::channel();
2473 let parent_handle = context.spawn(move |context| async move {
2474 let handle = context.spawn(|_| pending::<()>());
2476
2477 *child_handle2.lock() = Some(handle);
2479
2480 parent_complete_rx.await.unwrap();
2482 });
2483
2484 parent_complete_tx.send(()).unwrap();
2486
2487 assert!(parent_handle.await.is_ok());
2489
2490 let child_handle = child_handle.lock().take().unwrap();
2492 assert!(matches!(child_handle.await, Err(Error::Closed)));
2493 });
2494 }
2495
2496 #[rstest]
2497 #[case::deterministic(deterministic::Runner::default())]
2498 #[case::tokio(tokio::Runner::default())]
2499 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2500 fn test_spawn_cascading_abort<R: Runner>(#[case] runner: R)
2501 where
2502 R::Context: Spawner + Clock,
2503 {
2504 runner.start(|context| async move {
2505 let c0 = context.child("c0");
2515 let g0 = c0.child("g0");
2516 let g1 = c0.child("g1");
2517 let c1 = context.child("c1");
2518 let g2 = c1.child("g2");
2519 let g3 = c1.child("g3");
2520 let c2 = context.child("c2");
2521 let g4 = c2.child("g4");
2522 let g5 = c2.child("g5");
2523
2524 let handles = Arc::new(Mutex::new(Vec::new()));
2526 let (initialized_tx, mut initialized_rx) = mpsc::channel(9);
2527 let root_task = context.spawn({
2528 let handles = handles.clone();
2529 move |_| async move {
2530 for (context, grandchildren) in [(c0, [g0, g1]), (c1, [g2, g3]), (c2, [g4, g5])]
2531 {
2532 let handle = context.spawn({
2533 let handles = handles.clone();
2534 let initialized_tx = initialized_tx.clone();
2535 move |_| async move {
2536 for grandchild in grandchildren {
2537 let handle = grandchild.spawn(|_| async {
2538 pending::<()>().await;
2539 });
2540 handles.lock().push(handle);
2541 initialized_tx.send(()).await.unwrap();
2542 }
2543
2544 pending::<()>().await;
2545 }
2546 });
2547 handles.lock().push(handle);
2548 initialized_tx.send(()).await.unwrap();
2549 }
2550
2551 pending::<()>().await;
2552 }
2553 });
2554
2555 for _ in 0..9 {
2557 initialized_rx.recv().await.unwrap();
2558 }
2559
2560 assert_eq!(handles.lock().len(), 9);
2562
2563 root_task.abort();
2565 assert!(matches!(root_task.await, Err(Error::Closed)));
2566
2567 let handles = handles.lock().drain(..).collect::<Vec<_>>();
2569 for handle in handles {
2570 assert!(matches!(handle.await, Err(Error::Closed)));
2571 }
2572 });
2573 }
2574
2575 #[rstest]
2576 #[case::deterministic(deterministic::Runner::default())]
2577 #[case::tokio(tokio::Runner::default())]
2578 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2579 fn test_child_survives_sibling_completion<R: Runner>(#[case] runner: R)
2580 where
2581 R::Context: Spawner + Clock,
2582 {
2583 runner.start(|context| async move {
2584 let (child_started_tx, child_started_rx) = oneshot::channel();
2585 let (child_complete_tx, child_complete_rx) = oneshot::channel();
2586 let (child_handle_tx, child_handle_rx) = oneshot::channel();
2587 let (sibling_started_tx, sibling_started_rx) = oneshot::channel();
2588 let (sibling_complete_tx, sibling_complete_rx) = oneshot::channel();
2589 let (sibling_handle_tx, sibling_handle_rx) = oneshot::channel();
2590 let (parent_complete_tx, parent_complete_rx) = oneshot::channel();
2591
2592 let parent = context.spawn(move |context| async move {
2593 let child_handle = context.child("child").spawn(|_| async move {
2595 child_started_tx.send(()).unwrap();
2596 child_complete_rx.await.unwrap();
2598 });
2599 assert!(
2600 child_handle_tx.send(child_handle).is_ok(),
2601 "child handle receiver dropped"
2602 );
2603
2604 let sibling_handle = context.child("sibling").spawn(move |_| async move {
2606 sibling_started_tx.send(()).unwrap();
2607 sibling_complete_rx.await.unwrap();
2609 });
2610 assert!(
2611 sibling_handle_tx.send(sibling_handle).is_ok(),
2612 "sibling handle receiver dropped"
2613 );
2614
2615 parent_complete_rx.await.unwrap();
2617 });
2618
2619 child_started_rx.await.unwrap();
2621 sibling_started_rx.await.unwrap();
2622
2623 sibling_complete_tx.send(()).unwrap();
2625 assert!(sibling_handle_rx.await.is_ok());
2626
2627 child_complete_tx.send(()).unwrap();
2629 assert!(child_handle_rx.await.is_ok());
2630
2631 parent_complete_tx.send(()).unwrap();
2633 assert!(parent.await.is_ok());
2634 });
2635 }
2636
2637 #[rstest]
2638 #[case::deterministic(deterministic::Runner::default())]
2639 #[case::tokio(tokio::Runner::default())]
2640 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2641 fn test_spawn_clone_chain<R: Runner>(#[case] runner: R)
2642 where
2643 R::Context: Spawner + Clock,
2644 {
2645 runner.start(|context| async move {
2646 let (parent_started_tx, parent_started_rx) = oneshot::channel();
2647 let (child_started_tx, child_started_rx) = oneshot::channel();
2648 let (grandchild_started_tx, grandchild_started_rx) = oneshot::channel();
2649 let (child_handle_tx, child_handle_rx) = oneshot::channel();
2650 let (grandchild_handle_tx, grandchild_handle_rx) = oneshot::channel();
2651
2652 let parent = context.child("parent").spawn({
2653 move |context| async move {
2654 let child = context.child("child").spawn({
2655 move |context| async move {
2656 let grandchild = context.child("grandchild").spawn({
2657 move |_| async move {
2658 grandchild_started_tx.send(()).unwrap();
2659 pending::<()>().await;
2660 }
2661 });
2662 assert!(
2663 grandchild_handle_tx.send(grandchild).is_ok(),
2664 "grandchild handle receiver dropped"
2665 );
2666 child_started_tx.send(()).unwrap();
2667 pending::<()>().await;
2668 }
2669 });
2670 assert!(
2671 child_handle_tx.send(child).is_ok(),
2672 "child handle receiver dropped"
2673 );
2674 parent_started_tx.send(()).unwrap();
2675 pending::<()>().await;
2676 }
2677 });
2678
2679 parent_started_rx.await.unwrap();
2680 child_started_rx.await.unwrap();
2681 grandchild_started_rx.await.unwrap();
2682
2683 let child_handle = child_handle_rx.await.unwrap();
2684 let grandchild_handle = grandchild_handle_rx.await.unwrap();
2685
2686 parent.abort();
2687 assert!(parent.await.is_err());
2688
2689 assert!(child_handle.await.is_err());
2690 assert!(grandchild_handle.await.is_err());
2691 });
2692 }
2693
2694 #[rstest]
2695 #[case::deterministic(deterministic::Runner::default())]
2696 #[case::tokio(tokio::Runner::default())]
2697 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2698 fn test_spawn_sparse_clone_chain<R: Runner>(#[case] runner: R)
2699 where
2700 R::Context: Spawner + Clock,
2701 {
2702 runner.start(|context| async move {
2703 let (leaf_started_tx, leaf_started_rx) = oneshot::channel();
2704 let (leaf_handle_tx, leaf_handle_rx) = oneshot::channel();
2705
2706 let parent = context.child("parent").spawn({
2707 move |context| async move {
2708 let clone1 = context.child("clone1");
2709 let clone2 = clone1.child("clone2");
2710 let clone3 = clone2.child("clone3");
2711
2712 let leaf = clone3.spawn({
2713 move |_| async move {
2714 leaf_started_tx.send(()).unwrap();
2715 pending::<()>().await;
2716 }
2717 });
2718
2719 leaf_handle_tx
2720 .send(leaf)
2721 .unwrap_or_else(|_| panic!("leaf handle receiver dropped"));
2722 pending::<()>().await;
2723 }
2724 });
2725
2726 leaf_started_rx.await.unwrap();
2727 let leaf_handle = leaf_handle_rx.await.unwrap();
2728
2729 parent.abort();
2730 assert!(parent.await.is_err());
2731 assert!(leaf_handle.await.is_err());
2732 });
2733 }
2734
2735 #[rstest]
2736 #[case::deterministic(deterministic::Runner::default())]
2737 #[case::tokio(tokio::Runner::default())]
2738 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2739 fn test_spawn_blocking<R: Runner>(
2740 #[case] runner: R,
2741 #[values(Execution::Shared(true), Execution::Dedicated)] execution: Execution,
2742 ) where
2743 R::Context: Spawner,
2744 {
2745 runner.start(|context| async move {
2746 let context = match execution {
2747 Execution::Dedicated => context.dedicated(),
2748 Execution::Shared(blocking) => context.shared(blocking),
2749 };
2750
2751 let handle = context.spawn(|_| async move { 42 });
2752 let result = handle.await;
2753 assert!(matches!(result, Ok(42)));
2754 });
2755 }
2756
2757 #[rstest]
2758 #[case::deterministic(deterministic::Runner::default())]
2759 #[case::tokio(tokio::Runner::default())]
2760 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2761 #[should_panic(expected = "blocking task panicked")]
2762 fn test_spawn_blocking_panic<R: Runner>(
2763 #[case] runner: R,
2764 #[values(Execution::Shared(true), Execution::Dedicated)] execution: Execution,
2765 ) where
2766 R::Context: Spawner + Clock,
2767 {
2768 runner.start(|context| async move {
2769 let spawner = match execution {
2770 Execution::Dedicated => context.child("blocking").dedicated(),
2771 Execution::Shared(blocking) => context.child("blocking").shared(blocking),
2772 };
2773 spawner.spawn(|_| async move {
2774 panic!("blocking task panicked");
2775 });
2776
2777 loop {
2779 context.sleep(Duration::from_millis(100)).await;
2780 }
2781 });
2782 }
2783
2784 #[rstest]
2785 #[case::deterministic(deterministic::Runner::new(
2786 deterministic::Config::default().with_catch_panics(true)
2787 ))]
2788 #[case::tokio(tokio::Runner::new(tokio::Config::default().with_catch_panics(true)))]
2789 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::new(iouring::Config::default().with_catch_panics(true))))]
2790 fn test_spawn_blocking_panic_caught<R: Runner>(
2791 #[case] runner: R,
2792 #[values(Execution::Shared(true), Execution::Dedicated)] execution: Execution,
2793 ) where
2794 R::Context: Spawner + Clock,
2795 {
2796 let result: Result<(), Error> = runner.start(|context| async move {
2797 let spawner = match execution {
2798 Execution::Dedicated => context.child("blocking").dedicated(),
2799 Execution::Shared(blocking) => context.child("blocking").shared(blocking),
2800 };
2801 let handle = spawner.spawn(|_| async move {
2802 panic!("blocking task panicked");
2803 });
2804 handle.await
2805 });
2806 assert!(matches!(result, Err(Error::Exited)));
2807 }
2808
2809 #[rstest]
2810 #[case::deterministic(deterministic::Runner::default())]
2811 #[case::tokio(tokio::Runner::default())]
2812 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2813 fn test_circular_reference_prevents_cleanup<R: Runner>(#[case] runner: R) {
2814 runner.start(|_| async move {
2815 let dropper = Arc::new(());
2817 let executor = deterministic::Runner::default();
2818 executor.start({
2819 let dropper = dropper.clone();
2820 move |context| async move {
2821 let (setup_tx, mut setup_rx) = mpsc::unbounded_channel::<()>();
2823 let (tx1, mut rx1) = mpsc::unbounded_channel::<()>();
2824 let (tx2, mut rx2) = mpsc::unbounded_channel::<()>();
2825
2826 context.child("task1").spawn({
2828 let setup_tx = setup_tx.clone();
2829 let dropper = dropper.clone();
2830 move |_| async move {
2831 tx2.send(()).unwrap();
2833 rx1.recv().await.unwrap();
2834 setup_tx.send(()).unwrap();
2835
2836 while rx1.recv().await.is_some() {}
2838 drop(tx2);
2839 drop(dropper);
2840 }
2841 });
2842
2843 context.child("task2").spawn(move |_| async move {
2845 tx1.send(()).unwrap();
2847 rx2.recv().await.unwrap();
2848 setup_tx.send(()).unwrap();
2849
2850 while rx2.recv().await.is_some() {}
2852 drop(tx1);
2853 drop(dropper);
2854 });
2855
2856 setup_rx.recv().await.unwrap();
2858 setup_rx.recv().await.unwrap();
2859 }
2860 });
2861
2862 Arc::try_unwrap(dropper).expect("references remaining");
2864 });
2865 }
2866
2867 #[rstest]
2868 #[case::deterministic(deterministic::Runner::default())]
2869 #[case::tokio(tokio::Runner::default())]
2870 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2871 fn test_late_waker<R: Runner>(#[case] runner: R)
2872 where
2873 R::Context: Metrics + Spawner,
2874 {
2875 struct CaptureWaker {
2878 tx: Option<oneshot::Sender<Waker>>,
2879 sent: bool,
2880 }
2881 impl Future for CaptureWaker {
2882 type Output = ();
2883 fn poll(mut self: Pin<&mut Self>, cx: &mut TContext<'_>) -> Poll<Self::Output> {
2884 if !self.sent {
2885 if let Some(tx) = self.tx.take() {
2886 let _ = tx.send(cx.waker().clone());
2888 }
2889 self.sent = true;
2890 }
2891 Poll::Pending
2892 }
2893 }
2894
2895 struct WakeOnDrop(Option<Waker>);
2897 impl Drop for WakeOnDrop {
2898 fn drop(&mut self) {
2899 if let Some(w) = self.0.take() {
2900 w.wake_by_ref();
2901 }
2902 }
2903 }
2904
2905 let holder = runner.start(|context| async move {
2907 let (tx, rx) = oneshot::channel::<Waker>();
2909
2910 context.child("capture_waker").spawn(move |_| async move {
2912 CaptureWaker {
2913 tx: Some(tx),
2914 sent: false,
2915 }
2916 .await;
2917 });
2918
2919 utils::reschedule().await;
2921
2922 let waker = rx.await.expect("waker not received");
2924
2925 WakeOnDrop(Some(waker))
2927 });
2928
2929 drop(holder);
2932 }
2933
2934 #[rstest]
2935 #[case::deterministic(deterministic::Runner::default())]
2936 #[case::tokio(tokio::Runner::default())]
2937 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2938 fn test_factory_panic_finishes_running_metric<R: Runner>(#[case] runner: R)
2939 where
2940 R::Context: Spawner + Metrics,
2941 {
2942 runner.start(|context| async move {
2945 let child = context.child("panicking_factory");
2946 let panic = catch_unwind(AssertUnwindSafe(|| {
2947 child.spawn(|_| -> std::future::Ready<()> { panic!("factory failed") })
2948 }))
2949 .err()
2950 .expect("factory panic must reach its caller");
2951 assert_eq!(panic.downcast_ref::<&str>(), Some(&"factory failed"));
2952
2953 assert_eq!(count_running_tasks(&context, "panicking_factory"), 0);
2955 let buffer = context.encode();
2956 assert!(buffer.contains(
2957 "runtime_tasks_spawned_total{name=\"panicking_factory\",kind=\"Task\",execution=\"Shared\"} 1"
2958 ));
2959 });
2960 }
2961
2962 #[rstest]
2963 #[case::deterministic(deterministic::Runner::default())]
2964 #[case::tokio(tokio::Runner::default())]
2965 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
2966 fn test_metrics<R: Runner>(#[case] runner: R)
2967 where
2968 R::Context: Metrics,
2969 {
2970 runner.start(|context| async move {
2971 assert_eq!(context.name().label, "");
2973
2974 let counter = Counter::<u64>::default();
2976 let _registered = context.register("test", "test", counter.clone());
2977
2978 counter.inc();
2980
2981 let buffer = context.encode();
2983 assert!(buffer.contains("test_total 1"));
2984
2985 let context = context.child("nested");
2987 let nested_counter = Counter::<u64>::default();
2988 let _nested_registered = context.register("test", "test", nested_counter.clone());
2989
2990 nested_counter.inc();
2992
2993 let buffer = context.encode();
2995 assert!(buffer.contains("nested_test_total 1"));
2996 assert!(buffer.contains("test_total 1"));
2997 });
2998 }
2999
3000 #[rstest]
3001 #[case::deterministic(deterministic::Runner::default())]
3002 #[case::tokio(tokio::Runner::default())]
3003 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3004 fn test_metrics_with_attribute<R: Runner>(#[case] runner: R)
3005 where
3006 R::Context: Metrics,
3007 {
3008 runner.start(|context| async move {
3009 let ctx_epoch5 = context.child("consensus").with_attribute("epoch", "e5");
3011
3012 let counter = Counter::<u64>::default();
3014 let _epoch5 = ctx_epoch5.register("votes", "vote count", counter.clone());
3015 counter.inc();
3016
3017 let buffer = context.encode();
3019 assert!(
3020 buffer.contains("consensus_votes_total{epoch=\"e5\"} 1"),
3021 "Expected metric with epoch attribute, got: {}",
3022 buffer
3023 );
3024
3025 let ctx_epoch6 = context.child("consensus").with_attribute("epoch", "e6");
3027 let counter2 = Counter::<u64>::default();
3028 let _epoch6 = ctx_epoch6.register("votes", "vote count", counter2.clone());
3029 counter2.inc();
3030 counter2.inc();
3031
3032 let buffer = context.encode();
3034 assert!(
3035 buffer.contains("consensus_votes_total{epoch=\"e5\"} 1"),
3036 "Expected metric with epoch=e5, got: {}",
3037 buffer
3038 );
3039 assert!(
3040 buffer.contains("consensus_votes_total{epoch=\"e6\"} 2"),
3041 "Expected metric with epoch=e6, got: {}",
3042 buffer
3043 );
3044
3045 assert_eq!(
3047 buffer.matches("# HELP consensus_votes").count(),
3048 1,
3049 "HELP should appear exactly once, got: {}",
3050 buffer
3051 );
3052 assert_eq!(
3053 buffer.matches("# TYPE consensus_votes").count(),
3054 1,
3055 "TYPE should appear exactly once, got: {}",
3056 buffer
3057 );
3058
3059 let ctx_multi = context
3061 .child("engine")
3062 .with_attribute("region", "us")
3063 .with_attribute("instance", "i1");
3064 let counter3 = Counter::<u64>::default();
3065 let _multi = ctx_multi.register("requests", "request count", counter3.clone());
3066 counter3.inc();
3067
3068 let buffer = context.encode();
3069 assert!(
3070 buffer.contains("engine_requests_total{instance=\"i1\",region=\"us\"} 1"),
3071 "Expected metric with sorted attributes, got: {}",
3072 buffer
3073 );
3074 });
3075 }
3076
3077 #[rstest]
3078 #[case::deterministic(deterministic::Runner::default())]
3079 #[case::tokio(tokio::Runner::default())]
3080 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3081 fn test_metrics_attribute_with_nested_label<R: Runner>(#[case] runner: R)
3082 where
3083 R::Context: Metrics,
3084 {
3085 runner.start(|context| async move {
3086 let ctx = context
3088 .child("orchestrator")
3089 .with_attribute("epoch", "e5")
3090 .child("engine");
3091
3092 let counter = Counter::<u64>::default();
3094 let _registered = ctx.register("votes", "vote count", counter.clone());
3095 counter.inc();
3096
3097 let buffer = context.encode();
3099 assert!(
3100 buffer.contains("orchestrator_engine_votes_total{epoch=\"e5\"} 1"),
3101 "Expected metric with preserved epoch attribute, got: {}",
3102 buffer
3103 );
3104
3105 let ctx2 = context
3107 .child("outer")
3108 .with_attribute("region", "us")
3109 .child("middle")
3110 .with_attribute("az", "east")
3111 .child("inner");
3112
3113 let counter2 = Counter::<u64>::default();
3114 let _registered2 = ctx2.register("requests", "request count", counter2.clone());
3115 counter2.inc();
3116 counter2.inc();
3117
3118 let buffer = context.encode();
3119 assert!(
3120 buffer.contains("outer_middle_inner_requests_total{az=\"east\",region=\"us\"} 2"),
3121 "Expected metric with all attributes preserved and sorted, got: {}",
3122 buffer
3123 );
3124 });
3125 }
3126
3127 #[rstest]
3128 #[case::deterministic(deterministic::Runner::default())]
3129 #[case::tokio(tokio::Runner::default())]
3130 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3131 fn test_metrics_attributes_isolated_between_contexts<R: Runner>(#[case] runner: R)
3132 where
3133 R::Context: Metrics,
3134 {
3135 runner.start(|context| async move {
3136 let ctx_a = context.child("component_a").with_attribute("epoch", 1);
3138 let ctx_b = context.child("component_b").with_attribute("epoch", 2);
3139
3140 let c1 = Counter::<u64>::default();
3142 let _ctx_a_requests = ctx_a.register("requests", "help", c1);
3143
3144 let c2 = Counter::<u64>::default();
3146 let _ctx_b_requests = ctx_b.register("requests", "help", c2);
3147
3148 let c3 = Counter::<u64>::default();
3150 let _ctx_a_errors = ctx_a.register("errors", "help", c3);
3151
3152 let output = context.encode();
3153
3154 assert!(
3156 output.contains("component_a_requests_total{epoch=\"1\"} 0"),
3157 "ctx_a requests should have epoch=1: {output}"
3158 );
3159 assert!(
3160 output.contains("component_a_errors_total{epoch=\"1\"} 0"),
3161 "ctx_a errors should have epoch=1: {output}"
3162 );
3163 assert!(
3164 !output.contains("component_a_requests_total{epoch=\"2\"}"),
3165 "ctx_a requests should not have epoch=2: {output}"
3166 );
3167
3168 assert!(
3170 output.contains("component_b_requests_total{epoch=\"2\"} 0"),
3171 "ctx_b should have epoch=2: {output}"
3172 );
3173 assert!(
3174 !output.contains("component_b_requests_total{epoch=\"1\"}"),
3175 "ctx_b should not have epoch=1: {output}"
3176 );
3177 });
3178 }
3179
3180 #[rstest]
3187 #[case::deterministic(deterministic::Runner::default())]
3188 #[case::tokio(tokio::Runner::default())]
3189 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3190 fn test_metrics_spawn_attribute_cardinality<R: Runner>(#[case] runner: R)
3191 where
3192 R::Context: Spawner + Metrics + Clock,
3193 {
3194 runner.start(|context| async move {
3195 const ROUNDS: u64 = 128;
3196
3197 let mut handles = Vec::with_capacity(ROUNDS as usize);
3198 for round in 0..ROUNDS {
3199 let handle = context
3200 .child("deferred_verify")
3201 .with_attribute("round", round)
3202 .spawn(move |_| async move { round });
3203 handles.push(handle);
3204 }
3205 for (expected, handle) in handles.into_iter().enumerate() {
3206 assert_eq!(handle.await.expect("task failed"), expected as u64);
3207 }
3208
3209 while count_running_tasks(&context, "deferred_verify") > 0 {
3213 context.sleep(Duration::from_millis(10)).await;
3214 }
3215 let buffer = context.encode();
3216
3217 let spawned_lines = buffer
3221 .lines()
3222 .filter(|line| {
3223 line.starts_with("runtime_tasks_spawned_total{")
3224 && line.contains("name=\"deferred_verify\"")
3225 })
3226 .count();
3227 let running_lines = buffer
3228 .lines()
3229 .filter(|line| {
3230 line.starts_with("runtime_tasks_running{")
3231 && line.contains("name=\"deferred_verify\"")
3232 })
3233 .count();
3234 assert_eq!(
3235 spawned_lines, 1,
3236 "expected exactly 1 runtime_tasks_spawned entry for deferred_verify, got {spawned_lines}: {buffer}",
3237 );
3238 assert_eq!(
3239 running_lines, 1,
3240 "expected exactly 1 runtime_tasks_running entry for deferred_verify, got {running_lines}: {buffer}",
3241 );
3242
3243 let spawned_value = format!(
3245 "runtime_tasks_spawned_total{{name=\"deferred_verify\",kind=\"Task\",execution=\"Shared\"}} {ROUNDS}"
3246 );
3247 assert!(
3248 buffer.contains(&spawned_value),
3249 "expected accumulated spawned counter `{spawned_value}`, got: {buffer}",
3250 );
3251 let running_value = "runtime_tasks_running{name=\"deferred_verify\",kind=\"Task\",execution=\"Shared\"} 0";
3252 assert!(
3253 buffer.contains(running_value),
3254 "expected running gauge to return to 0, got: {buffer}",
3255 );
3256
3257 assert!(
3260 !buffer
3261 .lines()
3262 .any(|line| line.starts_with("runtime_tasks_")
3263 && line.contains("round=")),
3264 "task metrics must not carry `round` attribute: {buffer}",
3265 );
3266 });
3267 }
3268
3269 #[rstest]
3270 #[case::deterministic(deterministic::Runner::default())]
3271 #[case::tokio(tokio::Runner::default())]
3272 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3273 fn test_metrics_attributes_sorted_deterministically<R: Runner>(#[case] runner: R)
3274 where
3275 R::Context: Metrics,
3276 {
3277 runner.start(|context| async move {
3278 let ctx_ab = context
3280 .child("service")
3281 .with_attribute("region", "us")
3282 .with_attribute("env", "prod");
3283
3284 let ctx_ba = context
3285 .child("service")
3286 .with_attribute("env", "prod")
3287 .with_attribute("region", "us");
3288
3289 let c1 = Counter::<u64>::default();
3291 let _requests = ctx_ab.register("requests", "help", c1.clone());
3292 c1.inc();
3293
3294 let c2 = Counter::<u64>::default();
3296 let _errors = ctx_ba.register("errors", "help", c2.clone());
3297 c2.inc();
3298 c2.inc();
3299
3300 let output = context.encode();
3301
3302 assert!(
3304 output.contains("service_requests_total{env=\"prod\",region=\"us\"} 1"),
3305 "requests should have sorted labels: {output}"
3306 );
3307 assert!(
3308 output.contains("service_errors_total{env=\"prod\",region=\"us\"} 2"),
3309 "errors should have sorted labels: {output}"
3310 );
3311
3312 assert!(
3314 !output.contains("region=\"us\",env=\"prod\""),
3315 "should not have unsorted label order: {output}"
3316 );
3317 });
3318 }
3319
3320 #[rstest]
3321 #[case::deterministic(deterministic::Runner::default())]
3322 #[case::tokio(tokio::Runner::default())]
3323 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3324 fn test_metrics_nested_labels_with_attributes<R: Runner>(#[case] runner: R)
3325 where
3326 R::Context: Metrics,
3327 {
3328 runner.start(|context| async move {
3329 let svc_a = context.child("service_a");
3331
3332 let svc_a_v2 = context.child("service_a").with_attribute("version", 2);
3334
3335 let svc_b_worker = context.child("service_b").child("worker");
3337
3338 let svc_b_worker_shard = context
3340 .child("service_b")
3341 .child("worker")
3342 .with_attribute("shard", 99);
3343
3344 let svc_b_manager = context.child("service_b").child("manager");
3346
3347 let svc_c = context.child("service_c");
3349
3350 let c1 = Counter::<u64>::default();
3352 let _svc_a = svc_a.register("requests", "help", c1);
3353
3354 let c2 = Counter::<u64>::default();
3355 let _svc_a_v2 = svc_a_v2.register("requests", "help", c2);
3356
3357 let c3 = Counter::<u64>::default();
3358 let _svc_b_worker = svc_b_worker.register("tasks", "help", c3);
3359
3360 let c4 = Counter::<u64>::default();
3361 let _svc_b_worker_shard = svc_b_worker_shard.register("tasks", "help", c4);
3362
3363 let c5 = Counter::<u64>::default();
3364 let _svc_b_manager = svc_b_manager.register("decisions", "help", c5);
3365
3366 let c6 = Counter::<u64>::default();
3367 let _svc_c = svc_c.register("requests", "help", c6);
3368
3369 let output = context.encode();
3370
3371 assert!(
3373 output.contains("service_a_requests_total 0"),
3374 "svc_a plain should exist: {output}"
3375 );
3376 assert!(
3377 output.contains("service_a_requests_total{version=\"2\"} 0"),
3378 "svc_a_v2 should have version=2: {output}"
3379 );
3380
3381 assert!(
3383 output.contains("service_b_worker_tasks_total 0"),
3384 "svc_b_worker plain should exist: {output}"
3385 );
3386 assert!(
3387 output.contains("service_b_worker_tasks_total{shard=\"99\"} 0"),
3388 "svc_b_worker_shard should have shard=99: {output}"
3389 );
3390
3391 assert!(
3393 output.contains("service_b_manager_decisions_total 0"),
3394 "svc_b_manager should have no attributes: {output}"
3395 );
3396 assert!(
3397 !output.contains("service_b_manager_decisions_total{"),
3398 "svc_b_manager should have no attributes at all: {output}"
3399 );
3400
3401 assert!(
3403 output.contains("service_c_requests_total 0"),
3404 "svc_c should have no attributes: {output}"
3405 );
3406 assert!(
3407 !output.contains("service_c_requests_total{"),
3408 "svc_c should have no attributes at all: {output}"
3409 );
3410
3411 assert!(
3413 !output.contains("service_b_manager_decisions_total{shard="),
3414 "svc_b_manager should not have shard: {output}"
3415 );
3416 assert!(
3417 !output.contains("service_a_requests_total{shard="),
3418 "svc_a should not have shard: {output}"
3419 );
3420 assert!(
3421 !output.contains("service_c_requests_total{version="),
3422 "svc_c should not have version: {output}"
3423 );
3424 });
3425 }
3426
3427 #[rstest]
3428 #[case::deterministic(deterministic::Runner::default())]
3429 #[case::tokio(tokio::Runner::default())]
3430 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3431 fn test_metrics_family_with_attributes<R: Runner>(#[case] runner: R)
3432 where
3433 R::Context: Metrics,
3434 {
3435 runner.start(|context| async move {
3436 #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
3437 struct RequestLabels {
3438 method: String,
3439 status: u16,
3440 }
3441
3442 let ctx = context
3444 .child("api")
3445 .with_attribute("region", "us_east")
3446 .with_attribute("env", "prod");
3447
3448 let requests: Family<RequestLabels, Counter<u64>> = Family::default();
3450 let _requests = ctx.register("requests", "HTTP requests", requests.clone());
3451
3452 requests
3454 .get_or_create(&RequestLabels {
3455 method: "GET".to_string(),
3456 status: 200,
3457 })
3458 .inc();
3459 requests
3460 .get_or_create(&RequestLabels {
3461 method: "POST".to_string(),
3462 status: 201,
3463 })
3464 .inc();
3465 requests
3466 .get_or_create(&RequestLabels {
3467 method: "GET".to_string(),
3468 status: 404,
3469 })
3470 .inc();
3471
3472 let output = context.encode();
3473
3474 assert!(
3478 output.contains(
3479 "api_requests_total{env=\"prod\",region=\"us_east\",method=\"GET\",status=\"200\"} 1"
3480 ),
3481 "GET 200 should have merged labels: {output}"
3482 );
3483 assert!(
3484 output.contains(
3485 "api_requests_total{env=\"prod\",region=\"us_east\",method=\"POST\",status=\"201\"} 1"
3486 ),
3487 "POST 201 should have merged labels: {output}"
3488 );
3489 assert!(
3490 output.contains(
3491 "api_requests_total{env=\"prod\",region=\"us_east\",method=\"GET\",status=\"404\"} 1"
3492 ),
3493 "GET 404 should have merged labels: {output}"
3494 );
3495
3496 let ctx_plain = context.child("api_plain");
3498 let plain_requests: Family<RequestLabels, Counter<u64>> = Family::default();
3499 let _plain_requests =
3500 ctx_plain.register("requests", "HTTP requests", plain_requests.clone());
3501
3502 plain_requests
3503 .get_or_create(&RequestLabels {
3504 method: "DELETE".to_string(),
3505 status: 204,
3506 })
3507 .inc();
3508
3509 let output = context.encode();
3510
3511 assert!(
3513 output.contains("api_plain_requests_total{method=\"DELETE\",status=\"204\"} 1"),
3514 "plain DELETE should have only family labels: {output}"
3515 );
3516 assert!(
3517 !output.contains("api_plain_requests_total{env="),
3518 "plain should not have env attribute: {output}"
3519 );
3520 assert!(
3521 !output.contains("api_plain_requests_total{region="),
3522 "plain should not have region attribute: {output}"
3523 );
3524 });
3525 }
3526
3527 #[rstest]
3528 #[case::deterministic(deterministic::Runner::default())]
3529 #[case::tokio(tokio::Runner::default())]
3530 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3531 fn test_register_and_encode<R: Runner>(#[case] runner: R)
3532 where
3533 R::Context: Metrics,
3534 {
3535 runner.start(|context| async move {
3536 let counter =
3537 context
3538 .child("engine")
3539 .register("votes", "vote count", Counter::<u64>::default());
3540 counter.inc();
3541
3542 let buffer = context.encode();
3543 assert!(
3544 buffer.contains("engine_votes_total 1"),
3545 "registered metric should appear in encode: {buffer}"
3546 );
3547 });
3548 }
3549
3550 #[rstest]
3551 #[case::deterministic(deterministic::Runner::default())]
3552 #[case::tokio(tokio::Runner::default())]
3553 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3554 fn test_register_drop_removes_metrics<R: Runner>(#[case] runner: R)
3555 where
3556 R::Context: Metrics,
3557 {
3558 runner.start(|context| async move {
3559 let permanent = context.child("permanent").register(
3560 "counter",
3561 "permanent counter",
3562 Counter::<u64>::default(),
3563 );
3564 permanent.inc();
3565
3566 let counter =
3567 context
3568 .child("engine")
3569 .register("votes", "vote count", Counter::<u64>::default());
3570 counter.inc();
3571
3572 let buffer = context.encode();
3573 assert!(buffer.contains("permanent_counter_total 1"));
3574 assert!(buffer.contains("engine_votes_total 1"));
3575
3576 drop(counter);
3577
3578 let buffer = context.encode();
3579 assert!(
3580 buffer.contains("permanent_counter_total 1"),
3581 "other registered metrics should survive handle drop: {buffer}"
3582 );
3583 assert!(
3584 !buffer.contains("engine_votes"),
3585 "metric should be removed after handle drop: {buffer}"
3586 );
3587 });
3588 }
3589
3590 #[rstest]
3591 #[case::deterministic(deterministic::Runner::default())]
3592 #[case::tokio(tokio::Runner::default())]
3593 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3594 fn test_register_with_attributes<R: Runner>(#[case] runner: R)
3595 where
3596 R::Context: Metrics,
3597 {
3598 runner.start(|context| async move {
3599 let epoch1 = context.child("engine").with_attribute("epoch", 1).register(
3600 "votes",
3601 "vote count",
3602 Counter::<u64>::default(),
3603 );
3604 epoch1.inc();
3605
3606 let epoch2 = context.child("engine").with_attribute("epoch", 2).register(
3607 "votes",
3608 "vote count",
3609 Counter::<u64>::default(),
3610 );
3611 epoch2.inc();
3612 epoch2.inc();
3613
3614 let buffer = context.encode();
3615 assert!(buffer.contains("engine_votes_total{epoch=\"1\"} 1"));
3616 assert!(buffer.contains("engine_votes_total{epoch=\"2\"} 2"));
3617
3618 assert_eq!(
3619 buffer.matches("# HELP engine_votes").count(),
3620 1,
3621 "HELP should appear once: {buffer}"
3622 );
3623 assert_eq!(
3624 buffer.matches("# TYPE engine_votes").count(),
3625 1,
3626 "TYPE should appear once: {buffer}"
3627 );
3628
3629 drop(epoch1);
3630 let buffer = context.encode();
3631 assert!(
3632 !buffer.contains("epoch=\"1\""),
3633 "epoch 1 should be gone: {buffer}"
3634 );
3635 assert!(buffer.contains("engine_votes_total{epoch=\"2\"} 2"));
3636
3637 drop(epoch2);
3638 let buffer = context.encode();
3639 assert!(
3640 !buffer.contains("engine_votes"),
3641 "all epoch metrics should be gone: {buffer}"
3642 );
3643 });
3644 }
3645
3646 #[rstest]
3647 #[case::deterministic(deterministic::Runner::default())]
3648 #[case::tokio(tokio::Runner::default())]
3649 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3650 fn test_reregister_after_drop<R: Runner>(#[case] runner: R)
3651 where
3652 R::Context: Metrics,
3653 {
3654 runner.start(|context| async move {
3655 let votes = context.child("engine").with_attribute("epoch", 1).register(
3656 "votes",
3657 "vote count",
3658 Counter::<u64>::default(),
3659 );
3660 drop(votes);
3661
3662 let replacement = context.child("engine").with_attribute("epoch", 1).register(
3663 "votes",
3664 "vote count",
3665 Counter::<u64>::default(),
3666 );
3667 drop(replacement);
3668 });
3669 }
3670
3671 #[rstest]
3672 #[case::deterministic(deterministic::Runner::default())]
3673 #[case::tokio(tokio::Runner::default())]
3674 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3675 fn test_register_clone_keeps_metric_alive<R: Runner>(#[case] runner: R)
3676 where
3677 R::Context: Metrics,
3678 {
3679 runner.start(|context| async move {
3680 let registered =
3681 context
3682 .child("engine")
3683 .register("votes", "vote count", Counter::<u64>::default());
3684 registered.inc();
3685 let clone = registered.clone();
3686
3687 let buffer = context.encode();
3688 assert!(
3689 buffer.contains("engine_votes_total 1"),
3690 "metric should remain registered while any handle exists: {buffer}"
3691 );
3692
3693 drop(registered);
3694 let buffer = context.encode();
3695 assert!(
3696 buffer.contains("engine_votes_total 1"),
3697 "metric should survive while clone is retained: {buffer}"
3698 );
3699
3700 drop(clone);
3701 let buffer = context.encode();
3702 assert!(
3703 !buffer.contains("engine_votes"),
3704 "metric should be removed when all handle clones are dropped: {buffer}"
3705 );
3706 });
3707 }
3708
3709 #[rstest]
3710 #[case::deterministic(deterministic::Runner::default())]
3711 #[case::tokio(tokio::Runner::default())]
3712 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3713 fn test_encode_single_eof<R: Runner>(#[case] runner: R)
3714 where
3715 R::Context: Metrics,
3716 {
3717 runner.start(|context| async move {
3718 let root_counter = context.register("root", "root metric", Counter::<u64>::default());
3719 root_counter.inc();
3720
3721 let child =
3722 context
3723 .child("engine")
3724 .register("ops", "child metric", Counter::<u64>::default());
3725 child.inc();
3726
3727 let buffer = context.encode();
3728 assert!(
3729 buffer.contains("root_total 1"),
3730 "root metric missing: {buffer}"
3731 );
3732 assert!(
3733 buffer.contains("engine_ops_total 1"),
3734 "child metric missing: {buffer}"
3735 );
3736 assert_eq!(
3737 buffer.matches("# EOF").count(),
3738 1,
3739 "expected exactly one EOF marker: {buffer}"
3740 );
3741 assert!(
3742 buffer.ends_with("# EOF\n"),
3743 "EOF must be the last line: {buffer}"
3744 );
3745 });
3746 }
3747
3748 #[rstest]
3749 #[case::deterministic(deterministic::Runner::default())]
3750 #[case::tokio(tokio::Runner::default())]
3751 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3752 fn test_family_with_attributes<R: Runner>(#[case] runner: R)
3753 where
3754 R::Context: Metrics,
3755 {
3756 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
3757 struct Peer {
3758 name: String,
3759 }
3760 impl EncodeLabelSet for Peer {
3761 fn encode(&self, encoder: &mut LabelSetEncoder<'_>) -> Result<(), std::fmt::Error> {
3762 let mut label = encoder.encode_label();
3763 let mut key = label.encode_label_key()?;
3764 EncodeLabelKey::encode(&"peer", &mut key)?;
3765 let mut value = key.encode_label_value()?;
3766 EncodeLabelValue::encode(&self.name.as_str(), &mut value)?;
3767 value.finish()
3768 }
3769 }
3770
3771 runner.start(|context| async move {
3772 let family = context
3773 .child("batcher")
3774 .with_attribute("epoch", 1)
3775 .register(
3776 "votes",
3777 "votes per peer",
3778 Family::<Peer, Counter>::default(),
3779 );
3780 family
3781 .get_or_create(&Peer {
3782 name: "alice".into(),
3783 })
3784 .inc();
3785 family.get_or_create(&Peer { name: "bob".into() }).inc();
3786
3787 let buffer = context.encode();
3788 assert!(
3789 buffer.contains("batcher_votes_total{epoch=\"1\",peer=\"alice\"} 1"),
3790 "family with attributes should combine labels: {buffer}"
3791 );
3792 assert!(
3793 buffer.contains("batcher_votes_total{epoch=\"1\",peer=\"bob\"} 1"),
3794 "family with attributes should combine labels: {buffer}"
3795 );
3796
3797 drop(family);
3798 let buffer = context.encode();
3799 assert!(
3800 !buffer.contains("batcher_votes"),
3801 "family metrics should be removed: {buffer}"
3802 );
3803 });
3804 }
3805
3806 #[rstest]
3807 #[case::deterministic(deterministic::Runner::default())]
3808 #[case::tokio(tokio::Runner::default())]
3809 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3810 fn test_strategy<R: Runner>(#[case] runner: R)
3811 where
3812 R::Context: Strategizer + Metrics,
3813 {
3814 runner.start(|context| async move {
3815 let strategy = context.child("pool").strategy(NZUsize!(4));
3817
3818 let sum = strategy.fold(0..10000, || 0i32, |acc, n| acc + n, |a, b| a + b);
3820 assert_eq!(sum, 10000 * 9999 / 2);
3821 });
3822 }
3823
3824 #[rstest]
3825 #[case::deterministic(deterministic::Runner::default())]
3826 #[case::tokio(tokio::Runner::default())]
3827 #[cfg_attr(target_os = "linux", case::iouring(iouring::Runner::default()))]
3828 fn test_nested_strategy_runs_inline<R: Runner>(#[case] runner: R)
3829 where
3830 R::Context: Strategizer + Metrics,
3831 {
3832 runner.start(|context| async move {
3833 let strategy = context.child("pool").strategy(NZUsize!(1));
3834
3835 let output = strategy
3836 .spawn(2, |strategy| strategy.map_collect_vec(0..2, |i| i + 1))
3837 .await;
3838
3839 assert_eq!(output, vec![1, 2]);
3840 });
3841 }
3842}