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}