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