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//! 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}