nautilus_network/dst.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Deterministic simulation testing (DST) seam for network async primitives.
17//!
18//! Routes time primitives, owned WebSocket tasks, and supported HTTP/WebSocket
19//! byte streams through a network-local boundary. With `simulation` and
20//! `cfg(madsim)`, clocks, tasks, and streams use Madsim. Normal builds use
21//! Tokio clocks and tasks and the existing `crate::net` streams.
22//!
23//! `Instant` is routed the same way so that `now()` reads and `sleep`/`timeout`
24//! waits share a single clock base. Using `tokio::time::Instant` on normal
25//! builds keeps the seam compatible with `#[tokio::test(start_paused = true)]`
26//! tests that drive time via `tokio::time::advance`.
27//!
28//! `nautilus-network` sits below `nautilus-common` in the dependency graph and
29//! cannot import from `nautilus_common::live::dst`, which is why this module
30//! lives in the network crate.
31
32/// Clock and timeout primitives selected for deterministic simulation.
33pub mod time {
34 pub use std::time::Duration;
35
36 #[cfg(all(feature = "simulation", madsim))]
37 pub use madsim::time::{Instant, sleep, sleep_until, timeout};
38 #[cfg(not(all(feature = "simulation", madsim)))]
39 pub use tokio::time::{Instant, sleep, sleep_until, timeout};
40}
41
42/// Owned tasks selected for deterministic simulation.
43pub mod task {
44 #[cfg(all(feature = "simulation", madsim))]
45 pub(crate) use madsim::task::yield_now;
46 #[cfg(all(feature = "simulation", madsim))]
47 pub use madsim::task::{JoinHandle, spawn};
48 #[cfg(not(all(feature = "simulation", madsim)))]
49 pub(crate) use tokio::task::yield_now;
50 #[cfg(not(all(feature = "simulation", madsim)))]
51 pub use tokio::task::{JoinHandle, spawn};
52}
53
54/// Network byte streams selected for deterministic simulation.
55pub mod net {
56 #[cfg(all(feature = "simulation", madsim))]
57 pub use super::stream::{TcpListener, TcpStream};
58 #[cfg(not(all(feature = "simulation", madsim)))]
59 pub use crate::net::{TcpListener, TcpStream};
60}
61
62#[cfg(all(feature = "simulation", madsim))]
63mod stream;
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 // Under madsim, `time::Instant` and `time::sleep` both run on the virtual
70 // clock with sub-ms scheduling epsilon. If the cfg gate fell through to
71 // real tokio, `sleep` would block on the OS scheduler with ~5-15ms of
72 // jitter and the tight upper bound would fail.
73 #[cfg(all(feature = "simulation", madsim))]
74 #[madsim::test]
75 async fn test_dst_sleep_uses_virtual_time() {
76 let start = time::Instant::now();
77 time::sleep(time::Duration::from_millis(100)).await;
78 let elapsed = start.elapsed();
79 assert!(elapsed >= time::Duration::from_millis(100));
80 assert!(
81 elapsed < time::Duration::from_millis(101),
82 "virtual sleep showed real-tokio jitter: {elapsed:?}"
83 );
84 }
85
86 // Mirror under real tokio (paused clock) to keep both routes exercised.
87 #[cfg(not(all(feature = "simulation", madsim)))]
88 #[tokio::test(flavor = "current_thread", start_paused = true)]
89 async fn test_dst_sleep_advances_paused_clock() {
90 let start = time::Instant::now();
91 time::sleep(time::Duration::from_mins(1)).await;
92 assert!(start.elapsed() >= time::Duration::from_mins(1));
93 }
94
95 #[cfg(all(feature = "simulation", madsim))]
96 #[madsim::test]
97 async fn test_dst_timeout_fires_in_virtual_time() {
98 let result = time::timeout(
99 time::Duration::from_millis(10),
100 std::future::pending::<()>(),
101 )
102 .await;
103 assert!(
104 result.is_err(),
105 "timeout should fire on a never-completing future"
106 );
107 }
108}