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}