Skip to main content

moonpool_sim/sim/
state.rs

1//! Network state management for simulation.
2//!
3//! This module provides internal state types for managing connections,
4//! listeners, partitions, and clogs in the simulation environment.
5
6use std::{
7    collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
8    net::IpAddr,
9    time::Duration,
10};
11
12use moonpool_core::OpenOptions;
13
14use crate::{
15    network::{
16        NetworkConfiguration,
17        sim::{ConnectionId, ListenerId},
18    },
19    storage::{InMemoryStorage, StorageConfiguration},
20};
21
22/// Simple clog state - just tracks when it expires
23#[derive(Debug)]
24pub struct ClogState {
25    /// When the clog expires and writes can resume (in simulation time)
26    pub expires_at: Duration,
27}
28
29/// Reason for connection closure - distinguishes FIN vs RST semantics.
30///
31/// In real TCP:
32/// - FIN (graceful close): Peer gets EOF on read, writes may still work briefly
33/// - RST (aborted close): Peer gets ECONNRESET immediately on both read and write
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum CloseReason {
36    /// Connection is not closed
37    #[default]
38    None,
39    /// Graceful close (FIN) - peer will get EOF on read
40    Graceful,
41    /// Aborted close (RST) - peer will get ECONNRESET
42    Aborted,
43}
44
45/// Network partition state between two IP addresses
46#[derive(Debug, Clone)]
47pub struct PartitionState {
48    /// When the partition expires and connectivity is restored (in simulation time)
49    pub expires_at: Duration,
50}
51
52/// Bit-packed boolean flags for a [`ConnectionState`].
53///
54/// Storing the various closure / chaos flags in a single integer avoids the
55/// `clippy::struct_excessive_bools` lint while keeping a small, copy-friendly
56/// representation. Helper methods provide named access to each flag.
57#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
58pub struct ConnectionFlags(u16);
59
60impl ConnectionFlags {
61    const IS_CLOSED: u16 = 1 << 0;
62    const SEND_CLOSED: u16 = 1 << 1;
63    const RECV_CLOSED: u16 = 1 << 2;
64    const IS_CUT: u16 = 1 << 3;
65    const IS_HALF_OPEN: u16 = 1 << 4;
66    const IS_STABLE: u16 = 1 << 5;
67    const GRACEFUL_CLOSE_PENDING: u16 = 1 << 6;
68    const REMOTE_FIN_RECEIVED: u16 = 1 << 7;
69    const SEND_IN_PROGRESS: u16 = 1 << 8;
70
71    fn get(self, mask: u16) -> bool {
72        (self.0 & mask) != 0
73    }
74
75    fn set_bit(&mut self, mask: u16, value: bool) {
76        if value {
77            self.0 |= mask;
78        } else {
79            self.0 &= !mask;
80        }
81    }
82
83    /// Whether the connection has been permanently closed.
84    #[must_use]
85    pub fn is_closed(self) -> bool {
86        self.get(Self::IS_CLOSED)
87    }
88    /// Update the closed flag.
89    pub fn set_is_closed(&mut self, value: bool) {
90        self.set_bit(Self::IS_CLOSED, value);
91    }
92
93    /// Whether the send side has been closed.
94    #[must_use]
95    pub fn send_closed(self) -> bool {
96        self.get(Self::SEND_CLOSED)
97    }
98    /// Update the send-closed flag.
99    pub fn set_send_closed(&mut self, value: bool) {
100        self.set_bit(Self::SEND_CLOSED, value);
101    }
102
103    /// Whether the receive side has been closed.
104    #[must_use]
105    pub fn recv_closed(self) -> bool {
106        self.get(Self::RECV_CLOSED)
107    }
108    /// Update the recv-closed flag.
109    pub fn set_recv_closed(&mut self, value: bool) {
110        self.set_bit(Self::RECV_CLOSED, value);
111    }
112
113    /// Whether the connection is temporarily cut.
114    #[must_use]
115    pub fn is_cut(self) -> bool {
116        self.get(Self::IS_CUT)
117    }
118    /// Update the cut flag.
119    pub fn set_is_cut(&mut self, value: bool) {
120        self.set_bit(Self::IS_CUT, value);
121    }
122
123    /// Whether the connection is half-open (peer crashed).
124    #[must_use]
125    pub fn is_half_open(self) -> bool {
126        self.get(Self::IS_HALF_OPEN)
127    }
128    /// Update the half-open flag.
129    pub fn set_is_half_open(&mut self, value: bool) {
130        self.set_bit(Self::IS_HALF_OPEN, value);
131    }
132
133    /// Whether the connection is exempt from chaos (stable).
134    #[must_use]
135    pub fn is_stable(self) -> bool {
136        self.get(Self::IS_STABLE)
137    }
138    /// Update the stable flag.
139    pub fn set_is_stable(&mut self, value: bool) {
140        self.set_bit(Self::IS_STABLE, value);
141    }
142
143    /// Whether a graceful close (FIN) is pending delivery to the peer.
144    #[must_use]
145    pub fn graceful_close_pending(self) -> bool {
146        self.get(Self::GRACEFUL_CLOSE_PENDING)
147    }
148    /// Update the graceful-close-pending flag.
149    pub fn set_graceful_close_pending(&mut self, value: bool) {
150        self.set_bit(Self::GRACEFUL_CLOSE_PENDING, value);
151    }
152
153    /// Whether the peer's FIN has been received on this side.
154    #[must_use]
155    pub fn remote_fin_received(self) -> bool {
156        self.get(Self::REMOTE_FIN_RECEIVED)
157    }
158    /// Update the remote-fin-received flag.
159    pub fn set_remote_fin_received(&mut self, value: bool) {
160        self.set_bit(Self::REMOTE_FIN_RECEIVED, value);
161    }
162
163    /// Whether a `ProcessSendBuffer` event is currently scheduled.
164    #[must_use]
165    pub fn send_in_progress(self) -> bool {
166        self.get(Self::SEND_IN_PROGRESS)
167    }
168    /// Update the send-in-progress flag.
169    pub fn set_send_in_progress(&mut self, value: bool) {
170        self.set_bit(Self::SEND_IN_PROGRESS, value);
171    }
172}
173
174/// Internal connection state for simulation
175#[derive(Debug, Clone)]
176pub struct ConnectionState {
177    /// Local IP address for this connection
178    pub local_ip: Option<IpAddr>,
179
180    /// Remote IP address for this connection
181    pub remote_ip: Option<IpAddr>,
182
183    /// Peer address as seen by this connection.
184    ///
185    /// FDB Pattern (sim2.actor.cpp:1149-1175):
186    /// - For client-side connections: the destination address (server's listening address)
187    /// - For server-side connections: synthesized ephemeral address (random IP + port 40000-60000)
188    ///
189    /// This simulates real TCP behavior where servers see client ephemeral ports,
190    /// not the client's identity address. As FDB notes: "In the case of an incoming
191    /// connection, this may not be an address we can connect to!"
192    pub peer_address: String,
193
194    /// FIFO buffer for incoming data that hasn't been read by the application yet.
195    pub receive_buffer: VecDeque<u8>,
196
197    /// Reference to the other end of this bidirectional TCP connection.
198    pub paired_connection: Option<ConnectionId>,
199
200    /// FIFO buffer for outgoing data waiting to be sent over the network.
201    pub send_buffer: VecDeque<Vec<u8>>,
202
203    /// Next available time for sending messages from this connection.
204    pub next_send_time: Duration,
205
206    /// Boolean flags (closure, chaos, send-in-progress) packed into a bitfield.
207    /// Use the named accessor methods on [`ConnectionFlags`] to inspect or
208    /// update individual bits.
209    pub flags: ConnectionFlags,
210
211    /// When the cut expires and the connection is restored (in simulation time).
212    /// Only meaningful when `flags.is_cut()` is true.
213    pub cut_expiry: Option<Duration>,
214
215    /// Reason for connection closure - distinguishes FIN vs RST semantics.
216    /// When `flags.is_closed()` is true, this indicates whether it was graceful or aborted.
217    pub close_reason: CloseReason,
218
219    /// Send buffer capacity in bytes.
220    /// When the send buffer reaches this limit, `poll_write` returns Pending.
221    /// Calculated from BDP (Bandwidth-Delay Product): latency × bandwidth.
222    pub send_buffer_capacity: usize,
223
224    /// Per-connection send delay override (asymmetric latency).
225    /// If set, this delay is applied when sending data from this connection.
226    /// If None, the global `write_latency` from `NetworkConfiguration` is used.
227    pub send_delay: Option<Duration>,
228
229    /// Per-connection receive delay override (asymmetric latency).
230    /// If set, this delay is applied when receiving data on this connection.
231    /// If None, the global `read_latency` from `NetworkConfiguration` is used.
232    pub recv_delay: Option<Duration>,
233
234    /// When a half-open connection starts returning errors (in simulation time).
235    /// Before this time: writes succeed (data dropped), reads block.
236    /// After this time: both read and write return ECONNRESET.
237    pub half_open_error_at: Option<Duration>,
238
239    /// Time of the most recently scheduled `DataDelivery` event from this connection.
240    /// Used to ensure `FinDelivery` is scheduled after the last data arrives at the peer.
241    pub last_data_delivery_scheduled_at: Option<Duration>,
242}
243
244/// Network-related state management
245#[derive(Debug)]
246pub struct NetworkState {
247    /// Counter for generating unique connection IDs.
248    pub next_connection_id: u64,
249    /// Counter for generating unique listener IDs.
250    pub next_listener_id: u64,
251    /// Network configuration for this simulation.
252    pub config: NetworkConfiguration,
253    /// Active connections indexed by their ID.
254    pub connections: BTreeMap<ConnectionId, ConnectionState>,
255    /// Active listener IDs.
256    pub listeners: BTreeSet<ListenerId>,
257    /// Connections pending acceptance, indexed by address.
258    pub pending_connections: BTreeMap<String, ConnectionId>,
259
260    /// Write clog state (temporary write blocking).
261    pub connection_clogs: BTreeMap<ConnectionId, ClogState>,
262
263    /// Read clog state (temporary read blocking, symmetric with write clogging).
264    pub read_clogs: BTreeMap<ConnectionId, ClogState>,
265
266    /// Partitions between specific IP pairs (from, to) -> partition state
267    pub ip_partitions: BTreeMap<(IpAddr, IpAddr), PartitionState>,
268    /// Send partitions - IP cannot send to anyone
269    pub send_partitions: BTreeMap<IpAddr, Duration>,
270    /// Receive partitions - IP cannot receive from anyone
271    pub recv_partitions: BTreeMap<IpAddr, Duration>,
272
273    /// Last time a random close was triggered (global cooldown tracking)
274    /// FDB: g_simulator->lastConnectionFailure - see sim2.actor.cpp:583
275    pub last_random_close_time: Duration,
276
277    /// Per-IP-pair base latencies for consistent connection behavior.
278    /// Once set on first connection, all subsequent connections between the same
279    /// IP pair will use this base latency (with optional jitter on top).
280    pub pair_latencies: BTreeMap<(IpAddr, IpAddr), Duration>,
281}
282
283impl NetworkState {
284    /// Create a new network state with the given configuration.
285    #[must_use]
286    pub fn new(config: NetworkConfiguration) -> Self {
287        Self {
288            next_connection_id: 0,
289            next_listener_id: 0,
290            config,
291            connections: BTreeMap::new(),
292            listeners: BTreeSet::new(),
293            pending_connections: BTreeMap::new(),
294            connection_clogs: BTreeMap::new(),
295            read_clogs: BTreeMap::new(),
296            ip_partitions: BTreeMap::new(),
297            send_partitions: BTreeMap::new(),
298            recv_partitions: BTreeMap::new(),
299            last_random_close_time: Duration::ZERO,
300            pair_latencies: BTreeMap::new(),
301        }
302    }
303
304    /// Extract IP address from a network address string.
305    /// Supports formats like "127.0.0.1:8080", "\[`::1`\]:8080", etc.
306    #[must_use]
307    pub fn parse_ip_from_addr(addr: &str) -> Option<IpAddr> {
308        // Handle IPv6 addresses in brackets
309        if addr.starts_with('[')
310            && let Some(bracket_end) = addr.find(']')
311        {
312            return addr[1..bracket_end].parse().ok();
313        }
314
315        // Handle IPv4 addresses and unbracketed IPv6
316        if let Some(colon_pos) = addr.rfind(':') {
317            addr[..colon_pos].parse().ok()
318        } else {
319            addr.parse().ok()
320        }
321    }
322
323    /// Check if communication from source IP to destination IP is partitioned
324    #[must_use]
325    pub fn is_partitioned(&self, from_ip: IpAddr, to_ip: IpAddr, current_time: Duration) -> bool {
326        // Check IP pair partition
327        if let Some(partition) = self.ip_partitions.get(&(from_ip, to_ip))
328            && current_time < partition.expires_at
329        {
330            return true;
331        }
332
333        // Check send partition
334        if let Some(&partition_until) = self.send_partitions.get(&from_ip)
335            && current_time < partition_until
336        {
337            return true;
338        }
339
340        // Check receive partition
341        if let Some(&partition_until) = self.recv_partitions.get(&to_ip)
342            && current_time < partition_until
343        {
344            return true;
345        }
346
347        false
348    }
349
350    /// Check if a connection is partitioned (cannot send messages)
351    #[must_use]
352    pub fn is_connection_partitioned(
353        &self,
354        connection_id: ConnectionId,
355        current_time: Duration,
356    ) -> bool {
357        if let Some(conn) = self.connections.get(&connection_id)
358            && let (Some(local_ip), Some(remote_ip)) = (conn.local_ip, conn.remote_ip)
359        {
360            return self.is_partitioned(local_ip, remote_ip, current_time);
361        }
362        false
363    }
364}
365
366// =============================================================================
367// Storage State Types
368// =============================================================================
369
370/// Unique identifier for a simulated file within the simulation.
371#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
372pub struct FileId(pub u64);
373
374/// Type of pending storage operation.
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376pub enum PendingOpType {
377    /// Read operation
378    Read,
379    /// Write operation
380    Write,
381    /// Sync/flush operation
382    Sync,
383    /// Set file length operation
384    SetLen,
385    /// File open operation
386    Open,
387}
388
389/// A pending storage operation awaiting completion.
390#[derive(Debug, Clone)]
391pub struct PendingStorageOp {
392    /// Type of the operation
393    pub op_type: PendingOpType,
394    /// Offset within the file (for read/write)
395    pub offset: u64,
396    /// Length of the operation in bytes
397    pub len: usize,
398    /// Data for write operations
399    pub data: Option<Vec<u8>>,
400}
401
402/// Kind of dynamic disk-degradation episode currently affecting a process's disk.
403///
404/// Models `FoundationDB`'s `DiskFailureInjector` (`getDiskDelay()`): real disks
405/// degrade episodically rather than at a fixed steady-state rate.
406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub enum DiskEpisodeKind {
408    /// Disk is frozen until the episode expires; I/O waits out the remaining window.
409    Stall,
410    /// Effective IOPS/bandwidth are reduced for the duration of the episode.
411    Throttle,
412}
413
414/// Active disk-degradation episode (stall or throttle) on a process's disk, with expiry.
415///
416/// Per-owner state (keyed by owning process IP in [`StorageState::disk_episodes`]),
417/// evaluated before each latency calculation in `schedule_{read,write,sync}`: a
418/// single episode applies to every file owned by that process for its duration.
419/// Mirrors the existing [`ClogState`] / [`PartitionState`] expiry pattern.
420#[derive(Debug, Clone, Copy)]
421pub struct DiskDegradationState {
422    /// Which kind of degradation is active.
423    pub kind: DiskEpisodeKind,
424    /// When the episode expires (in simulation time).
425    pub expires_at: Duration,
426}
427
428/// State of an individual simulated file.
429#[derive(Debug)]
430pub struct StorageFileState {
431    /// Unique identifier for this file
432    pub id: FileId,
433    /// Path this file was opened with
434    pub path: String,
435    /// Current file position for sequential operations
436    pub position: u64,
437    /// Options the file was opened with
438    pub options: OpenOptions,
439    /// In-memory storage backing this file
440    pub storage: InMemoryStorage,
441    /// Whether the file has been closed
442    pub is_closed: bool,
443    /// Pending operations keyed by sequence number
444    pub pending_ops: BTreeMap<u64, PendingStorageOp>,
445    /// Next sequence number for operations on this file
446    pub next_op_seq: u64,
447    /// IP address of the process that owns this file.
448    pub owner_ip: IpAddr,
449}
450
451impl StorageFileState {
452    /// Create a new storage file state.
453    #[must_use]
454    pub fn new(
455        id: FileId,
456        path: String,
457        options: OpenOptions,
458        storage: InMemoryStorage,
459        owner_ip: IpAddr,
460    ) -> Self {
461        Self {
462            id,
463            path,
464            position: 0,
465            options,
466            storage,
467            is_closed: false,
468            pending_ops: BTreeMap::new(),
469            next_op_seq: 0,
470            owner_ip,
471        }
472    }
473}
474
475/// Storage-related state management for the simulation.
476#[derive(Debug)]
477pub struct StorageState {
478    /// Counter for generating unique file IDs
479    pub next_file_id: u64,
480    /// Default storage configuration (used when no per-process config is set)
481    pub config: StorageConfiguration,
482    /// Per-process storage configurations, keyed by IP address.
483    ///
484    /// When a file's owner IP has an entry here, that config is used for
485    /// fault injection and latency calculations instead of the global default.
486    pub per_process_configs: HashMap<IpAddr, StorageConfiguration>,
487    /// Active disk-degradation episode per owning process IP.
488    ///
489    /// Real disks degrade at the device level: a stall/throttle window applies
490    /// to every file owned by the process for the episode's duration, modelling
491    /// the correlated backpressure a per-file episode cannot (issue #147).
492    pub disk_episodes: HashMap<IpAddr, DiskDegradationState>,
493    /// Active files indexed by their ID
494    pub files: BTreeMap<FileId, StorageFileState>,
495    /// Mapping from path to file ID for quick lookup
496    pub path_to_file: BTreeMap<String, FileId>,
497    /// Set of paths that have been deleted (for `create_new` semantics)
498    pub deleted_paths: HashSet<String>,
499    /// Set of (`file_id`, `op_seq`) pairs for sync operations that failed
500    pub sync_failures: HashSet<(FileId, u64)>,
501}
502
503impl StorageState {
504    /// Create a new storage state with the given configuration.
505    #[must_use]
506    pub fn new(config: StorageConfiguration) -> Self {
507        Self {
508            next_file_id: 0,
509            config,
510            per_process_configs: HashMap::new(),
511            disk_episodes: HashMap::new(),
512            files: BTreeMap::new(),
513            path_to_file: BTreeMap::new(),
514            deleted_paths: HashSet::new(),
515            sync_failures: HashSet::new(),
516        }
517    }
518
519    /// Resolve storage configuration for a given IP address.
520    ///
521    /// Returns the per-process config if one is set, otherwise the global default.
522    #[must_use]
523    pub fn config_for(&self, ip: IpAddr) -> &StorageConfiguration {
524        self.per_process_configs.get(&ip).unwrap_or(&self.config)
525    }
526}
527
528impl Default for StorageState {
529    fn default() -> Self {
530        Self::new(StorageConfiguration::default())
531    }
532}