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