liminal_server/server/connection/loopback/duplex.rs
1//! The bounded byte duplex the loopback transport carries frames over (§3).
2//!
3//! Two SPSC byte rings — client-to-server and server-to-client — each a
4//! `VecDeque<u8>` behind a `Mutex` with a `Condvar` for the one blocking
5//! reader. Correctness over cleverness is the deliberate choice: the cost
6//! baseline this replaces is a syscall, so a lock-free ring would buy nothing
7//! measurable and would cost the ability to read the close semantics off the
8//! page.
9//!
10//! **Bounded is load-bearing.** A socket applies backpressure through kernel
11//! buffers and `WouldBlock`; an unbounded queue would give the loopback mount a
12//! semantics no other mount has (infinite buffering) and an unbounded
13//! idle-memory class. A full ring answers `WouldBlock`, a nearly-full ring
14//! accepts a partial write, and [`super::super::outbound::OutboundWriter`]'s
15//! budget and partial-write logic then behaves over a ring exactly as it
16//! behaves over a socket. Neither end's [`Write::write`] ever blocks. The
17//! client end additionally offers a BLOCKING
18//! [`write_timeout`](LoopbackClientEnd::write_timeout), the twin of its
19//! blocking read and for the same reason: a socket's `write_all` waits out a
20//! full send buffer under a write deadline, and a client that could only see
21//! an instant `WouldBlock` would have a backpressure semantics no other mount
22//! has.
23//!
24//! **The reader is TOLD, never polls.** The loopback has no descriptor, so the
25//! beamr readiness facility cannot arm it and the retired busy loop is not
26//! coming back. Instead the writer wakes the reader: a write that takes the
27//! client-to-server ring from empty to non-empty invokes the server end's
28//! registered waker, as does the client end's drop, so a parked connection
29//! learns about bytes and about hangups by the same mechanism. Writes into a
30//! ring that already has bytes wake nobody — the reader has already been told.
31//! The client end needs no waker: its blocking read parks on the condvar, and
32//! that condvar IS its wake.
33//!
34//! **Idle cost is zero.** A duplex owns no thread, arms no timer, and
35//! schedules nothing. Two idle rings are two empty `VecDeque`s and their
36//! synchronisation primitives; nothing runs until a byte is written or an end
37//! is dropped.
38
39use std::collections::VecDeque;
40use std::io::{self, Read, Write};
41use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
42use std::time::{Duration, Instant};
43
44use crate::ServerError;
45
46use super::super::process::InboundPending;
47
48/// The smallest ring a duplex will hand out, in bytes.
49///
50/// A zero-capacity ring can never accept a byte, so its writer would see
51/// `WouldBlock` forever while its reader waits for bytes that cannot arrive: a
52/// ring that can make no progress is not a legal state. The floor is applied
53/// rather than refused so that [`LoopbackDuplex::bounded`] stays infallible.
54const MIN_RING_CAPACITY_BYTES: usize = 1;
55
56/// Locks a mutex, adopting the guarded value through a poisoned lock.
57///
58/// A poisoned ring is a ring whose writer panicked mid-`extend`; the bytes
59/// behind it are still a well-formed `VecDeque` and the close flags are still
60/// booleans, so refusing to read them would turn one participant's panic into
61/// a wedged transport. This mirrors the supervisor's `recover_lock` discipline.
62fn recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
63 mutex.lock().unwrap_or_else(PoisonError::into_inner)
64}
65
66/// The mutable half of one ring: the bytes in flight and the two end-of-life
67/// flags that give the ring its close semantics.
68#[derive(Debug)]
69struct RingState {
70 /// Bytes written but not yet read, in order.
71 bytes: VecDeque<u8>,
72 /// The end that WRITES into this ring has been dropped. Once `bytes` is
73 /// drained the reader is at end of file.
74 writer_gone: bool,
75 /// The end that READS from this ring has been dropped. Further writes are
76 /// `BrokenPipe` immediately, drained or not — the bytes have nowhere to go.
77 reader_gone: bool,
78}
79
80/// One bounded direction of the duplex.
81#[derive(Debug)]
82struct Ring {
83 /// The hard byte bound. Never grows.
84 capacity: usize,
85 /// The bytes and the close flags.
86 state: Mutex<RingState>,
87 /// Signalled on every change a blocked reader could care about: bytes
88 /// arriving, and the writer end going away.
89 changed: Condvar,
90}
91
92impl Ring {
93 const fn new(capacity: usize) -> Self {
94 Self {
95 capacity,
96 state: Mutex::new(RingState {
97 bytes: VecDeque::new(),
98 writer_gone: false,
99 reader_gone: false,
100 }),
101 changed: Condvar::new(),
102 }
103 }
104
105 /// Bytes readable right now, consuming none of them.
106 fn readable_bytes(&self) -> usize {
107 recover(&self.state).bytes.len()
108 }
109
110 /// Whether a read would answer immediately — with bytes, or with the end
111 /// of file the writer's drop left behind.
112 fn read_would_answer(&self) -> bool {
113 let state = recover(&self.state);
114 !state.bytes.is_empty() || state.writer_gone
115 }
116
117 /// Writes what fits, never blocking.
118 ///
119 /// Returns the accepted byte count and whether this write took the ring
120 /// from empty to non-empty — the one edge a waker fires on.
121 ///
122 /// # Errors
123 /// `BrokenPipe` when the reading end has been dropped; `WouldBlock` when
124 /// the ring is full. Never `Ok(0)` for a non-empty `buf` on a live ring:
125 /// a zero-byte answer is what [`super::super::outbound::OutboundWriter`]
126 /// reads as a lost peer, so the honest full-ring answer is `WouldBlock`.
127 fn write(&self, buf: &[u8]) -> io::Result<(usize, bool)> {
128 let mut state = recover(&self.state);
129 if state.reader_gone {
130 return Err(io::Error::new(
131 io::ErrorKind::BrokenPipe,
132 "loopback peer end was dropped",
133 ));
134 }
135 if buf.is_empty() {
136 return Ok((0, false));
137 }
138 let free = self.capacity.saturating_sub(state.bytes.len());
139 if free == 0 {
140 return Err(io::Error::new(
141 io::ErrorKind::WouldBlock,
142 "loopback ring is full",
143 ));
144 }
145 let accepted = free.min(buf.len());
146 let was_empty = state.bytes.is_empty();
147 state.bytes.extend(buf.get(..accepted).unwrap_or(buf));
148 drop(state);
149 self.changed.notify_all();
150 Ok((accepted, was_empty))
151 }
152
153 /// Writes what fits, parking on the condvar until space appears, the
154 /// reading end goes away, or `timeout` elapses.
155 ///
156 /// `None` parks without a deadline; a `timeout` whose deadline is not
157 /// representable as an `Instant` is treated as `None`, matching
158 /// [`Self::read_until`]. Returns the accepted byte count and whether this
159 /// write took the ring from empty to non-empty — the one edge a waker
160 /// fires on.
161 ///
162 /// # Errors
163 /// `BrokenPipe` when the reading end has been dropped; `TimedOut` when the
164 /// window closes with the ring still full.
165 fn write_until(&self, buf: &[u8], timeout: Option<Duration>) -> io::Result<(usize, bool)> {
166 if buf.is_empty() {
167 return Ok((0, false));
168 }
169 let deadline = timeout.and_then(|window| Instant::now().checked_add(window));
170 let mut state = recover(&self.state);
171 loop {
172 if state.reader_gone {
173 return Err(io::Error::new(
174 io::ErrorKind::BrokenPipe,
175 "loopback peer end was dropped",
176 ));
177 }
178 let free = self.capacity.saturating_sub(state.bytes.len());
179 if free > 0 {
180 let accepted = free.min(buf.len());
181 let was_empty = state.bytes.is_empty();
182 state.bytes.extend(buf.get(..accepted).unwrap_or(buf));
183 drop(state);
184 self.changed.notify_all();
185 return Ok((accepted, was_empty));
186 }
187 let Some(deadline) = deadline else {
188 state = self
189 .changed
190 .wait(state)
191 .unwrap_or_else(PoisonError::into_inner);
192 continue;
193 };
194 // Re-derived every pass so a spurious wake cannot extend the window.
195 let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
196 return Err(io::Error::new(
197 io::ErrorKind::TimedOut,
198 "loopback write deadline expired",
199 ));
200 };
201 let (next, _) = self
202 .changed
203 .wait_timeout(state, remaining)
204 .unwrap_or_else(PoisonError::into_inner);
205 state = next;
206 }
207 }
208
209 /// Reads what is there, never blocking.
210 ///
211 /// # Errors
212 /// `WouldBlock` when the ring is empty and its writer is still alive. An
213 /// empty ring whose writer is gone is `Ok(0)` — end of file, exactly as a
214 /// hung-up socket reads.
215 fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
216 // The guard is confined to this block so the lock is never held while
217 // an error value is being built.
218 let (taken, writer_gone) = {
219 let mut state = recover(&self.state);
220 (take_from(&mut state.bytes, buf), state.writer_gone)
221 };
222 if taken > 0 {
223 // A read is the only event that frees ring space, so it is the only
224 // event that can tell a writer parked in `write_until` to try
225 // again. Without this the sole wake a blocked writer could ever
226 // receive is its reader disappearing.
227 self.changed.notify_all();
228 return Ok(taken);
229 }
230 if buf.is_empty() || writer_gone {
231 return Ok(taken);
232 }
233 Err(io::Error::new(
234 io::ErrorKind::WouldBlock,
235 "loopback ring is empty",
236 ))
237 }
238
239 /// Reads what is there, parking on the condvar until bytes arrive, the
240 /// writer goes away, or `timeout` elapses.
241 ///
242 /// `None` parks without a deadline. A `timeout` so large that the deadline
243 /// is not representable as an `Instant` is treated as `None`, which is the
244 /// honest reading of a deadline beyond the end of time.
245 ///
246 /// # Errors
247 /// `TimedOut` when the window closes with no bytes and a live writer.
248 fn read_until(&self, buf: &mut [u8], timeout: Option<Duration>) -> io::Result<usize> {
249 if buf.is_empty() {
250 return Ok(0);
251 }
252 let deadline = timeout.and_then(|window| Instant::now().checked_add(window));
253 let mut state = recover(&self.state);
254 loop {
255 let taken = take_from(&mut state.bytes, buf);
256 if taken > 0 {
257 drop(state);
258 // Freed space is a writer's only progress signal; see `read`.
259 self.changed.notify_all();
260 return Ok(taken);
261 }
262 if state.writer_gone {
263 return Ok(0);
264 }
265 let Some(deadline) = deadline else {
266 state = self
267 .changed
268 .wait(state)
269 .unwrap_or_else(PoisonError::into_inner);
270 continue;
271 };
272 // Re-derived every pass so a spurious wake cannot extend the window.
273 let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
274 return Err(io::Error::new(
275 io::ErrorKind::TimedOut,
276 "loopback read deadline expired",
277 ));
278 };
279 let (next, _) = self
280 .changed
281 .wait_timeout(state, remaining)
282 .unwrap_or_else(PoisonError::into_inner);
283 state = next;
284 }
285 }
286
287 /// Marks the writing end gone: the reader drains, then sees end of file.
288 fn close_writer(&self) {
289 recover(&self.state).writer_gone = true;
290 self.changed.notify_all();
291 }
292
293 /// Marks the reading end gone: the writer sees `BrokenPipe`.
294 fn close_reader(&self) {
295 recover(&self.state).reader_gone = true;
296 self.changed.notify_all();
297 }
298}
299
300/// Moves up to `buf.len()` bytes out of `bytes`, in order, returning how many.
301fn take_from(bytes: &mut VecDeque<u8>, buf: &mut [u8]) -> usize {
302 let taken = bytes.len().min(buf.len());
303 for (slot, byte) in buf.iter_mut().zip(bytes.drain(..taken)) {
304 *slot = byte;
305 }
306 taken
307}
308
309/// The server end's registered wake callback (§3's no-polling answer).
310///
311/// Held behind an `Arc` shared with the client end so the client's writes and
312/// the client's drop can fire it, and behind a `Mutex` so it can be registered
313/// after the duplex is built — the callback names a connection that does not
314/// exist until the process it belongs to has been spawned.
315#[derive(Default)]
316struct WakerSlot {
317 /// `None` until a reader registers. An unregistered slot is not an error:
318 /// a duplex with no parked reader has nothing to tell.
319 callback: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
320}
321
322impl WakerSlot {
323 fn set(&self, callback: Arc<dyn Fn() + Send + Sync>) {
324 *recover(&self.callback) = Some(callback);
325 }
326
327 /// Invokes the callback with NO lock held — neither the ring's nor the
328 /// slot's. A waker that writes back into the duplex, or that re-registers
329 /// itself, must not be able to deadlock the end that woke it.
330 fn fire(&self) {
331 let callback = recover(&self.callback).as_ref().map(Arc::clone);
332 if let Some(callback) = callback {
333 callback();
334 }
335 }
336}
337
338impl std::fmt::Debug for WakerSlot {
339 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 formatter
341 .debug_struct("WakerSlot")
342 .field("registered", &recover(&self.callback).is_some())
343 .finish()
344 }
345}
346
347/// The constructor for a loopback duplex.
348///
349/// Deliberately uninhabited: it names the constructor and owns its
350/// documentation, and there is no duplex value to hold — a duplex IS its two
351/// ends, and each end is owned by the side that uses it.
352#[derive(Debug)]
353pub enum LoopbackDuplex {}
354
355impl LoopbackDuplex {
356 /// Builds a duplex with `capacity_per_ring` bytes in each direction and
357 /// returns its two ends.
358 ///
359 /// The capacity is per ring, not shared: a full client-to-server ring does
360 /// not starve the server-to-client direction, which is the property that
361 /// lets a server drain a reply while its inbound side is backed up. The
362 /// capacity is floored at [`MIN_RING_CAPACITY_BYTES`] so a caller's zero
363 /// cannot build a duplex that can never move a byte.
364 #[must_use]
365 pub fn bounded(capacity_per_ring: usize) -> (LoopbackClientEnd, LoopbackServerEnd) {
366 let capacity = capacity_per_ring.max(MIN_RING_CAPACITY_BYTES);
367 let to_server = Arc::new(Ring::new(capacity));
368 let to_client = Arc::new(Ring::new(capacity));
369 let waker = Arc::new(WakerSlot::default());
370 let client = LoopbackClientEnd {
371 to_server: Arc::clone(&to_server),
372 from_server: Arc::clone(&to_client),
373 server_waker: Arc::clone(&waker),
374 };
375 let server = LoopbackServerEnd {
376 from_client: to_server,
377 to_client,
378 waker,
379 };
380 (client, server)
381 }
382}
383
384/// The client half of a loopback duplex: the end an embedded caller holds.
385///
386/// Reads BLOCK (with an optional deadline), mirroring the SDK's socket
387/// connection, whose reads are bounded by `set_read_timeout` and whose caller
388/// reads both `WouldBlock` and `TimedOut` as the same "no bytes in the window"
389/// answer. Writes never block: a full ring answers `WouldBlock`, exactly as a
390/// full socket send buffer does.
391#[derive(Debug)]
392pub struct LoopbackClientEnd {
393 /// Bytes this end writes; the server end reads them.
394 to_server: Arc<Ring>,
395 /// Bytes this end reads; the server end wrote them.
396 from_server: Arc<Ring>,
397 /// The server's wake callback, fired on the inbound write edge and on this
398 /// end's drop.
399 server_waker: Arc<WakerSlot>,
400}
401
402impl LoopbackClientEnd {
403 /// Bytes already waiting for this end, consuming none of them.
404 #[must_use]
405 pub fn readable_bytes(&self) -> usize {
406 self.from_server.readable_bytes()
407 }
408
409 /// Reads into `buf`, parking until bytes arrive, the server end is
410 /// dropped, or `timeout` elapses. `None` parks without a deadline.
411 ///
412 /// `Ok(0)` is end of file: the server end is gone and its ring is drained.
413 ///
414 /// # Errors
415 /// `TimedOut` when the window closes with no bytes and a live server end.
416 /// The SDK's socket path reads `WouldBlock` and `TimedOut` identically, so
417 /// either satisfies its contract; `TimedOut` is the one that names what
418 /// happened.
419 pub fn read_timeout(&mut self, buf: &mut [u8], timeout: Option<Duration>) -> io::Result<usize> {
420 self.from_server.read_until(buf, timeout)
421 }
422
423 /// Writes from `buf`, parking until ring space appears, the server end is
424 /// dropped, or `timeout` elapses. `None` parks without a deadline. Returns
425 /// the accepted byte count, which may be short of `buf.len()`.
426 ///
427 /// This is the write-side twin of [`Self::read_timeout`], and the socket it
428 /// stands in for is why it exists. A blocking `TcpStream` with a write
429 /// timeout BLOCKS while the kernel send buffer is full and fails only when
430 /// the window closes, so `write_all` over a socket makes progress across a
431 /// slow reader. [`Write::write`] on this end never blocks — a full ring
432 /// answers `WouldBlock` at once, which `io::Write::write_all` reads as a
433 /// hard failure — so a client driving the non-blocking half would see a
434 /// backpressure semantics no other mount has. Both halves are kept: the
435 /// non-blocking one is what the server end and the outbound budget want,
436 /// this one is what a client's `write_all` equivalent wants.
437 ///
438 /// # Errors
439 /// `BrokenPipe` when the server end has been dropped; `TimedOut` when the
440 /// window closes with the ring still full.
441 pub fn write_timeout(&mut self, buf: &[u8], timeout: Option<Duration>) -> io::Result<usize> {
442 let (written, opened_the_ring) = self.to_server.write_until(buf, timeout)?;
443 if opened_the_ring {
444 self.server_waker.fire();
445 }
446 Ok(written)
447 }
448}
449
450impl Read for LoopbackClientEnd {
451 /// Blocks without a deadline. Callers that need one use
452 /// [`LoopbackClientEnd::read_timeout`].
453 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
454 self.from_server.read_until(buf, None)
455 }
456}
457
458impl Write for LoopbackClientEnd {
459 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
460 let (written, opened_the_ring) = self.to_server.write(buf)?;
461 if opened_the_ring {
462 self.server_waker.fire();
463 }
464 Ok(written)
465 }
466
467 /// Nothing buffers behind the ring, so a flush has nothing to push.
468 fn flush(&mut self) -> io::Result<()> {
469 Ok(())
470 }
471}
472
473impl Drop for LoopbackClientEnd {
474 fn drop(&mut self) {
475 self.to_server.close_writer();
476 self.from_server.close_reader();
477 // A parked server learns about the hangup the same way it learns about
478 // bytes: it is told. Without this a connection whose client vanished
479 // would sit parked until some unrelated wake happened past it.
480 self.server_waker.fire();
481 }
482}
483
484/// The server half of a loopback duplex: the end a connection process holds.
485///
486/// Both halves are non-blocking, which is what makes this end a drop-in for
487/// the non-blocking `TcpStream` a socket connection owns: reads answer
488/// `WouldBlock` rather than parking, and the end implements
489/// [`InboundPending`] so the pre-park probe can ask it the one transport
490/// question that probe asks. As an [`io::Write`] it is usable directly as the
491/// `&mut dyn Write` sink [`super::super::outbound::OutboundWriter::drain`]
492/// takes.
493#[derive(Debug)]
494pub struct LoopbackServerEnd {
495 /// Bytes this end reads; the client end wrote them.
496 from_client: Arc<Ring>,
497 /// Bytes this end writes; the client end reads them.
498 to_client: Arc<Ring>,
499 /// This end's wake callback, fired by the client's writes and drop.
500 waker: Arc<WakerSlot>,
501}
502
503impl LoopbackServerEnd {
504 /// Registers the callback the client end fires when this end's inbound
505 /// ring goes from empty to non-empty, and when the client end is dropped.
506 ///
507 /// Registration is separate from construction because the callback names
508 /// the connection process this end belongs to, and that process does not
509 /// exist until after the duplex has been built and handed to it. Setting a
510 /// second waker replaces the first; a duplex whose waker is never set
511 /// simply tells nobody.
512 pub fn set_waker(&self, waker: Box<dyn Fn() + Send + Sync>) {
513 self.waker.set(Arc::from(waker));
514 }
515
516 /// Bytes already waiting for this end, consuming none of them.
517 #[must_use]
518 pub fn readable_bytes(&self) -> usize {
519 self.from_client.readable_bytes()
520 }
521}
522
523impl Read for LoopbackServerEnd {
524 /// Never blocks: an empty ring with a live client answers `WouldBlock`, a
525 /// drained ring with a dropped client answers `Ok(0)`.
526 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
527 self.from_client.read(buf)
528 }
529}
530
531impl Write for LoopbackServerEnd {
532 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
533 // No waker on this direction: the client's blocking read parks on the
534 // ring's own condvar, which `Ring::write` has already signalled.
535 let (written, _) = self.to_client.write(buf)?;
536 Ok(written)
537 }
538
539 fn flush(&mut self) -> io::Result<()> {
540 Ok(())
541 }
542}
543
544impl InboundPending for LoopbackServerEnd {
545 /// Answers from the ring's own bytes, and never fails.
546 ///
547 /// A hung-up client reports PENDING, not idle — the same answer a socket
548 /// gives, whose `peek` on a closed connection returns `Ok(0)` and lands on
549 /// the probe's `Ok(_) => Ok(true)` arm. The pending work in that case is
550 /// the end of file itself: a connection that parked on a dead peer would
551 /// never learn it was dead.
552 fn inbound_pending(&self) -> Result<bool, ServerError> {
553 Ok(self.from_client.read_would_answer())
554 }
555}
556
557impl Drop for LoopbackServerEnd {
558 fn drop(&mut self) {
559 self.to_client.close_writer();
560 self.from_client.close_reader();
561 }
562}