Skip to main content

moonpool_sim/network/sim/
stream.rs

1use super::types::ConnectionId;
2use crate::TcpListenerTrait;
3use crate::sim::state::CloseReason;
4use crate::{Event, WeakSimWorld};
5use futures::io::{AsyncRead, AsyncWrite};
6use std::{
7    future::Future,
8    io::{self, IoSlice},
9    pin::Pin,
10    task::{Context, Poll},
11};
12use tracing::instrument;
13
14/// Create an `io::Error` for simulation shutdown.
15///
16/// Used when the simulation world has been dropped but stream operations are still attempted.
17fn sim_shutdown_error() -> io::Error {
18    io::Error::new(io::ErrorKind::BrokenPipe, "simulation shutdown")
19}
20
21/// Create an `io::Error` for random connection failure (chaos injection).
22fn random_connection_failure_error() -> io::Error {
23    io::Error::new(
24        io::ErrorKind::ConnectionReset,
25        "Random connection failure (explicit)",
26    )
27}
28
29/// Create an `io::Error` for half-open connection timeout.
30fn half_open_timeout_error() -> io::Error {
31    io::Error::new(
32        io::ErrorKind::ConnectionReset,
33        "Connection reset (half-open timeout)",
34    )
35}
36
37/// Create an `io::Error` for aborted connection (RST).
38fn connection_aborted_error() -> io::Error {
39    io::Error::new(
40        io::ErrorKind::ConnectionReset,
41        "Connection was aborted (RST)",
42    )
43}
44
45/// Simulated TCP stream that implements async read/write operations.
46///
47/// `SimTcpStream` provides a realistic simulation of TCP socket behavior by implementing
48/// the `AsyncRead` and `AsyncWrite` traits. It interfaces with the simulation event system
49/// to provide ordered, reliable data delivery with configurable network delays.
50///
51/// ## Architecture Overview
52///
53/// Each `SimTcpStream` represents one endpoint of a TCP connection:
54///
55/// ```text
56/// Application Layer          SimTcpStream Layer          Simulation Layer
57/// ─────────────────          ──────────────────          ─────────────────
58///                                                        
59/// stream.write_all(data) ──► poll_write(data) ────────► buffer_send(data)
60///                                                        └─► ProcessSendBuffer event
61///                                                            └─► DataDelivery event
62///                                                                └─► paired connection
63///                                                        
64/// stream.read(buf) ◄────── poll_read(buf) ◄──────────── receive_buffer
65///                          │                           └─► waker registration
66///                          └─► Poll::Pending/Ready     
67/// ```
68///
69/// ## TCP Semantics Implemented
70///
71/// This implementation provides the core TCP guarantees required for realistic simulation:
72///
73/// ### 1. Reliable Delivery
74/// - All written data will eventually be delivered to the paired connection
75/// - No data loss (unless explicitly simulated via fault injection)
76/// - Delivery confirmation through the event system
77///
78/// ### 2. Ordered Delivery (FIFO)
79/// - Messages written first will arrive first at the destination
80/// - Achieved through per-connection send buffering
81/// - Critical for protocols that depend on message ordering
82///
83/// ### 3. Flow Control Simulation
84/// - Read operations block (`Poll::Pending`) when no data is available
85/// - Write operations complete immediately (buffering model)
86/// - Backpressure handled at the application layer
87///
88/// ## Usage Examples
89///
90/// Provides async read/write operations for client and server connections.
91///
92/// ## Performance Characteristics
93///
94/// - **Write Latency**: O(1) - writes are buffered immediately
95/// - **Read Latency**: `O(network_delay)` - depends on simulation configuration
96/// - **Memory Usage**: `O(buffered_data)` - proportional to unread data
97/// - **CPU Overhead**: Minimal - leverages efficient event system
98///
99/// ## Connection Lifecycle
100///
101/// 1. **Creation**: Stream created with reference to simulation and connection ID
102/// 2. **Active Phase**: Read/write operations interact with simulation buffers
103/// 3. **Data Transfer**: Asynchronous event processing handles network simulation
104/// 4. **Termination**: Stream dropped when connection ends (automatic cleanup)
105///
106/// ## Thread Safety
107///
108/// `SimTcpStream` is `Send + Sync + Unpin + 'static` via its `Arc<RwLock<…>>`
109/// backed `WeakSimWorld` handle. The simulation runtime itself runs on a
110/// single OS thread (`new_current_thread().build()`), but the stream type
111/// satisfies Send-bounded traits so it composes naturally with
112/// `tokio::spawn`, hyper/reqwest connectors, and customer code that uses
113/// `Arc<RwLock<…>>` / `DashMap` / `Arc<AtomicBool>`.
114pub struct SimTcpStream {
115    /// Weak reference to the simulation world.
116    ///
117    /// Uses `WeakSimWorld` to avoid circular references while allowing the stream
118    /// to detect if the simulation has been dropped. Operations return errors
119    /// gracefully if the simulation is no longer available.
120    sim: WeakSimWorld,
121
122    /// Unique identifier for this connection within the simulation.
123    ///
124    /// This ID corresponds to a `ConnectionState` entry in the simulation's
125    /// connection table. Used to route read/write operations to the correct
126    /// connection buffers and waker registrations.
127    connection_id: ConnectionId,
128}
129
130impl SimTcpStream {
131    /// Create a new simulated TCP stream
132    pub(crate) fn new(sim: WeakSimWorld, connection_id: ConnectionId) -> Self {
133        Self { sim, connection_id }
134    }
135
136    /// Get the connection ID (for test introspection and chaos injection)
137    #[must_use]
138    pub fn connection_id(&self) -> ConnectionId {
139        self.connection_id
140    }
141
142    /// Returns `true`: `SimTcpStream` implements an efficient vectored write that
143    /// records each `IoSlice` as its own ordered delivery event, so the chaos pack
144    /// can act on individual segments.
145    #[must_use]
146    pub fn is_write_vectored(&self) -> bool {
147        true
148    }
149
150    /// Run the chaos/closure checks that precede backpressure for a write.
151    ///
152    /// Returns `Some(poll)` to short-circuit, `None` to proceed.
153    fn write_guard_pre_backpressure(
154        &self,
155        sim: &crate::sim::SimWorld,
156        cx: &mut Context<'_>,
157    ) -> Option<Poll<Result<usize, io::Error>>> {
158        // Random close chaos injection (FDB rollRandomClose pattern)
159        // Check at start of every write operation - sim2.actor.cpp:423
160        // Returns Some(true) for explicit error, Some(false) for silent (connection marked closed)
161        if let Some(true) = sim.roll_random_close(self.connection_id) {
162            // 30% explicit exception - throw connection_failed immediately
163            return Some(Poll::Ready(Err(random_connection_failure_error())));
164            // 70% silent case: connection already marked as closed, will fail below
165        }
166
167        // Check if send side is closed (asymmetric closure)
168        if sim.is_send_closed(self.connection_id) {
169            return Some(Poll::Ready(Err(io::Error::new(
170                io::ErrorKind::BrokenPipe,
171                "Connection send side closed",
172            ))));
173        }
174
175        // Check if connection is closed or cut
176        if sim.is_connection_closed(self.connection_id) {
177            // Check how the connection was closed
178            return Some(match sim.close_reason(self.connection_id) {
179                CloseReason::Aborted => Poll::Ready(Err(connection_aborted_error())),
180                _ => Poll::Ready(Err(io::Error::new(
181                    io::ErrorKind::BrokenPipe,
182                    "Connection was closed (FIN)",
183                ))),
184            });
185        }
186
187        if sim.is_connection_cut(self.connection_id) {
188            // Connection is temporarily cut - register waker and wait for restoration
189            tracing::debug!(
190                "SimTcpStream::poll_write connection_id={} is cut, registering cut waker",
191                self.connection_id.0
192            );
193            sim.register_cut_waker(self.connection_id, cx.waker().clone());
194            tracing::debug!(
195                "SimTcpStream::poll_write connection_id={} registered waker for cut connection",
196                self.connection_id.0
197            );
198            return Some(Poll::Pending);
199        }
200
201        // Check for half-open connection (peer crashed)
202        if sim.is_half_open(self.connection_id) && sim.should_half_open_error(self.connection_id) {
203            // Error time reached - return ECONNRESET
204            tracing::debug!(
205                "SimTcpStream::poll_write connection_id={} half-open error time reached, returning ECONNRESET",
206                self.connection_id.0
207            );
208            return Some(Poll::Ready(Err(half_open_timeout_error())));
209        }
210        // Half-open but not yet error time - writes succeed but data goes nowhere
211        // (paired_connection is already None, so buffer_send will silently succeed)
212
213        None
214    }
215
216    /// Run the write-clog checks after the backpressure decision.
217    ///
218    /// Returns `Some(poll)` to short-circuit, `None` to proceed.
219    fn write_guard_clog(
220        &self,
221        sim: &crate::sim::SimWorld,
222        cx: &mut Context<'_>,
223    ) -> Option<Poll<Result<usize, io::Error>>> {
224        // Phase 7: Check for write clogging
225        if sim.is_write_clogged(self.connection_id) {
226            // Already clogged, register waker and return Pending
227            sim.register_clog_waker(self.connection_id, cx.waker().clone());
228            return Some(Poll::Pending);
229        }
230
231        // Check if this write should be clogged
232        if sim.should_clog_write(self.connection_id) {
233            sim.clog_write(self.connection_id);
234            sim.register_clog_waker(self.connection_id, cx.waker().clone());
235            return Some(Poll::Pending);
236        }
237
238        None
239    }
240}
241
242impl Drop for SimTcpStream {
243    fn drop(&mut self) {
244        // Close the connection in the simulation when the stream is dropped
245        // This matches real TCP behavior where dropping a socket always closes it
246        if let Ok(sim) = self.sim.upgrade() {
247            tracing::debug!(
248                "SimTcpStream dropping, closing connection {}",
249                self.connection_id.0
250            );
251            sim.close_connection(self.connection_id);
252        }
253    }
254}
255
256impl AsyncRead for SimTcpStream {
257    #[instrument(skip(self, cx, buf))]
258    fn poll_read(
259        self: Pin<&mut Self>,
260        cx: &mut Context<'_>,
261        buf: &mut [u8],
262    ) -> Poll<io::Result<usize>> {
263        tracing::trace!(
264            "SimTcpStream::poll_read called on connection_id={}",
265            self.connection_id.0
266        );
267        let sim = self.sim.upgrade().map_err(|_| sim_shutdown_error())?;
268
269        // Random close chaos injection (FDB rollRandomClose pattern)
270        // Check at start of every read operation - sim2.actor.cpp:408
271        // Returns Some(true) for explicit error, Some(false) for silent (connection marked closed)
272        if let Some(true) = sim.roll_random_close(self.connection_id) {
273            // 30% explicit exception - throw connection_failed immediately
274            return Poll::Ready(Err(random_connection_failure_error()));
275            // 70% silent case: connection already marked as closed, will return EOF below
276        }
277
278        // Check if receive side is closed (asymmetric closure)
279        if sim.is_recv_closed(self.connection_id) {
280            tracing::debug!(
281                "SimTcpStream::poll_read connection_id={} recv side closed, returning EOF",
282                self.connection_id.0
283            );
284            return Poll::Ready(Ok(0)); // EOF
285        }
286
287        // Check for half-open connection (peer crashed)
288        if sim.is_half_open(self.connection_id) && sim.should_half_open_error(self.connection_id) {
289            // Error time reached - return ECONNRESET
290            tracing::debug!(
291                "SimTcpStream::poll_read connection_id={} half-open error time reached, returning ECONNRESET",
292                self.connection_id.0
293            );
294            return Poll::Ready(Err(half_open_timeout_error()));
295        }
296        // Half-open but not yet error time - will block (Pending) below waiting for data
297        // that will never come, which is the correct half-open behavior
298
299        // Check for read clogging (symmetric with write clogging)
300        if sim.is_read_clogged(self.connection_id) {
301            // Already clogged, register waker and return Pending
302            sim.register_read_clog_waker(self.connection_id, cx.waker().clone());
303            return Poll::Pending;
304        }
305
306        // Check if this read should be clogged
307        if sim.should_clog_read(self.connection_id) {
308            sim.clog_read(self.connection_id);
309            sim.register_read_clog_waker(self.connection_id, cx.waker().clone());
310            return Poll::Pending;
311        }
312
313        // Try to read from connection's receive buffer first
314        // We should be able to read buffered data even if connection is currently cut
315        let mut temp_buf = vec![0u8; buf.len()];
316        let bytes_read = sim
317            .read_from_connection(self.connection_id, &mut temp_buf)
318            .map_err(|e| io::Error::other(format!("read error: {e}")))?;
319
320        tracing::trace!(
321            "SimTcpStream::poll_read connection_id={} read {} bytes",
322            self.connection_id.0,
323            bytes_read
324        );
325
326        if bytes_read > 0 {
327            let data_preview = String::from_utf8_lossy(&temp_buf[..std::cmp::min(bytes_read, 20)]);
328            tracing::trace!(
329                "SimTcpStream::poll_read connection_id={} returning data: '{}'",
330                self.connection_id.0,
331                data_preview
332            );
333            buf[..bytes_read].copy_from_slice(&temp_buf[..bytes_read]);
334            return Poll::Ready(Ok(bytes_read));
335        }
336        Self::poll_read_no_data(&sim, self.connection_id, cx, buf)
337    }
338}
339
340impl SimTcpStream {
341    /// Handle the `poll_read` branch where no buffered data is currently
342    /// available. Checks for graceful FIN, abort, cut, then registers a
343    /// read waker and rechecks the buffer to avoid races.
344    fn poll_read_no_data(
345        sim: &crate::sim::SimWorld,
346        connection_id: crate::network::sim::ConnectionId,
347        cx: &mut Context<'_>,
348        buf: &mut [u8],
349    ) -> Poll<io::Result<usize>> {
350        // No data available - check if connection has received FIN, is closed, or cut.
351        if sim.is_remote_fin_received(connection_id) {
352            tracing::info!(
353                "SimTcpStream::poll_read connection_id={} remote FIN received, returning EOF",
354                connection_id.0
355            );
356            return Poll::Ready(Ok(0));
357        }
358        if sim.is_connection_closed(connection_id) {
359            if sim.close_reason(connection_id) == CloseReason::Aborted {
360                tracing::info!(
361                    "SimTcpStream::poll_read connection_id={} was aborted (RST)",
362                    connection_id.0
363                );
364                return Poll::Ready(Err(connection_aborted_error()));
365            }
366            tracing::info!(
367                "SimTcpStream::poll_read connection_id={} closed gracefully (FIN)",
368                connection_id.0
369            );
370            return Poll::Ready(Ok(0));
371        }
372        if sim.is_connection_cut(connection_id) {
373            tracing::debug!(
374                "SimTcpStream::poll_read connection_id={} is cut, registering cut waker",
375                connection_id.0
376            );
377            sim.register_cut_waker(connection_id, cx.waker().clone());
378            return Poll::Pending;
379        }
380
381        // Register for notification when data arrives, then re-check for race.
382        tracing::trace!(
383            "SimTcpStream::poll_read connection_id={} no data, registering waker",
384            connection_id.0
385        );
386        sim.register_read_waker(connection_id, cx.waker().clone());
387
388        let mut temp_buf_recheck = vec![0u8; buf.len()];
389        let bytes_read_recheck = sim
390            .read_from_connection(connection_id, &mut temp_buf_recheck)
391            .map_err(|e| io::Error::other(format!("recheck read error: {e}")))?;
392
393        if bytes_read_recheck > 0 {
394            buf[..bytes_read_recheck].copy_from_slice(&temp_buf_recheck[..bytes_read_recheck]);
395            return Poll::Ready(Ok(bytes_read_recheck));
396        }
397
398        // Final check after waker registration.
399        if sim.is_remote_fin_received(connection_id) {
400            return Poll::Ready(Ok(0));
401        }
402        if sim.is_connection_closed(connection_id) {
403            if sim.close_reason(connection_id) == CloseReason::Aborted {
404                return Poll::Ready(Err(connection_aborted_error()));
405            }
406            return Poll::Ready(Ok(0));
407        }
408        Poll::Pending
409    }
410}
411
412impl AsyncWrite for SimTcpStream {
413    #[instrument(skip(self, cx, buf))]
414    fn poll_write(
415        self: Pin<&mut Self>,
416        cx: &mut Context<'_>,
417        buf: &[u8],
418    ) -> Poll<Result<usize, io::Error>> {
419        let sim = self.sim.upgrade().map_err(|_| sim_shutdown_error())?;
420
421        if let Some(poll) = self.write_guard_pre_backpressure(&sim, cx) {
422            return poll;
423        }
424
425        // Check for send buffer space (backpressure)
426        let available_buffer = sim.available_send_buffer(self.connection_id);
427        if available_buffer < buf.len() {
428            // Not enough buffer space, register waker and return Pending
429            tracing::debug!(
430                "SimTcpStream::poll_write connection_id={} buffer full (available={}, needed={}), waiting",
431                self.connection_id.0,
432                available_buffer,
433                buf.len()
434            );
435            sim.register_send_buffer_waker(self.connection_id, cx.waker().clone());
436            return Poll::Pending;
437        }
438
439        if let Some(poll) = self.write_guard_clog(&sim, cx) {
440            return poll;
441        }
442
443        // Use buffered send to maintain TCP ordering
444        let data_preview = String::from_utf8_lossy(&buf[..std::cmp::min(buf.len(), 20)]);
445        tracing::trace!(
446            "SimTcpStream::poll_write buffering {} bytes: '{}' for ordered delivery",
447            buf.len(),
448            data_preview
449        );
450
451        // Buffer the data for ordered processing instead of direct event scheduling
452        sim.buffer_send(self.connection_id, buf.to_vec())
453            .map_err(|e| io::Error::other(format!("buffer send error: {e}")))?;
454
455        Poll::Ready(Ok(buf.len()))
456    }
457
458    #[instrument(skip(self, cx, bufs))]
459    fn poll_write_vectored(
460        self: Pin<&mut Self>,
461        cx: &mut Context<'_>,
462        bufs: &[IoSlice<'_>],
463    ) -> Poll<Result<usize, io::Error>> {
464        let sim = self.sim.upgrade().map_err(|_| sim_shutdown_error())?;
465
466        if let Some(poll) = self.write_guard_pre_backpressure(&sim, cx) {
467            return poll;
468        }
469
470        let total: usize = bufs.iter().map(|slice| slice.len()).sum();
471        if total == 0 {
472            return Poll::Ready(Ok(0));
473        }
474
475        // writev(2) partial-accept semantics: if there's SOME room, accept what
476        // fits and report a short count; only block when there is NO room at all.
477        let available = sim.available_send_buffer(self.connection_id);
478        if available == 0 {
479            sim.register_send_buffer_waker(self.connection_id, cx.waker().clone());
480            return Poll::Pending;
481        }
482
483        if let Some(poll) = self.write_guard_clog(&sim, cx) {
484            return poll;
485        }
486
487        let accepted = total.min(available);
488
489        // Buffer each IoSlice as its own ordered delivery event, truncating the
490        // boundary slice when `accepted < total`. Skip empty slices so they do not
491        // create empty delivery events.
492        let mut remaining = accepted;
493        for slice in bufs {
494            if remaining == 0 {
495                break;
496            }
497            if slice.is_empty() {
498                continue;
499            }
500            let take = remaining.min(slice.len());
501            sim.buffer_send(self.connection_id, slice[..take].to_vec())
502                .map_err(|e| io::Error::other(format!("buffer send error: {e}")))?;
503            remaining -= take;
504        }
505
506        Poll::Ready(Ok(accepted))
507    }
508
509    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
510        Poll::Ready(Ok(()))
511    }
512
513    fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
514        let sim = self.sim.upgrade().map_err(|_| sim_shutdown_error())?;
515
516        // Close the connection in the simulation when close is called
517        tracing::debug!(
518            "SimTcpStream::poll_close closing connection {}",
519            self.connection_id.0
520        );
521        sim.close_connection(self.connection_id);
522
523        Poll::Ready(Ok(()))
524    }
525}
526
527/// Future representing an accept operation
528pub struct AcceptFuture {
529    sim: WeakSimWorld,
530    local_addr: String,
531}
532
533impl Future for AcceptFuture {
534    type Output = io::Result<(SimTcpStream, String)>;
535
536    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
537        let Ok(sim) = self.sim.upgrade() else {
538            return Poll::Ready(Err(sim_shutdown_error()));
539        };
540
541        let Some(connection_id) = sim.pending_connection(&self.local_addr) else {
542            // No connection available yet - register waker for when connection becomes available
543            sim.register_accept_waker(&self.local_addr, cx.waker().clone());
544            return Poll::Pending;
545        };
546
547        // Get accept delay from network configuration
548        let delay = sim
549            .with_network_config(|config| crate::network::sample_latency(&config.accept_latency));
550
551        // Schedule accept completion event to advance simulation time
552        sim.schedule_event(
553            Event::Connection {
554                id: connection_id.0,
555                state: crate::ConnectionStateChange::ConnectionReady,
556            },
557            delay,
558        );
559
560        // FDB Pattern (sim2.actor.cpp:1149-1175):
561        // Return the synthesized ephemeral peer address, not the client's real address.
562        // This simulates real TCP where servers see client ephemeral ports.
563        let peer_addr = sim
564            .connection_peer_address(connection_id)
565            .unwrap_or_else(|| "unknown:0".to_string());
566
567        let stream = SimTcpStream::new(self.sim.clone(), connection_id);
568        Poll::Ready(Ok((stream, peer_addr)))
569    }
570}
571
572/// Simulated TCP listener
573pub struct SimTcpListener {
574    sim: WeakSimWorld,
575    local_addr: String,
576}
577
578impl SimTcpListener {
579    /// Create a new simulated TCP listener
580    pub(crate) fn new(sim: WeakSimWorld, local_addr: String) -> Self {
581        Self { sim, local_addr }
582    }
583}
584
585impl TcpListenerTrait for SimTcpListener {
586    type TcpStream = SimTcpStream;
587
588    #[instrument(skip(self))]
589    async fn accept(&self) -> io::Result<(Self::TcpStream, String)> {
590        AcceptFuture {
591            sim: self.sim.clone(),
592            local_addr: self.local_addr.clone(),
593        }
594        .await
595    }
596
597    fn local_addr(&self) -> io::Result<String> {
598        Ok(self.local_addr.clone())
599    }
600}