Skip to main content

sozu_lib/protocol/mux/
buffer_source.rs

1//! Where the mux core gets memory, and the one way it is allowed to fail.
2//!
3//! The core does not allocate for itself and does not name
4//! [`crate::pool::Pool`]. Every buffer it needs — a connection's stream-0
5//! landing zone, a stream's request/response pair, the HPACK scratch — comes
6//! from a caller-supplied [`BufferSource`], which the caller implements and
7//! which **may refuse**. That is the whole contract, and both halves matter:
8//!
9//! - *Caller-supplied* is what lets something other than the real pool stand
10//!   behind it. A deterministic simulator that wants to drive the core into
11//!   memory pressure has somewhere to stand, instead of having to arrange the
12//!   pressure through a global allocator it does not control.
13//! - *May refuse* is what keeps the existing behaviour expressible. Refusal
14//!   is **not** an error condition here. A source that answers `None`
15//!   mid-request is reporting a transient shortage, and the caller's
16//!   obligation is to degrade the smallest unit it can: one stream is refused
17//!   with `RST_STREAM(REFUSED_STREAM)` and the connection carries on, because
18//!   the streams already running will hand their buffers back. Escalating a
19//!   shortage to a connection error would convert it into the loss of every
20//!   in-flight request that would have survived it.
21//!
22//! Neither half is new behaviour. Both checkout sites were already
23//! `Option`-fallible with a `REFUSED_STREAM`-shaped fallback; what was missing
24//! was a name for the thing being called, so the core reached for a concrete
25//! `Weak<RefCell<Pool>>` instead. This module supplies the name and
26//! [`PoolBufferSource`] supplies the one implementation the proxy itself uses,
27//! which does exactly what the direct reach did.
28//!
29//! ## What is under the contract, and what is not
30//!
31//! Under it: the connection's own `zero` buffer, a stream's request/response
32//! buffer pair, and the HPACK codec pair together with the three reusable
33//! scratch buffers beside them (see `HpackState`, `hpack_state.rs`).
34//!
35//! Not under it, deliberately:
36//!
37//! - **Growth of the HPACK codec's own buffers.** The dynamic tables of the
38//!   `crate::protocol::mux::hpack` pair and the decoder's Huffman scratch
39//!   buffer start empty and grow to a high-water mark bounded by the RFC 7541
40//!   §4.2 table-size settings and the longest decoded string. They are brought
41//!   under the contract at *acquisition* — the pair is built inside a fallible
42//!   `HpackState::new` — and their growth after acquisition reaches the global
43//!   allocator, like a scratch buffer's below.
44//! - **Growth of an already-acquired scratch buffer.** `scratch` hands over an
45//!   empty `Vec` that the core then grows and reclaims on its own schedule.
46//!   Pre-sizing one and refusing to grow it is a behaviour change — it needs
47//!   an answer for what a header block larger than the reservation does — and
48//!   is not made here.
49//! - **`H2Scheduler`'s pass-order buffer (`h2_scheduler.rs`).** It holds
50//!   stream ids rather than octets, and it is already bounded by the
51//!   concurrent-stream admission gate that decides how many ids can exist.
52
53use std::{cell::RefCell, rc::Weak};
54
55use crate::pool::{Checkout, Pool};
56
57/// The caller-supplied origin of every buffer the mux core uses.
58///
59/// Implementors may refuse either request at any time, including in the
60/// middle of a request. See the module documentation for what a refusal
61/// obliges the caller to do — in short, degrade one stream, never the
62/// connection.
63pub trait BufferSource {
64    /// A fixed-size buffer for wire octets: a connection's stream-0 landing
65    /// zone, or one half of a stream's request/response pair.
66    ///
67    /// `None` means the source is momentarily out. It is not a permanent
68    /// condition and must not be treated as one.
69    fn checkout(&mut self) -> Option<Checkout>;
70
71    /// A growable scratch buffer the core keeps for the lifetime of the
72    /// connection and reuses across passes, rather than one it hands back
73    /// per call.
74    ///
75    /// `None` refuses the construction that asked for it, which propagates as
76    /// a connection that is never built — the same outcome, on the same path,
77    /// as [`Self::checkout`] refusing the connection's own buffer.
78    fn scratch(&mut self) -> Option<Vec<u8>>;
79}
80
81/// The [`BufferSource`] the proxy itself runs on: the worker's shared
82/// [`Pool`], reached through the same weak handle the core used to hold
83/// directly.
84///
85/// Every method is the expression that used to sit at the call site, moved
86/// here unchanged — an upgrade-then-checkout for [`BufferSource::checkout`],
87/// and a plain empty `Vec` for [`BufferSource::scratch`]. The pool has no
88/// scratch arena to draw the latter from, and inventing one would be a
89/// behaviour change rather than the relocation this is.
90#[derive(Clone)]
91pub struct PoolBufferSource {
92    pool: Weak<RefCell<Pool>>,
93}
94
95impl PoolBufferSource {
96    pub fn new(pool: Weak<RefCell<Pool>>) -> Self {
97        PoolBufferSource { pool }
98    }
99}
100
101impl BufferSource for PoolBufferSource {
102    /// A dropped pool is indistinguishable from an exhausted one here, and
103    /// deliberately so: both mean "no buffer now", and the caller's answer to
104    /// either is the same refusal. This is the `upgrade()?` that
105    /// `Stream::new` and `ConnectionH2::new` each spelled out.
106    fn checkout(&mut self) -> Option<Checkout> {
107        self.pool.upgrade()?.borrow_mut().checkout()
108    }
109
110    fn scratch(&mut self) -> Option<Vec<u8>> {
111        Some(Vec::new())
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use std::rc::Rc;
118
119    use super::*;
120
121    /// The pool's ceiling is the source's ceiling, and reaching it produces a
122    /// refusal rather than a panic or a block. This is the property the H2
123    /// core's `REFUSED_STREAM`-on-exhaustion path stands on.
124    #[test]
125    fn a_pool_source_refuses_once_its_pool_is_exhausted() {
126        let pool = Rc::new(RefCell::new(Pool::with_capacity(1, 2, 4096)));
127        let mut source = PoolBufferSource::new(Rc::downgrade(&pool));
128
129        let first = source
130            .checkout()
131            .expect("a fresh pool yields its first buffer");
132        let second = source
133            .checkout()
134            .expect("a pool with a maximum of 2 yields a second");
135        assert!(
136            source.checkout().is_none(),
137            "a pool at its maximum must refuse rather than exceed it",
138        );
139
140        // Refusal is transient: returning a buffer makes the source answer
141        // again. A caller that treated `None` as terminal would strand a
142        // connection that was about to recover.
143        drop(first);
144        assert!(
145            source.checkout().is_some(),
146            "a returned buffer must make the source answer again",
147        );
148        drop(second);
149    }
150
151    /// A source whose pool has been dropped refuses in the same shape it
152    /// refuses exhaustion, so no caller needs a second failure path.
153    #[test]
154    fn a_pool_source_refuses_once_its_pool_is_gone() {
155        let pool = Rc::new(RefCell::new(Pool::with_capacity(1, 1, 4096)));
156        let mut source = PoolBufferSource::new(Rc::downgrade(&pool));
157        drop(pool);
158
159        assert!(
160            source.checkout().is_none(),
161            "a dropped pool must refuse, not panic",
162        );
163    }
164}