Skip to main content

moonpool_sim/sim/
world.rs

1//! Core simulation world and coordination logic.
2//!
3//! This module provides the central `SimWorld` coordinator that manages time,
4//! event processing, and network simulation state.
5
6use std::{
7    collections::{BTreeMap, HashSet, VecDeque},
8    net::IpAddr,
9    sync::{Arc, RwLock, Weak},
10    task::Waker,
11    time::Duration,
12};
13use tracing::instrument;
14
15use crate::{
16    SimulationError, SimulationResult,
17    chaos::fault_events::SimFaultEvent,
18    network::{
19        NetworkConfiguration, PartitionStrategy,
20        sim::{ConnectionId, ListenerId, SimNetworkProvider},
21    },
22};
23
24use super::{
25    events::{ConnectionStateChange, Event, EventQueue, NetworkOperation, ScheduledEvent},
26    rng::{reset_sim_rng, set_sim_seed, sim_random, sim_random_range},
27    sleep::SleepFuture,
28    state::{
29        ClogState, CloseReason, ConnectionFlags, ConnectionState, NetworkState, PartitionState,
30        StorageState,
31    },
32    wakers::WakerRegistry,
33};
34
35/// Internal simulation state holder
36#[derive(Debug)]
37pub(crate) struct SimInner {
38    pub(crate) current_time: Duration,
39    /// Drifted timer time (can be ahead of `current_time`)
40    /// FDB ref: sim2.actor.cpp:1058-1064 - `timer()` drifts 0-0.1s ahead of `now()`
41    pub(crate) timer_time: Duration,
42    pub(crate) event_queue: EventQueue,
43    pub(crate) next_sequence: u64,
44
45    // Network management
46    pub(crate) network: NetworkState,
47
48    // Storage management
49    pub(crate) storage: StorageState,
50
51    // Async coordination
52    pub(crate) wakers: WakerRegistry,
53
54    // Task management for sleep functionality
55    pub(crate) next_task_id: u64,
56    pub(crate) awakened_tasks: HashSet<u64>,
57
58    // Event processing metrics
59    pub(crate) events_processed: u64,
60
61    // Chaos tracking
62    pub(crate) last_bit_flip_time: Duration,
63
64    // Last event processed by step() — used by orchestrator to detect ProcessRestart
65    pub(crate) last_processed_event: Option<Event>,
66
67    /// Faults recorded by the engine since the last [`SimWorld::take_faults`]
68    /// drain. The runner pumps these into the observability timeline.
69    pub(crate) pending_faults: Vec<SimFaultRecord>,
70}
71
72impl SimInner {
73    pub(crate) fn new() -> Self {
74        Self {
75            current_time: Duration::ZERO,
76            timer_time: Duration::ZERO,
77            event_queue: EventQueue::new(),
78            next_sequence: 0,
79            network: NetworkState::new(NetworkConfiguration::default()),
80            storage: StorageState::default(),
81            wakers: WakerRegistry::default(),
82            next_task_id: 0,
83            awakened_tasks: HashSet::new(),
84            events_processed: 0,
85            last_bit_flip_time: Duration::ZERO,
86            last_processed_event: None,
87            pending_faults: Vec::new(),
88        }
89    }
90
91    pub(crate) fn new_with_config(network_config: NetworkConfiguration) -> Self {
92        Self {
93            current_time: Duration::ZERO,
94            timer_time: Duration::ZERO,
95            event_queue: EventQueue::new(),
96            next_sequence: 0,
97            network: NetworkState::new(network_config),
98            storage: StorageState::default(),
99            wakers: WakerRegistry::default(),
100            next_task_id: 0,
101            awakened_tasks: HashSet::new(),
102            events_processed: 0,
103            last_bit_flip_time: Duration::ZERO,
104            last_processed_event: None,
105            pending_faults: Vec::new(),
106        }
107    }
108
109    /// Record an engine-injected fault, stamped with the current sim time.
110    ///
111    /// Faults accumulate in [`SimInner::pending_faults`] until the runner
112    /// drains them via [`SimWorld::take_faults`] into the observability
113    /// timeline. The engine never touches `tracing` for faults.
114    pub(crate) fn record_fault(&mut self, event: SimFaultEvent) {
115        let time_ms = u64::try_from(self.current_time.as_millis()).unwrap_or(u64::MAX);
116        self.pending_faults.push(SimFaultRecord { time_ms, event });
117    }
118
119    /// Calculate the number of bits to flip using a power-law distribution.
120    ///
121    /// Uses the formula: 32 - `floor(log2(random_value))`
122    /// This creates a power-law distribution biased toward fewer bits:
123    /// - 1-2 bits: very common
124    /// - 16 bits: uncommon
125    /// - 32 bits: very rare
126    ///
127    /// Matches FDB's approach in FlowTransport.actor.cpp:1297
128    pub(crate) fn calculate_flip_bit_count(random_value: u32, min_bits: u32, max_bits: u32) -> u32 {
129        if random_value == 0 {
130            // Handle edge case: treat 0 as if it were 1
131            return max_bits.min(32);
132        }
133
134        // Formula: 32 - floor(log2(x)) = 1 + leading_zeros(x)
135        let bit_count = 1 + random_value.leading_zeros();
136
137        // Clamp to configured range
138        bit_count.clamp(min_bits, max_bits)
139    }
140}
141
142/// A fault recorded by the simulation engine, stamped with the sim time at
143/// which it occurred.
144///
145/// Drained by the runner via [`SimWorld::take_faults`] and recorded into the
146/// observability timeline under
147/// [`crate::SIM_FAULT_EVENT_NAME`](crate::chaos::SIM_FAULT_EVENT_NAME).
148#[derive(Debug, Clone)]
149pub struct SimFaultRecord {
150    /// Simulation time in milliseconds when the fault occurred.
151    pub time_ms: u64,
152    /// The fault event.
153    pub event: SimFaultEvent,
154}
155
156/// The central simulation coordinator that manages time and event processing.
157///
158/// `SimWorld` owns all mutable simulation state and provides the main interface
159/// for scheduling events and advancing simulation time. It uses a centralized
160/// ownership model with handle-based access to avoid borrow checker conflicts.
161#[derive(Debug)]
162pub struct SimWorld {
163    pub(crate) inner: Arc<RwLock<SimInner>>,
164}
165
166impl SimWorld {
167    /// Internal constructor that handles all initialization logic.
168    fn create(network_config: Option<NetworkConfiguration>, seed: u64) -> Self {
169        reset_sim_rng();
170        set_sim_seed(seed);
171        crate::chaos::assertions::reset_assertion_results();
172
173        let inner = match network_config {
174            Some(config) => SimInner::new_with_config(config),
175            None => SimInner::new(),
176        };
177
178        Self {
179            inner: Arc::new(RwLock::new(inner)),
180        }
181    }
182
183    /// Creates a new simulation world with default network configuration.
184    ///
185    /// Uses default seed (0) for reproducible testing. For custom seeds,
186    /// use [`SimWorld::new_with_seed`].
187    #[must_use]
188    pub fn new() -> Self {
189        Self::create(None, 0)
190    }
191
192    /// Drain engine-recorded faults accumulated since the last call.
193    ///
194    /// The runner calls this after each `step()` to pump faults into the
195    /// observability timeline; tests can call it directly to assert on the
196    /// fault sequence.
197    ///
198    /// # Panics
199    ///
200    /// Panics if the simulation lock is poisoned by a prior task panic.
201    #[must_use]
202    pub fn take_faults(&self) -> Vec<SimFaultRecord> {
203        let mut inner = self
204            .inner
205            .write()
206            .expect("RwLock poisoned: prior task panicked");
207        std::mem::take(&mut inner.pending_faults)
208    }
209
210    /// Creates a new simulation world with a specific seed for deterministic randomness.
211    ///
212    /// This method ensures clean thread-local RNG state by resetting before
213    /// setting the seed, making it safe for consecutive simulations on the same thread.
214    ///
215    /// # Parameters
216    ///
217    /// * `seed` - The seed value for deterministic randomness
218    #[must_use]
219    pub fn new_with_seed(seed: u64) -> Self {
220        Self::create(None, seed)
221    }
222
223    /// Creates a new simulation world with custom network configuration.
224    #[must_use]
225    pub fn new_with_network_config(network_config: NetworkConfiguration) -> Self {
226        Self::create(Some(network_config), 0)
227    }
228
229    /// Creates a new simulation world with both custom network configuration and seed.
230    ///
231    /// # Parameters
232    ///
233    /// * `network_config` - Network configuration for latency and fault simulation
234    /// * `seed` - The seed value for deterministic randomness
235    #[must_use]
236    pub fn new_with_network_config_and_seed(
237        network_config: NetworkConfiguration,
238        seed: u64,
239    ) -> Self {
240        Self::create(Some(network_config), seed)
241    }
242
243    /// Processes the next scheduled event and advances time.
244    ///
245    /// Returns `true` if more events are available for processing,
246    /// `false` if this was the last event or if no events are available.
247    ///
248    /// # Panics
249    ///
250    /// Panics if the simulation lock is poisoned by a prior task panic.
251    #[instrument(skip(self))]
252    pub fn step(&mut self) -> bool {
253        let mut inner = self
254            .inner
255            .write()
256            .expect("RwLock poisoned: prior task panicked");
257
258        if let Some(scheduled_event) = inner.event_queue.pop_earliest() {
259            // Advance logical time to event timestamp
260            inner.current_time = scheduled_event.time();
261
262            // Phase 7: Clear expired clogs after time advancement
263            Self::clear_expired_clogs_with_inner(&mut inner);
264
265            // Trigger random partitions based on configuration
266            Self::randomly_trigger_partitions_with_inner(&mut inner);
267
268            // Store the event for orchestrator inspection (e.g., ProcessRestart)
269            let event = scheduled_event.into_event();
270            inner.last_processed_event = Some(event.clone());
271
272            // Process the event with the mutable reference
273            Self::process_event_with_inner(&mut inner, event);
274
275            // Return true if more events are available
276            !inner.event_queue.is_empty()
277        } else {
278            inner.last_processed_event = None;
279            // No more events to process
280            false
281        }
282    }
283
284    /// Processes all scheduled events until the queue is empty or only infrastructure events remain.
285    ///
286    /// This method processes all workload-related events but stops early if only infrastructure
287    /// events (like connection restoration) remain. This prevents infinite loops where
288    /// infrastructure events keep the simulation running indefinitely after workloads complete.
289    ///
290    /// # Panics
291    ///
292    /// Panics if the simulation lock is poisoned by a prior task panic.
293    #[instrument(skip(self))]
294    pub fn run_until_empty(&mut self) {
295        while self.step() {
296            // Periodically check if we should stop early (every 50 events for performance)
297            if self
298                .inner
299                .read()
300                .expect("RwLock poisoned: prior task panicked")
301                .events_processed
302                .is_multiple_of(50)
303            {
304                let has_workload_events = !self
305                    .inner
306                    .read()
307                    .expect("RwLock poisoned: prior task panicked")
308                    .event_queue
309                    .has_only_infrastructure_events();
310                if !has_workload_events {
311                    tracing::debug!(
312                        "Early termination: only infrastructure events remain in queue"
313                    );
314                    break;
315                }
316            }
317        }
318    }
319
320    /// Returns the current simulation time.
321    ///
322    /// # Panics
323    ///
324    /// Panics if the simulation lock is poisoned by a prior task panic.
325    #[must_use]
326    pub fn current_time(&self) -> Duration {
327        self.inner
328            .read()
329            .expect("RwLock poisoned: prior task panicked")
330            .current_time
331    }
332
333    /// Returns the exact simulation time (equivalent to FDB's `now()`).
334    ///
335    /// This is the canonical simulation time used for scheduling events.
336    /// Use this for precise time comparisons and scheduling.
337    ///
338    /// # Panics
339    ///
340    /// Panics if the simulation lock is poisoned by a prior task panic.
341    #[must_use]
342    pub fn now(&self) -> Duration {
343        self.inner
344            .read()
345            .expect("RwLock poisoned: prior task panicked")
346            .current_time
347    }
348
349    /// Returns the drifted timer time (equivalent to FDB's `timer()`).
350    ///
351    /// The timer can be up to `clock_drift_max` (default 100ms) ahead of `now()`.
352    /// This simulates real-world clock drift between processes, which is important
353    /// for testing time-sensitive code like:
354    /// - Timeout handling
355    /// - Lease expiration
356    /// - Distributed consensus (leader election)
357    /// - Cache invalidation
358    /// - Heartbeat detection
359    ///
360    /// FDB formula: `timerTime += random01() * (time + 0.1 - timerTime) / 2.0`
361    ///
362    /// FDB ref: sim2.actor.cpp:1058-1064
363    ///
364    /// # Panics
365    ///
366    /// Panics if the simulation lock is poisoned by a prior task panic.
367    #[must_use]
368    pub fn timer(&self) -> Duration {
369        let mut inner = self
370            .inner
371            .write()
372            .expect("RwLock poisoned: prior task panicked");
373        let chaos = &inner.network.config.chaos;
374
375        // If clock drift is disabled, return exact simulation time
376        if !chaos.clock_drift_enabled {
377            return inner.current_time;
378        }
379
380        // FDB formula: timerTime += random01() * (time + 0.1 - timerTime) / 2.0
381        // This smoothly interpolates timerTime toward (time + drift_max)
382        // The /2.0 creates a damped approach (never overshoots)
383        let max_timer = inner.current_time + chaos.clock_drift_max;
384
385        // Only advance if timer is behind max
386        if inner.timer_time < max_timer {
387            let random_factor = sim_random::<f64>(); // 0.0 to 1.0
388            let gap = max_timer
389                .checked_sub(inner.timer_time)
390                .expect("timer_time < max_timer was just checked")
391                .as_secs_f64();
392            let delta = random_factor * gap / 2.0;
393            inner.timer_time += Duration::from_secs_f64(delta);
394        }
395
396        // Ensure timer never goes backwards relative to simulation time
397        inner.timer_time = inner.timer_time.max(inner.current_time);
398
399        inner.timer_time
400    }
401
402    /// Schedules an event to execute after the specified delay from the current time.
403    ///
404    /// # Panics
405    ///
406    /// Panics if the simulation lock is poisoned by a prior task panic.
407    #[instrument(skip(self))]
408    pub fn schedule_event(&self, event: Event, delay: Duration) {
409        let mut inner = self
410            .inner
411            .write()
412            .expect("RwLock poisoned: prior task panicked");
413        let scheduled_time = inner.current_time + delay;
414        let sequence = inner.next_sequence;
415        inner.next_sequence += 1;
416
417        let scheduled_event = ScheduledEvent::new(scheduled_time, event, sequence);
418        inner.event_queue.schedule(scheduled_event);
419    }
420
421    /// Schedules an event to execute at the specified absolute time.
422    ///
423    /// # Panics
424    ///
425    /// Panics if the simulation lock is poisoned by a prior task panic.
426    pub fn schedule_event_at(&self, event: Event, time: Duration) {
427        let mut inner = self
428            .inner
429            .write()
430            .expect("RwLock poisoned: prior task panicked");
431        let sequence = inner.next_sequence;
432        inner.next_sequence += 1;
433
434        let scheduled_event = ScheduledEvent::new(time, event, sequence);
435        inner.event_queue.schedule(scheduled_event);
436    }
437
438    /// Creates a weak reference to this simulation world.
439    ///
440    /// Weak references can be used to access the simulation without preventing
441    /// it from being dropped, enabling handle-based access patterns.
442    #[must_use]
443    pub fn downgrade(&self) -> WeakSimWorld {
444        WeakSimWorld {
445            inner: Arc::downgrade(&self.inner),
446        }
447    }
448
449    /// Returns `true` if there are events waiting to be processed.
450    ///
451    /// # Panics
452    ///
453    /// Panics if the simulation lock is poisoned by a prior task panic.
454    #[must_use]
455    pub fn has_pending_events(&self) -> bool {
456        !self
457            .inner
458            .read()
459            .expect("RwLock poisoned: prior task panicked")
460            .event_queue
461            .is_empty()
462    }
463
464    /// Returns the number of events waiting to be processed.
465    ///
466    /// # Panics
467    ///
468    /// Panics if the simulation lock is poisoned by a prior task panic.
469    #[must_use]
470    pub fn pending_event_count(&self) -> usize {
471        self.inner
472            .read()
473            .expect("RwLock poisoned: prior task panicked")
474            .event_queue
475            .len()
476    }
477
478    /// Create a network provider for this simulation
479    #[must_use]
480    pub fn network_provider(&self) -> SimNetworkProvider {
481        SimNetworkProvider::new(self.downgrade())
482    }
483
484    /// Create a time provider for this simulation
485    #[must_use]
486    pub fn time_provider(&self) -> crate::providers::SimTimeProvider {
487        crate::providers::SimTimeProvider::new(self.downgrade())
488    }
489
490    /// Create a task provider for this simulation
491    #[must_use]
492    pub fn task_provider(&self) -> crate::providers::SimTaskProvider {
493        crate::providers::SimTaskProvider
494    }
495
496    /// Create a storage provider for this simulation scoped to a process IP.
497    #[must_use]
498    pub fn storage_provider(&self, ip: std::net::IpAddr) -> crate::storage::SimStorageProvider {
499        crate::storage::SimStorageProvider::new(self.downgrade(), ip)
500    }
501
502    /// Set the default storage configuration for this simulation.
503    ///
504    /// Used as fallback when no per-process config is set for a given IP.
505    ///
506    /// # Panics
507    ///
508    /// Panics if the simulation lock is poisoned by a prior task panic.
509    pub fn set_storage_config(&mut self, config: crate::storage::StorageConfiguration) {
510        self.inner
511            .write()
512            .expect("RwLock poisoned: prior task panicked")
513            .storage
514            .config = config;
515    }
516
517    /// Set the storage configuration for a single process IP.
518    ///
519    /// Overrides the default ([`set_storage_config`](Self::set_storage_config))
520    /// for files owned by `ip`, letting different machines run with different
521    /// storage timing and fault profiles in the same simulation.
522    ///
523    /// # Panics
524    ///
525    /// Panics if the simulation lock is poisoned by a prior task panic.
526    pub fn set_storage_config_for(
527        &mut self,
528        ip: std::net::IpAddr,
529        config: crate::storage::StorageConfiguration,
530    ) {
531        self.inner
532            .write()
533            .expect("RwLock poisoned: prior task panicked")
534            .storage
535            .per_process_configs
536            .insert(ip, config);
537    }
538
539    /// Active disk-degradation episode for a process IP, if any.
540    ///
541    /// Episodes are scoped per owner: one stall/throttle window applies to every
542    /// file the process owns. Observability accessor for tests and diagnostics.
543    ///
544    /// # Panics
545    ///
546    /// Panics if the simulation lock is poisoned by a prior task panic.
547    #[must_use]
548    pub fn disk_episode_for(
549        &self,
550        ip: std::net::IpAddr,
551    ) -> Option<super::state::DiskDegradationState> {
552        self.inner
553            .read()
554            .expect("RwLock poisoned: prior task panicked")
555            .storage
556            .disk_episodes
557            .get(&ip)
558            .copied()
559    }
560
561    /// Access network configuration for latency calculations using thread-local RNG.
562    ///
563    /// This method provides access to the network configuration for calculating
564    /// latencies and other network parameters. Random values should be generated
565    /// using the thread-local RNG functions like `sim_random()`.
566    ///
567    /// Access the network configuration for this simulation.
568    ///
569    /// # Panics
570    ///
571    /// Panics if the simulation lock is poisoned by a prior task panic.
572    pub fn with_network_config<F, R>(&self, f: F) -> R
573    where
574        F: FnOnce(&NetworkConfiguration) -> R,
575    {
576        let inner = self
577            .inner
578            .read()
579            .expect("RwLock poisoned: prior task panicked");
580        f(&inner.network.config)
581    }
582
583    /// Create a listener in the simulation (used by `SimNetworkProvider`)
584    pub(crate) fn create_listener(&self) -> ListenerId {
585        let mut inner = self
586            .inner
587            .write()
588            .expect("RwLock poisoned: prior task panicked");
589        let listener_id = ListenerId(inner.network.next_listener_id);
590        inner.network.next_listener_id += 1;
591
592        inner.network.listeners.insert(listener_id);
593
594        listener_id
595    }
596
597    /// Read data from connection's receive buffer (used by `SimTcpStream`)
598    pub(crate) fn read_from_connection(
599        &self,
600        connection_id: ConnectionId,
601        buf: &mut [u8],
602    ) -> SimulationResult<usize> {
603        let mut inner = self
604            .inner
605            .write()
606            .expect("RwLock poisoned: prior task panicked");
607
608        // Read the config knob before borrowing the connection mutably (disjoint
609        // fields, but keeping it a local sidesteps any borrow friction).
610        let partial_read_max_bytes = inner.network.config.chaos.partial_read_max_bytes;
611
612        if let Some(connection) = inner.network.connections.get_mut(&connection_id) {
613            let available = std::cmp::min(buf.len(), connection.receive_buffer.len());
614
615            // BUGGIFY: simulate a partial read by delivering fewer bytes than are
616            // available, leaving the remainder buffered for the next read (mirrors
617            // FDB's Sim2Conn receiver). Skip stable connections. Always deliver at
618            // least one byte so the caller never mistakes a partial read for EOF,
619            // which would leave poll_read waiting on data already in the buffer.
620            let limit = if available > 0 && !connection.flags.is_stable() && crate::buggify!() {
621                let max_read = std::cmp::min(available, partial_read_max_bytes);
622                if max_read >= 1 {
623                    let n = sim_random_range(1..max_read + 1);
624                    if n < available {
625                        tracing::debug!(
626                            "BUGGIFY: Partial read on connection {} - delivering {} of {} bytes",
627                            connection_id.0,
628                            n,
629                            available
630                        );
631                    }
632                    n
633                } else {
634                    available
635                }
636            } else {
637                available
638            };
639
640            let mut bytes_read = 0;
641            while bytes_read < limit {
642                match connection.receive_buffer.pop_front() {
643                    Some(byte) => {
644                        buf[bytes_read] = byte;
645                        bytes_read += 1;
646                    }
647                    None => break,
648                }
649            }
650            Ok(bytes_read)
651        } else {
652            Err(SimulationError::InvalidState(
653                "connection not found".to_string(),
654            ))
655        }
656    }
657
658    /// Write data to connection's receive buffer (used by `SimTcpStream` write operations)
659    pub(crate) fn write_to_connection(
660        &self,
661        connection_id: ConnectionId,
662        data: &[u8],
663    ) -> SimulationResult<()> {
664        let mut inner = self
665            .inner
666            .write()
667            .expect("RwLock poisoned: prior task panicked");
668
669        if let Some(connection) = inner.network.connections.get_mut(&connection_id) {
670            for &byte in data {
671                connection.receive_buffer.push_back(byte);
672            }
673            Ok(())
674        } else {
675            Err(SimulationError::InvalidState(
676                "connection not found".to_string(),
677            ))
678        }
679    }
680
681    /// Buffer data for ordered sending on a TCP connection.
682    ///
683    /// This method implements the core TCP ordering guarantee by ensuring that all
684    /// write operations on a single connection are processed in FIFO order.
685    pub(crate) fn buffer_send(
686        &self,
687        connection_id: ConnectionId,
688        data: Vec<u8>,
689    ) -> SimulationResult<()> {
690        tracing::debug!(
691            "buffer_send called for connection_id={} with {} bytes",
692            connection_id.0,
693            data.len()
694        );
695        let mut inner = self
696            .inner
697            .write()
698            .expect("RwLock poisoned: prior task panicked");
699
700        if let Some(conn) = inner.network.connections.get_mut(&connection_id) {
701            // Always add data to send buffer for TCP ordering
702            conn.send_buffer.push_back(data);
703            tracing::debug!(
704                "buffer_send: added data to send_buffer, new length: {}",
705                conn.send_buffer.len()
706            );
707
708            // If sender is not already active, start processing the buffer
709            if conn.flags.send_in_progress() {
710                tracing::debug!(
711                    "buffer_send: sender already in progress, not scheduling new event"
712                );
713            } else {
714                tracing::debug!(
715                    "buffer_send: sender not in progress, scheduling ProcessSendBuffer event"
716                );
717                conn.flags.set_send_in_progress(true);
718
719                // Schedule immediate processing of the buffer
720                let scheduled_time = inner.current_time + std::time::Duration::ZERO;
721                let sequence = inner.next_sequence;
722                inner.next_sequence += 1;
723                let scheduled_event = ScheduledEvent::new(
724                    scheduled_time,
725                    Event::Network {
726                        connection_id: connection_id.0,
727                        operation: NetworkOperation::ProcessSendBuffer,
728                    },
729                    sequence,
730                );
731                inner.event_queue.schedule(scheduled_event);
732                tracing::debug!(
733                    "buffer_send: scheduled ProcessSendBuffer event with sequence {}",
734                    sequence
735                );
736            }
737
738            Ok(())
739        } else {
740            tracing::debug!(
741                "buffer_send: connection_id={} not found in connections table",
742                connection_id.0
743            );
744            Err(SimulationError::InvalidState(
745                "connection not found".to_string(),
746            ))
747        }
748    }
749
750    /// Create a bidirectional TCP connection pair for client-server communication.
751    ///
752    /// FDB Pattern (sim2.actor.cpp:1149-1175):
753    /// - Client connection stores server's real address as `peer_address`
754    /// - Server connection stores synthesized ephemeral address (random IP + port 40000-60000)
755    ///
756    /// This simulates real TCP behavior where servers see client ephemeral ports.
757    pub(crate) fn create_connection_pair(
758        &self,
759        client_addr: &str,
760        server_addr: &str,
761    ) -> (ConnectionId, ConnectionId) {
762        // Default send buffer capacity (Bandwidth-Delay Product); 64 KB is
763        // typical for TCP socket buffers. Real simulations could compute
764        // this as max_latency_ms * bandwidth_bytes_per_ms.
765        const DEFAULT_SEND_BUFFER_CAPACITY: usize = 64 * 1024;
766
767        let mut inner = self
768            .inner
769            .write()
770            .expect("RwLock poisoned: prior task panicked");
771
772        let client_conn = ConnectionId(inner.network.next_connection_id);
773        inner.network.next_connection_id += 1;
774
775        let server_conn = ConnectionId(inner.network.next_connection_id);
776        inner.network.next_connection_id += 1;
777
778        // Capture current time to avoid borrow conflicts
779        let current_time = inner.current_time;
780
781        // Parse IP addresses for partition tracking
782        let client_endpoint = NetworkState::parse_ip_from_addr(client_addr);
783        let server_endpoint = NetworkState::parse_ip_from_addr(server_addr);
784
785        // FDB Pattern: Synthesize ephemeral address for server-side connection
786        // sim2.actor.cpp:1149-1175: randomInt(0,256) for IP offset, randomInt(40000,60000) for port
787        // Use thread-local sim_random_range for deterministic randomness
788        let ephemeral_peer_addr = match client_endpoint {
789            Some(std::net::IpAddr::V4(ipv4)) => {
790                let octets = ipv4.octets();
791                let ip_offset =
792                    u8::try_from(sim_random_range(0u32..256)).expect("range bounded to u8");
793                let new_last_octet = octets[3].wrapping_add(ip_offset);
794                let ephemeral_ip =
795                    std::net::Ipv4Addr::new(octets[0], octets[1], octets[2], new_last_octet);
796                let ephemeral_port = sim_random_range(40000u16..60000);
797                format!("{ephemeral_ip}:{ephemeral_port}")
798            }
799            Some(std::net::IpAddr::V6(ipv6)) => {
800                // For IPv6, just modify the last segment
801                let segments = ipv6.segments();
802                let mut new_segments = segments;
803                let ip_offset = sim_random_range(0u16..256);
804                new_segments[7] = new_segments[7].wrapping_add(ip_offset);
805                let ephemeral_ip = std::net::Ipv6Addr::from(new_segments);
806                let ephemeral_port = sim_random_range(40000u16..60000);
807                format!("[{ephemeral_ip}]:{ephemeral_port}")
808            }
809            None => {
810                // Fallback: use client address with random port
811                let ephemeral_port = sim_random_range(40000u16..60000);
812                format!("unknown:{ephemeral_port}")
813            }
814        };
815
816        // Create paired connections.
817        // Client stores server's real address as peer_address
818        inner.network.connections.insert(
819            client_conn,
820            ConnectionState {
821                local_ip: client_endpoint,
822                remote_ip: server_endpoint,
823                peer_address: server_addr.to_owned(),
824                receive_buffer: VecDeque::new(),
825                paired_connection: Some(server_conn),
826                send_buffer: VecDeque::new(),
827                next_send_time: current_time,
828                flags: ConnectionFlags::default(),
829                cut_expiry: None,
830                close_reason: CloseReason::None,
831                send_buffer_capacity: DEFAULT_SEND_BUFFER_CAPACITY,
832                send_delay: None,
833                recv_delay: None,
834                half_open_error_at: None,
835                last_data_delivery_scheduled_at: None,
836            },
837        );
838
839        // Server stores synthesized ephemeral address as peer_address
840        inner.network.connections.insert(
841            server_conn,
842            ConnectionState {
843                local_ip: server_endpoint,
844                remote_ip: client_endpoint,
845                peer_address: ephemeral_peer_addr,
846                receive_buffer: VecDeque::new(),
847                paired_connection: Some(client_conn),
848                send_buffer: VecDeque::new(),
849                next_send_time: current_time,
850                flags: ConnectionFlags::default(),
851                cut_expiry: None,
852                close_reason: CloseReason::None,
853                send_buffer_capacity: DEFAULT_SEND_BUFFER_CAPACITY,
854                send_delay: None,
855                recv_delay: None,
856                half_open_error_at: None,
857                last_data_delivery_scheduled_at: None,
858            },
859        );
860
861        (client_conn, server_conn)
862    }
863
864    /// Register a waker for read operations
865    pub(crate) fn register_read_waker(&self, connection_id: ConnectionId, waker: Waker) {
866        let mut inner = self
867            .inner
868            .write()
869            .expect("RwLock poisoned: prior task panicked");
870        let is_replacement = inner.wakers.reads.contains_key(&connection_id);
871        inner.wakers.reads.insert(connection_id, waker);
872        tracing::debug!(
873            "register_read_waker: connection_id={}, replacement={}, total_wakers={}",
874            connection_id.0,
875            is_replacement,
876            inner.wakers.reads.len()
877        );
878    }
879
880    /// Register a waker for accept operations
881    pub(crate) fn register_accept_waker(&self, addr: &str, waker: Waker) {
882        use std::collections::hash_map::DefaultHasher;
883        use std::hash::{Hash, Hasher};
884
885        let mut inner = self
886            .inner
887            .write()
888            .expect("RwLock poisoned: prior task panicked");
889        // For simplicity, we'll use addr hash as listener ID for waker storage
890        let mut hasher = DefaultHasher::new();
891        addr.hash(&mut hasher);
892        let listener_key = ListenerId(hasher.finish());
893
894        inner.wakers.listeners.insert(listener_key, waker);
895    }
896
897    /// Store a pending connection for later `accept()` call
898    pub(crate) fn store_pending_connection(&self, addr: &str, connection_id: ConnectionId) {
899        use std::collections::hash_map::DefaultHasher;
900        use std::hash::{Hash, Hasher};
901
902        let mut inner = self
903            .inner
904            .write()
905            .expect("RwLock poisoned: prior task panicked");
906        inner
907            .network
908            .pending_connections
909            .insert(addr.to_string(), connection_id);
910
911        // Wake any accept() calls waiting for this connection
912        let mut hasher = DefaultHasher::new();
913        addr.hash(&mut hasher);
914        let listener_key = ListenerId(hasher.finish());
915
916        if let Some(waker) = inner.wakers.listeners.remove(&listener_key) {
917            waker.wake();
918        }
919    }
920
921    /// Get a pending connection for `accept()` call
922    pub(crate) fn pending_connection(&self, addr: &str) -> Option<ConnectionId> {
923        let mut inner = self
924            .inner
925            .write()
926            .expect("RwLock poisoned: prior task panicked");
927        inner.network.pending_connections.remove(addr)
928    }
929
930    /// Get the peer address for a connection.
931    ///
932    /// FDB Pattern (sim2.actor.cpp):
933    /// - For client-side connections: returns server's listening address
934    /// - For server-side connections: returns synthesized ephemeral address
935    ///
936    /// The returned address may not be connectable for server-side connections,
937    /// matching real TCP behavior where servers see client ephemeral ports.
938    pub(crate) fn connection_peer_address(&self, connection_id: ConnectionId) -> Option<String> {
939        let inner = self
940            .inner
941            .read()
942            .expect("RwLock poisoned: prior task panicked");
943        inner
944            .network
945            .connections
946            .get(&connection_id)
947            .map(|conn| conn.peer_address.clone())
948    }
949
950    /// Sleep for the specified duration in simulation time.
951    ///
952    /// Returns a future that will complete when the simulation time has advanced
953    /// by the specified duration.
954    #[instrument(skip(self))]
955    pub fn sleep(&self, duration: Duration) -> SleepFuture {
956        let task_id = self.generate_task_id();
957
958        // Apply buggified delay if enabled
959        let actual_duration = self.apply_buggified_delay(duration);
960
961        // Schedule a wake event for this task
962        self.schedule_event(Event::Timer { task_id }, actual_duration);
963
964        // Return a future that will be woken when the event is processed
965        SleepFuture::new(self.downgrade(), task_id)
966    }
967
968    /// Apply buggified delay to a duration if chaos is enabled.
969    fn apply_buggified_delay(&self, duration: Duration) -> Duration {
970        let inner = self
971            .inner
972            .read()
973            .expect("RwLock poisoned: prior task panicked");
974        let chaos = &inner.network.config.chaos;
975
976        if !chaos.buggified_delay_enabled || chaos.buggified_delay_max == Duration::ZERO {
977            return duration;
978        }
979
980        // 25% probability per FDB
981        if sim_random::<f64>() < chaos.buggified_delay_probability {
982            // Power-law distribution: pow(random01(), 1000) creates very skewed delays
983            let random_factor = sim_random::<f64>().powf(1000.0);
984            let extra_delay = chaos.buggified_delay_max.mul_f64(random_factor);
985            tracing::trace!(
986                extra_delay_ms = extra_delay.as_millis(),
987                "Buggified delay applied"
988            );
989            duration + extra_delay
990        } else {
991            duration
992        }
993    }
994
995    /// Generate a unique task ID for sleep operations.
996    fn generate_task_id(&self) -> u64 {
997        let mut inner = self
998            .inner
999            .write()
1000            .expect("RwLock poisoned: prior task panicked");
1001        let task_id = inner.next_task_id;
1002        inner.next_task_id += 1;
1003        task_id
1004    }
1005
1006    /// Wake all tasks associated with a connection
1007    fn wake_all(wakers: &mut BTreeMap<ConnectionId, Vec<Waker>>, connection_id: ConnectionId) {
1008        if let Some(waker_list) = wakers.remove(&connection_id) {
1009            for waker in waker_list {
1010                waker.wake();
1011            }
1012        }
1013    }
1014
1015    /// Check if a task has been awakened.
1016    pub(crate) fn is_task_awake(&self, task_id: u64) -> bool {
1017        let inner = self
1018            .inner
1019            .read()
1020            .expect("RwLock poisoned: prior task panicked");
1021        inner.awakened_tasks.contains(&task_id)
1022    }
1023
1024    /// Register a waker for a task.
1025    pub(crate) fn register_task_waker(&self, task_id: u64, waker: Waker) {
1026        let mut inner = self
1027            .inner
1028            .write()
1029            .expect("RwLock poisoned: prior task panicked");
1030        inner.wakers.tasks.insert(task_id, waker);
1031    }
1032
1033    /// Clear expired clogs and wake pending tasks (helper for use with `SimInner`)
1034    fn clear_expired_clogs_with_inner(inner: &mut SimInner) {
1035        let now = inner.current_time;
1036        let expired: Vec<ConnectionId> = inner
1037            .network
1038            .connection_clogs
1039            .iter()
1040            .filter_map(|(id, state)| (now >= state.expires_at).then_some(*id))
1041            .collect();
1042
1043        for id in expired {
1044            inner.network.connection_clogs.remove(&id);
1045            Self::wake_all(&mut inner.wakers.write_clogs, id);
1046        }
1047    }
1048
1049    /// Static event processor for simulation events.
1050    #[instrument(skip(inner))]
1051    fn process_event_with_inner(inner: &mut SimInner, event: Event) {
1052        inner.events_processed += 1;
1053
1054        match event {
1055            Event::Timer { task_id } => Self::handle_timer_event(inner, task_id),
1056            Event::Connection { id, state } => Self::handle_connection_event(inner, id, state),
1057            Event::Network {
1058                connection_id,
1059                operation,
1060            } => Self::handle_network_event(inner, connection_id, operation),
1061            Event::Storage { file_id, operation } => {
1062                super::storage_ops::handle_storage_event(inner, file_id, operation);
1063            }
1064            Event::Shutdown => Self::handle_shutdown_event(inner),
1065            Event::ProcessRestart { ip }
1066            | Event::ProcessGracefulShutdown { ip, .. }
1067            | Event::ProcessForceKill { ip, .. } => {
1068                // Process lifecycle events are handled by the orchestrator, not SimWorld.
1069                // We just log them here; the orchestrator reads the event after step().
1070                tracing::debug!("Process lifecycle event for IP {}", ip);
1071            }
1072        }
1073    }
1074
1075    /// Handle timer events - wake sleeping tasks.
1076    fn handle_timer_event(inner: &mut SimInner, task_id: u64) {
1077        inner.awakened_tasks.insert(task_id);
1078        if let Some(waker) = inner.wakers.tasks.remove(&task_id) {
1079            waker.wake();
1080        }
1081    }
1082
1083    /// Handle connection state change events.
1084    fn handle_connection_event(inner: &mut SimInner, id: u64, state: ConnectionStateChange) {
1085        let connection_id = ConnectionId(id);
1086
1087        match state {
1088            ConnectionStateChange::BindComplete | ConnectionStateChange::ConnectionReady => {
1089                // No action needed for these states
1090            }
1091            ConnectionStateChange::ClogClear => {
1092                inner.network.connection_clogs.remove(&connection_id);
1093                Self::wake_all(&mut inner.wakers.write_clogs, connection_id);
1094            }
1095            ConnectionStateChange::ReadClogClear => {
1096                inner.network.read_clogs.remove(&connection_id);
1097                Self::wake_all(&mut inner.wakers.read_clogs, connection_id);
1098            }
1099            ConnectionStateChange::PartitionRestore => {
1100                Self::clear_expired_partitions(inner);
1101            }
1102            ConnectionStateChange::SendPartitionClear => {
1103                Self::clear_expired_send_partitions(inner);
1104            }
1105            ConnectionStateChange::RecvPartitionClear => {
1106                Self::clear_expired_recv_partitions(inner);
1107            }
1108            ConnectionStateChange::CutRestore => {
1109                if let Some(conn) = inner.network.connections.get_mut(&connection_id)
1110                    && conn.flags.is_cut()
1111                {
1112                    conn.flags.set_is_cut(false);
1113                    conn.cut_expiry = None;
1114                    inner.record_fault(SimFaultEvent::CutRestored { connection_id: id });
1115                    tracing::debug!("Connection {} restored via scheduled event", id);
1116                    Self::wake_all(&mut inner.wakers.cuts, connection_id);
1117                }
1118            }
1119            ConnectionStateChange::HalfOpenError => {
1120                inner.record_fault(SimFaultEvent::HalfOpenError { connection_id: id });
1121                tracing::debug!("Connection {} half-open error time reached", id);
1122                if let Some(waker) = inner.wakers.reads.remove(&connection_id) {
1123                    waker.wake();
1124                }
1125                Self::wake_all(&mut inner.wakers.send_buffers, connection_id);
1126            }
1127        }
1128    }
1129
1130    /// Clear expired IP partitions.
1131    fn clear_expired_partitions(inner: &mut SimInner) {
1132        let now = inner.current_time;
1133        let expired: Vec<_> = inner
1134            .network
1135            .ip_partitions
1136            .iter()
1137            .filter_map(|(pair, state)| (now >= state.expires_at).then_some(*pair))
1138            .collect();
1139
1140        for pair in expired {
1141            inner.network.ip_partitions.remove(&pair);
1142            tracing::debug!("Restored IP partition {} -> {}", pair.0, pair.1);
1143        }
1144    }
1145
1146    /// Clear expired send partitions.
1147    fn clear_expired_send_partitions(inner: &mut SimInner) {
1148        let now = inner.current_time;
1149        let expired: Vec<_> = inner
1150            .network
1151            .send_partitions
1152            .iter()
1153            .filter_map(|(ip, &expires_at)| (now >= expires_at).then_some(*ip))
1154            .collect();
1155
1156        for ip in expired {
1157            inner.network.send_partitions.remove(&ip);
1158            tracing::debug!("Cleared send partition for {}", ip);
1159        }
1160    }
1161
1162    /// Clear expired receive partitions.
1163    fn clear_expired_recv_partitions(inner: &mut SimInner) {
1164        let now = inner.current_time;
1165        let expired: Vec<_> = inner
1166            .network
1167            .recv_partitions
1168            .iter()
1169            .filter_map(|(ip, &expires_at)| (now >= expires_at).then_some(*ip))
1170            .collect();
1171
1172        for ip in expired {
1173            inner.network.recv_partitions.remove(&ip);
1174            tracing::debug!("Cleared receive partition for {}", ip);
1175        }
1176    }
1177
1178    /// Handle network events (data delivery and send buffer processing).
1179    fn handle_network_event(inner: &mut SimInner, conn_id: u64, operation: NetworkOperation) {
1180        let connection_id = ConnectionId(conn_id);
1181
1182        match operation {
1183            NetworkOperation::DataDelivery { data } => {
1184                Self::handle_data_delivery(inner, connection_id, data);
1185            }
1186            NetworkOperation::ProcessSendBuffer => {
1187                Self::handle_process_send_buffer(inner, connection_id);
1188            }
1189            NetworkOperation::FinDelivery => {
1190                Self::handle_fin_delivery(inner, connection_id);
1191            }
1192        }
1193    }
1194
1195    /// Handle data delivery to a connection's receive buffer.
1196    fn handle_data_delivery(inner: &mut SimInner, connection_id: ConnectionId, data: Vec<u8>) {
1197        tracing::trace!(
1198            "DataDelivery: {} bytes to connection {}",
1199            data.len(),
1200            connection_id.0
1201        );
1202
1203        // Check connection exists and if it's stable
1204        let is_stable = inner
1205            .network
1206            .connections
1207            .get(&connection_id)
1208            .is_some_and(|conn| conn.flags.is_stable());
1209
1210        if !inner.network.connections.contains_key(&connection_id) {
1211            tracing::warn!("DataDelivery: connection {} not found", connection_id.0);
1212            return;
1213        }
1214
1215        // Apply bit flipping chaos (needs mutable inner access) - skip for stable connections
1216        let data_to_deliver = if is_stable {
1217            data
1218        } else {
1219            Self::maybe_corrupt_data(inner, connection_id, &data)
1220        };
1221
1222        // Now get connection reference and deliver data
1223        let Some(conn) = inner.network.connections.get_mut(&connection_id) else {
1224            return;
1225        };
1226
1227        for &byte in &data_to_deliver {
1228            conn.receive_buffer.push_back(byte);
1229        }
1230
1231        if let Some(waker) = inner.wakers.reads.remove(&connection_id) {
1232            waker.wake();
1233        }
1234    }
1235
1236    /// Handle FIN delivery to a connection's receive side (TCP half-close).
1237    ///
1238    /// Sets `remote_fin_received` on the receiving connection so that `poll_read`
1239    /// returns EOF after the receive buffer is drained. Ignores FIN if the connection
1240    /// was already aborted (stale event).
1241    fn handle_fin_delivery(inner: &mut SimInner, connection_id: ConnectionId) {
1242        tracing::debug!(
1243            "FinDelivery: FIN received on connection {}",
1244            connection_id.0
1245        );
1246
1247        // If connection was aborted after FIN was scheduled, ignore the stale FIN
1248        let is_closed = inner
1249            .network
1250            .connections
1251            .get(&connection_id)
1252            .is_some_and(|conn| conn.flags.is_closed());
1253
1254        if is_closed {
1255            tracing::debug!(
1256                "FinDelivery: connection {} already closed, ignoring stale FIN",
1257                connection_id.0
1258            );
1259            return;
1260        }
1261
1262        if let Some(conn) = inner.network.connections.get_mut(&connection_id) {
1263            conn.flags.set_remote_fin_received(true);
1264        }
1265
1266        if let Some(waker) = inner.wakers.reads.remove(&connection_id) {
1267            waker.wake();
1268        }
1269    }
1270
1271    /// Schedule a `FinDelivery` event to the peer connection.
1272    ///
1273    /// The FIN is scheduled after the last `DataDelivery` event to ensure all data
1274    /// arrives at the peer before EOF is signaled.
1275    fn schedule_fin_delivery(
1276        inner: &mut SimInner,
1277        paired_id: Option<ConnectionId>,
1278        last_delivery_time: Option<Duration>,
1279    ) {
1280        let Some(peer_id) = paired_id else {
1281            return;
1282        };
1283
1284        // FIN must arrive after all DataDelivery events
1285        let fin_time = match last_delivery_time {
1286            Some(t) if t >= inner.current_time => t + Duration::from_nanos(1),
1287            _ => inner.current_time + Duration::from_nanos(1),
1288        };
1289
1290        let sequence = inner.next_sequence;
1291        inner.next_sequence += 1;
1292
1293        tracing::debug!(
1294            "Scheduling FinDelivery to connection {} at {:?}",
1295            peer_id.0,
1296            fin_time
1297        );
1298
1299        inner.event_queue.schedule(ScheduledEvent::new(
1300            fin_time,
1301            Event::Network {
1302                connection_id: peer_id.0,
1303                operation: NetworkOperation::FinDelivery,
1304            },
1305            sequence,
1306        ));
1307    }
1308
1309    /// Maybe corrupt data with bit flips based on chaos configuration.
1310    fn maybe_corrupt_data(
1311        inner: &mut SimInner,
1312        connection_id: ConnectionId,
1313        data: &[u8],
1314    ) -> Vec<u8> {
1315        if data.is_empty() {
1316            return data.to_vec();
1317        }
1318
1319        let chaos = &inner.network.config.chaos;
1320        let now = inner.current_time;
1321        let cooldown_elapsed =
1322            now.saturating_sub(inner.last_bit_flip_time) >= chaos.bit_flip_cooldown;
1323
1324        if !cooldown_elapsed || !crate::buggify_with_prob!(chaos.bit_flip_probability) {
1325            return data.to_vec();
1326        }
1327
1328        let random_value = sim_random::<u32>();
1329        let flip_count = SimInner::calculate_flip_bit_count(
1330            random_value,
1331            chaos.bit_flip_min_bits,
1332            chaos.bit_flip_max_bits,
1333        );
1334
1335        let mut corrupted_data = data.to_vec();
1336        let mut flipped_positions = std::collections::HashSet::new();
1337
1338        for _ in 0..flip_count {
1339            // `% len` keeps the result well within `usize`; truncation is intentional.
1340            let raw_byte = sim_random::<u64>();
1341            let raw_bit = sim_random::<u64>();
1342            let len_u64 =
1343                u64::try_from(corrupted_data.len()).expect("corrupted_data length fits in u64");
1344            let byte_idx =
1345                usize::try_from(raw_byte % len_u64).expect("modulus bounded by usize length");
1346            let bit_idx = usize::try_from(raw_bit % 8).expect("modulus 8 fits in usize");
1347            let position = (byte_idx, bit_idx);
1348
1349            if !flipped_positions.contains(&position) {
1350                flipped_positions.insert(position);
1351                corrupted_data[byte_idx] ^= 1 << bit_idx;
1352            }
1353        }
1354
1355        inner.last_bit_flip_time = now;
1356        tracing::info!(
1357            "BitFlipInjected: bytes={} bits_flipped={} unique_positions={}",
1358            data.len(),
1359            flip_count,
1360            flipped_positions.len()
1361        );
1362
1363        inner.record_fault(SimFaultEvent::BitFlip {
1364            connection_id: connection_id.0,
1365            flip_count: flipped_positions.len(),
1366        });
1367
1368        corrupted_data
1369    }
1370
1371    /// Handle processing of a connection's send buffer.
1372    fn handle_process_send_buffer(inner: &mut SimInner, connection_id: ConnectionId) {
1373        let is_partitioned = inner
1374            .network
1375            .is_connection_partitioned(connection_id, inner.current_time);
1376
1377        if is_partitioned {
1378            Self::handle_partitioned_send(inner, connection_id);
1379        } else {
1380            Self::handle_normal_send(inner, connection_id);
1381        }
1382    }
1383
1384    /// Handle send when connection is partitioned.
1385    fn handle_partitioned_send(inner: &mut SimInner, connection_id: ConnectionId) {
1386        let Some(conn) = inner.network.connections.get_mut(&connection_id) else {
1387            return;
1388        };
1389
1390        if let Some(data) = conn.send_buffer.pop_front() {
1391            tracing::debug!(
1392                "Connection {} partitioned, failing send of {} bytes",
1393                connection_id.0,
1394                data.len()
1395            );
1396            Self::wake_all(&mut inner.wakers.send_buffers, connection_id);
1397
1398            if conn.send_buffer.is_empty() {
1399                conn.flags.set_send_in_progress(false);
1400                // Check for pending graceful close when pipeline drains
1401                if conn.flags.graceful_close_pending() {
1402                    conn.flags.set_graceful_close_pending(false);
1403                    let peer_id = conn.paired_connection;
1404                    let last_time = conn.last_data_delivery_scheduled_at;
1405                    Self::schedule_fin_delivery(inner, peer_id, last_time);
1406                }
1407            } else {
1408                Self::schedule_process_send_buffer(inner, connection_id);
1409            }
1410        } else {
1411            conn.flags.set_send_in_progress(false);
1412            // Check for pending graceful close when pipeline drains
1413            if conn.flags.graceful_close_pending() {
1414                conn.flags.set_graceful_close_pending(false);
1415                let peer_id = conn.paired_connection;
1416                let last_time = conn.last_data_delivery_scheduled_at;
1417                Self::schedule_fin_delivery(inner, peer_id, last_time);
1418            }
1419        }
1420    }
1421
1422    /// Handle normal (non-partitioned) send.
1423    fn handle_normal_send(inner: &mut SimInner, connection_id: ConnectionId) {
1424        // Extract connection info first
1425        let Some(conn) = inner.network.connections.get(&connection_id) else {
1426            return;
1427        };
1428
1429        let paired_id = conn.paired_connection;
1430        let send_delay = conn.send_delay;
1431        let next_send_time = conn.next_send_time;
1432        let has_data = !conn.send_buffer.is_empty();
1433        let is_stable = conn.flags.is_stable(); // For stable connection checks
1434        let local_ip = conn.local_ip;
1435        let remote_ip = conn.remote_ip;
1436
1437        let recv_delay = paired_id.and_then(|pid| {
1438            inner
1439                .network
1440                .connections
1441                .get(&pid)
1442                .and_then(|c| c.recv_delay)
1443        });
1444
1445        // Permanent per-pair latency, memoized at connect (FDB SimClogging). Pure
1446        // map read on a field disjoint from `connections`; ZERO when the feature is
1447        // off (map empty) or endpoints are unknown.
1448        let pair_extra = match (local_ip, remote_ip) {
1449            (Some(src), Some(dst)) => inner
1450                .network
1451                .pair_latencies
1452                .get(&(src, dst))
1453                .copied()
1454                .unwrap_or(Duration::ZERO),
1455            _ => Duration::ZERO,
1456        };
1457
1458        if !has_data {
1459            if let Some(conn) = inner.network.connections.get_mut(&connection_id) {
1460                conn.flags.set_send_in_progress(false);
1461                // Check for pending graceful close when pipeline drains
1462                if conn.flags.graceful_close_pending() {
1463                    conn.flags.set_graceful_close_pending(false);
1464                    let peer_id = conn.paired_connection;
1465                    let last_time = conn.last_data_delivery_scheduled_at;
1466                    Self::schedule_fin_delivery(inner, peer_id, last_time);
1467                }
1468            }
1469            return;
1470        }
1471
1472        let Some(conn) = inner.network.connections.get_mut(&connection_id) else {
1473            return;
1474        };
1475
1476        let Some(mut data) = conn.send_buffer.pop_front() else {
1477            conn.flags.set_send_in_progress(false);
1478            return;
1479        };
1480
1481        Self::wake_all(&mut inner.wakers.send_buffers, connection_id);
1482
1483        // BUGGIFY: Simulate partial/short writes (skip for stable connections)
1484        if !is_stable && crate::buggify!() && !data.is_empty() {
1485            let max_send = std::cmp::min(
1486                data.len(),
1487                inner.network.config.chaos.partial_write_max_bytes,
1488            );
1489            let truncate_to = sim_random_range(0..max_send + 1);
1490
1491            if truncate_to < data.len() {
1492                let remainder = data.split_off(truncate_to);
1493                conn.send_buffer.push_front(remainder);
1494                tracing::debug!(
1495                    "BUGGIFY: Partial write on connection {} - sending {} bytes",
1496                    connection_id.0,
1497                    data.len()
1498                );
1499            }
1500        }
1501
1502        let has_more = !conn.send_buffer.is_empty();
1503        let base_delay = if has_more {
1504            Duration::from_nanos(1)
1505        } else {
1506            send_delay.unwrap_or_else(|| {
1507                crate::network::sample_latency(&inner.network.config.write_latency)
1508            })
1509        };
1510
1511        let earliest_time = std::cmp::max(inner.current_time + base_delay, next_send_time);
1512        conn.next_send_time = earliest_time + Duration::from_nanos(1);
1513
1514        // Schedule data delivery to paired connection
1515        if let Some(paired_id) = paired_id {
1516            let scheduled_time = earliest_time + recv_delay.unwrap_or(Duration::ZERO) + pair_extra;
1517            let sequence = inner.next_sequence;
1518            inner.next_sequence += 1;
1519
1520            inner.event_queue.schedule(ScheduledEvent::new(
1521                scheduled_time,
1522                Event::Network {
1523                    connection_id: paired_id.0,
1524                    operation: NetworkOperation::DataDelivery { data },
1525                },
1526                sequence,
1527            ));
1528
1529            // Track delivery time for FIN ordering (must be after last DataDelivery)
1530            conn.last_data_delivery_scheduled_at = Some(scheduled_time);
1531        }
1532
1533        // Schedule next send if more data
1534        if conn.send_buffer.is_empty() {
1535            conn.flags.set_send_in_progress(false);
1536            // Check for pending graceful close when pipeline drains
1537            if conn.flags.graceful_close_pending() {
1538                conn.flags.set_graceful_close_pending(false);
1539                let peer_id = conn.paired_connection;
1540                let last_time = conn.last_data_delivery_scheduled_at;
1541                Self::schedule_fin_delivery(inner, peer_id, last_time);
1542            }
1543        } else {
1544            Self::schedule_process_send_buffer(inner, connection_id);
1545        }
1546    }
1547
1548    /// Schedule a `ProcessSendBuffer` event for the given connection.
1549    fn schedule_process_send_buffer(inner: &mut SimInner, connection_id: ConnectionId) {
1550        let sequence = inner.next_sequence;
1551        inner.next_sequence += 1;
1552
1553        inner.event_queue.schedule(ScheduledEvent::new(
1554            inner.current_time,
1555            Event::Network {
1556                connection_id: connection_id.0,
1557                operation: NetworkOperation::ProcessSendBuffer,
1558            },
1559            sequence,
1560        ));
1561    }
1562
1563    /// Handle shutdown event - wake all pending tasks.
1564    fn handle_shutdown_event(inner: &mut SimInner) {
1565        tracing::debug!("Processing Shutdown event - waking all pending tasks");
1566
1567        for (task_id, waker) in std::mem::take(&mut inner.wakers.tasks) {
1568            tracing::trace!("Waking task {}", task_id);
1569            waker.wake();
1570        }
1571
1572        for (_conn_id, waker) in std::mem::take(&mut inner.wakers.reads) {
1573            waker.wake();
1574        }
1575
1576        tracing::debug!("Shutdown event processed");
1577    }
1578
1579    /// Get current assertion results for all tracked assertions.
1580    #[must_use]
1581    pub fn assertion_results(
1582        &self,
1583    ) -> std::collections::HashMap<String, crate::chaos::AssertionStats> {
1584        crate::chaos::assertion_results()
1585    }
1586
1587    /// Reset assertion statistics to empty state.
1588    pub fn reset_assertion_results(&self) {
1589        crate::chaos::reset_assertion_results();
1590    }
1591
1592    /// Abort all connections involving a specific IP address.
1593    ///
1594    /// This is used during process reboot to immediately kill all network
1595    /// connections for the rebooted process. Both local and remote connections
1596    /// are aborted (RST semantics — peer sees ECONNRESET).
1597    ///
1598    /// # Panics
1599    ///
1600    /// Panics if the simulation lock is poisoned by a prior task panic.
1601    pub fn abort_all_connections_for_ip(&self, ip: std::net::IpAddr) {
1602        let connection_ids: Vec<ConnectionId> = {
1603            let inner = self
1604                .inner
1605                .read()
1606                .expect("RwLock poisoned: prior task panicked");
1607            inner
1608                .network
1609                .connections
1610                .iter()
1611                .filter_map(|(id, conn)| {
1612                    if conn.local_ip == Some(ip) || conn.remote_ip == Some(ip) {
1613                        Some(*id)
1614                    } else {
1615                        None
1616                    }
1617                })
1618                .collect()
1619        };
1620
1621        let count = connection_ids.len();
1622        for conn_id in connection_ids {
1623            self.close_connection_abort(conn_id);
1624        }
1625
1626        if count > 0 {
1627            tracing::debug!("Aborted {} connections for rebooted IP {}", count, ip);
1628        }
1629    }
1630
1631    /// Schedule a `ProcessRestart` event after a recovery delay.
1632    ///
1633    /// Called after a process is killed to schedule its restart.
1634    pub fn schedule_process_restart(
1635        &self,
1636        ip: std::net::IpAddr,
1637        recovery_delay: std::time::Duration,
1638    ) {
1639        self.schedule_event(Event::ProcessRestart { ip }, recovery_delay);
1640        tracing::debug!(
1641            "Scheduled process restart for IP {} in {:?}",
1642            ip,
1643            recovery_delay
1644        );
1645    }
1646
1647    /// Returns the last event processed by `step()`, if any.
1648    ///
1649    /// This is used by the orchestrator to detect `ProcessRestart` events
1650    /// and handle them (respawn the process).
1651    ///
1652    /// # Panics
1653    ///
1654    /// Panics if the simulation lock is poisoned by a prior task panic.
1655    #[must_use]
1656    pub fn last_processed_event(&self) -> Option<Event> {
1657        self.inner
1658            .read()
1659            .expect("RwLock poisoned: prior task panicked")
1660            .last_processed_event
1661            .clone()
1662    }
1663
1664    /// Extract simulation metrics (simulated time, events processed).
1665    ///
1666    /// # Panics
1667    ///
1668    /// Panics if the simulation lock is poisoned by a prior task panic.
1669    #[must_use]
1670    pub fn extract_metrics(&self) -> crate::runner::SimulationMetrics {
1671        let inner = self
1672            .inner
1673            .read()
1674            .expect("RwLock poisoned: prior task panicked");
1675
1676        crate::runner::SimulationMetrics {
1677            wall_time: std::time::Duration::ZERO,
1678            simulated_time: inner.current_time,
1679            events_processed: inner.events_processed,
1680        }
1681    }
1682
1683    // Phase 7: Simple write clogging methods
1684
1685    /// Check if a write should be clogged based on probability
1686    ///
1687    /// # Panics
1688    ///
1689    /// Panics if the simulation lock is poisoned by a prior task panic.
1690    #[must_use]
1691    pub fn should_clog_write(&self, connection_id: ConnectionId) -> bool {
1692        let inner = self
1693            .inner
1694            .read()
1695            .expect("RwLock poisoned: prior task panicked");
1696        let config = &inner.network.config;
1697
1698        // Skip stable connections (FDB: stableConnection exempt from chaos)
1699        if inner
1700            .network
1701            .connections
1702            .get(&connection_id)
1703            .is_some_and(|conn| conn.flags.is_stable())
1704        {
1705            return false;
1706        }
1707
1708        // Skip if already clogged
1709        if let Some(clog_state) = inner.network.connection_clogs.get(&connection_id) {
1710            return inner.current_time < clog_state.expires_at;
1711        }
1712
1713        // Check probability
1714        config.chaos.clog_probability > 0.0 && sim_random::<f64>() < config.chaos.clog_probability
1715    }
1716
1717    /// Clog a connection's write operations
1718    ///
1719    /// # Panics
1720    ///
1721    /// Panics if the simulation lock is poisoned by a prior task panic.
1722    pub fn clog_write(&self, connection_id: ConnectionId) {
1723        let mut inner = self
1724            .inner
1725            .write()
1726            .expect("RwLock poisoned: prior task panicked");
1727        let config = &inner.network.config;
1728
1729        let clog_duration = crate::network::sample_duration(&config.chaos.clog_duration);
1730        let expires_at = inner.current_time + clog_duration;
1731        inner
1732            .network
1733            .connection_clogs
1734            .insert(connection_id, ClogState { expires_at });
1735
1736        // Schedule an event to clear this clog
1737        let clear_event = Event::Connection {
1738            id: connection_id.0,
1739            state: ConnectionStateChange::ClogClear,
1740        };
1741        let sequence = inner.next_sequence;
1742        inner.next_sequence += 1;
1743        inner
1744            .event_queue
1745            .schedule(ScheduledEvent::new(expires_at, clear_event, sequence));
1746    }
1747
1748    /// Check if a connection's writes are currently clogged
1749    ///
1750    /// # Panics
1751    ///
1752    /// Panics if the simulation lock is poisoned by a prior task panic.
1753    #[must_use]
1754    pub fn is_write_clogged(&self, connection_id: ConnectionId) -> bool {
1755        let inner = self
1756            .inner
1757            .read()
1758            .expect("RwLock poisoned: prior task panicked");
1759
1760        if let Some(clog_state) = inner.network.connection_clogs.get(&connection_id) {
1761            inner.current_time < clog_state.expires_at
1762        } else {
1763            false
1764        }
1765    }
1766
1767    /// Register a waker for when write clog clears
1768    ///
1769    /// # Panics
1770    ///
1771    /// Panics if the simulation lock is poisoned by a prior task panic.
1772    pub fn register_clog_waker(&self, connection_id: ConnectionId, waker: Waker) {
1773        let mut inner = self
1774            .inner
1775            .write()
1776            .expect("RwLock poisoned: prior task panicked");
1777        inner
1778            .wakers
1779            .write_clogs
1780            .entry(connection_id)
1781            .or_default()
1782            .push(waker);
1783    }
1784
1785    // Read clogging methods (symmetric with write clogging)
1786
1787    /// Check if a read should be clogged based on probability
1788    ///
1789    /// # Panics
1790    ///
1791    /// Panics if the simulation lock is poisoned by a prior task panic.
1792    #[must_use]
1793    pub fn should_clog_read(&self, connection_id: ConnectionId) -> bool {
1794        let inner = self
1795            .inner
1796            .read()
1797            .expect("RwLock poisoned: prior task panicked");
1798        let config = &inner.network.config;
1799
1800        // Skip stable connections (FDB: stableConnection exempt from chaos)
1801        if inner
1802            .network
1803            .connections
1804            .get(&connection_id)
1805            .is_some_and(|conn| conn.flags.is_stable())
1806        {
1807            return false;
1808        }
1809
1810        // Skip if already clogged
1811        if let Some(clog_state) = inner.network.read_clogs.get(&connection_id) {
1812            return inner.current_time < clog_state.expires_at;
1813        }
1814
1815        // Check probability (same as write clogging)
1816        config.chaos.clog_probability > 0.0 && sim_random::<f64>() < config.chaos.clog_probability
1817    }
1818
1819    /// Clog a connection's read operations
1820    ///
1821    /// # Panics
1822    ///
1823    /// Panics if the simulation lock is poisoned by a prior task panic.
1824    pub fn clog_read(&self, connection_id: ConnectionId) {
1825        let mut inner = self
1826            .inner
1827            .write()
1828            .expect("RwLock poisoned: prior task panicked");
1829        let config = &inner.network.config;
1830
1831        let clog_duration = crate::network::sample_duration(&config.chaos.clog_duration);
1832        let expires_at = inner.current_time + clog_duration;
1833        inner
1834            .network
1835            .read_clogs
1836            .insert(connection_id, ClogState { expires_at });
1837
1838        // Schedule an event to clear this read clog
1839        let clear_event = Event::Connection {
1840            id: connection_id.0,
1841            state: ConnectionStateChange::ReadClogClear,
1842        };
1843        let sequence = inner.next_sequence;
1844        inner.next_sequence += 1;
1845        inner
1846            .event_queue
1847            .schedule(ScheduledEvent::new(expires_at, clear_event, sequence));
1848    }
1849
1850    /// Check if a connection's reads are currently clogged
1851    ///
1852    /// # Panics
1853    ///
1854    /// Panics if the simulation lock is poisoned by a prior task panic.
1855    #[must_use]
1856    pub fn is_read_clogged(&self, connection_id: ConnectionId) -> bool {
1857        let inner = self
1858            .inner
1859            .read()
1860            .expect("RwLock poisoned: prior task panicked");
1861
1862        if let Some(clog_state) = inner.network.read_clogs.get(&connection_id) {
1863            inner.current_time < clog_state.expires_at
1864        } else {
1865            false
1866        }
1867    }
1868
1869    /// Register a waker for when read clog clears
1870    ///
1871    /// # Panics
1872    ///
1873    /// Panics if the simulation lock is poisoned by a prior task panic.
1874    pub fn register_read_clog_waker(&self, connection_id: ConnectionId, waker: Waker) {
1875        let mut inner = self
1876            .inner
1877            .write()
1878            .expect("RwLock poisoned: prior task panicked");
1879        inner
1880            .wakers
1881            .read_clogs
1882            .entry(connection_id)
1883            .or_default()
1884            .push(waker);
1885    }
1886
1887    /// Clear expired clogs and wake pending tasks
1888    ///
1889    /// # Panics
1890    ///
1891    /// Panics if the simulation lock is poisoned by a prior task panic.
1892    pub fn clear_expired_clogs(&self) {
1893        let mut inner = self
1894            .inner
1895            .write()
1896            .expect("RwLock poisoned: prior task panicked");
1897        let now = inner.current_time;
1898        let expired: Vec<ConnectionId> = inner
1899            .network
1900            .connection_clogs
1901            .iter()
1902            .filter_map(|(id, state)| (now >= state.expires_at).then_some(*id))
1903            .collect();
1904
1905        for id in expired {
1906            inner.network.connection_clogs.remove(&id);
1907            Self::wake_all(&mut inner.wakers.write_clogs, id);
1908        }
1909    }
1910
1911    // Connection Cut Methods (temporary network outage simulation)
1912
1913    /// Check if a connection is temporarily cut.
1914    ///
1915    /// A cut connection is temporarily unavailable but will be restored.
1916    /// This is different from `is_connection_closed` which indicates permanent closure.
1917    ///
1918    /// # Panics
1919    ///
1920    /// Panics if the simulation lock is poisoned by a prior task panic.
1921    #[must_use]
1922    pub fn is_connection_cut(&self, connection_id: ConnectionId) -> bool {
1923        let inner = self
1924            .inner
1925            .read()
1926            .expect("RwLock poisoned: prior task panicked");
1927        inner
1928            .network
1929            .connections
1930            .get(&connection_id)
1931            .is_some_and(|conn| {
1932                conn.flags.is_cut()
1933                    && conn
1934                        .cut_expiry
1935                        .is_some_and(|expiry| inner.current_time < expiry)
1936            })
1937    }
1938
1939    /// Register a waker for when a cut connection is restored.
1940    ///
1941    /// # Panics
1942    ///
1943    /// Panics if the simulation lock is poisoned by a prior task panic.
1944    pub fn register_cut_waker(&self, connection_id: ConnectionId, waker: Waker) {
1945        let mut inner = self
1946            .inner
1947            .write()
1948            .expect("RwLock poisoned: prior task panicked");
1949        inner
1950            .wakers
1951            .cuts
1952            .entry(connection_id)
1953            .or_default()
1954            .push(waker);
1955    }
1956
1957    // Send buffer management methods
1958
1959    /// Get the send buffer capacity for a connection.
1960    ///
1961    /// # Panics
1962    ///
1963    /// Panics if the simulation lock is poisoned by a prior task panic.
1964    #[must_use]
1965    pub fn send_buffer_capacity(&self, connection_id: ConnectionId) -> usize {
1966        let inner = self
1967            .inner
1968            .read()
1969            .expect("RwLock poisoned: prior task panicked");
1970        inner
1971            .network
1972            .connections
1973            .get(&connection_id)
1974            .map_or(0, |conn| conn.send_buffer_capacity)
1975    }
1976
1977    /// Get the current send buffer usage for a connection.
1978    ///
1979    /// # Panics
1980    ///
1981    /// Panics if the simulation lock is poisoned by a prior task panic.
1982    #[must_use]
1983    pub fn send_buffer_used(&self, connection_id: ConnectionId) -> usize {
1984        let inner = self
1985            .inner
1986            .read()
1987            .expect("RwLock poisoned: prior task panicked");
1988        inner
1989            .network
1990            .connections
1991            .get(&connection_id)
1992            .map_or(0, |conn| {
1993                conn.send_buffer.iter().map(std::vec::Vec::len).sum()
1994            })
1995    }
1996
1997    /// Get the available send buffer space for a connection.
1998    #[must_use]
1999    pub fn available_send_buffer(&self, connection_id: ConnectionId) -> usize {
2000        let capacity = self.send_buffer_capacity(connection_id);
2001        let used = self.send_buffer_used(connection_id);
2002        capacity.saturating_sub(used)
2003    }
2004
2005    /// Register a waker for when send buffer space becomes available.
2006    ///
2007    /// # Panics
2008    ///
2009    /// Panics if the simulation lock is poisoned by a prior task panic.
2010    pub fn register_send_buffer_waker(&self, connection_id: ConnectionId, waker: Waker) {
2011        let mut inner = self
2012            .inner
2013            .write()
2014            .expect("RwLock poisoned: prior task panicked");
2015        inner
2016            .wakers
2017            .send_buffers
2018            .entry(connection_id)
2019            .or_default()
2020            .push(waker);
2021    }
2022
2023    // Per-IP-pair base latency methods
2024
2025    /// Get the base latency for a connection pair.
2026    /// Returns the latency if already set, otherwise None.
2027    ///
2028    /// # Panics
2029    ///
2030    /// Panics if the simulation lock is poisoned by a prior task panic.
2031    #[must_use]
2032    pub fn pair_latency(&self, src: IpAddr, dst: IpAddr) -> Option<Duration> {
2033        let inner = self
2034            .inner
2035            .read()
2036            .expect("RwLock poisoned: prior task panicked");
2037        inner.network.pair_latencies.get(&(src, dst)).copied()
2038    }
2039
2040    /// Set the base latency for a connection pair if not already set.
2041    /// Returns the latency (existing or newly set).
2042    ///
2043    /// # Panics
2044    ///
2045    /// Panics if the simulation lock is poisoned by a prior task panic.
2046    #[must_use]
2047    pub fn set_pair_latency_if_not_set(
2048        &self,
2049        src: IpAddr,
2050        dst: IpAddr,
2051        latency: Duration,
2052    ) -> Duration {
2053        let mut inner = self
2054            .inner
2055            .write()
2056            .expect("RwLock poisoned: prior task panicked");
2057        *inner
2058            .network
2059            .pair_latencies
2060            .entry((src, dst))
2061            .or_insert_with(|| {
2062                tracing::debug!(
2063                    "Setting base latency for IP pair {} -> {} to {:?}",
2064                    src,
2065                    dst,
2066                    latency
2067                );
2068                latency
2069            })
2070    }
2071
2072    /// Get the permanent per-pair base latency for a connection, memoizing it on
2073    /// first contact (FDB `SimClogging`).
2074    ///
2075    /// Returns [`Duration::ZERO`] — without drawing from the RNG or touching the
2076    /// pair map — when the `max_pair_latency` range is disabled (its `end` is zero)
2077    /// or the connection's endpoints are unknown. Otherwise samples a fixed latency
2078    /// from `max_pair_latency` once per ordered IP pair and reuses it for the run.
2079    ///
2080    /// # Panics
2081    ///
2082    /// Panics if the simulation lock is poisoned by a prior task panic.
2083    #[must_use]
2084    pub fn connection_base_latency(&self, connection_id: ConnectionId) -> Duration {
2085        let range = self.with_network_config(|config| config.chaos.max_pair_latency.clone());
2086        if range.end.is_zero() {
2087            return Duration::ZERO;
2088        }
2089
2090        let inner = self
2091            .inner
2092            .read()
2093            .expect("RwLock poisoned: prior task panicked");
2094        let endpoints = inner
2095            .network
2096            .connections
2097            .get(&connection_id)
2098            .and_then(|conn| Some((conn.local_ip?, conn.remote_ip?)));
2099        drop(inner);
2100
2101        let Some((local_ip, remote_ip)) = endpoints else {
2102            return Duration::ZERO;
2103        };
2104
2105        // Check if latency is already set
2106        if let Some(latency) = self.pair_latency(local_ip, remote_ip) {
2107            return latency;
2108        }
2109
2110        // Sample a fixed latency for this pair and memoize it for the whole run.
2111        let latency = crate::network::sample_duration(&range);
2112        self.set_pair_latency_if_not_set(local_ip, remote_ip, latency)
2113    }
2114
2115    // Per-connection asymmetric delay methods
2116
2117    /// Get the send delay for a connection.
2118    /// Returns the per-connection override if set, otherwise None.
2119    ///
2120    /// # Panics
2121    ///
2122    /// Panics if the simulation lock is poisoned by a prior task panic.
2123    #[must_use]
2124    pub fn send_delay(&self, connection_id: ConnectionId) -> Option<Duration> {
2125        let inner = self
2126            .inner
2127            .read()
2128            .expect("RwLock poisoned: prior task panicked");
2129        inner
2130            .network
2131            .connections
2132            .get(&connection_id)
2133            .and_then(|conn| conn.send_delay)
2134    }
2135
2136    /// Get the receive delay for a connection.
2137    /// Returns the per-connection override if set, otherwise None.
2138    ///
2139    /// # Panics
2140    ///
2141    /// Panics if the simulation lock is poisoned by a prior task panic.
2142    #[must_use]
2143    pub fn recv_delay(&self, connection_id: ConnectionId) -> Option<Duration> {
2144        let inner = self
2145            .inner
2146            .read()
2147            .expect("RwLock poisoned: prior task panicked");
2148        inner
2149            .network
2150            .connections
2151            .get(&connection_id)
2152            .and_then(|conn| conn.recv_delay)
2153    }
2154
2155    /// Check if a connection is permanently closed
2156    ///
2157    /// # Panics
2158    ///
2159    /// Panics if the simulation lock is poisoned by a prior task panic.
2160    #[must_use]
2161    pub fn is_connection_closed(&self, connection_id: ConnectionId) -> bool {
2162        let inner = self
2163            .inner
2164            .read()
2165            .expect("RwLock poisoned: prior task panicked");
2166        inner
2167            .network
2168            .connections
2169            .get(&connection_id)
2170            .is_some_and(|conn| conn.flags.is_closed())
2171    }
2172
2173    /// Close a connection gracefully (FIN semantics).
2174    ///
2175    /// The peer will receive EOF on read operations.
2176    pub fn close_connection(&self, connection_id: ConnectionId) {
2177        self.close_connection_with_reason(connection_id, CloseReason::Graceful);
2178    }
2179
2180    /// Close a connection abruptly (RST semantics).
2181    ///
2182    /// The peer will receive ECONNRESET on both read and write operations.
2183    pub fn close_connection_abort(&self, connection_id: ConnectionId) {
2184        self.close_connection_with_reason(connection_id, CloseReason::Aborted);
2185    }
2186
2187    /// Get the close reason for a connection.
2188    ///
2189    /// # Panics
2190    ///
2191    /// Panics if the simulation lock is poisoned by a prior task panic.
2192    #[must_use]
2193    pub fn close_reason(&self, connection_id: ConnectionId) -> CloseReason {
2194        let inner = self
2195            .inner
2196            .read()
2197            .expect("RwLock poisoned: prior task panicked");
2198        inner
2199            .network
2200            .connections
2201            .get(&connection_id)
2202            .map_or(CloseReason::None, |conn| conn.close_reason)
2203    }
2204
2205    /// Close a connection with a specific close reason.
2206    fn close_connection_with_reason(&self, connection_id: ConnectionId, reason: CloseReason) {
2207        match reason {
2208            CloseReason::Graceful => self.close_connection_graceful(connection_id),
2209            CloseReason::Aborted => self.close_connection_aborted(connection_id),
2210            CloseReason::None => {}
2211        }
2212    }
2213
2214    /// Graceful close (FIN semantics) — TCP half-close.
2215    ///
2216    /// Marks the local write side as closed. The peer can still read all buffered
2217    /// and in-flight data. A `FinDelivery` event is scheduled after the last
2218    /// `DataDelivery` to signal EOF to the peer.
2219    fn close_connection_graceful(&self, connection_id: ConnectionId) {
2220        let mut inner = self
2221            .inner
2222            .write()
2223            .expect("RwLock poisoned: prior task panicked");
2224
2225        // Extract connection info
2226        let conn_info = inner.network.connections.get(&connection_id).map(|conn| {
2227            (
2228                conn.paired_connection,
2229                conn.flags.send_closed(),
2230                conn.flags.is_closed(),
2231                conn.flags.send_in_progress(),
2232                conn.send_buffer.is_empty(),
2233                conn.last_data_delivery_scheduled_at,
2234            )
2235        });
2236
2237        let Some((
2238            paired_id,
2239            was_send_closed,
2240            was_closed,
2241            send_in_progress,
2242            send_buffer_empty,
2243            last_delivery_time,
2244        )) = conn_info
2245        else {
2246            return;
2247        };
2248
2249        // Idempotent: if already closed or send_closed (by chaos or previous close), skip
2250        if was_closed || was_send_closed {
2251            tracing::debug!(
2252                "Connection {} already closed/send_closed, skipping graceful close",
2253                connection_id.0
2254            );
2255            return;
2256        }
2257
2258        // Mark local side as closed with send side shut down
2259        if let Some(conn) = inner.network.connections.get_mut(&connection_id) {
2260            conn.flags.set_is_closed(true);
2261            conn.close_reason = CloseReason::Graceful;
2262            conn.flags.set_send_closed(true);
2263            tracing::debug!(
2264                "Connection {} graceful close (FIN) - local write shut down",
2265                connection_id.0
2266            );
2267        }
2268
2269        // Wake local read waker (so local poll_read returns EOF if needed)
2270        if let Some(waker) = inner.wakers.reads.remove(&connection_id) {
2271            waker.wake();
2272        }
2273
2274        // Do NOT set is_closed on the peer — they can still read buffered data.
2275        // Instead, schedule a FIN delivery after all data has been delivered.
2276        if send_in_progress || !send_buffer_empty {
2277            // Pipeline has data — defer FIN until pipeline drains
2278            if let Some(conn) = inner.network.connections.get_mut(&connection_id) {
2279                conn.flags.set_graceful_close_pending(true);
2280                tracing::debug!(
2281                    "Connection {} graceful close deferred (send pipeline active)",
2282                    connection_id.0
2283                );
2284            }
2285        } else {
2286            // Pipeline is idle — schedule FIN delivery now
2287            Self::schedule_fin_delivery(&mut inner, paired_id, last_delivery_time);
2288        }
2289    }
2290
2291    /// Aborted close (RST semantics) — immediate connection kill.
2292    ///
2293    /// Immediately sets `is_closed` on both endpoints. The peer will receive
2294    /// ECONNRESET on both read and write operations.
2295    fn close_connection_aborted(&self, connection_id: ConnectionId) {
2296        let mut inner = self
2297            .inner
2298            .write()
2299            .expect("RwLock poisoned: prior task panicked");
2300
2301        let paired_connection_id = inner
2302            .network
2303            .connections
2304            .get(&connection_id)
2305            .and_then(|conn| conn.paired_connection);
2306
2307        if let Some(conn) = inner.network.connections.get_mut(&connection_id)
2308            && !conn.flags.is_closed()
2309        {
2310            conn.flags.set_is_closed(true);
2311            conn.close_reason = CloseReason::Aborted;
2312            // Cancel any pending graceful close
2313            conn.flags.set_graceful_close_pending(false);
2314            tracing::debug!(
2315                "Connection {} closed permanently (reason: Aborted)",
2316                connection_id.0
2317            );
2318        }
2319
2320        if let Some(paired_id) = paired_connection_id
2321            && let Some(paired_conn) = inner.network.connections.get_mut(&paired_id)
2322            && !paired_conn.flags.is_closed()
2323        {
2324            paired_conn.flags.set_is_closed(true);
2325            paired_conn.close_reason = CloseReason::Aborted;
2326            tracing::debug!(
2327                "Paired connection {} also closed (reason: Aborted)",
2328                paired_id.0
2329            );
2330        }
2331
2332        if let Some(waker) = inner.wakers.reads.remove(&connection_id) {
2333            tracing::debug!(
2334                "Waking read waker for aborted connection {}",
2335                connection_id.0
2336            );
2337            waker.wake();
2338        }
2339
2340        if let Some(paired_id) = paired_connection_id
2341            && let Some(paired_waker) = inner.wakers.reads.remove(&paired_id)
2342        {
2343            tracing::debug!(
2344                "Waking read waker for paired aborted connection {}",
2345                paired_id.0
2346            );
2347            paired_waker.wake();
2348        }
2349    }
2350
2351    /// Close connection asymmetrically (FDB rollRandomClose pattern)
2352    ///
2353    /// # Panics
2354    ///
2355    /// Panics if the simulation lock is poisoned by a prior task panic.
2356    pub fn close_connection_asymmetric(
2357        &self,
2358        connection_id: ConnectionId,
2359        close_send: bool,
2360        close_recv: bool,
2361    ) {
2362        let mut inner = self
2363            .inner
2364            .write()
2365            .expect("RwLock poisoned: prior task panicked");
2366
2367        let paired_id = inner
2368            .network
2369            .connections
2370            .get(&connection_id)
2371            .and_then(|conn| conn.paired_connection);
2372
2373        if close_send && let Some(conn) = inner.network.connections.get_mut(&connection_id) {
2374            conn.flags.set_send_closed(true);
2375            conn.send_buffer.clear();
2376            tracing::debug!(
2377                "Connection {} send side closed (asymmetric)",
2378                connection_id.0
2379            );
2380        }
2381
2382        if close_recv
2383            && let Some(paired) = paired_id
2384            && let Some(paired_conn) = inner.network.connections.get_mut(&paired)
2385        {
2386            paired_conn.flags.set_recv_closed(true);
2387            tracing::debug!(
2388                "Connection {} recv side closed (asymmetric via peer)",
2389                paired.0
2390            );
2391        }
2392
2393        if close_send && let Some(waker) = inner.wakers.reads.remove(&connection_id) {
2394            waker.wake();
2395        }
2396        if close_recv
2397            && let Some(paired) = paired_id
2398            && let Some(waker) = inner.wakers.reads.remove(&paired)
2399        {
2400            waker.wake();
2401        }
2402    }
2403
2404    /// Roll random close chaos injection (FDB rollRandomClose pattern)
2405    ///
2406    /// # Panics
2407    ///
2408    /// Panics if the simulation lock is poisoned by a prior task panic.
2409    pub fn roll_random_close(&self, connection_id: ConnectionId) -> Option<bool> {
2410        let mut inner = self
2411            .inner
2412            .write()
2413            .expect("RwLock poisoned: prior task panicked");
2414        let config = &inner.network.config;
2415
2416        // Skip stable connections (FDB: stableConnection exempt from chaos)
2417        if inner
2418            .network
2419            .connections
2420            .get(&connection_id)
2421            .is_some_and(|conn| conn.flags.is_stable())
2422        {
2423            return None;
2424        }
2425
2426        if config.chaos.random_close_probability <= 0.0 {
2427            return None;
2428        }
2429
2430        let current_time = inner.current_time;
2431        let time_since_last = current_time.saturating_sub(inner.network.last_random_close_time);
2432        if time_since_last < config.chaos.random_close_cooldown {
2433            return None;
2434        }
2435
2436        if !crate::buggify_with_prob!(config.chaos.random_close_probability) {
2437            return None;
2438        }
2439
2440        inner.network.last_random_close_time = current_time;
2441
2442        inner.record_fault(SimFaultEvent::RandomClose {
2443            connection_id: connection_id.0,
2444        });
2445
2446        let paired_id = inner
2447            .network
2448            .connections
2449            .get(&connection_id)
2450            .and_then(|conn| conn.paired_connection);
2451
2452        let a = super::rng::sim_random_f64();
2453        let close_recv = a < 0.66;
2454        let close_send = a > 0.33;
2455
2456        tracing::info!(
2457            "Random connection failure triggered on connection {} (send={}, recv={}, a={:.3})",
2458            connection_id.0,
2459            close_send,
2460            close_recv,
2461            a
2462        );
2463
2464        if close_send && let Some(conn) = inner.network.connections.get_mut(&connection_id) {
2465            conn.flags.set_send_closed(true);
2466            conn.send_buffer.clear();
2467        }
2468
2469        if close_recv
2470            && let Some(paired) = paired_id
2471            && let Some(paired_conn) = inner.network.connections.get_mut(&paired)
2472        {
2473            paired_conn.flags.set_recv_closed(true);
2474        }
2475
2476        if close_send && let Some(waker) = inner.wakers.reads.remove(&connection_id) {
2477            waker.wake();
2478        }
2479        if close_recv
2480            && let Some(paired) = paired_id
2481            && let Some(waker) = inner.wakers.reads.remove(&paired)
2482        {
2483            waker.wake();
2484        }
2485
2486        let b = super::rng::sim_random_f64();
2487        let explicit = b < inner.network.config.chaos.random_close_explicit_ratio;
2488
2489        tracing::debug!(
2490            "Random close explicit={} (b={:.3}, ratio={:.2})",
2491            explicit,
2492            b,
2493            inner.network.config.chaos.random_close_explicit_ratio
2494        );
2495
2496        Some(explicit)
2497    }
2498
2499    /// Check if a connection's send side is closed
2500    ///
2501    /// # Panics
2502    ///
2503    /// Panics if the simulation lock is poisoned by a prior task panic.
2504    #[must_use]
2505    pub fn is_send_closed(&self, connection_id: ConnectionId) -> bool {
2506        let inner = self
2507            .inner
2508            .read()
2509            .expect("RwLock poisoned: prior task panicked");
2510        inner
2511            .network
2512            .connections
2513            .get(&connection_id)
2514            .is_some_and(|conn| conn.flags.send_closed() || conn.flags.is_closed())
2515    }
2516
2517    /// Check if a connection's receive side is closed
2518    ///
2519    /// # Panics
2520    ///
2521    /// Panics if the simulation lock is poisoned by a prior task panic.
2522    #[must_use]
2523    pub fn is_recv_closed(&self, connection_id: ConnectionId) -> bool {
2524        let inner = self
2525            .inner
2526            .read()
2527            .expect("RwLock poisoned: prior task panicked");
2528        inner
2529            .network
2530            .connections
2531            .get(&connection_id)
2532            .is_some_and(|conn| conn.flags.recv_closed() || conn.flags.is_closed())
2533    }
2534
2535    /// Check if a FIN has been received from the remote peer (graceful close).
2536    ///
2537    /// When true, `poll_read` should return EOF after draining the receive buffer.
2538    /// Distinct from `is_recv_closed` which is used for chaos/asymmetric closure.
2539    ///
2540    /// # Panics
2541    ///
2542    /// Panics if the simulation lock is poisoned by a prior task panic.
2543    #[must_use]
2544    pub fn is_remote_fin_received(&self, connection_id: ConnectionId) -> bool {
2545        let inner = self
2546            .inner
2547            .read()
2548            .expect("RwLock poisoned: prior task panicked");
2549        inner
2550            .network
2551            .connections
2552            .get(&connection_id)
2553            .is_some_and(|conn| conn.flags.remote_fin_received())
2554    }
2555
2556    // Half-Open Connection Simulation Methods
2557
2558    /// Check if a connection is in half-open state
2559    ///
2560    /// # Panics
2561    ///
2562    /// Panics if the simulation lock is poisoned by a prior task panic.
2563    #[must_use]
2564    pub fn is_half_open(&self, connection_id: ConnectionId) -> bool {
2565        let inner = self
2566            .inner
2567            .read()
2568            .expect("RwLock poisoned: prior task panicked");
2569        inner
2570            .network
2571            .connections
2572            .get(&connection_id)
2573            .is_some_and(|conn| conn.flags.is_half_open())
2574    }
2575
2576    /// Check if a half-open connection should return errors now
2577    ///
2578    /// # Panics
2579    ///
2580    /// Panics if the simulation lock is poisoned by a prior task panic.
2581    #[must_use]
2582    pub fn should_half_open_error(&self, connection_id: ConnectionId) -> bool {
2583        let inner = self
2584            .inner
2585            .read()
2586            .expect("RwLock poisoned: prior task panicked");
2587        let current_time = inner.current_time;
2588        inner
2589            .network
2590            .connections
2591            .get(&connection_id)
2592            .is_some_and(|conn| {
2593                conn.flags.is_half_open()
2594                    && conn
2595                        .half_open_error_at
2596                        .is_some_and(|error_at| current_time >= error_at)
2597            })
2598    }
2599
2600    // Stable Connection Methods
2601
2602    /// Mark a connection as stable, exempting it from chaos injection.
2603    ///
2604    /// Stable connections are exempt from:
2605    /// - Random close (`roll_random_close`)
2606    /// - Write clogging
2607    /// - Read clogging
2608    /// - Bit flip corruption
2609    /// - Partial write truncation
2610    ///
2611    /// FDB ref: sim2.actor.cpp:357-362 (`stableConnection` flag)
2612    ///
2613    /// # Real-World Scenario
2614    /// Use this for parent-child process connections or supervision channels
2615    /// that should remain reliable even during chaos testing.
2616    ///
2617    /// # Panics
2618    ///
2619    /// Panics if the simulation lock is poisoned by a prior task panic.
2620    pub fn mark_connection_stable(&self, connection_id: ConnectionId) {
2621        let mut inner = self
2622            .inner
2623            .write()
2624            .expect("RwLock poisoned: prior task panicked");
2625        if let Some(conn) = inner.network.connections.get_mut(&connection_id) {
2626            conn.flags.set_is_stable(true);
2627            tracing::debug!("Connection {} marked as stable", connection_id.0);
2628
2629            // Also mark the paired connection as stable
2630            if let Some(paired_id) = conn.paired_connection
2631                && let Some(paired_conn) = inner.network.connections.get_mut(&paired_id)
2632            {
2633                paired_conn.flags.set_is_stable(true);
2634                tracing::debug!("Paired connection {} also marked as stable", paired_id.0);
2635            }
2636        }
2637    }
2638
2639    // Network Partition Control Methods
2640
2641    /// Partition communication between two IP addresses for a specified duration
2642    ///
2643    /// # Panics
2644    ///
2645    /// Panics if the simulation lock is poisoned by a prior task panic.
2646    ///
2647    /// # Errors
2648    ///
2649    /// Returns an error if the operation is rejected by the simulator (for example, the simulation has not been started).
2650    pub fn partition_pair(
2651        &self,
2652        from_ip: std::net::IpAddr,
2653        to_ip: std::net::IpAddr,
2654        duration: Duration,
2655    ) -> SimulationResult<()> {
2656        let mut inner = self
2657            .inner
2658            .write()
2659            .expect("RwLock poisoned: prior task panicked");
2660        let expires_at = inner.current_time + duration;
2661
2662        inner
2663            .network
2664            .ip_partitions
2665            .insert((from_ip, to_ip), PartitionState { expires_at });
2666
2667        let restore_event = Event::Connection {
2668            id: 0,
2669            state: ConnectionStateChange::PartitionRestore,
2670        };
2671        let sequence = inner.next_sequence;
2672        inner.next_sequence += 1;
2673        let scheduled_event = ScheduledEvent::new(expires_at, restore_event, sequence);
2674        inner.event_queue.schedule(scheduled_event);
2675
2676        inner.record_fault(SimFaultEvent::PartitionCreated {
2677            from: from_ip.to_string(),
2678            to: to_ip.to_string(),
2679        });
2680
2681        tracing::debug!(
2682            "Partitioned {} -> {} until {:?}",
2683            from_ip,
2684            to_ip,
2685            expires_at
2686        );
2687        Ok(())
2688    }
2689
2690    /// Block all outgoing communication from an IP address
2691    ///
2692    /// # Panics
2693    ///
2694    /// Panics if the simulation lock is poisoned by a prior task panic.
2695    ///
2696    /// # Errors
2697    ///
2698    /// Returns an error if the operation is rejected by the simulator (for example, the simulation has not been started).
2699    pub fn partition_send_from(
2700        &self,
2701        ip: std::net::IpAddr,
2702        duration: Duration,
2703    ) -> SimulationResult<()> {
2704        let mut inner = self
2705            .inner
2706            .write()
2707            .expect("RwLock poisoned: prior task panicked");
2708        let expires_at = inner.current_time + duration;
2709
2710        inner.network.send_partitions.insert(ip, expires_at);
2711
2712        let clear_event = Event::Connection {
2713            id: 0,
2714            state: ConnectionStateChange::SendPartitionClear,
2715        };
2716        let sequence = inner.next_sequence;
2717        inner.next_sequence += 1;
2718        let scheduled_event = ScheduledEvent::new(expires_at, clear_event, sequence);
2719        inner.event_queue.schedule(scheduled_event);
2720
2721        inner.record_fault(SimFaultEvent::SendPartitionCreated { ip: ip.to_string() });
2722        tracing::debug!("Partitioned sends from {} until {:?}", ip, expires_at);
2723        Ok(())
2724    }
2725
2726    /// Block all incoming communication to an IP address
2727    ///
2728    /// # Panics
2729    ///
2730    /// Panics if the simulation lock is poisoned by a prior task panic.
2731    ///
2732    /// # Errors
2733    ///
2734    /// Returns an error if the operation is rejected by the simulator (for example, the simulation has not been started).
2735    pub fn partition_recv_to(
2736        &self,
2737        ip: std::net::IpAddr,
2738        duration: Duration,
2739    ) -> SimulationResult<()> {
2740        let mut inner = self
2741            .inner
2742            .write()
2743            .expect("RwLock poisoned: prior task panicked");
2744        let expires_at = inner.current_time + duration;
2745
2746        inner.network.recv_partitions.insert(ip, expires_at);
2747
2748        let clear_event = Event::Connection {
2749            id: 0,
2750            state: ConnectionStateChange::RecvPartitionClear,
2751        };
2752        let sequence = inner.next_sequence;
2753        inner.next_sequence += 1;
2754        let scheduled_event = ScheduledEvent::new(expires_at, clear_event, sequence);
2755        inner.event_queue.schedule(scheduled_event);
2756
2757        inner.record_fault(SimFaultEvent::RecvPartitionCreated { ip: ip.to_string() });
2758        tracing::debug!("Partitioned receives to {} until {:?}", ip, expires_at);
2759        Ok(())
2760    }
2761
2762    /// Immediately restore communication between two IP addresses
2763    ///
2764    /// # Panics
2765    ///
2766    /// Panics if the simulation lock is poisoned by a prior task panic.
2767    ///
2768    /// # Errors
2769    ///
2770    /// Returns an error if the operation is rejected by the simulator (for example, the simulation has not been started).
2771    pub fn restore_partition(
2772        &self,
2773        from_ip: std::net::IpAddr,
2774        to_ip: std::net::IpAddr,
2775    ) -> SimulationResult<()> {
2776        let mut inner = self
2777            .inner
2778            .write()
2779            .expect("RwLock poisoned: prior task panicked");
2780        inner.network.ip_partitions.remove(&(from_ip, to_ip));
2781        inner.record_fault(SimFaultEvent::PartitionHealed {
2782            from: from_ip.to_string(),
2783            to: to_ip.to_string(),
2784        });
2785        tracing::debug!("Restored partition {} -> {}", from_ip, to_ip);
2786        Ok(())
2787    }
2788
2789    /// Check if communication between two IP addresses is currently partitioned
2790    ///
2791    /// # Panics
2792    ///
2793    /// Panics if the simulation lock is poisoned by a prior task panic.
2794    ///
2795    /// # Errors
2796    ///
2797    /// Returns an error if the operation is rejected by the simulator (for example, the simulation has not been started).
2798    pub fn is_partitioned(
2799        &self,
2800        from_ip: std::net::IpAddr,
2801        to_ip: std::net::IpAddr,
2802    ) -> SimulationResult<bool> {
2803        let inner = self
2804            .inner
2805            .read()
2806            .expect("RwLock poisoned: prior task panicked");
2807        Ok(inner
2808            .network
2809            .is_partitioned(from_ip, to_ip, inner.current_time))
2810    }
2811
2812    /// Helper method for use with `SimInner` - randomly trigger partitions
2813    ///
2814    /// Supports different partition strategies based on configuration:
2815    /// - Random: randomly partition individual IP pairs
2816    /// - `UniformSize`: create uniform-sized partition groups
2817    /// - `IsolateSingle`: isolate exactly one node from the rest
2818    fn randomly_trigger_partitions_with_inner(inner: &mut SimInner) {
2819        let partition_config = &inner.network.config;
2820
2821        if partition_config.chaos.partition_probability == 0.0 {
2822            return;
2823        }
2824
2825        // Check if we should trigger a partition this step
2826        if sim_random::<f64>() >= partition_config.chaos.partition_probability {
2827            return;
2828        }
2829
2830        // Collect unique IPs from connections
2831        let unique_ips: HashSet<IpAddr> = inner
2832            .network
2833            .connections
2834            .values()
2835            .filter_map(|conn| conn.local_ip)
2836            .collect();
2837
2838        if unique_ips.len() < 2 {
2839            return; // Need at least 2 IPs to partition
2840        }
2841
2842        let ip_list: Vec<IpAddr> = unique_ips.into_iter().collect();
2843        let partition_duration =
2844            crate::network::sample_duration(&partition_config.chaos.partition_duration);
2845        let expires_at = inner.current_time + partition_duration;
2846
2847        // Select IPs to partition based on strategy
2848        let partitioned_ips: Vec<IpAddr> = match partition_config.chaos.partition_strategy {
2849            PartitionStrategy::Random => {
2850                // Original behavior: randomly decide for each IP
2851                ip_list
2852                    .iter()
2853                    .filter(|_| sim_random::<f64>() < 0.5)
2854                    .copied()
2855                    .collect()
2856            }
2857            PartitionStrategy::UniformSize => {
2858                // TigerBeetle-style: random partition size from 1 to n-1
2859                let partition_size = sim_random_range(1..ip_list.len());
2860                // Shuffle and take first N IPs
2861                let mut shuffled = ip_list.clone();
2862                // Simple Fisher-Yates shuffle
2863                for i in (1..shuffled.len()).rev() {
2864                    let j = sim_random_range(0..i + 1);
2865                    shuffled.swap(i, j);
2866                }
2867                shuffled.into_iter().take(partition_size).collect()
2868            }
2869            PartitionStrategy::IsolateSingle => {
2870                // Isolate exactly one node
2871                let idx = sim_random_range(0..ip_list.len());
2872                vec![ip_list[idx]]
2873            }
2874        };
2875
2876        // Don't partition if we selected all IPs or none
2877        if partitioned_ips.is_empty() || partitioned_ips.len() == ip_list.len() {
2878            return;
2879        }
2880
2881        // Create bi-directional partitions between partitioned and non-partitioned groups
2882        let non_partitioned: Vec<IpAddr> = ip_list
2883            .iter()
2884            .filter(|ip| !partitioned_ips.contains(ip))
2885            .copied()
2886            .collect();
2887
2888        for &from_ip in &partitioned_ips {
2889            for &to_ip in &non_partitioned {
2890                // Skip if already partitioned
2891                if inner
2892                    .network
2893                    .is_partitioned(from_ip, to_ip, inner.current_time)
2894                {
2895                    continue;
2896                }
2897
2898                // Partition in both directions
2899                inner
2900                    .network
2901                    .ip_partitions
2902                    .insert((from_ip, to_ip), PartitionState { expires_at });
2903                inner
2904                    .network
2905                    .ip_partitions
2906                    .insert((to_ip, from_ip), PartitionState { expires_at });
2907
2908                // Disjoint-field push: `partition_config` still borrows
2909                // `inner.network`, so go through `pending_faults` directly.
2910                let time_ms = u64::try_from(inner.current_time.as_millis()).unwrap_or(u64::MAX);
2911                inner.pending_faults.push(SimFaultRecord {
2912                    time_ms,
2913                    event: SimFaultEvent::PartitionCreated {
2914                        from: from_ip.to_string(),
2915                        to: to_ip.to_string(),
2916                    },
2917                });
2918
2919                tracing::debug!(
2920                    "Partition triggered: {} <-> {} until {:?} (strategy: {:?})",
2921                    from_ip,
2922                    to_ip,
2923                    expires_at,
2924                    partition_config.chaos.partition_strategy
2925                );
2926            }
2927        }
2928
2929        // Schedule restoration event
2930        let restore_event = Event::Connection {
2931            id: 0,
2932            state: ConnectionStateChange::PartitionRestore,
2933        };
2934        let sequence = inner.next_sequence;
2935        inner.next_sequence += 1;
2936        let scheduled_event = ScheduledEvent::new(expires_at, restore_event, sequence);
2937        inner.event_queue.schedule(scheduled_event);
2938    }
2939}
2940
2941impl Default for SimWorld {
2942    fn default() -> Self {
2943        Self::new()
2944    }
2945}
2946
2947/// A weak reference to a simulation world.
2948///
2949/// This provides handle-based access to the simulation without holding
2950/// a strong reference that would prevent cleanup.
2951#[derive(Debug)]
2952pub struct WeakSimWorld {
2953    pub(crate) inner: Weak<RwLock<SimInner>>,
2954}
2955
2956/// Macro to generate `WeakSimWorld` forwarding methods that wrap `SimWorld` results.
2957macro_rules! weak_forward {
2958    // For methods returning T that need Ok() wrapping
2959    (wrap $(#[$meta:meta])* $method:ident(&self $(, $arg:ident : $arg_ty:ty)*) -> $ret:ty) => {
2960        $(#[$meta])*
2961        ///
2962        /// # Errors
2963        ///
2964        /// Returns an error if the simulation has been dropped.
2965        pub fn $method(&self $(, $arg: $arg_ty)*) -> SimulationResult<$ret> {
2966            Ok(self.upgrade()?.$method($($arg),*))
2967        }
2968    };
2969    // For methods already returning SimulationResult
2970    (pass $(#[$meta:meta])* $method:ident(&self $(, $arg:ident : $arg_ty:ty)*) -> $ret:ty) => {
2971        $(#[$meta])*
2972        ///
2973        /// # Errors
2974        ///
2975        /// Returns an error if the simulation has been dropped or the
2976        /// underlying operation is rejected by the simulator.
2977        pub fn $method(&self $(, $arg: $arg_ty)*) -> SimulationResult<$ret> {
2978            self.upgrade()?.$method($($arg),*)
2979        }
2980    };
2981    // For methods returning () that need Ok(()) wrapping
2982    (unit $(#[$meta:meta])* $method:ident(&self $(, $arg:ident : $arg_ty:ty)*)) => {
2983        $(#[$meta])*
2984        ///
2985        /// # Errors
2986        ///
2987        /// Returns an error if the simulation has been dropped.
2988        pub fn $method(&self $(, $arg: $arg_ty)*) -> SimulationResult<()> {
2989            self.upgrade()?.$method($($arg),*);
2990            Ok(())
2991        }
2992    };
2993}
2994
2995impl WeakSimWorld {
2996    /// Attempts to upgrade this weak reference to a strong reference.
2997    ///
2998    /// # Errors
2999    ///
3000    /// Returns an error if the operation is rejected by the simulator (for example, the simulation has not been started).
3001    pub fn upgrade(&self) -> SimulationResult<SimWorld> {
3002        self.inner
3003            .upgrade()
3004            .map(|inner| SimWorld { inner })
3005            .ok_or(SimulationError::SimulationShutdown)
3006    }
3007
3008    weak_forward!(wrap #[doc = "Returns the current simulation time."] current_time(&self) -> Duration);
3009    weak_forward!(wrap #[doc = "Returns the exact simulation time (equivalent to FDB's `now()`)."] now(&self) -> Duration);
3010    weak_forward!(wrap #[doc = "Returns the drifted timer time (equivalent to FDB's `timer()`)."] timer(&self) -> Duration);
3011    weak_forward!(unit #[doc = "Schedules an event to execute after the specified delay."] schedule_event(&self, event: Event, delay: Duration));
3012    weak_forward!(unit #[doc = "Schedules an event to execute at the specified absolute time."] schedule_event_at(&self, event: Event, time: Duration));
3013    weak_forward!(pass #[doc = "Read data from connection's receive buffer."] read_from_connection(&self, connection_id: ConnectionId, buf: &mut [u8]) -> usize);
3014    weak_forward!(pass #[doc = "Write data to connection's receive buffer."] write_to_connection(&self, connection_id: ConnectionId, data: &[u8]) -> ());
3015    weak_forward!(pass #[doc = "Buffer data for ordered sending on a connection."] buffer_send(&self, connection_id: ConnectionId, data: Vec<u8>) -> ());
3016    weak_forward!(wrap #[doc = "Get a network provider for the simulation."] network_provider(&self) -> SimNetworkProvider);
3017    weak_forward!(wrap #[doc = "Get a time provider for the simulation."] time_provider(&self) -> crate::providers::SimTimeProvider);
3018    weak_forward!(wrap #[doc = "Sleep for the specified duration in simulation time."] sleep(&self, duration: Duration) -> SleepFuture);
3019
3020    /// Access network configuration for latency calculations.
3021    ///
3022    /// # Errors
3023    ///
3024    /// Returns an error if the operation is rejected by the simulator (for example, the simulation has not been started).
3025    pub fn with_network_config<F, R>(&self, f: F) -> SimulationResult<R>
3026    where
3027        F: FnOnce(&NetworkConfiguration) -> R,
3028    {
3029        Ok(self.upgrade()?.with_network_config(f))
3030    }
3031}
3032
3033impl Clone for WeakSimWorld {
3034    fn clone(&self) -> Self {
3035        Self {
3036            inner: self.inner.clone(),
3037        }
3038    }
3039}
3040
3041#[cfg(test)]
3042mod tests {
3043    use super::*;
3044
3045    #[test]
3046    fn sim_world_basic_lifecycle() {
3047        let mut sim = SimWorld::new();
3048
3049        // Initial state
3050        assert_eq!(sim.current_time(), Duration::ZERO);
3051        assert!(!sim.has_pending_events());
3052        assert_eq!(sim.pending_event_count(), 0);
3053
3054        // Schedule an event
3055        sim.schedule_event(Event::Timer { task_id: 1 }, Duration::from_millis(100));
3056
3057        assert!(sim.has_pending_events());
3058        assert_eq!(sim.pending_event_count(), 1);
3059        assert_eq!(sim.current_time(), Duration::ZERO);
3060
3061        // Process the event
3062        let has_more = sim.step();
3063        assert!(!has_more);
3064        assert_eq!(sim.current_time(), Duration::from_millis(100));
3065        assert!(!sim.has_pending_events());
3066        assert_eq!(sim.pending_event_count(), 0);
3067    }
3068
3069    #[test]
3070    fn sim_world_multiple_events() {
3071        let mut sim = SimWorld::new();
3072
3073        // Schedule multiple events
3074        sim.schedule_event(Event::Timer { task_id: 3 }, Duration::from_millis(300));
3075        sim.schedule_event(Event::Timer { task_id: 1 }, Duration::from_millis(100));
3076        sim.schedule_event(Event::Timer { task_id: 2 }, Duration::from_millis(200));
3077
3078        assert_eq!(sim.pending_event_count(), 3);
3079
3080        // Process events - should happen in time order
3081        assert!(sim.step());
3082        assert_eq!(sim.current_time(), Duration::from_millis(100));
3083        assert_eq!(sim.pending_event_count(), 2);
3084
3085        assert!(sim.step());
3086        assert_eq!(sim.current_time(), Duration::from_millis(200));
3087        assert_eq!(sim.pending_event_count(), 1);
3088
3089        assert!(!sim.step());
3090        assert_eq!(sim.current_time(), Duration::from_millis(300));
3091        assert_eq!(sim.pending_event_count(), 0);
3092    }
3093
3094    #[test]
3095    fn sim_world_run_until_empty() {
3096        let mut sim = SimWorld::new();
3097
3098        // Schedule multiple events
3099        sim.schedule_event(Event::Timer { task_id: 1 }, Duration::from_millis(100));
3100        sim.schedule_event(Event::Timer { task_id: 2 }, Duration::from_millis(200));
3101        sim.schedule_event(Event::Timer { task_id: 3 }, Duration::from_millis(300));
3102
3103        // Run until all events are processed
3104        sim.run_until_empty();
3105
3106        assert_eq!(sim.current_time(), Duration::from_millis(300));
3107        assert!(!sim.has_pending_events());
3108    }
3109
3110    #[test]
3111    fn pair_latency_deterministic_per_seed() {
3112        use crate::network::{ChaosConfiguration, NetworkConfiguration};
3113
3114        let client_ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 1, 1));
3115        let server_ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 0, 1, 2));
3116        let range = Duration::from_millis(10)..Duration::from_millis(50);
3117
3118        // Build a fast-local config that only enables per-pair latency.
3119        let on_config = || NetworkConfiguration {
3120            chaos: ChaosConfiguration {
3121                max_pair_latency: range.clone(),
3122                ..ChaosConfiguration::disabled()
3123            },
3124            ..NetworkConfiguration::fast_local()
3125        };
3126
3127        // Establish one connection pair and memoize both directions.
3128        let assign = || {
3129            let sim = SimWorld::new_with_network_config_and_seed(on_config(), 42);
3130            let (client, server) = sim.create_connection_pair("10.0.1.1:0", "10.0.1.2:0");
3131            let _ = sim.connection_base_latency(client);
3132            let _ = sim.connection_base_latency(server);
3133            (
3134                sim.pair_latency(client_ip, server_ip),
3135                sim.pair_latency(server_ip, client_ip),
3136            )
3137        };
3138
3139        let first = assign();
3140        let second = assign();
3141
3142        // Same seed -> identical, fully-populated assignments.
3143        assert_eq!(first, second, "pair latency must be deterministic per seed");
3144        let c2s = first.0.expect("client->server pair latency assigned");
3145        let s2c = first.1.expect("server->client pair latency assigned");
3146        for latency in [c2s, s2c] {
3147            assert!(
3148                range.contains(&latency),
3149                "pair latency {latency:?} must fall in the configured range {range:?}"
3150            );
3151        }
3152
3153        // Disabled range (default) -> no map writes at all.
3154        let off =
3155            SimWorld::new_with_network_config_and_seed(NetworkConfiguration::fast_local(), 42);
3156        let (client, server) = off.create_connection_pair("10.0.1.1:0", "10.0.1.2:0");
3157        let _ = off.connection_base_latency(client);
3158        let _ = off.connection_base_latency(server);
3159        assert_eq!(off.pair_latency(client_ip, server_ip), None);
3160        assert_eq!(off.pair_latency(server_ip, client_ip), None);
3161    }
3162
3163    #[test]
3164    fn sim_world_schedule_at_specific_time() {
3165        let mut sim = SimWorld::new();
3166
3167        // Schedule event at specific time
3168        sim.schedule_event_at(Event::Timer { task_id: 1 }, Duration::from_millis(500));
3169
3170        assert_eq!(sim.current_time(), Duration::ZERO);
3171
3172        sim.step();
3173
3174        assert_eq!(sim.current_time(), Duration::from_millis(500));
3175    }
3176
3177    #[test]
3178    fn weak_sim_world_lifecycle() {
3179        let sim = SimWorld::new();
3180        let weak = sim.downgrade();
3181
3182        // Can upgrade and use weak reference
3183        assert_eq!(
3184            weak.current_time().expect("should get time"),
3185            Duration::ZERO
3186        );
3187
3188        // Schedule event through weak reference
3189        weak.schedule_event(Event::Timer { task_id: 1 }, Duration::from_millis(100))
3190            .expect("should schedule event");
3191
3192        // Verify event was scheduled
3193        assert!(sim.has_pending_events());
3194
3195        // Drop the original simulation
3196        drop(sim);
3197
3198        // Weak reference should now fail
3199        assert_eq!(
3200            weak.current_time(),
3201            Err(SimulationError::SimulationShutdown)
3202        );
3203        assert_eq!(
3204            weak.schedule_event(Event::Timer { task_id: 2 }, Duration::from_millis(200)),
3205            Err(SimulationError::SimulationShutdown)
3206        );
3207    }
3208
3209    #[test]
3210    fn deterministic_event_ordering() {
3211        let mut sim = SimWorld::new();
3212
3213        // Schedule events at the same time
3214        sim.schedule_event(Event::Timer { task_id: 2 }, Duration::from_millis(100));
3215        sim.schedule_event(Event::Timer { task_id: 1 }, Duration::from_millis(100));
3216        sim.schedule_event(Event::Timer { task_id: 3 }, Duration::from_millis(100));
3217
3218        // All events are at the same time, but should be processed in sequence order
3219        assert!(sim.step());
3220        assert_eq!(sim.current_time(), Duration::from_millis(100));
3221        assert!(sim.step());
3222        assert_eq!(sim.current_time(), Duration::from_millis(100));
3223        assert!(!sim.step());
3224        assert_eq!(sim.current_time(), Duration::from_millis(100));
3225    }
3226
3227    #[test]
3228    fn record_fault_stamps_current_time() {
3229        let mut inner = SimInner::new();
3230        inner.current_time = Duration::from_millis(500);
3231        inner.record_fault(SimFaultEvent::StorageCrash {
3232            ip: "10.0.1.1".to_string(),
3233        });
3234
3235        assert_eq!(inner.pending_faults.len(), 1);
3236        let record = &inner.pending_faults[0];
3237        assert_eq!(record.time_ms, 500);
3238        assert!(matches!(record.event, SimFaultEvent::StorageCrash { .. }));
3239    }
3240
3241    #[test]
3242    fn partition_pair_records_fault() {
3243        let sim = SimWorld::new();
3244        let from: std::net::IpAddr = "10.0.1.1".parse().expect("valid ip");
3245        let to: std::net::IpAddr = "10.0.1.2".parse().expect("valid ip");
3246        sim.partition_pair(from, to, Duration::from_secs(10))
3247            .expect("partition should succeed");
3248
3249        let faults = sim.take_faults();
3250        assert_eq!(faults.len(), 1);
3251        assert!(matches!(
3252            &faults[0].event,
3253            SimFaultEvent::PartitionCreated { from, to }
3254            if from == "10.0.1.1" && to == "10.0.1.2"
3255        ));
3256        assert!(sim.take_faults().is_empty(), "drained on take");
3257    }
3258
3259    #[test]
3260    fn restore_partition_records_fault() {
3261        let sim = SimWorld::new();
3262        let from: std::net::IpAddr = "10.0.1.1".parse().expect("valid ip");
3263        let to: std::net::IpAddr = "10.0.1.2".parse().expect("valid ip");
3264        sim.partition_pair(from, to, Duration::from_secs(10))
3265            .expect("partition");
3266        sim.restore_partition(from, to).expect("restore");
3267
3268        let faults = sim.take_faults();
3269        assert_eq!(faults.len(), 2);
3270        assert!(matches!(
3271            &faults[0].event,
3272            SimFaultEvent::PartitionCreated { .. }
3273        ));
3274        assert!(matches!(
3275            &faults[1].event,
3276            SimFaultEvent::PartitionHealed { .. }
3277        ));
3278    }
3279}