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}