Skip to main content

melin_server_runtime/
early_reply.rs

1//! The answers a reader decides on without publishing the request, on
2//! their way to the response stage.
3//!
4//! # Early replies
5//!
6//! Four answers need nothing from the pipeline: a write refused while the
7//! node is halted (the application's rejection, see [`crate::halt`]), a
8//! request the application's decoder filters (an empty batch) or refuses
9//! (a `Refused` frame naming why), and a request shed because the input
10//! ring is full (`ServerBusy`). All of them reach the response stage
11//! through one queue, from the reader (or the DPDK poll thread) that
12//! decided them, and every one names its request's ID.
13//!
14//! # Released between replies, in no order
15//!
16//! Every final frame names its request, so an early reply does not wait
17//! for the replies to requests sent before it: a refusal goes out while
18//! a write ahead of it is still held by the ack policy. The one rule left
19//! is that it never splits another request's reply, so the response stage
20//! sends early replies only between two events' replies, which a
21//! `BatchBoundary` tracker vouches for with an `AtBoundary` token (both
22//! crate-internal, in `batch_boundary.rs`).
23//!
24//! The response stage works from a snapshot of the queue: it takes one,
25//! then drains its control channel, and only then sends what the snapshot
26//! holds (`EarlyReplyQueue::register`). In that order because an early
27//! reply must never reach the stage ahead of its connection's
28//! registration: the reader answers a connection only once its
29//! `Connected` is queued, so a snapshot taken before the drain holds no
30//! reply for a connection the drain does not register. The stage takes a
31//! snapshot once per iteration, after reading the output ring, and again
32//! on each turn of a wait on the durability gate that holds an event's
33//! first slot. One per iteration is all the steady state pays: no poll per
34//! event.
35//!
36//! # Counted
37//!
38//! An early reply's request leaves no other trace — nothing is journaled
39//! — so the reader counts each kind once per receive, once its answer is
40//! queued (a request parked on is counted when it is finally answered),
41//! and the health endpoint exports the counts: halt refusals through
42//! [`HaltGate::record_refused`](crate::halt::HaltGate::record_refused) as
43//! `melin_writes_refused_total`; shed, filtered and refused requests
44//! through [`IngressMetrics`](melin_transport_core::health::IngressMetrics)
45//! as `melin_requests_shed_total`, `melin_requests_filtered_total` and
46//! `melin_requests_refused_total` (by kind), beside the connections
47//! parked on a full queue.
48
49use melin_pipeline::spsc;
50use melin_pipeline::wait::WaitStrategy;
51use melin_transport_core::pipeline::RequestId;
52use melin_wire_protocol::control::RefusalKind;
53
54use crate::batch_boundary::AtBoundary;
55
56/// Early replies in flight from a reader to the response stage before the
57/// reader can queue no more: the capacity a node gives
58/// [`early_reply_channel`].
59///
60/// The response stage drains the queue between every two events' replies,
61/// and while the ack policy holds an event's first slot, so it fills only
62/// if the stage stops draining (an event whose slots it is waiting on, or
63/// a stage that is not running), not because a write is held. Past it the
64/// reader parks the connection whose answer has nowhere to go, and stops
65/// reading it until the queue has room. A power of two, as the ring
66/// requires; each entry is the application's rejection, or a refusal's
67/// reason, plus a few words.
68pub const EARLY_REPLY_QUEUE_CAPACITY: usize = 4096;
69
70/// An answer a reader decided on without publishing the request, on its
71/// way to the response stage.
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub struct EarlyReply<R> {
74    /// Connection the request arrived on.
75    pub connection_id: u64,
76    /// The client's ID for the request, echoed on the reply's final frame.
77    pub request_id: RequestId,
78    /// What the answer is.
79    pub kind: EarlyKind<R>,
80}
81
82/// What an [`EarlyReply`] answers, and so the frames it is sent as.
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub enum EarlyKind<R> {
85    /// A write refused while the node is halted: the application's
86    /// rejection, then `BatchEnd`.
87    Rejected(R),
88    /// A request the application's decoder filters: an empty batch, a
89    /// `BatchEnd` alone.
90    Filtered,
91    /// A request the application's decoder refuses: a `Refused` frame with
92    /// the decoder's reason.
93    Refused {
94        kind: RefusalKind,
95        reason: &'static str,
96    },
97    /// A request shed because the input ring is full: `ServerBusy`.
98    Busy,
99}
100
101/// Creates the queue a reader hands early replies to the response stage
102/// through, holding at most `capacity` replies in flight: a node passes
103/// [`EARLY_REPLY_QUEUE_CAPACITY`]; a test that wants the queue to fill
104/// passes less.
105///
106/// # Panics
107///
108/// If `capacity` is not a power of two of at least 2, as the ring
109/// requires.
110pub fn early_reply_channel<R: Copy>(capacity: usize) -> (EarlyReplySender<R>, EarlyReplyQueue<R>) {
111    // `Option` because the ring wants `Default` slots and an application's
112    // report need not have one. The wait strategy is unused: the reader
113    // never blocks on this queue (see `EarlyReplySender::try_send`).
114    let (tx, rx) = spsc::channel::<Option<EarlyReply<R>>>(capacity, WaitStrategy::SpinThenYield);
115    (EarlyReplySender { tx }, EarlyReplyQueue { rx })
116}
117
118/// The reader's end of the early-reply queue.
119pub struct EarlyReplySender<R> {
120    tx: spsc::Producer<Option<EarlyReply<R>>>,
121}
122
123impl<R: Copy> EarlyReplySender<R> {
124    /// Queue an early reply, invisible to the response stage until
125    /// [`flush`](Self::flush). Never blocks: a full queue is `Err`, and the
126    /// caller parks the connection whose answer has nowhere to go.
127    pub(crate) fn try_send(&mut self, reply: EarlyReply<R>) -> Result<(), spsc::Full> {
128        self.tx
129            .try_push_with(|slot| *slot = Some(reply))
130            .map(|_| ())
131    }
132
133    /// Make every queued early reply visible to the response stage. The
134    /// reader calls it once on every way out of processing a receive
135    /// (`process_client_frames`), so no reply it decided is left unseen;
136    /// where that falls relative to the input ring's commit does not
137    /// matter, since an early reply waits for nothing published.
138    pub(crate) fn flush(&mut self) {
139        self.tx.flush();
140    }
141}
142
143/// The response stage's end of the early-reply queue.
144///
145/// Public only so it can be handed to a response stage: it has no method
146/// outside the crate, so nothing else can read it. Read only through the
147/// crate's `register`, which takes a snapshot of
148/// what the reader has made visible and then drains the stage's control
149/// channel, and hands back the token that sends the snapshot's replies.
150/// Those made visible since wait for the next snapshot.
151pub struct EarlyReplyQueue<R> {
152    rx: spsc::Consumer<Option<EarlyReply<R>>>,
153}
154
155impl<R: Copy> EarlyReplyQueue<R> {
156    /// Take a snapshot of the early replies the reader has made visible,
157    /// then run `drain_control`, the stage's drain of its control channel,
158    /// and return the token that sends the snapshot's replies. One load of
159    /// the queue's cursor.
160    ///
161    /// The one place the two happen, in that order, so a reply sent
162    /// through the token always finds its connection registered: the
163    /// reader answers a connection only once its `Connected` is queued,
164    /// so every reply in a snapshot taken before the drain belongs to a
165    /// connection the drain registers.
166    #[inline]
167    pub(crate) fn register(&mut self, drain_control: impl FnOnce()) -> Registered<'_, R> {
168        let mut registered = Registered { queue: self };
169        registered.reregister(drain_control);
170        registered
171    }
172}
173
174/// A snapshot of the early-reply queue taken before the response stage's
175/// control channel was drained (see [`EarlyReplyQueue::register`]), and so
176/// the right to send what it holds. Borrows the queue: nothing reads it
177/// past the token's back.
178pub(crate) struct Registered<'q, R> {
179    queue: &'q mut EarlyReplyQueue<R>,
180}
181
182impl<R: Copy> Registered<'_, R> {
183    /// Take a fresh snapshot, then run `drain_control`, as
184    /// [`EarlyReplyQueue::register`] does: for a stage that keeps the token
185    /// across a wait (on the durability gate) and wants what the reader
186    /// decided meanwhile.
187    #[inline]
188    pub(crate) fn reregister(&mut self, drain_control: impl FnOnce()) {
189        self.queue.rx.refresh();
190        drain_control();
191    }
192
193    /// Hand `emit` every early reply in the snapshot, oldest first, and
194    /// return how many.
195    ///
196    /// `AtBoundary` is the stage's word that it is between two events'
197    /// replies: an early reply sent anywhere else would split one.
198    #[inline]
199    pub(crate) fn drain(
200        &mut self,
201        _between_events: &AtBoundary<'_>,
202        mut emit: impl FnMut(EarlyReply<R>),
203    ) -> usize {
204        let mut sent = 0;
205        // Only `try_send` writes the ring, and it never writes `None`.
206        // `try_consume_visible` never reads the queue's cursor, so this
207        // cannot see past the snapshot.
208        while let Some((_, Some(reply))) = self.queue.rx.try_consume_visible() {
209            emit(reply);
210            sent += 1;
211        }
212        sent
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::batch_boundary::BatchBoundary;
220
221    fn refusal(connection_id: u64) -> EarlyReply<u64> {
222        EarlyReply {
223            connection_id,
224            request_id: RequestId(connection_id * 10),
225            kind: EarlyKind::Rejected(connection_id * 100),
226        }
227    }
228
229    /// Every early reply `registered` holds, by connection, drained
230    /// between two events.
231    fn drained(registered: &mut Registered<'_, u64>) -> Vec<u64> {
232        let boundary = BatchBoundary::new();
233        let at = boundary
234            .at_boundary()
235            .expect("a fresh tracker is between events");
236        let mut out = Vec::new();
237        let count = registered.drain(&at, |reply| out.push(reply.connection_id));
238        assert_eq!(count, out.len(), "the count is of what was handed over");
239        out
240    }
241
242    /// A reply is seen only once the reader has flushed it and a snapshot
243    /// has been taken since; a snapshot drained is not drained twice.
244    #[test]
245    fn a_refusal_waits_for_both_the_flush_and_a_snapshot() {
246        let (mut tx, mut rx) = early_reply_channel::<u64>(EARLY_REPLY_QUEUE_CAPACITY);
247        tx.try_send(refusal(1)).unwrap();
248        let mut registered = rx.register(|| {});
249        assert!(drained(&mut registered).is_empty(), "not flushed");
250
251        tx.flush();
252        assert!(
253            drained(&mut registered).is_empty(),
254            "flushed, but after the snapshot"
255        );
256
257        registered.reregister(|| {});
258        assert_eq!(drained(&mut registered), [1]);
259        assert!(drained(&mut registered).is_empty(), "drained once");
260        registered.reregister(|| {});
261        assert!(drained(&mut registered).is_empty(), "the queue is empty");
262    }
263
264    /// The snapshot is taken before the control channel is drained, so a
265    /// reply made visible during the drain (a reply for a connection whose
266    /// `Connected` the drain may already be past) waits for the next
267    /// snapshot, which a drain follows again. On both ways to take one.
268    #[test]
269    fn a_snapshot_is_taken_before_the_control_drain() {
270        let (mut tx, mut rx) = early_reply_channel::<u64>(EARLY_REPLY_QUEUE_CAPACITY);
271        tx.try_send(refusal(1)).unwrap();
272        tx.flush();
273        let mut drains = 0;
274        let mut registered = rx.register(|| {
275            drains += 1;
276            tx.try_send(refusal(2)).unwrap();
277            tx.flush();
278        });
279        assert_eq!(drains, 1, "registering drains the control channel once");
280        assert_eq!(drained(&mut registered), [1], "the snapshot predates 2");
281
282        registered.reregister(|| {
283            drains += 1;
284            tx.try_send(refusal(3)).unwrap();
285            tx.flush();
286        });
287        assert_eq!(drains, 2);
288        assert_eq!(drained(&mut registered), [2], "the snapshot predates 3");
289
290        let mut registered = rx.register(|| drains += 1);
291        assert_eq!(drained(&mut registered), [3]);
292        assert_eq!(drains, 3);
293    }
294
295    #[test]
296    fn a_full_queue_is_an_error_not_a_wait() {
297        let (mut tx, _rx) = early_reply_channel::<u64>(EARLY_REPLY_QUEUE_CAPACITY);
298        for _ in 0..EARLY_REPLY_QUEUE_CAPACITY {
299            tx.try_send(refusal(1)).unwrap();
300        }
301        assert!(tx.try_send(refusal(1)).is_err());
302    }
303
304    /// The capacity is the caller's: a queue built smaller fills at its
305    /// own size.
306    #[test]
307    fn a_queue_holds_the_capacity_it_was_built_with() {
308        for capacity in [2, 8] {
309            let (mut tx, _rx) = early_reply_channel::<u64>(capacity);
310            for _ in 0..capacity {
311                tx.try_send(refusal(1)).unwrap();
312            }
313            assert!(tx.try_send(refusal(1)).is_err(), "{capacity}");
314        }
315    }
316
317    /// A drained queue has room again: capacity bounds replies in flight,
318    /// not replies over the queue's life.
319    #[test]
320    fn a_drained_queue_takes_more() {
321        let (mut tx, mut rx) = early_reply_channel::<u64>(EARLY_REPLY_QUEUE_CAPACITY);
322        for round in 0..3u64 {
323            for _ in 0..EARLY_REPLY_QUEUE_CAPACITY {
324                tx.try_send(refusal(round)).unwrap();
325            }
326            tx.flush();
327            let mut registered = rx.register(|| {});
328            assert_eq!(
329                drained(&mut registered).len(),
330                EARLY_REPLY_QUEUE_CAPACITY,
331                "round {round}"
332            );
333        }
334    }
335
336    /// Every kind of early reply goes through the queue as it was sent,
337    /// with its request's ID, in order among the others.
338    #[test]
339    fn every_kind_travels_with_its_request_id() {
340        let (mut tx, mut rx) = early_reply_channel::<u64>(EARLY_REPLY_QUEUE_CAPACITY);
341        let kinds = [
342            EarlyKind::Rejected(7),
343            EarlyKind::Filtered,
344            EarlyKind::Refused {
345                kind: RefusalKind::Malformed,
346                reason: "bad",
347            },
348            EarlyKind::Busy,
349        ];
350        for (n, kind) in (0u64..).zip(kinds) {
351            tx.try_send(EarlyReply {
352                connection_id: 1,
353                request_id: RequestId(100 + n),
354                kind,
355            })
356            .unwrap();
357        }
358        tx.flush();
359        let boundary = BatchBoundary::new();
360        let at = boundary.at_boundary().expect("between events");
361        let mut released = Vec::new();
362        rx.register(|| {})
363            .drain(&at, |r| released.push((r.request_id, r.kind)));
364        assert_eq!(
365            released,
366            (100..).map(RequestId).zip(kinds).collect::<Vec<_>>()
367        );
368    }
369}