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}