Skip to main content

proxima_primitives/pipe/
handler.rs

1//! [`Handler`] is the served-HTTP face of the [`SendPipe`] root form: a
2//! `Request<Bytes> -> Response<Bytes>` pipe with `Err` pinned to
3//! [`ProximaError`]. Every upstream, every middleware, every composition
4//! primitive that dispatches HTTP traffic implements it. Listeners dispatch
5//! into a [`PipeHandle`]; pipelines record from one; the daemon hot-swaps
6//! them.
7//!
8//! # Handler is the served boundary
9//!
10//! ```text
11//!        request                                response
12//!           │                                      ▲
13//!           ▼                                      │
14//!        ┌──────────────────────────────────────────┐
15//!        │                Handler                   │
16//!        │  async fn call(Request) -> Result<...>   │
17//!        └──────────────────────────────────────────┘
18//! ```
19//!
20//! `Handler` is a blanket impl over
21//! `SendPipe<In = Request<Bytes>, Out = Response<Bytes>, Err = ProximaError>` —
22//! nothing to implement, any qualifying `SendPipe` already satisfies it.
23//! [`PipeHandle`] is the runtime-dispatch alias for the erased form so
24//! heterogeneous handlers can sit side by side. [`ThreadLocalHandler`] /
25//! [`ThreadLocalPipeHandle`] are the `!Send` sibling for `Rc` / `RefCell`
26//! per-core state.
27//!
28//! # Core primitives
29//!
30//! These are also `Handler`s — they wrap any inner handler and don't care
31//! what it does. [`Diff`](crate::pipe::Diff) runs two handlers in parallel and
32//! reports divergence; [`Isolate`](crate::pipe::Isolate) catches panics and
33//! enforces a time budget; [`SwappablePipe`](crate::pipe::SwappablePipe) hot-swaps
34//! the inner handle at runtime without tearing in-flight calls; the daemon
35//! control plane's `apply` verb rides on it.
36//!
37//! # Recording wraps any Pipe
38//!
39//! Recording is not a separate tracing subsystem — it composes from the
40//! same primitives. `Tee`-shaped wrapping (record a copy of the
41//! request/response while passing through) lives downstream in
42//! `proxima-recording`, built over this crate's [`Handler`] boundary the
43//! same way every other composition primitive is.
44//!
45//! # Serving a Handler
46//!
47//! A `Handler` doesn't serve traffic by itself. What this module owns is the
48//! step before that: a qualifying [`SendPipe`] already IS a `Handler` (the
49//! blanket below), and [`into_handle`] erases it into the [`PipeHandle`] a
50//! listener dispatches to. Wiring that handle to a socket is
51//! `proxima_listen::registry::ServeBuilder`'s job, one layer up — not
52//! linkable from here, and deliberately not shown here as an uncompiled
53//! sketch.
54//!
55//! ```
56//! use bytes::Bytes;
57//! use core::future::Future;
58//! use proxima_core::ProximaError;
59//! use proxima_primitives::pipe::request::{Request, Response};
60//! use proxima_primitives::pipe::{Handler, PipeHandle, SendPipe, into_handle};
61//!
62//! struct Health;
63//!
64//! impl SendPipe for Health {
65//!     type In = Request<Bytes>;
66//!     type Out = Response<Bytes>;
67//!     type Err = ProximaError;
68//!
69//!     fn call(
70//!         &self,
71//!         _request: Request<Bytes>,
72//!     ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
73//!         async { Ok(Response::typed(200, Bytes::from_static(b"ok"))) }
74//!     }
75//! }
76//!
77//! fn assert_handler<H: Handler>(_: &H) {}
78//! assert_handler(&Health);
79//!
80//! let handle: PipeHandle = into_handle(Health);
81//! ```
82
83// every item here touches Box/Rc/Arc unconditionally via alloc_tier;
84// there is no alloc-free subset.
85#![cfg(feature = "alloc")]
86
87use bytes::Bytes;
88use proxima_core::ProximaError;
89
90use crate::pipe::primitives::Pipe;
91use crate::pipe::request::{Request, Response};
92use crate::pipe::{SendPipe, alloc_tier};
93
94/// The served-HTTP face of [`SendPipe`]: `Request<Bytes> -> Response<Bytes>`,
95/// `Err` pinned to [`ProximaError`]. Blanket-implemented for every qualifying
96/// `SendPipe` — nothing to implement directly.
97pub trait Handler:
98    SendPipe<In = Request<Bytes>, Out = Response<Bytes>, Err = ProximaError>
99{
100}
101
102impl<Implementor> Handler for Implementor where
103    Implementor: SendPipe<In = Request<Bytes>, Out = Response<Bytes>, Err = ProximaError>
104{
105}
106
107/// Erased, shareable served handler — the runtime-dispatch alias `Arc<dyn
108/// SendDynPipe<Request<Bytes>, Response<Bytes>>>` so heterogeneous handlers
109/// sit side by side in routing tables and swap cells.
110pub type PipeHandle = alloc_tier::PipeHandle<Request<Bytes>, Response<Bytes>>;
111
112/// Erase a [`Handler`] into a shareable [`PipeHandle`].
113pub fn into_handle<Implementor>(pipe: Implementor) -> PipeHandle
114where
115    Implementor: SendPipe<In = Request<Bytes>, Out = Response<Bytes>, Err = ProximaError> + 'static,
116{
117    alloc_tier::into_handle(pipe)
118}
119
120/// Per-thread sibling of [`Handler`] for runtimes that pin work to a single
121/// core (DPDK, per-core executors) and may hold `Rc` / `RefCell` state.
122/// Blanket-implemented over the no-Send root [`Pipe`].
123pub trait ThreadLocalHandler:
124    Pipe<In = Request<Bytes>, Out = Response<Bytes>, Err = ProximaError>
125{
126}
127
128impl<Implementor> ThreadLocalHandler for Implementor where
129    Implementor: Pipe<In = Request<Bytes>, Out = Response<Bytes>, Err = ProximaError>
130{
131}
132
133/// Erased, shareable per-thread handler — the `Rc`-backed sibling of
134/// [`PipeHandle`].
135pub type ThreadLocalPipeHandle = alloc_tier::LocalPipeHandle<Request<Bytes>, Response<Bytes>>;
136
137/// Erase a [`ThreadLocalHandler`] into a shareable [`ThreadLocalPipeHandle`].
138pub fn into_thread_local_handle<Implementor>(pipe: Implementor) -> ThreadLocalPipeHandle
139where
140    Implementor: Pipe<In = Request<Bytes>, Out = Response<Bytes>, Err = ProximaError> + 'static,
141{
142    alloc_tier::into_local_handle(pipe)
143}
144
145// `#[proxima::test]` and inline `tokio::spawn` pull in the `proxima` /
146// `tokio` dev-dependencies, which the loom build keeps out of the graph
147// (see `[target.'cfg(not(loom))'.dev-dependencies]` in Cargo.toml); these
148// tests are unrelated to the Notify/watch loom protocol.
149#[cfg(all(test, not(loom)))]
150#[allow(clippy::unwrap_used, clippy::expect_used)]
151mod tests {
152    use super::*;
153    use core::future::Future;
154
155    struct EchoPipe {
156        label: alloc::string::String,
157    }
158
159    impl SendPipe for EchoPipe {
160        type In = Request<Bytes>;
161        type Out = Response<Bytes>;
162        type Err = ProximaError;
163
164        fn call(
165            &self,
166            request: Request<Bytes>,
167        ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
168            let label = self.label.clone();
169            async move {
170                let (_, bytes) = request.body_bytes().await?;
171                Ok(Response::ok(bytes).with_header("x-pipe", label))
172            }
173        }
174    }
175
176    impl Pipe for EchoPipe {
177        type In = Request<Bytes>;
178        type Out = Response<Bytes>;
179        type Err = ProximaError;
180
181        fn call(
182            &self,
183            request: Request<Bytes>,
184        ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> {
185            SendPipe::call(self, request)
186        }
187    }
188
189    #[proxima::test]
190    async fn dyn_handle_dispatches_to_inner() {
191        let inner: PipeHandle = into_handle(EchoPipe {
192            label: "echo".into(),
193        });
194        let request = Request::builder()
195            .method("POST")
196            .path("/")
197            .body("ping")
198            .build()
199            .expect("builder");
200        let response = SendPipe::call(&inner, request).await.expect("call");
201        let body = response.payload;
202        assert_eq!(&body[..], b"ping");
203    }
204
205    // proves the Handler::call future is Send: tokio::spawn requires Send
206    // and the test would fail to compile otherwise. guards against an
207    // accidental ?Send weakening of the trait surface.
208    #[proxima::test(flavor = "multi_thread", worker_threads = 2)]
209    async fn pipe_call_future_is_send_for_tokio_spawn() {
210        let handle: PipeHandle = into_handle(EchoPipe {
211            label: "spawnable".into(),
212        });
213        let request = Request::builder()
214            .method("POST")
215            .path("/")
216            .body("ok")
217            .build()
218            .expect("builder");
219        let join = tokio::spawn(async move { SendPipe::call(&handle, request).await });
220        let response = join.await.expect("join").expect("call");
221        let body = response.payload;
222        assert_eq!(&body[..], b"ok");
223    }
224
225    use alloc::rc::Rc as TestRc;
226    use core::cell::RefCell;
227
228    struct CounterPipe {
229        // Rc<RefCell<_>> deliberately disqualifies this from Handler: not Send.
230        count: TestRc<RefCell<usize>>,
231    }
232
233    impl Pipe for CounterPipe {
234        type In = Request<Bytes>;
235        type Out = Response<Bytes>;
236        type Err = ProximaError;
237
238        fn call(
239            &self,
240            _request: Request<Bytes>,
241        ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> {
242            let count = self.count.clone();
243            async move {
244                *count.borrow_mut() += 1;
245                Ok(Response::ok("counted"))
246            }
247        }
248    }
249
250    // `Rc<RefCell<_>>` state is deliberately `!Send`, so this cannot ride
251    // the `#[proxima::test]` harness (its `run` bounds the body `Send` under
252    // the prime driver) — a plain synchronous executor sidesteps that bound
253    // without pulling tokio in.
254    #[test]
255    fn thread_local_pipe_runs_with_rc_state() {
256        futures::executor::block_on(async {
257            let count: TestRc<RefCell<usize>> = TestRc::new(RefCell::new(0));
258            let pipe = CounterPipe {
259                count: count.clone(),
260            };
261            let handle: ThreadLocalPipeHandle = into_thread_local_handle(pipe);
262            for _ in 0..3 {
263                let request = Request::builder()
264                    .method("GET")
265                    .path("/")
266                    .build()
267                    .expect("builder");
268                let _ = Pipe::call(&handle, request).await.expect("call");
269            }
270            assert_eq!(*count.borrow(), 3);
271        });
272    }
273
274    #[proxima::test]
275    async fn blanket_lifts_a_send_pipe_into_thread_local() {
276        // EchoPipe impls SendPipe with the served In/Out/Err shape; the
277        // blanket auto-derives Handler. Dispatching a Handler-erased handle
278        // must call into the same impl.
279        let pipe = EchoPipe {
280            label: "blanket".into(),
281        };
282        let request = Request::builder()
283            .method("POST")
284            .path("/")
285            .body("via-blanket")
286            .build()
287            .expect("builder");
288        let response = SendPipe::call(&pipe, request).await.expect("call");
289        let body = response.payload;
290        assert_eq!(&body[..], b"via-blanket");
291    }
292}