Skip to main content

ace_sim/
tcp_bus.rs

1//! TCP-aware simulation bus.
2//!
3//! Models a connection-oriented transport between exactly two node addresses. Connection state is
4//! owned by the bus - nodes do not track TCP life-cycle. The bus rejects messages between
5//! disconnected nodes and can inject TCP-level faults independently of message-level faults.
6
7// region: Imports
8
9use crate::{
10    bus::{Envelope, SimBus},
11    clock::{Duration, Instant},
12    fault::FaultConfig,
13    io::NodeAddress,
14    rng::{Rng, Xorshift64},
15};
16use ace_core::Vec;
17
18// endregion: Imports
19
20// region: TcpFaultConfig
21
22/// TCP-level fault configuration - extends `FaultConfig` with connection-oriented faults.
23///
24/// Applied independently from the message-level faults in `FaultConfig`. This allows modeling a
25/// reliable TCP connection with unreliable message delivery, or a flaky TCP connection that resets
26/// mid-session.
27#[derive(Debug, Clone)]
28pub struct TcpFaultConfig {
29    /// Underlying message-level fault config.
30    pub message: FaultConfig,
31
32    /// Probability that a new connection attempt is refused. Models a gateway that is at capacity
33    /// or unreachable.
34    pub connection_refused: (u32, u32),
35
36    /// Probability that an established connection is reset mid-session. Models ignition off,
37    /// network fault, or gateway restart.
38    pub connection_reset: (u32, u32),
39
40    /// Probability that a connection attempt times out without response.
41    pub connection_timeout: (u32, u32),
42}
43
44impl TcpFaultConfig {
45    pub fn none() -> Self {
46        Self {
47            message: FaultConfig::none(),
48            connection_refused: (0, 1),
49            connection_reset: (0, 1),
50            connection_timeout: (0, 1),
51        }
52    }
53
54    pub fn light() -> Self {
55        Self {
56            message: FaultConfig::light(),
57            connection_refused: (1, 200),
58            connection_reset: (1, 200),
59            connection_timeout: (1, 200),
60        }
61    }
62
63    pub fn chaos() -> Self {
64        Self {
65            message: FaultConfig::chaos(),
66            connection_refused: (1, 20),
67            connection_reset: (1, 20),
68            connection_timeout: (1, 20),
69        }
70    }
71}
72
73// endregion: TcpFaultConfig
74
75// region: TcpConnectionState
76
77/// The state of a TCP connection between two nodes on the bus.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum TcpConnectionState {
80    /// No connection exists - messages are rejected.
81    Disconnected,
82
83    /// A connection request is in progress.
84    Connecting {
85        initiated_by: NodeAddress,
86        at: Instant,
87    },
88
89    /// Connection is established - messages flow normally.
90    Connected {
91        initiator: NodeAddress,
92        acceptor: NodeAddress,
93    },
94
95    /// Connection was reset by the bus fault injector. Nodes will observe this as a
96    /// `TcpEvent::ConnectionReset`
97    Reset,
98}
99
100impl TcpConnectionState {
101    pub fn is_connected(&self) -> bool {
102        matches!(self, Self::Connected { .. })
103    }
104}
105
106// endregion: TcpConnectionState
107
108// region: TcpEvent
109
110/// Events the `TcpSimBus` delivers to nodes alongside messages.
111///
112/// Nodes train these via `TcpSimBus::drain_events()` after each tick.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum TcpEvent {
115    /// A connection request from `from` to `to` was accepted.
116    ConnectionEstablished { from: NodeAddress, to: NodeAddress },
117
118    /// A connection request was refused by the bus fault injector.
119    ConnectionRefused { from: NodeAddress, to: NodeAddress },
120
121    /// A connection request timed out
122    ConnectionTimeout { from: NodeAddress, to: NodeAddress },
123
124    /// An established connection was reset by the bus fault injector. Both nodes should threat
125    /// this as a TCP RST
126    ConnectionReset { from: NodeAddress, to: NodeAddress },
127
128    /// The connection was closed cleanly by one of the nodes.
129    ConnectionClosed { from: NodeAddress, to: NodeAddress },
130}
131
132// endregion: TcpEvent
133
134// region: TcpSimBus
135
136/// A TCP-aware simulation bus.
137///
138/// Wraps `SimBus` for message delivery and adds connection state tracking and TCP-level fault
139/// injection on top. The bus owns the connection state - nodes request connections and observe
140/// events but do not track TCP state themselves.
141///
142/// `N` - max message payload bytes
143/// `Q` - max messages in-flight simultaneously
144#[derive(Debug)]
145pub struct TcpSimBus<const MAX_DATA: usize, const MAX_QUEUED: usize, const TCP_MAX_EVENTS: usize> {
146    /// Underlying message bus - handles delivery, delays, and message faults.
147    inner: SimBus<MAX_DATA, MAX_QUEUED>,
148
149    /// TCP fault configuration.
150    tcp_faults: TcpFaultConfig,
151
152    /// Current connection state between node pairs. For simplicity each bus instance models one
153    /// logical TCP connection between a tester and a gateway. Multi-connection scenarios use
154    /// multiple `TcpSimBus` instances.
155    connection: TcpConnectionState,
156
157    /// Connect timeout duration - if a connecting state persists longer than this without the bus
158    /// processing a tick, it times out.
159    connect_timeout: Duration,
160
161    /// Accumulated TCP events for nodes to drain.
162    events: Vec<TcpEvent, TCP_MAX_EVENTS>,
163
164    /// Dedicated RNG for TCP-level fault decisions, seeded independently from the message-level
165    /// RNG so fault regimes can be composed freely.
166    rng: Xorshift64,
167}
168
169impl<const MAX_DATA: usize, const MAX_QUEUED: usize, const TCP_MAX_EVENTS: usize>
170    TcpSimBus<MAX_DATA, MAX_QUEUED, TCP_MAX_EVENTS>
171{
172    /// Creates a new `TcpSimBus`.
173    ///
174    /// `seed` - seeds both the message bus RNG and the TCP fault RNG. The TCP RNG uses
175    /// `seed.wrapping_add(1)` so the two RNGs produce independent sequences from the same seed.
176    pub fn new(seed: u64, faults: TcpFaultConfig) -> Self {
177        Self {
178            inner: SimBus::new(seed, faults.message.clone()),
179            tcp_faults: faults,
180            connection: TcpConnectionState::Disconnected,
181            connect_timeout: Duration::from_millis(5_000),
182            events: Vec::new(),
183            rng: Xorshift64::new(seed.wrapping_add(1)),
184        }
185    }
186
187    // region: Connection management
188
189    /// Requests a TCP connection from `from` to `to`.
190    ///
191    /// The connection may be established, refused, or timeout depending on the `TcpFaultConfig`.
192    /// The outcome appears as a `TcpEvent` on the next `tick`.
193    pub fn connect(&mut self, from: NodeAddress, to: NodeAddress) {
194        if self.connection.is_connected() {
195            return;
196        }
197
198        let now = self.inner.now();
199
200        // Fault: connection refused
201        if self.rng.chance(
202            self.tcp_faults.connection_refused.0,
203            self.tcp_faults.connection_refused.1,
204        ) {
205            let _ = self.events.push(TcpEvent::ConnectionRefused {
206                from: from.clone(),
207                to: to.clone(),
208            });
209            return;
210        }
211
212        // Fault: connection timeout - record connecting state, timeout is check in tick()
213        if self.rng.chance(
214            self.tcp_faults.connection_timeout.0,
215            self.tcp_faults.connection_timeout.1,
216        ) {
217            self.connection = TcpConnectionState::Connecting {
218                initiated_by: from,
219                at: now,
220            };
221            return;
222        }
223
224        // Success
225        self.connection = TcpConnectionState::Connected {
226            initiator: from.clone(),
227            acceptor: to.clone(),
228        };
229
230        let _ = self
231            .events
232            .push(TcpEvent::ConnectionEstablished { from, to });
233    }
234
235    /// Closes the connection cleanly from the given node.
236    pub fn disconnect(&mut self, from: NodeAddress, to: NodeAddress) {
237        if self.connection.is_connected() {
238            self.connection = TcpConnectionState::Disconnected;
239            let _ = self.events.push(TcpEvent::ConnectionClosed { from, to });
240        }
241    }
242
243    pub fn connection_state(&self) -> &TcpConnectionState {
244        &self.connection
245    }
246
247    pub fn is_connected(&self) -> bool {
248        self.connection.is_connected()
249    }
250
251    // endregion: Connection management
252
253    // region: Message delivery
254
255    /// Enqueues a message - rejected if the connection is not established.
256    ///
257    /// Returns `true` if the message was accepted, `false` if rejected due to connection state or
258    /// message-level fault injection.
259    pub fn send(&mut self, src: NodeAddress, dst: NodeAddress, data: &[u8]) -> bool {
260        if !self.connection.is_connected() {
261            return false;
262        }
263        self.inner.send(src, dst, data)
264    }
265
266    // endregion: Message delivery
267
268    // region: Tick
269
270    /// Advances simulation time, delivers due messages, and checks connection-level fault
271    /// injection.
272    pub fn tick(&mut self, duration: Duration) -> Vec<Envelope<MAX_DATA>, MAX_QUEUED> {
273        let now = self.inner.now();
274
275        // Check connecting timeout
276        if let TcpConnectionState::Connecting { initiated_by, at } = &self.connection.clone() {
277            let elapsed = now.checked_duration_since(*at).unwrap_or(Duration::ZERO);
278
279            if elapsed >= self.connect_timeout {
280                let from = initiated_by.clone();
281
282                self.connection = TcpConnectionState::Disconnected;
283
284                let _ = self.events.push(TcpEvent::ConnectionTimeout {
285                    from: from.clone(),
286                    to: NodeAddress(0),
287                });
288            }
289        }
290
291        // Check mid-session connection reset fault
292        if self.connection.is_connected() {
293            if self.rng.chance(
294                self.tcp_faults.connection_reset.0,
295                self.tcp_faults.connection_reset.1,
296            ) {
297                if let TcpConnectionState::Connected {
298                    initiator,
299                    acceptor,
300                } = self.connection.clone()
301                {
302                    self.connection = TcpConnectionState::Reset;
303
304                    let _ = self.events.push(TcpEvent::ConnectionReset {
305                        from: initiator,
306                        to: acceptor,
307                    });
308                }
309            }
310        }
311
312        // If connection was just reset, clear the queue and return nothing
313        if matches!(self.connection, TcpConnectionState::Reset) {
314            self.connection = TcpConnectionState::Disconnected;
315
316            return Vec::new();
317        }
318
319        self.inner.tick(duration)
320    }
321
322    // endregion: Tick
323
324    // region: Accessors
325
326    pub fn now(&self) -> Instant {
327        self.inner.now()
328    }
329
330    /// Drains accumulated TCP events.
331    pub fn drain_events(&mut self) -> impl Iterator<Item = TcpEvent> + '_ {
332        self.events.drain(..)
333    }
334
335    pub fn set_connect_timeout(&mut self, timeout: Duration) {
336        self.connect_timeout = timeout;
337    }
338
339    pub fn set_faults(&mut self, faults: TcpFaultConfig) {
340        self.inner.set_faults(faults.message.clone());
341        self.tcp_faults = faults;
342    }
343
344    pub fn inner_mut(&mut self) -> &mut SimBus<MAX_DATA, MAX_QUEUED> {
345        &mut self.inner
346    }
347
348    // endregion: Accessors
349}
350
351// endregion: TcpSimBus