Skip to main content

subetha_cxc/
net_bridge.rs

1//! `net_bridge`: a TCP bridge with NO async runtime. One connection
2//! ferries a producer ring on one host to a consumer ring on another,
3//! using blocking `std::net` sockets on dedicated threads.
4//!
5//! Where the feature-gated `tcp_bridge` module uses `tokio::net`, this
6//! path needs no executor at all. The bridge watches exactly one
7//! socket, so a blocking `read()` on its own thread is the right shape:
8//! it parks in the kernel until a packet arrives - the kernel's socket
9//! wait IS the network reactor - costs zero CPU while idle, and pulls
10//! in no runtime. Async socket I/O earns its keep multiplexing many
11//! sockets on few threads; a single-connection ferry has nothing to
12//! multiplex.
13//!
14//! # The wake hand-off
15//!
16//! The server reads slots off the wire, pushes them into the local
17//! consumer ring, and fires the consumer's [`CrossProcessWaker`] once
18//! per socket read that produced slots. That wake is what lets a
19//! parked `recv().await` (driven by [`crate::reactor`]) resolve when a
20//! NETWORK packet arrives - the same reactor that bridges a sibling
21//! process's push bridges a remote host's packet, because both reduce
22//! to "the consumer ring advanced, fire the local Waker."
23//!
24//! # Data path
25//!
26//! Egress burst-batches: every already-available producer slot (up to
27//! [`EGRESS_BATCH_SLOTS`]) ships in one write. Ingress is chunked: each
28//! `read` takes whatever the wire has, complete 64-byte slots are
29//! pushed as they assemble, and a partial slot carries to the next
30//! read. `TCP_NODELAY` is set on both ends.
31
32use std::io::{Read, Write};
33use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
34use std::sync::Arc;
35
36use crate::cross_process_waker::CrossProcessWaker;
37use crate::spsc_ring::{SpscRingCore, SPSC_PAYLOAD_BYTES};
38
39/// One wire slot equals one ring payload.
40const SLOT: usize = SPSC_PAYLOAD_BYTES;
41
42/// Slots per batched egress write (16 KiB of payload per syscall at the
43/// 64-byte slot size).
44pub const EGRESS_BATCH_SLOTS: usize = 256;
45
46/// Ingress socket-read buffer in bytes.
47const INGRESS_BUF_BYTES: usize = 64 * 1024;
48
49/// Connect to `addr` and ship `n_items` slots drained from
50/// `producer_ring` across a blocking TCP connection. Burst-batched:
51/// every already-available slot (up to [`EGRESS_BATCH_SLOTS`]) goes out
52/// in one write; a lone item ships immediately.
53pub fn ship(
54    addr: SocketAddr,
55    producer_ring: Arc<SpscRingCore>,
56    n_items: u64,
57) -> std::io::Result<()> {
58    let mut stream = TcpStream::connect(addr)?;
59    stream.set_nodelay(true)?;
60    // Frame header: 8-byte big-endian item count.
61    stream.write_all(&n_items.to_be_bytes())?;
62
63    let mut batch = vec![0u8; EGRESS_BATCH_SLOTS * SLOT];
64    let mut slot = [0u8; SLOT];
65    let mut shipped: u64 = 0;
66    while shipped < n_items {
67        let budget = EGRESS_BATCH_SLOTS.min((n_items - shipped) as usize);
68        let mut filled = 0usize;
69        while filled < budget {
70            match producer_ring.try_pop(&mut slot) {
71                Ok(_) => {
72                    batch[filled * SLOT..(filled + 1) * SLOT]
73                        .copy_from_slice(&slot);
74                    filled += 1;
75                }
76                Err(_) => break,
77            }
78        }
79        if filled == 0 {
80            // Producer ring momentarily empty; yield and retry.
81            std::thread::yield_now();
82            continue;
83        }
84        stream.write_all(&batch[..filled * SLOT])?;
85        shipped += filled as u64;
86    }
87    stream.shutdown(Shutdown::Write)?;
88    Ok(())
89}
90
91/// Accept one connection on `listener`, read its framed slot stream,
92/// push each complete slot into `consumer_ring`, and fire `xwaker` once
93/// per socket read that produced slots - waking the consumer's reactor
94/// so a parked `recv().await` resolves on the arriving packet. Returns
95/// the count of items received.
96///
97/// The blocking `accept` + `read` are the network reactor: the thread
98/// parks in the kernel until the peer connects / sends, then signals
99/// the consumer ring.
100pub fn serve_one(
101    listener: &TcpListener,
102    consumer_ring: &Arc<SpscRingCore>,
103    xwaker: &Arc<CrossProcessWaker>,
104) -> std::io::Result<u64> {
105    let (mut stream, _) = listener.accept()?;
106    stream.set_nodelay(true)?;
107    let mut header = [0u8; 8];
108    stream.read_exact(&mut header)?;
109    let total: u64 = u64::from_be_bytes(header);
110
111    let mut buf = vec![0u8; INGRESS_BUF_BYTES];
112    let mut carry: Vec<u8> = Vec::with_capacity(SLOT);
113    let mut received: u64 = 0;
114    let trace = crate::reactor::wake_trace();
115    let (mut reads, mut eor_wakes) = (0u64, 0u64);
116    let mut last_snap = std::time::Instant::now();
117    while received < total {
118        if trace && last_snap.elapsed() >= std::time::Duration::from_secs(1) {
119            last_snap = std::time::Instant::now();
120            eprintln!(
121                "subetha: serve_one ring={:p} reads={reads} received={received}/{total} \
122                 eor_wakes={eor_wakes} full_wakes={} head={}",
123                Arc::as_ptr(consumer_ring),
124                FULL_WAKES.load(std::sync::atomic::Ordering::Relaxed),
125                consumer_ring.head(),
126            );
127        }
128        let n = stream.read(&mut buf)?;
129        reads += 1;
130        if n == 0 {
131            return Err(std::io::Error::new(
132                std::io::ErrorKind::UnexpectedEof,
133                "peer closed before sending all framed items",
134            ));
135        }
136        let mut pushed_any = false;
137        let mut data: &[u8] = &buf[..n];
138
139        // Complete a carried-over partial slot first.
140        if !carry.is_empty() {
141            let need = SLOT - carry.len();
142            let take = need.min(data.len());
143            carry.extend_from_slice(&data[..take]);
144            data = &data[take..];
145            if carry.len() == SLOT {
146                push_spin(consumer_ring, xwaker, &carry);
147                carry.clear();
148                received += 1;
149                pushed_any = true;
150            }
151        }
152        // Drain whole slots out of this read.
153        while data.len() >= SLOT && received < total {
154            push_spin(consumer_ring, xwaker, &data[..SLOT]);
155            data = &data[SLOT..];
156            received += 1;
157            pushed_any = true;
158        }
159        // Stash any trailing partial slot for the next read.
160        if !data.is_empty() {
161            carry.extend_from_slice(data);
162        }
163
164        // One wake per read that landed slots: the reactor coalesces it
165        // into a single local Waker fire and the consumer drains all
166        // newly-available items.
167        if pushed_any {
168            eor_wakes += 1;
169            xwaker.wake_up_to(consumer_ring.head());
170        }
171    }
172    Ok(total)
173}
174
175/// Full-path wakes fired from `push_spin`, process-wide.
176static FULL_WAKES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
177
178fn push_spin(ring: &SpscRingCore, xwaker: &CrossProcessWaker, slot: &[u8]) {
179    while ring.try_push(slot).is_err() {
180        // Ring full and a parked consumer has not yet been signalled for
181        // this read (the end-of-read wake fires later). Wake it now so it
182        // drains, else this push and the parked recv() would deadlock.
183        FULL_WAKES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
184        xwaker.wake_up_to(ring.head());
185        std::hint::spin_loop();
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::cross_process_waker::MAX_WAITERS_DEFAULT;
193    use crate::reactor::{block_on, receiver_cross};
194
195    #[test]
196    fn network_packet_wakes_parked_recv() {
197        // A real localhost TCP connection ferries items into a consumer
198        // ring; the parked recv().await is woken by the server's signal,
199        // which is driven by the arriving packet. No async runtime.
200        const N: u64 = 5_000;
201        const CAP: usize = 256;
202
203        let consumer_ring = Arc::new(SpscRingCore::create_anon(CAP).unwrap());
204        let xwaker = Arc::new(
205            CrossProcessWaker::create_anon(MAX_WAITERS_DEFAULT).unwrap(),
206        );
207        let rx = receiver_cross(Arc::clone(&consumer_ring), Arc::clone(&xwaker));
208
209        let producer_ring = Arc::new(SpscRingCore::create_anon(CAP).unwrap());
210
211        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
212        let addr = listener.local_addr().unwrap();
213
214        // Server thread: socket -> consumer ring + signal.
215        let server = {
216            let consumer_ring = Arc::clone(&consumer_ring);
217            let xwaker = Arc::clone(&xwaker);
218            std::thread::spawn(move || {
219                serve_one(&listener, &consumer_ring, &xwaker).unwrap()
220            })
221        };
222
223        // Feeder thread: items -> producer ring (with a couple of pauses
224        // so the consumer genuinely parks awaiting a packet).
225        let feeder = {
226            let producer_ring = Arc::clone(&producer_ring);
227            std::thread::spawn(move || {
228                let mut buf = [0u8; SLOT];
229                for i in 0..N {
230                    if i > 0 && i % (N / 3) == 0 {
231                        std::thread::sleep(std::time::Duration::from_millis(10));
232                    }
233                    buf[..8].copy_from_slice(&i.to_le_bytes());
234                    while producer_ring.try_push(&buf).is_err() {
235                        std::hint::spin_loop();
236                    }
237                }
238            })
239        };
240
241        // Client thread: producer ring -> socket.
242        let client = std::thread::spawn(move || {
243            ship(addr, producer_ring, N).unwrap();
244        });
245
246        // Consumer: parked recv().await, woken by the network packets.
247        let sum = block_on(async move {
248            let mut s = 0u64;
249            for expected in 0..N {
250                let item = rx.recv().await;
251                let seq = u64::from_le_bytes(item[..8].try_into().unwrap());
252                assert_eq!(seq, expected, "network FIFO order violated");
253                s = s.wrapping_add(seq);
254            }
255            s
256        });
257
258        feeder.join().unwrap();
259        client.join().unwrap();
260        assert_eq!(server.join().unwrap(), N);
261        assert_eq!(sum, (0..N).sum());
262    }
263
264    #[test]
265    fn tiny_consumer_ring_does_not_deadlock_on_full() {
266        // A 4-slot consumer ring against a batch-shipping client forces
267        // the server's push_spin to hit Full constantly while the
268        // consumer is often parked. Without the wake-on-Full hand-off
269        // this deadlocks; with it, every item still drains.
270        const N: u64 = 4_000;
271        const CAP: usize = 4;
272
273        let consumer_ring = Arc::new(SpscRingCore::create_anon(CAP).unwrap());
274        let xwaker = Arc::new(
275            CrossProcessWaker::create_anon(MAX_WAITERS_DEFAULT).unwrap(),
276        );
277        let rx = receiver_cross(Arc::clone(&consumer_ring), Arc::clone(&xwaker));
278        let producer_ring = Arc::new(SpscRingCore::create_anon(CAP).unwrap());
279
280        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
281        let addr = listener.local_addr().unwrap();
282
283        let server = {
284            let consumer_ring = Arc::clone(&consumer_ring);
285            let xwaker = Arc::clone(&xwaker);
286            std::thread::spawn(move || {
287                serve_one(&listener, &consumer_ring, &xwaker).unwrap()
288            })
289        };
290        let feeder = {
291            let producer_ring = Arc::clone(&producer_ring);
292            std::thread::spawn(move || {
293                let mut buf = [0u8; SLOT];
294                for i in 0..N {
295                    buf[..8].copy_from_slice(&i.to_le_bytes());
296                    while producer_ring.try_push(&buf).is_err() {
297                        std::hint::spin_loop();
298                    }
299                }
300            })
301        };
302        let client = std::thread::spawn(move || {
303            ship(addr, producer_ring, N).unwrap();
304        });
305
306        let sum = block_on(async move {
307            let mut s = 0u64;
308            for expected in 0..N {
309                let item = rx.recv().await;
310                let seq = u64::from_le_bytes(item[..8].try_into().unwrap());
311                assert_eq!(seq, expected, "FIFO order violated under Full pressure");
312                s = s.wrapping_add(seq);
313            }
314            s
315        });
316
317        feeder.join().unwrap();
318        client.join().unwrap();
319        assert_eq!(server.join().unwrap(), N);
320        assert_eq!(sum, (0..N).sum());
321    }
322}