Skip to main content

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}