Skip to main content

melin_server_runtime/
halt.rs

1//! Refusing client writes while a node cannot honour durability.
2//!
3//! A node halts when replication is configured and no replica is
4//! connected: it can no longer promise what the ack policy promises, so it
5//! takes no new writes. Each is answered with the application's rejection
6//! for [`RejectReason::ReplicaDisconnected`]; queries still run. A node a
7//! newer primary has fenced is stopping and answers nothing: a connection
8//! that sends anything while it winds down is closed, and the client
9//! reconnects to the new primary.
10//!
11//! # Refused at ingress
12//!
13//! The readers decide, before a request is published. A refused write
14//! never enters the input ring, so the journal never records it, replicas
15//! never see it, and replay agrees with the live engine. Refusing any later
16//! cannot have that property: the journal stage reads the input ring in
17//! parallel with the matching stage, so a write the matching stage refuses
18//! is already on its way to disk, and replay would apply what the live
19//! engine turned down.
20//!
21//! A write published just before the halt was observed is applied like any
22//! other, and its reply waits on the ack policy — sent once the policy is
23//! met again, never if the node is superseded or stopped first.
24//!
25//! Only client frames pass the gate. Events the runtime publishes itself
26//! — the seed set, a promotion marker — go straight to the input ring and
27//! are applied whether or not a replica is attached: they have no client
28//! to answer, and a primary that seeds before its first replica connects
29//! (the DPDK start-up order) must not lose its seeds to the halt. A
30//! request refused for the halt never reached the application, so a
31//! client may resend it as it was once the node takes writes again.
32//!
33//! # Delivered in order
34//!
35//! Replies carry no request identifier: a client pairs them with its
36//! requests by order. A refusal can therefore not go out as soon as it is
37//! decided — the connection may still be waiting on replies to requests it
38//! sent before, held by the ack policy. The reader stamps the refusal with
39//! the input-ring sequence the request would have taken, and the response
40//! stage sends it once every event published before that sequence has been
41//! answered ([`RefusalQueue::release`]). The reader makes each refusal
42//! visible before it commits anything published after it, so the response
43//! stage cannot reach a later event without seeing the refusal first.
44//!
45//! The response stage works from a snapshot of the queue, taken once per
46//! iteration between reading the output ring and draining its control
47//! channel ([`RefusalQueue::sync`]). Before the drain, because a refusal
48//! must never reach the stage ahead of its connection's registration;
49//! after the read, because that is when every refusal due before an event
50//! in the batch is already visible. One snapshot per iteration is also
51//! all the steady state pays: no poll per event.
52//!
53//! # Counted
54//!
55//! A refused write leaves no other trace — nothing is journaled — so the
56//! reader counts them ([`HaltGate::record_refused`]) and the health
57//! endpoint exports the count as `melin_writes_refused_total`. A write
58//! shed for a full refusal queue is counted too: the halt is why it was
59//! turned away, whatever the client was told.
60
61use std::sync::Arc;
62use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
63
64use melin_app::RejectReason;
65use melin_pipeline::padding::Sequence;
66use melin_pipeline::spsc;
67use melin_pipeline::wait::WaitStrategy;
68use melin_transport_core::fence::FenceState;
69
70/// Refusals the response stage can hold before a reader has to shed load.
71///
72/// Refusals only queue while the response stage is held up behind a reply
73/// the ack policy has not released, so this bounds memory during a halt
74/// rather than sizing a steady-state flow. Past it a refused write is
75/// answered like a full input ring, with `ServerBusy`. A power of two, as
76/// the ring requires; each entry is the application's rejection plus two
77/// words.
78pub(crate) const REFUSAL_QUEUE_CAPACITY: usize = 4096;
79
80/// What a reader does with a client write right now.
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub enum Verdict {
83    /// Publish it.
84    Take,
85    /// Refuse it with this reason; the connection stays open.
86    Refuse(RejectReason),
87    /// Close the connection: the node is superseded and stopping.
88    Close,
89}
90
91/// A reader's view of whether this node takes client writes.
92pub struct HaltGate {
93    /// Replicas currently streaming, maintained by the replication
94    /// senders. `None` in standalone mode, which never halts for want of
95    /// a replica.
96    replicas_connected: Option<Arc<AtomicU32>>,
97    /// Latched once a newer primary is observed.
98    fence_state: Arc<FenceState>,
99    /// Writes refused so far; shared with the health endpoint.
100    refused: Arc<AtomicU64>,
101}
102
103impl HaltGate {
104    /// A gate over the node's replica count (`None` in standalone mode)
105    /// and its fence latch — the same two the health endpoint folds into
106    /// its status word — that counts what it refuses into `refused`.
107    pub fn new(
108        replicas_connected: Option<Arc<AtomicU32>>,
109        fence_state: Arc<FenceState>,
110        refused: Arc<AtomicU64>,
111    ) -> Self {
112        Self {
113            replicas_connected,
114            fence_state,
115            refused,
116        }
117    }
118
119    /// Count `count` refused writes. Meant for one call per receive with
120    /// the receive's tally, not one per write: a single relaxed add is the
121    /// whole cost.
122    #[inline]
123    pub fn record_refused(&self, count: u64) {
124        self.refused.fetch_add(count, Ordering::Relaxed);
125    }
126
127    /// The verdict on a client write arriving now. Fencing wins: a
128    /// superseded node is shutting down whatever its replica count.
129    ///
130    /// Two relaxed loads. Relaxed is enough: a halt that starts between the
131    /// check and the publish is the case a write published just before the
132    /// halt already covers — see the module docs.
133    #[inline]
134    pub fn verdict(&self) -> Verdict {
135        if self.fence_state.is_fenced() {
136            Verdict::Close
137        } else if self
138            .replicas_connected
139            .as_ref()
140            .is_some_and(|count| count.load(Ordering::Relaxed) == 0)
141        {
142            Verdict::Refuse(RejectReason::ReplicaDisconnected)
143        } else {
144            Verdict::Take
145        }
146    }
147}
148
149/// A client write refused at ingress, on its way to the response stage.
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub struct Refusal<R> {
152    /// Connection the request arrived on.
153    pub connection_id: u64,
154    /// Input-ring sequence the request would have taken: every event
155    /// published before it has a lower one.
156    pub input_seq: u64,
157    /// The application's rejection for the request.
158    pub report: R,
159}
160
161/// Creates the queue a reader hands refusals to the response stage through.
162///
163/// `matching_progress` is the matching stage's consumer progress on the
164/// input ring, which tells an idle response stage that everything
165/// published before a refusal has been answered.
166pub fn refusal_channel<R: Copy>(
167    matching_progress: Arc<Sequence>,
168) -> (RefusalSender<R>, RefusalQueue<R>) {
169    // `Option` because the ring wants `Default` slots and an application's
170    // report need not have one. The wait strategy is unused: the reader
171    // never blocks on this queue (see `RefusalSender::try_send`).
172    let (tx, rx) =
173        spsc::channel::<Option<Refusal<R>>>(REFUSAL_QUEUE_CAPACITY, WaitStrategy::SpinThenYield);
174    (
175        RefusalSender { tx },
176        RefusalQueue {
177            rx,
178            head: None,
179            matching_progress,
180        },
181    )
182}
183
184/// The reader's end of the refusal queue.
185pub struct RefusalSender<R> {
186    tx: spsc::Producer<Option<Refusal<R>>>,
187}
188
189impl<R: Copy> RefusalSender<R> {
190    /// Queue a refusal, invisible to the response stage until
191    /// [`flush`](Self::flush). Never blocks: a full queue is `Err`, and the
192    /// caller sheds the request as it would on a full input ring.
193    pub(crate) fn try_send(&mut self, refusal: Refusal<R>) -> Result<(), spsc::Full> {
194        self.tx
195            .try_push_with(|slot| *slot = Some(refusal))
196            .map(|_| ())
197    }
198
199    /// Make every queued refusal visible to the response stage. Call before
200    /// committing the input-ring entries published after them: the
201    /// response stage relies on a refusal being visible by the time it
202    /// handles any event that came later.
203    pub(crate) fn flush(&mut self) {
204        self.tx.flush();
205    }
206}
207
208/// The response stage's end of the refusal queue.
209///
210/// Everything here works on the refusals taken in by the last
211/// [`sync`](Self::sync); those made visible since wait for the next one.
212/// A stage iteration goes:
213///
214/// 1. [`idle_bound`](Self::idle_bound), then read the output ring;
215/// 2. [`sync`](Self::sync), then drain the control channel;
216/// 3. [`release`](Self::release) — with the idle bound if the read came
217///    back empty, else before the first slot of each event in the batch.
218pub struct RefusalQueue<R> {
219    rx: spsc::Consumer<Option<Refusal<R>>>,
220    /// The oldest synced refusal, taken off the ring but not yet released.
221    head: Option<Refusal<R>>,
222    matching_progress: Arc<Sequence>,
223}
224
225impl<R: Copy> RefusalQueue<R> {
226    /// The release bound for an iteration whose output read comes back
227    /// empty: the matching stage's progress on the input ring. `None` when
228    /// no synced refusal waits, so the steady state leaves the matching
229    /// stage's counter alone.
230    ///
231    /// Must be taken *before* the read. Every event below the progress had
232    /// all its output slots published before the progress moved, so a read
233    /// that finds the ring empty afterwards has seen all of them.
234    #[inline]
235    pub fn idle_bound(&mut self) -> Option<u64> {
236        self.peek()?;
237        Some(self.matching_progress.get().load(Ordering::Acquire))
238    }
239
240    /// Take in the refusals the reader has made visible so far, and report
241    /// whether any waits. One load of the queue's cursor.
242    ///
243    /// Call after reading the output ring, so that every refusal due before
244    /// an event in that batch is taken in, and before draining the control
245    /// channel, so that every refusal taken in belongs to a connection the
246    /// drain registers.
247    #[inline]
248    pub fn sync(&mut self) -> bool {
249        self.rx.refresh();
250        self.peek().is_some()
251    }
252
253    /// Hand `emit` every synced refusal whose request came before `bound`,
254    /// oldest first.
255    ///
256    /// Call with the input sequence of an output slot before handling the
257    /// first slot of that event — everything published before it has been
258    /// answered — or with [`idle_bound`](Self::idle_bound) when the output
259    /// ring is empty. Never between two slots of one event: the refusal
260    /// would split that event's reply.
261    #[inline]
262    pub fn release(&mut self, bound: u64, mut emit: impl FnMut(Refusal<R>)) {
263        while let Some(refusal) = self.peek() {
264            if refusal.input_seq > bound {
265                return;
266            }
267            self.head = None;
268            emit(refusal);
269        }
270    }
271
272    /// The oldest synced refusal. Never reads the queue's cursor, so it
273    /// cannot see past the last [`sync`](Self::sync).
274    fn peek(&mut self) -> Option<Refusal<R>> {
275        if self.head.is_none() {
276            // Only `try_send` writes the ring, and it never writes `None`.
277            self.head = self
278                .rx
279                .try_consume_visible()
280                .and_then(|(_, refusal)| refusal);
281        }
282        self.head
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use melin_pipeline::padding::CachePadded;
290    use std::sync::atomic::AtomicBool;
291
292    fn progress(value: u64) -> Arc<Sequence> {
293        Arc::new(CachePadded::new(AtomicU64::new(value)))
294    }
295
296    fn refusal(connection_id: u64, input_seq: u64) -> Refusal<u64> {
297        Refusal {
298            connection_id,
299            input_seq,
300            report: connection_id * 100,
301        }
302    }
303
304    fn gate(replicas_connected: Option<Arc<AtomicU32>>, fence: Arc<FenceState>) -> HaltGate {
305        HaltGate::new(replicas_connected, fence, Arc::new(AtomicU64::new(0)))
306    }
307
308    #[test]
309    fn standalone_never_refuses() {
310        let gate = gate(None, Arc::new(FenceState::new(0)));
311        assert_eq!(gate.verdict(), Verdict::Take);
312    }
313
314    #[test]
315    fn no_replica_refuses_with_replica_disconnected_until_one_connects() {
316        let count = Arc::new(AtomicU32::new(0));
317        let gate = gate(Some(Arc::clone(&count)), Arc::new(FenceState::new(0)));
318        assert_eq!(
319            gate.verdict(),
320            Verdict::Refuse(RejectReason::ReplicaDisconnected)
321        );
322        count.store(1, Ordering::Relaxed);
323        assert_eq!(gate.verdict(), Verdict::Take);
324    }
325
326    #[test]
327    fn a_fenced_node_closes_whatever_its_replica_count() {
328        let fence = Arc::new(FenceState::new(1));
329        let gate = gate(Some(Arc::new(AtomicU32::new(1))), Arc::clone(&fence));
330        assert_eq!(gate.verdict(), Verdict::Take);
331        assert_eq!(
332            fence.fence_if_superseded(2, &AtomicBool::new(false)),
333            Some(true)
334        );
335        assert_eq!(gate.verdict(), Verdict::Close);
336    }
337
338    #[test]
339    fn refusals_are_counted() {
340        let refused = Arc::new(AtomicU64::new(0));
341        let gate = HaltGate::new(None, Arc::new(FenceState::new(0)), Arc::clone(&refused));
342        gate.record_refused(3);
343        gate.record_refused(2);
344        assert_eq!(refused.load(Ordering::Relaxed), 5);
345    }
346
347    #[test]
348    fn a_refusal_waits_for_both_the_flush_and_a_sync() {
349        let (mut tx, mut rx) = refusal_channel::<u64>(progress(10));
350        tx.try_send(refusal(1, 0)).unwrap();
351        assert!(!rx.sync(), "not flushed");
352
353        tx.flush();
354        assert_eq!(rx.idle_bound(), None, "flushed, not yet synced");
355        let mut released = Vec::new();
356        rx.release(u64::MAX, |r| released.push(r.connection_id));
357        assert!(released.is_empty(), "release sees only synced refusals");
358
359        assert!(rx.sync());
360        assert_eq!(rx.idle_bound(), Some(10));
361    }
362
363    /// The snapshot is what keeps a refusal behind its connection's
364    /// registration: one made visible after the sync — after the stage has
365    /// drained its control channel — waits for the next iteration.
366    #[test]
367    fn release_does_not_reach_past_the_sync() {
368        let (mut tx, mut rx) = refusal_channel::<u64>(progress(0));
369        tx.try_send(refusal(1, 0)).unwrap();
370        tx.flush();
371        assert!(rx.sync());
372        tx.try_send(refusal(2, 0)).unwrap();
373        tx.flush();
374
375        let mut released = Vec::new();
376        rx.release(u64::MAX, |r| released.push(r.connection_id));
377        assert_eq!(released, [1]);
378
379        assert!(rx.sync());
380        rx.release(u64::MAX, |r| released.push(r.connection_id));
381        assert_eq!(released, [1, 2]);
382        assert!(!rx.sync(), "queue drained");
383    }
384
385    #[test]
386    fn release_stops_at_the_first_refusal_past_the_bound() {
387        let (mut tx, mut rx) = refusal_channel::<u64>(progress(0));
388        for (conn, seq) in [(1, 3), (2, 3), (3, 5), (4, 9)] {
389            tx.try_send(refusal(conn, seq)).unwrap();
390        }
391        tx.flush();
392        assert!(rx.sync());
393
394        let mut released = Vec::new();
395        rx.release(2, |r| released.push(r.connection_id));
396        assert!(released.is_empty(), "no event before seq 3 is answered yet");
397
398        rx.release(5, |r| released.push(r.connection_id));
399        assert_eq!(
400            released,
401            [1, 2, 3],
402            "in order, up to and including the bound"
403        );
404
405        rx.release(8, |r| released.push(r.connection_id));
406        assert_eq!(released, [1, 2, 3], "the held head is not re-emitted");
407
408        rx.release(9, |r| released.push(r.connection_id));
409        assert_eq!(released, [1, 2, 3, 4]);
410        assert!(!rx.sync(), "queue drained");
411        assert_eq!(rx.idle_bound(), None);
412    }
413
414    #[test]
415    fn a_full_queue_is_an_error_not_a_wait() {
416        let (mut tx, _rx) = refusal_channel::<u64>(progress(0));
417        for seq in 0..REFUSAL_QUEUE_CAPACITY as u64 {
418            tx.try_send(refusal(1, seq)).unwrap();
419        }
420        assert!(tx.try_send(refusal(1, 0)).is_err());
421    }
422}