featherbit/context/stream.rs
1//! The out-of-band streaming response body.
2//!
3//! A streaming body cannot live on `Context` as an ordinary field: `Context`
4//! is `Serialize`/`Deserialize` because it marshals into Lua scripts and into
5//! debug trace snapshots. The handle is therefore `#[serde(skip)]`, the same
6//! way the WebSocket upgrade handle is carried out-of-band by the listener.
7
8use bytes::Bytes;
9use http_body_util::combinators::BoxBody;
10
11use crate::outbound::BoxError;
12
13/// An upstream response body being relayed to the client unbuffered, plus any
14/// guards whose lifetime must match the stream rather than the node that
15/// produced it (balancer in-flight counters, `limit-conn` permits).
16pub struct ResponseStream {
17 // Read back out by `into_parts`, called by the listener's
18 // `build_response` once it decides to relay a stream instead of
19 // buffering it.
20 // `BoxBody` is already `Send + Sync` (see `http_body_util`'s
21 // `combinators::BoxBody`, as opposed to the `Send`-only
22 // `UnsyncBoxBody`), so it needs no help to keep `ResponseStream: Sync`.
23 body: BoxBody<Bytes, BoxError>,
24 // `Box<dyn Send + 'static>` guards are `Send`-only, not `Sync`, so a bare
25 // `Vec` here would make `ResponseStream` — and, nested inside
26 // `GatewayResponse`, `Context` — lose `Sync`. Several existing plugins
27 // hold `&Context` across an `.await`, which requires `Context: Sync` for
28 // their `execute` future to stay `Send`. `Mutex<T>` is `Sync` whenever
29 // `T: Send`, restoring `Sync` without `unsafe`. Neither `hold` nor
30 // `into_parts` below actually contends the lock: both already have
31 // exclusive access (`&mut self` / `self`), so this costs nothing at
32 // runtime.
33 guards: std::sync::Mutex<Vec<Box<dyn Send + 'static>>>,
34}
35
36impl ResponseStream {
37 pub fn new(body: BoxBody<Bytes, BoxError>) -> Self {
38 Self {
39 body,
40 guards: std::sync::Mutex::new(Vec::new()),
41 }
42 }
43
44 /// Attaches a guard released when the stream is consumed or dropped.
45 /// Production code binds guards in at construction instead (see
46 /// `upstream.rs`'s use of `body_holding`); this is exercised directly by
47 /// this module's own tests.
48 #[allow(dead_code)]
49 pub fn hold(&mut self, guard: Box<dyn Send + 'static>) {
50 // `&mut self` already guarantees exclusive access; this never blocks.
51 self.guards.get_mut().unwrap().push(guard);
52 }
53
54 /// Takes the body for transmission. The guards travel with the returned
55 /// `Vec`, not with `self` (which is consumed here) — the caller must keep
56 /// that `Vec` alive until the returned body has finished streaming to the
57 /// client; dropping it early releases the guards early.
58 pub fn into_parts(self) -> (BoxBody<Bytes, BoxError>, Vec<Box<dyn Send + 'static>>) {
59 // `self` is owned here, so nothing else can hold the lock; never blocks.
60 (self.body, self.guards.into_inner().unwrap())
61 }
62}
63
64// `Box<dyn Send>` has no Debug; the struct is only ever shown as a marker.
65impl std::fmt::Debug for ResponseStream {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("ResponseStream")
68 .field("guards", &self.guards.lock().map(|g| g.len()).unwrap_or(0))
69 .finish()
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use bytes::Bytes;
77 use http_body_util::{BodyExt, Full};
78
79 fn boxed(text: &str) -> BoxBody<Bytes, BoxError> {
80 Full::new(Bytes::from(text.to_owned()))
81 .map_err(|never| match never {})
82 .boxed()
83 }
84
85 /// The stream handle must carry arbitrary guards (balancer in-flight,
86 /// limit-conn) so they release when the stream ends, not when the node
87 /// that created it returns.
88 #[tokio::test]
89 async fn test_stream_holds_guards_until_dropped() {
90 use std::sync::atomic::{AtomicUsize, Ordering};
91 use std::sync::Arc;
92
93 struct Guard(Arc<AtomicUsize>);
94 impl Drop for Guard {
95 fn drop(&mut self) {
96 self.0.fetch_add(1, Ordering::SeqCst);
97 }
98 }
99
100 let dropped = Arc::new(AtomicUsize::new(0));
101 let mut stream = ResponseStream::new(boxed("data: hello\n\n"));
102 stream.hold(Box::new(Guard(dropped.clone())));
103
104 assert_eq!(
105 dropped.load(Ordering::SeqCst),
106 0,
107 "guard released too early"
108 );
109 drop(stream);
110 assert_eq!(
111 dropped.load(Ordering::SeqCst),
112 1,
113 "guard not released on drop"
114 );
115 }
116
117 /// `into_parts` is the accessor Task 6 depends on: it must hand the
118 /// guards over alongside the body rather than dropping them, and the
119 /// guards' lifetime must be tied to the returned `Vec`, not to the body
120 /// or to `self` (which no longer exists once `into_parts` returns).
121 #[tokio::test]
122 async fn test_into_parts_hands_guards_to_the_caller_with_the_body() {
123 use std::sync::atomic::{AtomicUsize, Ordering};
124 use std::sync::Arc;
125
126 struct Guard(Arc<AtomicUsize>);
127 impl Drop for Guard {
128 fn drop(&mut self) {
129 self.0.fetch_add(1, Ordering::SeqCst);
130 }
131 }
132
133 let dropped = Arc::new(AtomicUsize::new(0));
134 let mut stream = ResponseStream::new(boxed("data: hello\n\n"));
135 stream.hold(Box::new(Guard(dropped.clone())));
136
137 let (body, guards) = stream.into_parts();
138
139 drop(body);
140 assert_eq!(
141 dropped.load(Ordering::SeqCst),
142 0,
143 "guard released when the body was dropped, before the guard vec was"
144 );
145
146 drop(guards);
147 assert_eq!(
148 dropped.load(Ordering::SeqCst),
149 1,
150 "guard not released when the returned guard vec was dropped"
151 );
152 }
153
154 /// `Context` gained a nested `Send`-only member (`ResponseStream`'s
155 /// `BoxBody` and guards) in this change; pin down that it stays both
156 /// `Send` (required for `Plugin::execute`'s boxed future) and `Sync`
157 /// (required because several plugins hold `&Context` across an
158 /// `.await`), so a future change can't silently regress either.
159 #[test]
160 fn test_context_stays_send_and_sync() {
161 fn assert_send_sync<T: Send + Sync>() {}
162 assert_send_sync::<crate::context::Context>();
163 }
164}