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