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//! streaming (authenticated and its handshake accepted: catching up or
5//! live): it can no longer promise what the ack policy promises, so it
6//! takes no new writes. Each is answered with the application's rejection
7//! for [`RejectReason::ReplicaDisconnected`]; queries still run. A node a
8//! newer primary has fenced is stopping and answers nothing: a connection
9//! that sends anything while it winds down is closed, and the client
10//! reconnects to the new primary.
11//!
12//! # Lifted by the operator's consent
13//!
14//! The halt applies under every ack policy, `disk` included: it is what
15//! stops a primary a partition has cut off from its replicas, which
16//! fencing reaches only on contact. An operator who knows the node has no
17//! replica can consent to serving on its disk alone with `ACK-POLICY
18//! disk`: sent while no replica is streaming, the change is journaled as
19//! forced, and journaling it lifts the halt
20//! (`HaltGate::record_journaled`). The override lasts until a replica
21//! streams and acknowledges an entry, or a policy that needs one is
22//! journaled; the next loss of the last replica then halts the node again.
23//! A replica streamed to that leaves without acking (one refusing the
24//! stream, retrying) leaves it standing. A swap to `disk` made while a
25//! replica is still streaming lifts nothing when it leaves: consent counts
26//! only once the operator knows the node has none. The input ring's
27//! producer, which journals the change, is also the reader that enforces
28//! the halt, so the first write after the change's entry is taken. The
29//! override and the count live together in
30//! [`ReplicaPresence`], which the health endpoint reads too.
31//!
32//! # Refused at ingress
33//!
34//! The readers decide, before a request is published. A refused write
35//! never enters the input ring, so the journal never records it, replicas
36//! never see it, and replay agrees with the live engine. Refusing any later
37//! cannot have that property: the journal stage reads the input ring in
38//! parallel with the matching stage, so a write the matching stage refuses
39//! is already on its way to disk, and replay would apply what the live
40//! engine turned down.
41//!
42//! A write published just before the halt was observed is applied like any
43//! other, and its reply waits on the ack policy — sent once the policy is
44//! met again, never if the node is superseded or stopped first.
45//!
46//! Only client frames pass the gate. Events the runtime publishes itself
47//! — the seed set, a promotion marker — go straight to the input ring and
48//! are applied whether or not a replica is attached: they have no client
49//! to answer, and a primary that seeds before its first replica connects
50//! (the DPDK start-up order) must not lose its seeds to the halt. A
51//! request refused for the halt never reached the application, so a
52//! client may resend it as it was once the node takes writes again.
53//!
54//! # Delivered in order
55//!
56//! Replies carry no request identifier: a client pairs them with its
57//! requests by order. A refusal can therefore not go out as soon as it is
58//! decided — the connection may still be waiting on replies to requests it
59//! sent before, held by the ack policy. The reader stamps the refusal with
60//! the input-ring sequence the request would have taken, and the response
61//! stage sends it once every event published before that sequence has been
62//! answered ([`RefusalQueue::release`]). The reader makes each refusal
63//! visible before it commits anything published after it, so the response
64//! stage cannot reach a later event without seeing the refusal first.
65//!
66//! The response stage works from a snapshot of the queue, taken once per
67//! iteration between reading the output ring and draining its control
68//! channel ([`RefusalQueue::sync`]). Before the drain, because a refusal
69//! must never reach the stage ahead of its connection's registration;
70//! after the read, because that is when every refusal due before an event
71//! in the batch is already visible. One snapshot per iteration is also
72//! all the steady state pays: no poll per event.
73//!
74//! # Counted
75//!
76//! A refused write leaves no other trace — nothing is journaled — so the
77//! reader counts them ([`HaltGate::record_refused`]) and the health
78//! endpoint exports the count as `melin_writes_refused_total`. A write
79//! shed for a full refusal queue is counted too: the halt is why it was
80//! turned away, whatever the client was told.
81
82use std::sync::Arc;
83use std::sync::atomic::{AtomicU64, Ordering};
84use std::time::Duration;
85
86use melin_app::RejectReason;
87use melin_journal::AckPolicyChange;
88use melin_pipeline::padding::Sequence;
89use melin_pipeline::spsc;
90use melin_pipeline::wait::WaitStrategy;
91use melin_transport_core::fence::FenceState;
92use melin_transport_core::replica_presence::{Observed, ReplicaPresence};
93
94use crate::ack_policy::AckPolicy;
95use crate::policy_control::{Origin, is_unwitnessed};
96
97/// Refusals the response stage can hold before a reader has to shed load.
98///
99/// Refusals only queue while the response stage is held up behind a reply
100/// the ack policy has not released, so this bounds memory during a halt
101/// rather than sizing a steady-state flow. Past it a refused write is
102/// answered like a full input ring, with `ServerBusy`. A power of two, as
103/// the ring requires; each entry is the application's rejection plus two
104/// words.
105pub(crate) const REFUSAL_QUEUE_CAPACITY: usize = 4096;
106
107/// What the response stage needs to answer a halted node's held replies
108/// once its own journal holds them: the replies to writes it sequenced
109/// before the halt, and queries, which no replica can confirm while none
110/// is connected. Each goes out terminated by `BatchEndDegraded`, which
111/// tells the client only the primary's disk backs it.
112///
113/// `None` where a stage takes one is the operator switch off
114/// (`--no-degraded-acks`): every held reply, and every refusal and query
115/// queued behind one, waits for a replica or a policy swap. A standalone
116/// node never halts and takes `None` too.
117pub struct DegradedRelease {
118 /// The node's replica presence: whether a replica is streaming, and
119 /// since when none has been.
120 pub presence: Arc<ReplicaPresence>,
121 /// How long the halt must last before the release starts
122 /// (`--degraded-ack-grace-ms`).
123 pub grace: Duration,
124}
125
126/// What a reader does with a client write right now.
127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128pub enum Verdict {
129 /// Publish it.
130 Take,
131 /// Refuse it with this reason; the connection stays open.
132 Refuse(RejectReason),
133 /// Close the connection: the node is superseded and stopping.
134 Close,
135}
136
137/// A reader's view of whether this node takes client writes.
138pub struct HaltGate {
139 /// Replicas currently streaming, maintained by the replication
140 /// senders, and the operator's halt override. `None` in standalone
141 /// mode, which never halts for want of a replica.
142 replicas_connected: Option<Arc<ReplicaPresence>>,
143 /// Latched once a newer primary is observed.
144 fence_state: Arc<FenceState>,
145 /// Writes refused so far; shared with the health endpoint.
146 refused: Arc<AtomicU64>,
147}
148
149/// Whether a node replicates: the one shape of "standalone or replicated"
150/// every ack-policy decision reads. Its source is
151/// `ServerConfig::replication` (whether `--replication-bind` is set): the
152/// ack-policy mailbox is built from it directly, and a primary pipeline
153/// gives its halt gate a replica presence exactly when it is `Enabled`, so
154/// the gate's start and the producer's entries, which read it back through
155/// [`HaltGate::replication`], cannot disagree with the mailbox's refusal.
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub(crate) enum Replication {
158 /// Replication is configured: replicas may stream, and witness changes.
159 Enabled,
160 /// Standalone: nothing can witness a change.
161 Disabled,
162}
163
164impl Replication {
165 /// Whether `policy` can ever be met on a node of this shape. A policy
166 /// that needs a replica cannot on a node that accepts none: no replica
167 /// will ever attach to supply the second copy, so its gate would hold
168 /// every reply. The admin `ACK-POLICY` command refuses such a request
169 /// (`AckPolicyControl::request`). An exhaustive match, so a new shape
170 /// has to decide.
171 pub(crate) fn can_meet(self, policy: AckPolicy) -> bool {
172 match self {
173 Replication::Enabled => true,
174 Replication::Disabled => !policy.needs_replica(),
175 }
176 }
177}
178
179/// An ack-policy change decided on one look at who was streaming, taken
180/// just before the input ring's producer journals it: the policy, whether
181/// its entry is unwitnessed, and what was seen. Built only by
182/// [`HaltGate::decide`] and consumed only by [`HaltGate::record_journaled`],
183/// both private to [`HaltGate::journal`], so the entry the producer
184/// publishes and the policy and replica count the halt acts on cannot
185/// disagree, and a published entry is recorded exactly once. Neither
186/// `Copy` nor `Clone`, for the same reason (and the observation it holds
187/// is neither).
188#[derive(Debug)]
189struct Decision {
190 policy: AckPolicy,
191 /// Whether the entry is unwitnessed ([`is_unwitnessed`]).
192 unwitnessed: bool,
193 /// Who was streaming; `None` in standalone mode.
194 seen: Option<Observed>,
195}
196
197impl Decision {
198 /// The entry that journals the change: the policy's byte, derived
199 /// here rather than stored beside the policy, so the two cannot
200 /// disagree.
201 fn change(&self) -> AckPolicyChange {
202 AckPolicyChange {
203 policy: self.policy.to_byte(),
204 unwitnessed: self.unwitnessed,
205 }
206 }
207
208 /// Whether the change is a forced swap: unwitnessed for want of a
209 /// connected replica, rather than because replication is disabled.
210 /// Only an explicit runtime `disk` is (see [`is_unwitnessed`]). It
211 /// warns, and lifts the halt ([`HaltGate::record_journaled`]).
212 fn is_forced(&self) -> bool {
213 self.unwitnessed && matches!(self.seen, Some(Observed::NoneConnected(_)))
214 }
215}
216
217/// An ack-policy change the input ring's producer has journaled, and the
218/// halt has recorded ([`HaltGate::journal`]): what the caller logs.
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub(crate) struct Journaled {
221 policy: AckPolicy,
222 unwitnessed: bool,
223 forced: bool,
224 /// The input-ring sequence the entry was published at: what the
225 /// producer's publish returned, so a change is recorded only with
226 /// the index of the entry that journals it.
227 input_seq: u64,
228}
229
230impl Journaled {
231 /// The policy changed to.
232 pub(crate) fn policy(self) -> AckPolicy {
233 self.policy
234 }
235
236 /// Whether the entry was journaled unwitnessed.
237 pub(crate) fn unwitnessed(self) -> bool {
238 self.unwitnessed
239 }
240
241 /// Whether it was a forced swap, which lifted the halt (see
242 /// [`HaltGate::record_journaled`]).
243 pub(crate) fn is_forced(self) -> bool {
244 self.forced
245 }
246
247 /// The input-ring sequence the entry was published at.
248 pub(crate) fn input_seq(self) -> u64 {
249 self.input_seq
250 }
251}
252
253impl HaltGate {
254 /// A gate over the node's replica presence (`None` in standalone mode)
255 /// and its fence latch — the same two the health endpoint folds into
256 /// its status word — that counts what it refuses into `refused`.
257 pub fn new(
258 replicas_connected: Option<Arc<ReplicaPresence>>,
259 fence_state: Arc<FenceState>,
260 refused: Arc<AtomicU64>,
261 ) -> Self {
262 Self {
263 replicas_connected,
264 fence_state,
265 refused,
266 }
267 }
268
269 /// Whether this node replicates: whether the gate watches a replica
270 /// presence at all.
271 pub(crate) fn replication(&self) -> Replication {
272 match self.replicas_connected {
273 Some(_) => Replication::Enabled,
274 None => Replication::Disabled,
275 }
276 }
277
278 /// Journal a change to `policy` from `origin`: decide its entry on who
279 /// is streaming now, hand the entry to `publish` (the input ring's
280 /// producer putting it on the ring, returning the sequence it was
281 /// published at), and, once it is published, record it against the
282 /// halt ([`Self::record_journaled`]). An entry `publish` refuses is
283 /// recorded nowhere, and its error returned.
284 ///
285 /// The one way to journal a change, so a published entry is always
286 /// recorded, once, from the very look at who was streaming that
287 /// decided it. `publish` returns the ring's publish index, which the
288 /// record carries: recording a change without publishing its entry
289 /// takes a closure that makes an index up, not one that merely forgot
290 /// to push.
291 pub(crate) fn journal<E>(
292 &self,
293 policy: AckPolicy,
294 origin: Origin,
295 publish: impl FnOnce(AckPolicyChange) -> Result<u64, E>,
296 ) -> Result<Journaled, E> {
297 let decision = self.decide(policy, origin);
298 let input_seq = publish(decision.change())?;
299 Ok(self.record_journaled(decision, input_seq))
300 }
301
302 /// Decide the entry that journals a change to `policy` from `origin`,
303 /// on who is streaming now (two loads) — see [`is_unwitnessed`].
304 fn decide(&self, policy: AckPolicy, origin: Origin) -> Decision {
305 let seen = self.replicas_connected.as_ref().map(|p| p.observe());
306 Decision {
307 policy,
308 unwitnessed: is_unwitnessed(policy, origin, seen.as_ref()),
309 seen,
310 }
311 }
312
313 /// The input ring's producer journaled `decision`'s change, at
314 /// `input_seq`: a forced change (the operator's explicit `disk` with
315 /// no replica streaming) lifts the halt; a change to a policy that
316 /// needs a replica drops any such override, so the node halts again
317 /// while none is streaming. Any other change leaves the override as it
318 /// is: a witnessed `disk` was decided with a replica streaming, which
319 /// ends any override with its first ack (this change's entry, if no
320 /// earlier one); if it leaves without acking anything, the operator's
321 /// earlier consent stands. Standalone never halts, so there is nothing
322 /// to record. Either change to the override consumes the decision's
323 /// observation, the evidence the presence asks for.
324 fn record_journaled(&self, decision: Decision, input_seq: u64) -> Journaled {
325 let forced = decision.is_forced();
326 let Decision {
327 policy,
328 unwitnessed,
329 seen,
330 } = decision;
331 let journaled = Journaled {
332 policy,
333 unwitnessed,
334 forced,
335 input_seq,
336 };
337 let (Some(presence), Some(seen)) = (&self.replicas_connected, seen) else {
338 return journaled;
339 };
340 if policy.needs_replica() {
341 presence.clear_override(seen);
342 } else if let Observed::NoneConnected(since) = seen
343 && forced
344 {
345 presence.latch_override(since);
346 }
347 journaled
348 }
349
350 /// Count `count` refused writes. Meant for one call per receive with
351 /// the receive's tally, not one per write: a single relaxed add is the
352 /// whole cost.
353 #[inline]
354 pub fn record_refused(&self, count: u64) {
355 self.refused.fetch_add(count, Ordering::Relaxed);
356 }
357
358 /// The verdict on a client write arriving now. Fencing wins: a
359 /// superseded node is shutting down whatever its replica count.
360 ///
361 /// Two relaxed loads while a replica is connected, the override's two
362 /// more while none is. Relaxed is enough: a halt that starts between
363 /// the check and the publish is the case a write published just before
364 /// the halt already covers — see the module docs.
365 #[inline]
366 pub fn verdict(&self) -> Verdict {
367 if self.fence_state.is_fenced() {
368 Verdict::Close
369 } else if self
370 .replicas_connected
371 .as_ref()
372 .is_some_and(|presence| presence.halted())
373 {
374 Verdict::Refuse(RejectReason::ReplicaDisconnected)
375 } else {
376 Verdict::Take
377 }
378 }
379}
380
381/// A client write refused at ingress, on its way to the response stage.
382#[derive(Clone, Copy, Debug, PartialEq, Eq)]
383pub struct Refusal<R> {
384 /// Connection the request arrived on.
385 pub connection_id: u64,
386 /// Input-ring sequence the request would have taken: every event
387 /// published before it has a lower one.
388 pub input_seq: u64,
389 /// The application's rejection for the request.
390 pub report: R,
391}
392
393/// Creates the queue a reader hands refusals to the response stage through.
394///
395/// `matching_progress` is the matching stage's consumer progress on the
396/// input ring, which tells an idle response stage that everything
397/// published before a refusal has been answered.
398pub fn refusal_channel<R: Copy>(
399 matching_progress: Arc<Sequence>,
400) -> (RefusalSender<R>, RefusalQueue<R>) {
401 // `Option` because the ring wants `Default` slots and an application's
402 // report need not have one. The wait strategy is unused: the reader
403 // never blocks on this queue (see `RefusalSender::try_send`).
404 let (tx, rx) =
405 spsc::channel::<Option<Refusal<R>>>(REFUSAL_QUEUE_CAPACITY, WaitStrategy::SpinThenYield);
406 (
407 RefusalSender { tx },
408 RefusalQueue {
409 rx,
410 head: None,
411 matching_progress,
412 },
413 )
414}
415
416/// The reader's end of the refusal queue.
417pub struct RefusalSender<R> {
418 tx: spsc::Producer<Option<Refusal<R>>>,
419}
420
421impl<R: Copy> RefusalSender<R> {
422 /// Queue a refusal, invisible to the response stage until
423 /// [`flush`](Self::flush). Never blocks: a full queue is `Err`, and the
424 /// caller sheds the request as it would on a full input ring.
425 pub(crate) fn try_send(&mut self, refusal: Refusal<R>) -> Result<(), spsc::Full> {
426 self.tx
427 .try_push_with(|slot| *slot = Some(refusal))
428 .map(|_| ())
429 }
430
431 /// Make every queued refusal visible to the response stage. Call before
432 /// committing the input-ring entries published after them: the
433 /// response stage relies on a refusal being visible by the time it
434 /// handles any event that came later.
435 pub(crate) fn flush(&mut self) {
436 self.tx.flush();
437 }
438}
439
440/// The response stage's end of the refusal queue.
441///
442/// Everything here works on the refusals taken in by the last
443/// [`sync`](Self::sync); those made visible since wait for the next one.
444/// A stage iteration goes:
445///
446/// 1. [`idle_bound`](Self::idle_bound), then read the output ring;
447/// 2. [`sync`](Self::sync), then drain the control channel;
448/// 3. [`release`](Self::release) — with the idle bound if the read came
449/// back empty, else before the first slot of each event in the batch.
450pub struct RefusalQueue<R> {
451 rx: spsc::Consumer<Option<Refusal<R>>>,
452 /// The oldest synced refusal, taken off the ring but not yet released.
453 head: Option<Refusal<R>>,
454 matching_progress: Arc<Sequence>,
455}
456
457impl<R: Copy> RefusalQueue<R> {
458 /// The release bound for an iteration whose output read comes back
459 /// empty: the matching stage's progress on the input ring. `None` when
460 /// no synced refusal waits, so the steady state leaves the matching
461 /// stage's counter alone.
462 ///
463 /// Must be taken *before* the read. Every event below the progress had
464 /// all its output slots published before the progress moved, so a read
465 /// that finds the ring empty afterwards has seen all of them.
466 #[inline]
467 pub fn idle_bound(&mut self) -> Option<u64> {
468 self.peek()?;
469 Some(self.matching_progress.get().load(Ordering::Acquire))
470 }
471
472 /// Take in the refusals the reader has made visible so far, and report
473 /// whether any waits. One load of the queue's cursor.
474 ///
475 /// Call after reading the output ring, so that every refusal due before
476 /// an event in that batch is taken in, and before draining the control
477 /// channel, so that every refusal taken in belongs to a connection the
478 /// drain registers.
479 #[inline]
480 pub fn sync(&mut self) -> bool {
481 self.rx.refresh();
482 self.peek().is_some()
483 }
484
485 /// Hand `emit` every synced refusal whose request came before `bound`,
486 /// oldest first.
487 ///
488 /// Call with the input sequence of an output slot before handling the
489 /// first slot of that event — everything published before it has been
490 /// answered — or with [`idle_bound`](Self::idle_bound) when the output
491 /// ring is empty. Never between two slots of one event: the refusal
492 /// would split that event's reply.
493 #[inline]
494 pub fn release(&mut self, bound: u64, mut emit: impl FnMut(Refusal<R>)) {
495 while let Some(refusal) = self.peek() {
496 if refusal.input_seq > bound {
497 return;
498 }
499 self.head = None;
500 emit(refusal);
501 }
502 }
503
504 /// The oldest synced refusal. Never reads the queue's cursor, so it
505 /// cannot see past the last [`sync`](Self::sync).
506 fn peek(&mut self) -> Option<Refusal<R>> {
507 if self.head.is_none() {
508 // Only `try_send` writes the ring, and it never writes `None`.
509 self.head = self
510 .rx
511 .try_consume_visible()
512 .and_then(|(_, refusal)| refusal);
513 }
514 self.head
515 }
516}
517
518/// Halt gates for the crate's tests.
519#[cfg(test)]
520pub(crate) mod test_support {
521 use super::*;
522
523 /// A replica presence with `connected` replicas streaming, none of
524 /// which has acked yet. (The health endpoint's tests in
525 /// `melin-transport-core` keep their own: a test helper cannot cross
526 /// the crate boundary.)
527 pub(crate) fn presence(connected: u32) -> Arc<ReplicaPresence> {
528 let presence = Arc::new(ReplicaPresence::new());
529 for _ in 0..connected {
530 drop(presence.lift());
531 }
532 presence
533 }
534
535 /// An unfenced gate over `replicas` (`None`: replication disabled)
536 /// that counts its refusals nowhere anyone reads.
537 pub(crate) fn gate_over(replicas: Option<Arc<ReplicaPresence>>) -> HaltGate {
538 HaltGate::new(
539 replicas,
540 Arc::new(FenceState::new(0)),
541 Arc::new(AtomicU64::new(0)),
542 )
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549 use melin_pipeline::padding::CachePadded;
550 use melin_transport_core::replica_presence::Lowered;
551 use std::sync::atomic::AtomicBool;
552
553 fn progress(value: u64) -> Arc<Sequence> {
554 Arc::new(CachePadded::new(AtomicU64::new(value)))
555 }
556
557 fn refusal(connection_id: u64, input_seq: u64) -> Refusal<u64> {
558 Refusal {
559 connection_id,
560 input_seq,
561 report: connection_id * 100,
562 }
563 }
564
565 fn gate(replicas_connected: Option<Arc<ReplicaPresence>>, fence: Arc<FenceState>) -> HaltGate {
566 HaltGate::new(replicas_connected, fence, Arc::new(AtomicU64::new(0)))
567 }
568
569 const HALTED: Verdict = Verdict::Refuse(RejectReason::ReplicaDisconnected);
570
571 /// Standalone never refuses, and journaling a change records nothing.
572 #[test]
573 fn standalone_never_refuses() {
574 let gate = gate(None, Arc::new(FenceState::new(0)));
575 assert_eq!(gate.verdict(), Verdict::Take);
576 for policy in [AckPolicy::Disk, AckPolicy::TwoDisks] {
577 journal(&gate, policy, Origin::Runtime);
578 assert_eq!(gate.verdict(), Verdict::Take);
579 }
580 }
581
582 #[test]
583 fn no_replica_refuses_with_replica_disconnected_until_one_connects() {
584 let replicas = test_support::presence(0);
585 let gate = gate(Some(Arc::clone(&replicas)), Arc::new(FenceState::new(0)));
586 assert_eq!(gate.verdict(), HALTED);
587 drop(replicas.lift());
588 assert_eq!(gate.verdict(), Verdict::Take);
589 }
590
591 /// A decision follows the replica count at the time it is made: a
592 /// runtime `disk` is unwitnessed standalone (not forced), forced with
593 /// no replica connected, and witnessed once one connects.
594 #[test]
595 fn a_decision_follows_the_replica_count() {
596 let standalone =
597 gate(None, Arc::new(FenceState::new(0))).decide(AckPolicy::Disk, Origin::Runtime);
598 assert!(standalone.unwitnessed && !standalone.is_forced());
599 let replicas = test_support::presence(0);
600 let gate = gate(Some(Arc::clone(&replicas)), Arc::new(FenceState::new(0)));
601 let none = gate.decide(AckPolicy::Disk, Origin::Runtime);
602 assert!(none.unwitnessed && none.is_forced());
603 drop(replicas.lift());
604 drop(replicas.lift());
605 let connected = gate.decide(AckPolicy::Disk, Origin::Runtime);
606 assert!(!connected.unwitnessed && !connected.is_forced());
607 }
608
609 /// Journal `policy` as the producer would, onto a ring that always
610 /// takes the entry (at a sequence nothing here reads).
611 fn journal(gate: &HaltGate, policy: AckPolicy, origin: Origin) -> Journaled {
612 let Ok(journaled) = gate.journal(policy, origin, |_| Ok::<_, std::convert::Infallible>(0));
613 journaled
614 }
615
616 /// The entry a journaled change carries is the policy's byte, and it
617 /// is the one handed to the producer, recorded at the sequence the
618 /// producer published it at; standalone, it is unwitnessed (nothing
619 /// can witness it) but not forced, whatever the policy and origin. The
620 /// rule with replication on is `policy_control`'s `the_forced_rule`,
621 /// and the count it reads is `a_decision_follows_the_replica_count`.
622 #[test]
623 fn a_journaled_change_carries_its_policy_as_the_observation_rules() {
624 let standalone = gate(None, Arc::new(FenceState::new(0)));
625 let mut published = None;
626 let Ok(journaled) = standalone.journal(AckPolicy::Ram, Origin::Staged, |change| {
627 published = Some(change);
628 Ok::<_, std::convert::Infallible>(42)
629 });
630 assert_eq!(journaled.policy(), AckPolicy::Ram);
631 assert_eq!(
632 published,
633 Some(AckPolicyChange {
634 policy: AckPolicy::Ram.to_byte(),
635 unwitnessed: journaled.unwitnessed(),
636 }),
637 "the entry published"
638 );
639 assert_eq!(journaled.input_seq(), 42, "where it was published");
640 assert!(journaled.unwitnessed(), "nothing can witness it");
641 assert!(
642 !journaled.is_forced(),
643 "unwitnessed for want of replication"
644 );
645 }
646
647 /// An entry the producer could not publish (a full ring) is recorded
648 /// nowhere: a forced `disk` refused that way lifts no halt, and the
649 /// error comes back.
650 #[test]
651 fn an_entry_not_published_is_not_recorded() {
652 let gate = gate(
653 Some(test_support::presence(0)),
654 Arc::new(FenceState::new(0)),
655 );
656 assert_eq!(
657 gate.journal(AckPolicy::Disk, Origin::Runtime, |_| Err("full")),
658 Err("full")
659 );
660 assert_eq!(gate.verdict(), HALTED, "still halted");
661 assert!(journal(&gate, AckPolicy::Disk, Origin::Runtime).is_forced());
662 assert_eq!(gate.verdict(), Verdict::Take, "published: lifted");
663 }
664
665 /// The replication shape is the presence's: watched or not.
666 #[test]
667 fn the_replication_shape_is_the_presence() {
668 let fence = || Arc::new(FenceState::new(0));
669 assert_eq!(gate(None, fence()).replication(), Replication::Disabled);
670 assert_eq!(
671 gate(Some(test_support::presence(0)), fence()).replication(),
672 Replication::Enabled
673 );
674 }
675
676 /// A replicating node can meet every policy; one that accepts no
677 /// replica only those that need none.
678 #[test]
679 fn a_node_without_replication_meets_only_policies_that_need_no_replica() {
680 for policy in [
681 AckPolicy::Disk,
682 AckPolicy::Ram,
683 AckPolicy::DiskAndRam,
684 AckPolicy::TwoDisks,
685 ] {
686 assert!(Replication::Enabled.can_meet(policy), "{policy}");
687 assert_eq!(
688 Replication::Disabled.can_meet(policy),
689 policy == AckPolicy::Disk,
690 "{policy}"
691 );
692 }
693 }
694
695 /// Halt-plan step 0. A node under `disk` that loses its replica halts
696 /// like any other; the operator's `disk` during the halt lifts it; a
697 /// returning replica that acks voids the override, so its next
698 /// departure halts the node again.
699 #[test]
700 fn a_forced_disk_lifts_the_halt_until_a_replica_returns() {
701 let replicas = test_support::presence(1);
702 let gate = gate(Some(Arc::clone(&replicas)), Arc::new(FenceState::new(0)));
703 assert_eq!(gate.verdict(), Verdict::Take);
704 assert_eq!(replicas.lower(), Lowered::LastHalted);
705 assert_eq!(gate.verdict(), HALTED, "the halt ignores the policy");
706
707 journal(&gate, AckPolicy::Disk, Origin::Runtime);
708 assert_eq!(gate.verdict(), Verdict::Take, "consent lifts the halt");
709 assert!(
710 gate.decide(AckPolicy::Disk, Origin::Runtime).is_forced(),
711 "still no witness: a later `disk` is forced too"
712 );
713 journal(&gate, AckPolicy::Disk, Origin::Runtime);
714 assert_eq!(gate.verdict(), Verdict::Take, "re-sent, it stays lifted");
715
716 let mut first_ack = replicas.lift();
717 assert_eq!(gate.verdict(), Verdict::Take);
718 first_ack.acked(&replicas);
719 assert_eq!(replicas.lower(), Lowered::LastHalted);
720 assert_eq!(
721 gate.verdict(),
722 HALTED,
723 "the override did not outlive the replica"
724 );
725 }
726
727 /// A replica that authenticates and is then refused (a lineage it will
728 /// not follow, a primary it takes for stale) never acks: counted once
729 /// its handshake is accepted, then leaving, however often it retries,
730 /// it leaves the operator's override standing, and a decision while it
731 /// is counted is witnessed. One that acks ends it.
732 #[test]
733 fn a_replica_refused_after_authenticating_leaves_the_override() {
734 let replicas = test_support::presence(0);
735 let gate = gate(Some(Arc::clone(&replicas)), Arc::new(FenceState::new(0)));
736 journal(&gate, AckPolicy::Disk, Origin::Runtime);
737 assert_eq!(gate.verdict(), Verdict::Take);
738 for _ in 0..3 {
739 let refused = replicas.lift();
740 assert!(
741 !gate.decide(AckPolicy::Disk, Origin::Runtime).is_forced(),
742 "streaming, so witnessed"
743 );
744 drop(refused);
745 assert_eq!(replicas.lower(), Lowered::LastOverridden);
746 assert_eq!(gate.verdict(), Verdict::Take, "the override held");
747 }
748
749 replicas.lift().acked(&replicas);
750 assert_eq!(replicas.lower(), Lowered::LastHalted);
751 assert_eq!(gate.verdict(), HALTED, "a replica that acked ended it");
752 }
753
754 /// A swap back to a policy that needs a replica, with none connected,
755 /// restores the halt; each of them does.
756 #[test]
757 fn a_swap_to_a_replica_backed_policy_restores_the_halt() {
758 for policy in [AckPolicy::Ram, AckPolicy::DiskAndRam, AckPolicy::TwoDisks] {
759 let gate = gate(
760 Some(test_support::presence(0)),
761 Arc::new(FenceState::new(0)),
762 );
763 journal(&gate, AckPolicy::Disk, Origin::Runtime);
764 assert_eq!(gate.verdict(), Verdict::Take);
765 journal(&gate, policy, Origin::Runtime);
766 assert_eq!(gate.verdict(), HALTED, "{policy}");
767 journal(&gate, AckPolicy::Disk, Origin::Runtime);
768 assert_eq!(
769 gate.verdict(),
770 Verdict::Take,
771 "a forced swap again: {policy}"
772 );
773 }
774 }
775
776 /// Consent counts only once the operator knows the node has no
777 /// replica: a `disk` journaled while one is connected lifts nothing
778 /// when it leaves, and neither does a staged one (a boot or promotion
779 /// entry) with none connected.
780 #[test]
781 fn a_disk_not_forced_does_not_lift_the_halt() {
782 let replicas = test_support::presence(1);
783 let gate = gate(Some(Arc::clone(&replicas)), Arc::new(FenceState::new(0)));
784 journal(&gate, AckPolicy::Disk, Origin::Runtime);
785 assert_eq!(replicas.lower(), Lowered::LastHalted);
786 assert_eq!(gate.verdict(), HALTED, "witnessed: no consent");
787
788 journal(&gate, AckPolicy::Disk, Origin::Staged);
789 assert_eq!(gate.verdict(), HALTED, "staged: never forced");
790 }
791
792 /// The fence wins over the override: a superseded node closes.
793 #[test]
794 fn a_fenced_node_closes_despite_the_override() {
795 let fence = Arc::new(FenceState::new(0));
796 let gate = gate(Some(test_support::presence(0)), Arc::clone(&fence));
797 journal(&gate, AckPolicy::Disk, Origin::Runtime);
798 fence.fence();
799 assert_eq!(gate.verdict(), Verdict::Close);
800 }
801
802 #[test]
803 fn a_fenced_node_closes_whatever_its_replica_count() {
804 let fence = Arc::new(FenceState::new(1));
805 let gate = gate(Some(test_support::presence(1)), Arc::clone(&fence));
806 assert_eq!(gate.verdict(), Verdict::Take);
807 assert_eq!(
808 fence.fence_if_superseded(2, &AtomicBool::new(false)),
809 Some(true)
810 );
811 assert_eq!(gate.verdict(), Verdict::Close);
812 }
813
814 #[test]
815 fn refusals_are_counted() {
816 let refused = Arc::new(AtomicU64::new(0));
817 let gate = HaltGate::new(None, Arc::new(FenceState::new(0)), Arc::clone(&refused));
818 gate.record_refused(3);
819 gate.record_refused(2);
820 assert_eq!(refused.load(Ordering::Relaxed), 5);
821 }
822
823 #[test]
824 fn a_refusal_waits_for_both_the_flush_and_a_sync() {
825 let (mut tx, mut rx) = refusal_channel::<u64>(progress(10));
826 tx.try_send(refusal(1, 0)).unwrap();
827 assert!(!rx.sync(), "not flushed");
828
829 tx.flush();
830 assert_eq!(rx.idle_bound(), None, "flushed, not yet synced");
831 let mut released = Vec::new();
832 rx.release(u64::MAX, |r| released.push(r.connection_id));
833 assert!(released.is_empty(), "release sees only synced refusals");
834
835 assert!(rx.sync());
836 assert_eq!(rx.idle_bound(), Some(10));
837 }
838
839 /// The snapshot is what keeps a refusal behind its connection's
840 /// registration: one made visible after the sync — after the stage has
841 /// drained its control channel — waits for the next iteration.
842 #[test]
843 fn release_does_not_reach_past_the_sync() {
844 let (mut tx, mut rx) = refusal_channel::<u64>(progress(0));
845 tx.try_send(refusal(1, 0)).unwrap();
846 tx.flush();
847 assert!(rx.sync());
848 tx.try_send(refusal(2, 0)).unwrap();
849 tx.flush();
850
851 let mut released = Vec::new();
852 rx.release(u64::MAX, |r| released.push(r.connection_id));
853 assert_eq!(released, [1]);
854
855 assert!(rx.sync());
856 rx.release(u64::MAX, |r| released.push(r.connection_id));
857 assert_eq!(released, [1, 2]);
858 assert!(!rx.sync(), "queue drained");
859 }
860
861 #[test]
862 fn release_stops_at_the_first_refusal_past_the_bound() {
863 let (mut tx, mut rx) = refusal_channel::<u64>(progress(0));
864 for (conn, seq) in [(1, 3), (2, 3), (3, 5), (4, 9)] {
865 tx.try_send(refusal(conn, seq)).unwrap();
866 }
867 tx.flush();
868 assert!(rx.sync());
869
870 let mut released = Vec::new();
871 rx.release(2, |r| released.push(r.connection_id));
872 assert!(released.is_empty(), "no event before seq 3 is answered yet");
873
874 rx.release(5, |r| released.push(r.connection_id));
875 assert_eq!(
876 released,
877 [1, 2, 3],
878 "in order, up to and including the bound"
879 );
880
881 rx.release(8, |r| released.push(r.connection_id));
882 assert_eq!(released, [1, 2, 3], "the held head is not re-emitted");
883
884 rx.release(9, |r| released.push(r.connection_id));
885 assert_eq!(released, [1, 2, 3, 4]);
886 assert!(!rx.sync(), "queue drained");
887 assert_eq!(rx.idle_bound(), None);
888 }
889
890 #[test]
891 fn a_full_queue_is_an_error_not_a_wait() {
892 let (mut tx, _rx) = refusal_channel::<u64>(progress(0));
893 for seq in 0..REFUSAL_QUEUE_CAPACITY as u64 {
894 tx.try_send(refusal(1, seq)).unwrap();
895 }
896 assert!(tx.try_send(refusal(1, 0)).is_err());
897 }
898}