Skip to main content

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}