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}