1use std::collections::VecDeque;
68use std::sync::Arc;
69use std::sync::atomic::{AtomicBool, Ordering};
70use std::time::{Duration, Instant};
71
72use melin_app::amortized_timer::AmortizedTimer;
73use melin_journal::{AckPolicyByte, AckPolicyChange};
74use melin_pipeline::wait::WaitStrategy;
75use melin_transport_core::lineage::{ForcedPolicyReader, LineagePolicy};
76use melin_transport_core::pipeline::{OutputPayload, OutputSlot, StageUtilization};
77use melin_transport_core::replica_presence::LastLoss;
78#[cfg(feature = "tick-to-trade")]
79use melin_transport_core::trace;
80use melin_transport_core::{DurableWireSeqCursor, WireSeq};
81use melin_wire_protocol::control::TransportResponse;
82use melin_wire_protocol::control_codec;
83
84use crate::ack_policy::{AckPolicy, Blocker, Policy};
85use crate::halt::{DegradedRelease, Replication};
86use crate::replication::ReplicationMetrics;
87use crate::response::{DegradationLogger, evaluate_durability, evaluate_gate, slot_needs_gate};
88#[cfg(feature = "tick-to-trade")]
89use crate::response::{GateCrossTracker, policy_replica_cursor};
90
91const DEGRADED_LOG_INTERVAL: Duration = Duration::from_secs(5);
93
94const POLICY_CHECK_INTERVAL: Duration = Duration::from_secs(1);
99
100const GATE_ACCRUAL_INTERVAL: Duration = Duration::from_millis(10);
110
111pub(crate) const DEFAULT_DEGRADED_ACK_GRACE_MS: u64 = 2_000;
124
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128pub(crate) enum Backing {
129 Policy,
131 PrimaryOnly,
134}
135
136impl Backing {
137 fn min(self, other: Self) -> Self {
140 match (self, other) {
141 (Self::Policy, Self::Policy) => Self::Policy,
142 (Self::PrimaryOnly, _) | (_, Self::PrimaryOnly) => Self::PrimaryOnly,
143 }
144 }
145}
146
147pub(crate) struct BatchEnds {
152 pub policy: Vec<u8>,
154 pub primary_only: Vec<u8>,
156}
157
158impl BatchEnds {
159 pub(crate) fn new() -> Self {
160 let encode = |response| {
161 let mut buf = [0u8; 8];
162 let written = control_codec::encode_transport_response(&response, &mut buf)
163 .expect("a tag-only control frame encodes into eight bytes");
164 buf[..written].to_vec()
165 };
166 Self {
167 policy: encode(TransportResponse::BatchEnd),
168 primary_only: encode(TransportResponse::BatchEndDegraded),
169 }
170 }
171}
172
173#[must_use]
175enum GateOutcome {
176 Open(Backing),
178 Shutdown,
182}
183
184#[must_use]
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub(crate) enum Admission {
189 Reply {
193 connection_id: u64,
194 backing: Backing,
195 },
196 GateOnly,
201 Shutdown,
204}
205
206pub struct GatePolicy {
211 pub start: GateStart,
214 pub forced: ForcedPolicyReader,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub struct GateStart {
235 pub(crate) in_force: AckPolicy,
237 pub(crate) pending: Option<PendingChange>,
239}
240
241impl GateStart {
242 pub fn settled(policy: AckPolicy) -> Self {
246 Self {
247 in_force: policy,
248 pending: None,
249 }
250 }
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256pub(crate) struct PendingChange {
257 pub(crate) wire_seq: WireSeq,
259 pub(crate) policy: AckPolicy,
261 pub(crate) unwitnessed: bool,
265}
266
267pub(crate) fn initial_gate_policy(
302 lineage: Option<LineagePolicy>,
303 requested: AckPolicy,
304 replication: Replication,
305) -> Result<GateStart, String> {
306 let Some(lineage) = lineage else {
307 return Ok(GateStart::settled(requested));
308 };
309 let newest = known_lineage_policy(lineage.policy())?;
310 match replication {
311 Replication::Disabled => return Ok(GateStart::settled(requested)),
312 Replication::Enabled => {}
313 }
314 let Some(previous) = lineage.previous else {
315 return Ok(GateStart::settled(newest));
316 };
317 Ok(GateStart {
318 in_force: known_lineage_policy(previous)?,
319 pending: Some(PendingChange {
320 wire_seq: lineage.wire_seq,
321 policy: newest,
322 unwitnessed: lineage.change.unwitnessed,
323 }),
324 })
325}
326
327fn known_lineage_policy(byte: AckPolicyByte) -> Result<AckPolicy, String> {
329 AckPolicy::from_byte(byte).ok_or_else(|| {
330 format!(
331 "the journal's ack policy (byte {}) is unknown to this build; \
332 refusing to serve under it",
333 byte.0
334 )
335 })
336}
337
338pub(crate) struct GateInputs {
342 pub journal_persisted_wire_seq: DurableWireSeqCursor,
345 pub replication_metrics: Option<Arc<ReplicationMetrics>>,
349 pub replica_active: Option<[Arc<AtomicBool>; 2]>,
356 pub utilization: Arc<StageUtilization>,
360 pub wait: WaitStrategy,
362 pub degraded_release: Option<DegradedRelease>,
365}
366
367pub(crate) struct DurabilityGate {
372 inputs: GateInputs,
373 stage: &'static str,
375 forced: ForcedPolicyReader,
377 applied_through: Option<WireSeq>,
382 last_refused: Option<WireSeq>,
389 pending: VecDeque<PendingChange>,
397 active_policy: AckPolicy,
398 needs_replica: bool,
402 policy: Policy,
403 cached_durable_pos: u64,
407 degraded_logger: DegradationLogger,
408 last_policy_check: Instant,
411 accrual_timer: AmortizedTimer,
418 halt_started: Option<Instant>,
422 releasing_degraded: bool,
425}
426
427impl DurabilityGate {
428 pub(crate) fn new(inputs: GateInputs, policy: GatePolicy, stage: &'static str) -> Self {
435 let GatePolicy {
436 start:
437 GateStart {
438 in_force: active_policy,
439 pending,
440 },
441 forced,
442 } = policy;
443 inputs
444 .utilization
445 .ack_policy_in_force
446 .store(Some(active_policy.to_byte()));
447 let policy = active_policy.to_policy();
448 let now = Instant::now();
449 let journal_pos = inputs.journal_persisted_wire_seq.load();
450 let status = evaluate_durability(
451 &policy,
452 journal_pos,
453 inputs.replication_metrics.as_deref(),
454 inputs.replica_active.as_ref(),
455 );
456 inputs
457 .utilization
458 .policy_degraded
459 .store(status.degraded, Ordering::Relaxed);
460 let degraded_logger = if status.degraded {
461 DegradationLogger::new_starting_degraded(now, &policy)
462 } else {
463 DegradationLogger::new(now)
464 };
465 if let Some(pending) = pending {
466 tracing::info!(
467 stage,
468 in_force = active_policy.as_str(),
469 pending = pending.policy.as_str(),
470 wire_seq = pending.wire_seq.get(),
471 unwitnessed = pending.unwitnessed,
472 "the lineage's newest ack-policy change is applied again before any reply"
473 );
474 }
475 let mut gate = Self {
476 inputs,
477 stage,
478 forced,
479 applied_through: None,
480 last_refused: None,
481 pending: pending.into_iter().collect(),
482 active_policy,
483 needs_replica: active_policy.needs_replica(),
484 policy,
485 cached_durable_pos: status.durable_pos,
486 degraded_logger,
487 last_policy_check: now,
488 accrual_timer: AmortizedTimer::new(),
489 halt_started: None,
490 releasing_degraded: false,
491 };
492 gate.settle_pending_if_durable(journal_pos);
493 gate
494 }
495
496 fn settle_pending_if_durable(&mut self, journal_pos: WireSeq) {
509 while let Some(&pending) = self.pending.front() {
510 if self.cached_durable_pos < pending.wire_seq.get()
511 && !(pending.unwitnessed && journal_pos >= pending.wire_seq)
512 {
513 return;
514 }
515 self.apply_front_pending();
516 }
517 }
518
519 fn apply_front_pending(&mut self) {
523 let Some(pending) = self.pending.pop_front() else {
524 return;
525 };
526 let cause = if pending.unwitnessed {
527 "pending unwitnessed change, on disk"
528 } else {
529 "pending change, durable under the policy before it"
530 };
531 self.apply_change(pending.wire_seq, pending.policy.to_byte(), cause);
532 }
533
534 #[inline]
538 pub(crate) fn batch_end<'e>(&self, backing: Backing, ends: &'e BatchEnds) -> &'e [u8] {
539 match backing {
540 Backing::Policy => &ends.policy,
541 Backing::PrimaryOnly => {
542 self.inputs
543 .utilization
544 .degraded_acks
545 .fetch_add(1, Ordering::Relaxed);
546 &ends.primary_only
547 }
548 }
549 }
550
551 fn degraded_release_open(&mut self) -> bool {
562 let Some(release) = self.inputs.degraded_release.as_ref() else {
563 return false;
564 };
565 let grace = release.grace;
566 let Some(loss) = release.presence.last_loss() else {
567 self.halt_started = None;
568 if self.releasing_degraded {
569 self.log_degraded_release(false, true, grace);
570 }
571 return false;
572 };
573 let now = Instant::now();
574 let started = halt_start(loss, self.halt_started, now);
575 self.halt_started = Some(started);
576 let open = self.needs_replica && now.saturating_duration_since(started) >= grace;
580 if open != self.releasing_degraded {
581 self.log_degraded_release(open, false, grace);
582 }
583 open
584 }
585
586 #[cold]
588 fn log_degraded_release(&mut self, open: bool, replica_connected: bool, grace: Duration) {
589 self.releasing_degraded = open;
590 if open {
591 tracing::warn!(
592 stage = self.stage,
593 policy = self.active_policy.as_str(),
594 grace_ms = grace.as_millis() as u64,
595 "no replica for the grace period: replies held for the ack policy are now \
596 released once this node's own journal holds them, marked as backed by the \
597 primary alone (BatchEndDegraded)"
598 );
599 } else {
600 tracing::warn!(
601 stage = self.stage,
602 policy = self.active_policy.as_str(),
603 replica_connected,
604 "degraded release stopped: held replies wait for the ack policy again"
605 );
606 }
607 }
608
609 fn switch_to(&mut self, next: AckPolicy, cause: &'static str) {
624 if next == self.active_policy {
625 return;
626 }
627 tracing::info!(
628 stage = self.stage,
629 prev = self.active_policy.as_str(),
630 next = next.as_str(),
631 cause,
632 "ack policy switched"
633 );
634 self.active_policy = next;
635 self.needs_replica = next.needs_replica();
636 self.policy = next.to_policy();
637 self.cached_durable_pos = 0;
638 self.degraded_logger
639 .reseed(&self.inputs.utilization, Instant::now());
640 self.inputs
641 .utilization
642 .ack_policy_in_force
643 .store(Some(next.to_byte()));
644 }
645
646 fn apply_change(&mut self, wire_seq: WireSeq, policy: AckPolicyByte, cause: &'static str) {
651 if self.is_stale(wire_seq) {
652 return;
653 }
654 let Some(next) = AckPolicy::from_byte(policy) else {
655 self.refuse_unknown(wire_seq, policy);
656 return;
657 };
658 self.applied_through = Some(wire_seq);
659 self.switch_to(next, cause);
660 }
661
662 fn is_stale(&self, wire_seq: WireSeq) -> bool {
665 self.applied_through
666 .is_some_and(|through| wire_seq <= through)
667 }
668
669 #[cold]
678 fn refuse_unknown(&mut self, wire_seq: WireSeq, policy: AckPolicyByte) {
679 if self.last_refused == Some(wire_seq) {
680 return;
681 }
682 self.last_refused = Some(wire_seq);
683 tracing::error!(
684 stage = self.stage,
685 byte = policy.0,
686 wire_seq = wire_seq.get(),
687 "journaled ack-policy change names an unknown policy; retaining prior policy"
688 );
689 }
690
691 #[inline]
701 fn observe_forced(&mut self, journal_pos: WireSeq) {
702 self.settle_pending_if_durable(journal_pos);
703 if let Some(forced) = self.forced.load()
704 && journal_pos >= forced.wire_seq
705 {
706 self.apply_change(forced.wire_seq, forced.policy, "forced change");
707 }
708 }
709
710 #[inline]
738 pub(crate) fn admit<R: Copy, Q: Copy>(
739 &mut self,
740 slot: &OutputSlot<R, Q>,
741 shutdown: &AtomicBool,
742 #[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
743 before_wait: impl FnOnce(),
744 ) -> Admission {
745 let mut before_wait = Some(before_wait);
746 match self.admit_pending(
747 shutdown,
748 #[cfg(feature = "tick-to-trade")]
749 tracker,
750 &mut before_wait,
751 ) {
752 GateOutcome::Shutdown => Admission::Shutdown,
753 GateOutcome::Open(backing) => self.admit_slot(
754 slot,
755 backing,
756 shutdown,
757 #[cfg(feature = "tick-to-trade")]
758 tracker,
759 &mut before_wait,
760 ),
761 }
762 }
763
764 #[inline]
771 fn admit_pending(
772 &mut self,
773 shutdown: &AtomicBool,
774 #[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
775 before_wait: &mut Option<impl FnOnce()>,
776 ) -> GateOutcome {
777 while let Some(&pending) = self.pending.front() {
778 if pending.wire_seq.get() > self.cached_durable_pos {
779 if let Some(before_wait) = before_wait.take() {
780 before_wait();
781 }
782 match self.wait_durable(
783 pending.wire_seq.get(),
784 shutdown,
785 #[cfg(feature = "tick-to-trade")]
786 tracker,
787 ) {
788 GateOutcome::Shutdown => return GateOutcome::Shutdown,
789 GateOutcome::Open(Backing::PrimaryOnly) => {
790 return GateOutcome::Open(Backing::PrimaryOnly);
791 }
792 GateOutcome::Open(Backing::Policy) => {}
793 }
794 if self.pending.front() != Some(&pending) {
797 continue;
798 }
799 }
800 self.apply_front_pending();
806 }
807 GateOutcome::Open(Backing::Policy)
808 }
809
810 #[inline]
815 fn admit_slot<R: Copy, Q: Copy>(
816 &mut self,
817 slot: &OutputSlot<R, Q>,
818 mut backing: Backing,
819 shutdown: &AtomicBool,
820 #[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
821 before_wait: &mut Option<impl FnOnce()>,
822 ) -> Admission {
823 let change = match slot.payload {
824 OutputPayload::AckPolicyChange(change) => Some(change),
825 OutputPayload::Report(_)
826 | OutputPayload::QueryResponse(_)
827 | OutputPayload::BatchEnd
828 | OutputPayload::EngineError => None,
829 };
830 if slot_needs_gate(slot, self.cached_durable_pos) {
831 if let Some(before_wait) = before_wait.take() {
832 before_wait();
833 }
834 match self.wait_durable(
835 slot.wire_seq,
836 shutdown,
837 #[cfg(feature = "tick-to-trade")]
838 tracker,
839 ) {
840 GateOutcome::Shutdown => return Admission::Shutdown,
841 GateOutcome::Open(released) => backing = backing.min(released),
842 }
843 }
844 let Some(change) = change else {
845 return Admission::Reply {
846 connection_id: slot.connection_id,
847 backing,
848 };
849 };
850 let wire_seq = WireSeq::new(slot.wire_seq);
851 match backing {
852 Backing::Policy => {
853 let cause = if change.unwitnessed {
854 "unwitnessed change, in order"
855 } else {
856 "journaled change"
857 };
858 self.apply_change(wire_seq, change.policy, cause);
859 }
860 Backing::PrimaryOnly => self.defer_change(wire_seq, change),
861 }
862 Admission::GateOnly
863 }
864
865 fn defer_change(&mut self, wire_seq: WireSeq, change: AckPolicyChange) {
871 if self.is_stale(wire_seq) {
872 return;
873 }
874 let Some(policy) = AckPolicy::from_byte(change.policy) else {
875 self.refuse_unknown(wire_seq, change.policy);
876 return;
877 };
878 tracing::info!(
879 stage = self.stage,
880 policy = policy.as_str(),
881 wire_seq = wire_seq.get(),
882 "ack-policy change released on this node's disk alone: applied once durable under \
883 the policy before it"
884 );
885 self.pending.push_back(PendingChange {
886 wire_seq,
887 policy,
888 unwitnessed: change.unwitnessed,
889 });
890 }
891
892 #[inline]
913 fn wait_durable(
914 &mut self,
915 needed: u64,
916 shutdown: &AtomicBool,
917 #[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
918 ) -> GateOutcome {
919 let mut gate_waiter = self.inputs.wait.waiter();
920 loop {
921 if shutdown.load(Ordering::Relaxed) {
922 return GateOutcome::Shutdown;
923 }
924
925 let journal_pos = self.inputs.journal_persisted_wire_seq.load();
926 self.observe_forced(journal_pos);
927 let metrics_ref = self.inputs.replication_metrics.as_deref();
928 let active_ref = self.inputs.replica_active.as_ref();
929
930 #[cfg(feature = "tick-to-trade")]
940 tracker.observe(
941 journal_pos.get(),
942 policy_replica_cursor(&self.policy, journal_pos, metrics_ref, active_ref),
950 trace::mono_trace_ns(),
951 );
952
953 let (status, blocker) =
954 evaluate_gate(&self.policy, needed, journal_pos, metrics_ref, active_ref);
955 self.cached_durable_pos = status.durable_pos;
956 self.inputs
957 .utilization
958 .policy_degraded
959 .store(status.degraded, Ordering::Relaxed);
960
961 if self
970 .accrual_timer
971 .tick(GATE_ACCRUAL_INTERVAL, gate_waiter.spinning())
972 .is_some()
973 {
974 self.degraded_logger.tick(
975 &self.policy,
976 &self.inputs.utilization,
977 status.degraded,
978 Instant::now(),
979 DEGRADED_LOG_INTERVAL,
980 );
981 }
982
983 if self.cached_durable_pos >= needed {
984 match blocker {
995 Some(Blocker::Journal) => {
996 self.inputs
997 .utilization
998 .gate_journal
999 .fetch_add(1, Ordering::Relaxed);
1000 }
1001 Some(Blocker::Replication) => {
1002 self.inputs
1003 .utilization
1004 .gate_replication
1005 .fetch_add(1, Ordering::Relaxed);
1006 }
1007 None => {}
1008 }
1009 return GateOutcome::Open(Backing::Policy);
1010 }
1011
1012 if journal_pos.get() >= needed && self.degraded_release_open() {
1018 return GateOutcome::Open(Backing::PrimaryOnly);
1019 }
1020 gate_waiter.idle();
1021 }
1022 }
1023
1024 #[inline]
1035 pub(crate) fn idle_recheck(&mut self, now: Instant) {
1036 if now.duration_since(self.last_policy_check) < POLICY_CHECK_INTERVAL {
1037 return;
1038 }
1039 self.last_policy_check = now;
1040 let journal_pos = self.inputs.journal_persisted_wire_seq.load();
1041 let status = evaluate_durability(
1042 &self.policy,
1043 journal_pos,
1044 self.inputs.replication_metrics.as_deref(),
1045 self.inputs.replica_active.as_ref(),
1046 );
1047 self.degraded_logger.tick(
1048 &self.policy,
1049 &self.inputs.utilization,
1050 status.degraded,
1051 now,
1052 DEGRADED_LOG_INTERVAL,
1053 );
1054 self.cached_durable_pos = status.durable_pos;
1055 self.settle_pending_if_durable(journal_pos);
1056 self.degraded_release_open();
1061 }
1062
1063 #[inline]
1075 pub(crate) fn after_batch(&mut self) {
1076 let ticked_at = Instant::now();
1077 let degraded_now = self
1078 .inputs
1079 .utilization
1080 .policy_degraded
1081 .load(Ordering::Relaxed);
1082 self.degraded_logger.tick(
1083 &self.policy,
1084 &self.inputs.utilization,
1085 degraded_now,
1086 ticked_at,
1087 DEGRADED_LOG_INTERVAL,
1088 );
1089 self.last_policy_check = ticked_at;
1090 }
1091}
1092
1093fn halt_start(loss: LastLoss, seen: Option<Instant>, now: Instant) -> Instant {
1101 match loss {
1102 LastLoss::At(at) => at,
1103 LastLoss::Unknown => seen.unwrap_or(now),
1104 }
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109 use std::sync::mpsc;
1110
1111 use melin_transport_core::WireSeq;
1112 use melin_transport_core::lineage::{ForcedPolicy, ForcedPolicyWriter, forced_policy_cell};
1113 use melin_transport_core::replica_presence::ReplicaPresence;
1114
1115 use super::*;
1116 use crate::policy_control::test_support::lineage;
1117
1118 const CORRUPT: u8 = 0xEE;
1121
1122 fn build(
1125 initial: AckPolicy,
1126 journal_pos: u64,
1127 replication_metrics: Option<Arc<ReplicationMetrics>>,
1128 replica_active: Option<[Arc<AtomicBool>; 2]>,
1129 ) -> (DurabilityGate, ForcedPolicyWriter) {
1130 build_from(
1131 GateStart::settled(initial),
1132 journal_pos,
1133 replication_metrics,
1134 replica_active,
1135 )
1136 }
1137
1138 fn build_from(
1140 start: GateStart,
1141 journal_pos: u64,
1142 replication_metrics: Option<Arc<ReplicationMetrics>>,
1143 replica_active: Option<[Arc<AtomicBool>; 2]>,
1144 ) -> (DurabilityGate, ForcedPolicyWriter) {
1145 let (forced_writer, forced) = forced_policy_cell();
1146 let inputs = GateInputs {
1147 journal_persisted_wire_seq: DurableWireSeqCursor::detached(WireSeq::new(journal_pos)),
1148 replication_metrics,
1149 replica_active,
1150 utilization: Arc::new(StageUtilization::new()),
1151 wait: WaitStrategy::SpinThenYield,
1152 degraded_release: None,
1153 };
1154 let policy = GatePolicy { start, forced };
1155 (DurabilityGate::new(inputs, policy, "test"), forced_writer)
1156 }
1157
1158 fn pending_start(
1161 in_force: AckPolicy,
1162 wire_seq: u64,
1163 policy: AckPolicy,
1164 unwitnessed: bool,
1165 ) -> GateStart {
1166 GateStart {
1167 in_force,
1168 pending: Some(PendingChange {
1169 wire_seq: WireSeq::new(wire_seq),
1170 policy,
1171 unwitnessed,
1172 }),
1173 }
1174 }
1175
1176 fn forced_change(wire_seq: u64, policy: AckPolicyByte) -> ForcedPolicy {
1178 ForcedPolicy {
1179 wire_seq: WireSeq::new(wire_seq),
1180 policy,
1181 }
1182 }
1183
1184 #[track_caller]
1186 fn assert_applied_through(gate: &DurabilityGate, wire_seq: u64) {
1187 assert_eq!(gate.applied_through, Some(WireSeq::new(wire_seq)));
1188 }
1189
1190 fn gate(policy: AckPolicy, journal_pos: u64) -> DurabilityGate {
1194 build(policy, journal_pos, None, None).0
1195 }
1196
1197 fn gate_with_replicas(policy: AckPolicy, pos: u64) -> DurabilityGate {
1200 let metrics = ReplicationMetrics::default();
1201 for slot in 0..2 {
1202 metrics.acked_sequence[slot].store(pos, Ordering::Relaxed);
1203 metrics.in_memory_sequence[slot].store(pos, Ordering::Relaxed);
1204 }
1205 build(
1206 policy,
1207 pos,
1208 Some(Arc::new(metrics)),
1209 Some([
1210 Arc::new(AtomicBool::new(true)),
1211 Arc::new(AtomicBool::new(true)),
1212 ]),
1213 )
1214 .0
1215 }
1216
1217 fn gate_with_one_replica(
1222 initial: AckPolicy,
1223 journal_pos: u64,
1224 ) -> (DurabilityGate, Arc<ReplicationMetrics>, ForcedPolicyWriter) {
1225 gate_with_one_replica_from(GateStart::settled(initial), journal_pos)
1226 }
1227
1228 fn gate_with_one_replica_from(
1230 start: GateStart,
1231 journal_pos: u64,
1232 ) -> (DurabilityGate, Arc<ReplicationMetrics>, ForcedPolicyWriter) {
1233 let metrics = Arc::new(ReplicationMetrics::default());
1234 let (gate, forced) = build_from(
1235 start,
1236 journal_pos,
1237 Some(Arc::clone(&metrics)),
1238 Some([
1239 Arc::new(AtomicBool::new(true)),
1240 Arc::new(AtomicBool::new(false)),
1241 ]),
1242 );
1243 (gate, metrics, forced)
1244 }
1245
1246 fn replica_holds(metrics: &ReplicationMetrics, pos: u64) {
1249 metrics.in_memory_sequence[0].store(pos, Ordering::Release);
1250 }
1251
1252 fn replica_persists(metrics: &ReplicationMetrics, pos: u64) {
1255 replica_holds(metrics, pos);
1256 metrics.acked_sequence[0].store(pos, Ordering::Release);
1257 }
1258
1259 fn reply(wire_seq: u64) -> OutputSlot<u8, u8> {
1261 OutputSlot {
1262 wire_seq,
1263 payload: OutputPayload::Report(0),
1264 is_last_in_request: true,
1265 ..OutputSlot::default()
1266 }
1267 }
1268
1269 fn change(wire_seq: u64, policy: AckPolicy, unwitnessed: bool) -> OutputSlot<u8, u8> {
1271 change_byte(wire_seq, policy.to_byte(), unwitnessed)
1272 }
1273
1274 fn change_byte(wire_seq: u64, policy: AckPolicyByte, unwitnessed: bool) -> OutputSlot<u8, u8> {
1275 OutputSlot {
1276 wire_seq,
1277 payload: OutputPayload::AckPolicyChange(melin_journal::AckPolicyChange {
1278 policy,
1279 unwitnessed,
1280 }),
1281 ..OutputSlot::default()
1282 }
1283 }
1284
1285 fn admit(
1286 gate: &mut DurabilityGate,
1287 slot: &OutputSlot<u8, u8>,
1288 shutdown: &AtomicBool,
1289 ) -> Admission {
1290 #[cfg(feature = "tick-to-trade")]
1291 let mut tracker = GateCrossTracker::new(slot.wire_seq);
1292 gate.admit(
1293 slot,
1294 shutdown,
1295 #[cfg(feature = "tick-to-trade")]
1296 &mut tracker,
1297 || {},
1298 )
1299 }
1300
1301 const HOLD: Duration = Duration::from_millis(50);
1304
1305 const OPEN_WITHIN: Duration = Duration::from_secs(10);
1307
1308 fn admit_watched(
1314 gate: &mut DurabilityGate,
1315 slot: &OutputSlot<u8, u8>,
1316 hold: Option<Duration>,
1317 release: impl FnOnce(),
1318 ) -> (bool, bool) {
1319 let shutdown = AtomicBool::new(false);
1320 std::thread::scope(|s| {
1321 let admitting = s.spawn(|| admit(gate, slot, &shutdown));
1322 let held = hold.is_some_and(|hold| {
1323 std::thread::sleep(hold);
1324 !admitting.is_finished()
1325 });
1326 release();
1327 let deadline = Instant::now() + OPEN_WITHIN;
1328 while !admitting.is_finished() && Instant::now() < deadline {
1329 std::thread::sleep(Duration::from_millis(1));
1330 }
1331 shutdown.store(true, Ordering::Relaxed);
1332 let outcome = admitting.join().expect("the admitting thread panicked");
1333 (held, outcome != Admission::Shutdown)
1334 })
1335 }
1336
1337 #[track_caller]
1339 fn admit_now(gate: &mut DurabilityGate, slot: &OutputSlot<u8, u8>) {
1340 let (_, opened) = admit_watched(gate, slot, None, || {});
1341 assert!(opened, "slot {} did not open", slot.wire_seq);
1342 }
1343
1344 #[track_caller]
1347 fn assert_held_until(
1348 gate: &mut DurabilityGate,
1349 slot: &OutputSlot<u8, u8>,
1350 release: impl FnOnce(),
1351 ) {
1352 let (held, opened) = admit_watched(gate, slot, Some(HOLD), release);
1353 assert!(
1354 held,
1355 "slot {} was admitted before its release",
1356 slot.wire_seq
1357 );
1358 assert!(
1359 opened,
1360 "slot {} stayed held after its release",
1361 slot.wire_seq
1362 );
1363 }
1364
1365 fn in_force(gate: &DurabilityGate) -> Option<AckPolicy> {
1367 gate.inputs
1368 .utilization
1369 .ack_policy_in_force
1370 .load()
1371 .map(|byte| AckPolicy::from_byte(byte).expect("a known policy"))
1372 }
1373
1374 fn wait(gate: &mut DurabilityGate, needed: u64, shutdown: &AtomicBool) -> GateOutcome {
1375 #[cfg(feature = "tick-to-trade")]
1376 let mut tracker = GateCrossTracker::new(needed);
1377 gate.wait_durable(
1378 needed,
1379 shutdown,
1380 #[cfg(feature = "tick-to-trade")]
1381 &mut tracker,
1382 )
1383 }
1384
1385 fn degraded_nanos(gate: &DurabilityGate) -> u64 {
1386 gate.inputs
1387 .utilization
1388 .policy_degraded_nanos
1389 .load(Ordering::Relaxed)
1390 }
1391
1392 fn attributions(gate: &DurabilityGate) -> (u64, u64) {
1393 let u = &gate.inputs.utilization;
1394 (
1395 u.gate_journal.load(Ordering::Relaxed),
1396 u.gate_replication.load(Ordering::Relaxed),
1397 )
1398 }
1399
1400 fn let_time_pass() {
1402 std::thread::sleep(Duration::from_millis(2));
1403 }
1404
1405 #[test]
1406 fn startup_resolves_the_policy_and_its_durable_position() {
1407 let g = gate(AckPolicy::Disk, 5);
1408 assert_eq!(g.active_policy, AckPolicy::Disk);
1409 assert_eq!(g.cached_durable_pos, 5);
1410 assert!(!g.inputs.utilization.policy_degraded.load(Ordering::Relaxed));
1411
1412 let g = gate(AckPolicy::DiskAndRam, 5);
1413 assert_eq!(g.cached_durable_pos, 0, "an unsatisfiable policy pins 0");
1414 assert!(g.inputs.utilization.policy_degraded.load(Ordering::Relaxed));
1415 }
1416
1417 #[test]
1421 fn a_switch_resets_the_cached_position_and_reseeds_the_logger() {
1422 let mut g = gate(AckPolicy::Disk, 5);
1423 g.apply_change(WireSeq::new(1), AckPolicy::TwoDisks.to_byte(), "test");
1424 assert_eq!(g.active_policy, AckPolicy::TwoDisks);
1425 assert_eq!(g.policy, AckPolicy::TwoDisks.to_policy());
1426 assert_eq!(g.cached_durable_pos, 0);
1427 assert_eq!(in_force(&g), Some(AckPolicy::TwoDisks));
1428
1429 let mut g = gate(AckPolicy::DiskAndRam, 5);
1430 let_time_pass();
1431 g.apply_change(WireSeq::new(1), AckPolicy::Disk.to_byte(), "test");
1432 assert_eq!(g.active_policy, AckPolicy::Disk);
1433 assert!(
1434 degraded_nanos(&g) > 0,
1435 "the re-seed flushes the degraded interval before the switch"
1436 );
1437 }
1438
1439 #[test]
1443 fn a_change_to_the_policy_in_force_is_applied_without_a_switch() {
1444 let mut g = gate(AckPolicy::Disk, 5);
1445 g.apply_change(WireSeq::new(3), AckPolicy::Disk.to_byte(), "test");
1446 assert_eq!(g.active_policy, AckPolicy::Disk);
1447 assert_eq!(g.cached_durable_pos, 5, "no switch: cached position kept");
1448 assert_applied_through(&g, 3);
1449 g.apply_change(WireSeq::new(2), AckPolicy::TwoDisks.to_byte(), "test");
1450 assert_eq!(g.active_policy, AckPolicy::Disk, "stale behind it");
1451
1452 let mut g = gate(AckPolicy::DiskAndRam, 5);
1453 let_time_pass();
1454 g.apply_change(WireSeq::new(1), AckPolicy::DiskAndRam.to_byte(), "test");
1455 assert_eq!(degraded_nanos(&g), 0, "no re-seed, so nothing flushed");
1456 }
1457
1458 #[test]
1466 fn the_initial_policy_is_the_lineages_with_its_newest_change_pending() {
1467 let disk = AckPolicy::Disk.to_byte();
1468 let two_disks = AckPolicy::TwoDisks.to_byte();
1469 let corrupt = AckPolicyByte(CORRUPT);
1470 use Replication::{Disabled, Enabled};
1471 for replication in [Enabled, Disabled] {
1472 assert_eq!(
1473 initial_gate_policy(None, AckPolicy::Disk, replication),
1474 Ok(GateStart::settled(AckPolicy::Disk))
1475 );
1476 let refused = initial_gate_policy(
1477 lineage(corrupt, false, 3, None),
1478 AckPolicy::Disk,
1479 replication,
1480 )
1481 .expect_err("an unknown byte is refused");
1482 assert!(refused.contains("238"), "names the byte: {refused}");
1483 assert_eq!(
1484 initial_gate_policy(
1485 lineage(disk, true, 9, Some(two_disks)),
1486 AckPolicy::Ram,
1487 replication,
1488 ),
1489 Ok(match replication {
1490 Enabled => pending_start(AckPolicy::TwoDisks, 9, AckPolicy::Disk, true),
1491 Disabled => GateStart::settled(AckPolicy::Ram),
1492 }),
1493 "replication {replication:?}"
1494 );
1495 }
1496 assert_eq!(
1497 initial_gate_policy(lineage(two_disks, false, 1, None), AckPolicy::Disk, Enabled),
1498 Ok(GateStart::settled(AckPolicy::TwoDisks)),
1499 "the lineage's first change: nothing to apply again"
1500 );
1501 assert_eq!(
1502 initial_gate_policy(
1503 lineage(disk, false, 4, Some(two_disks)),
1504 AckPolicy::Disk,
1505 Enabled
1506 ),
1507 Ok(pending_start(
1508 AckPolicy::TwoDisks,
1509 4,
1510 AckPolicy::Disk,
1511 false
1512 )),
1513 "requested equal to the newest: still pending"
1514 );
1515 let refused = initial_gate_policy(
1516 lineage(disk, false, 4, Some(corrupt)),
1517 AckPolicy::Disk,
1518 Enabled,
1519 )
1520 .expect_err("an unknown previous byte is refused");
1521 assert!(refused.contains("238"), "names the byte: {refused}");
1522 }
1523
1524 #[test]
1531 fn a_pending_change_is_held_until_durable_under_the_policy_before_it() {
1532 let (mut g, metrics, _forced) = gate_with_one_replica_from(
1533 pending_start(AckPolicy::DiskAndRam, 5, AckPolicy::Disk, false),
1534 10,
1535 );
1536 assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam));
1537 replica_holds(&metrics, 4);
1538 assert_held_until(&mut g, &reply(8), || replica_holds(&metrics, 5));
1539 assert_eq!(g.active_policy, AckPolicy::Disk);
1540 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
1541 assert!(g.pending.is_empty(), "consumed");
1542 assert_applied_through(&g, 5);
1543 admit_now(&mut g, &reply(9));
1544 }
1545
1546 #[test]
1551 fn an_unwitnessed_pending_change_is_applied_once_on_disk() {
1552 let (g, _) = build_from(
1554 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Disk, true),
1555 4,
1556 None,
1557 None,
1558 );
1559 assert_eq!(g.active_policy, AckPolicy::Disk);
1560 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
1561 assert!(g.pending.is_empty());
1562
1563 let (mut g, _) = build_from(
1566 pending_start(AckPolicy::DiskAndRam, 6, AckPolicy::Disk, true),
1567 4,
1568 None,
1569 None,
1570 );
1571 assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam));
1572 let journal = g.inputs.journal_persisted_wire_seq.clone();
1573 assert_held_until(&mut g, &reply(7), || journal.store(WireSeq::new(7)));
1574 assert_eq!(g.active_policy, AckPolicy::Disk);
1575 assert_applied_through(&g, 6);
1576 }
1577
1578 #[test]
1581 fn a_pending_change_already_durable_is_applied_at_the_start() {
1582 let metrics = ReplicationMetrics::default();
1583 for slot in 0..2 {
1584 metrics.in_memory_sequence[slot].store(6, Ordering::Relaxed);
1585 }
1586 let (g, _) = build_from(
1587 pending_start(AckPolicy::Ram, 3, AckPolicy::Disk, false),
1588 6,
1589 Some(Arc::new(metrics)),
1590 Some([
1591 Arc::new(AtomicBool::new(true)),
1592 Arc::new(AtomicBool::new(true)),
1593 ]),
1594 );
1595 assert_eq!(g.active_policy, AckPolicy::Disk);
1596 assert!(g.pending.is_empty());
1597 }
1598
1599 #[test]
1604 fn a_boot_change_after_a_pending_one_is_applied_after_it() {
1605 let (mut g, metrics, _forced) = gate_with_one_replica_from(
1606 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Disk, false),
1607 10,
1608 );
1609 assert_held_until(&mut g, &change(4, AckPolicy::TwoDisks, false), || {
1610 replica_holds(&metrics, 3)
1611 });
1612 assert_eq!(g.active_policy, AckPolicy::TwoDisks);
1613 replica_holds(&metrics, 5);
1615 assert_held_until(&mut g, &reply(5), || replica_persists(&metrics, 5));
1616 }
1617
1618 #[test]
1622 fn the_idle_recheck_applies_a_confirmed_pending_change() {
1623 let (mut g, metrics, _forced) = gate_with_one_replica_from(
1624 pending_start(AckPolicy::DiskAndRam, 5, AckPolicy::Disk, false),
1625 10,
1626 );
1627 let checked = g.last_policy_check;
1628 replica_holds(&metrics, 4);
1629 g.idle_recheck(checked + POLICY_CHECK_INTERVAL);
1630 assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam), "not yet held");
1631 assert!(!g.pending.is_empty());
1632
1633 replica_holds(&metrics, 5);
1634 g.idle_recheck(checked + 2 * POLICY_CHECK_INTERVAL);
1635 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
1636 assert!(g.pending.is_empty());
1637 }
1638
1639 #[test]
1643 fn a_forced_change_overtakes_a_pending_one() {
1644 let (mut g, mut forced) = build_from(
1645 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Ram, false),
1646 4,
1647 None,
1648 None,
1649 );
1650 let journal = g.inputs.journal_persisted_wire_seq.clone();
1651 forced.store(forced_change(5, AckPolicy::Disk.to_byte()));
1652 assert_held_until(&mut g, &reply(6), || journal.store(WireSeq::new(6)));
1653 assert_eq!(
1654 g.active_policy,
1655 AckPolicy::Disk,
1656 "not the stale pending `ram`"
1657 );
1658 assert_applied_through(&g, 5);
1659 assert!(g.pending.is_empty());
1660 }
1661
1662 #[test]
1664 fn shutdown_releases_a_wait_on_the_pending_change() {
1665 let (mut g, _) = build_from(
1666 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Disk, false),
1667 4,
1668 None,
1669 None,
1670 );
1671 let shutdown = AtomicBool::new(true);
1672 assert_eq!(admit(&mut g, &reply(4), &shutdown), Admission::Shutdown);
1673 assert!(!g.pending.is_empty());
1674 assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
1675 }
1676
1677 #[test]
1678 fn an_open_gate_attributes_exactly_once() {
1679 let mut g = gate(AckPolicy::Disk, 5);
1680 let shutdown = AtomicBool::new(false);
1681 assert!(matches!(
1682 wait(&mut g, 3, &shutdown),
1683 GateOutcome::Open(Backing::Policy)
1684 ));
1685 assert_eq!(attributions(&g), (1, 0), "disk opens on the journal");
1686 assert_eq!(g.cached_durable_pos, 5);
1687 }
1688
1689 #[test]
1694 fn shutdown_releases_a_wedged_gate_without_attribution() {
1695 let (tx, rx) = mpsc::channel();
1696 std::thread::spawn(move || {
1697 let mut g = gate(AckPolicy::DiskAndRam, 5);
1698 let shutdown = AtomicBool::new(true);
1699 let outcome = wait(&mut g, 3, &shutdown);
1700 let _ = tx.send((matches!(outcome, GateOutcome::Shutdown), attributions(&g)));
1703 });
1704 let (was_shutdown, attributed) = rx
1705 .recv_timeout(Duration::from_secs(10))
1706 .expect("the wedged gate ignored shutdown");
1707 assert!(was_shutdown);
1708 assert_eq!(attributed, (0, 0));
1709 }
1710
1711 #[test]
1715 fn shutdown_wins_over_a_gate_ready_to_open() {
1716 let mut g = gate_with_replicas(AckPolicy::Disk, 5);
1717 let shutdown = AtomicBool::new(true);
1718 assert!(matches!(wait(&mut g, 3, &shutdown), GateOutcome::Shutdown));
1719 assert_eq!(attributions(&g), (0, 0));
1720 }
1721
1722 #[test]
1726 fn a_change_applies_from_its_own_slot_on() {
1727 let (mut g, metrics, _forced) = gate_with_one_replica(AckPolicy::Disk, 10);
1728 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
1729 admit_now(&mut g, &reply(4));
1730 admit_now(&mut g, &change(5, AckPolicy::DiskAndRam, false));
1731 assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
1732 assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam));
1733 assert_held_until(&mut g, &reply(6), || replica_holds(&metrics, 6));
1735 }
1736
1737 #[test]
1740 fn a_change_slot_goes_to_no_connection() {
1741 let mut g = gate(AckPolicy::Disk, 10);
1742 let shutdown = AtomicBool::new(false);
1743 let to = |connection_id, slot: OutputSlot<u8, u8>| OutputSlot {
1744 connection_id,
1745 ..slot
1746 };
1747 assert_eq!(
1748 admit(&mut g, &to(9, reply(3)), &shutdown),
1749 Admission::Reply {
1750 connection_id: 9,
1751 backing: Backing::Policy
1752 }
1753 );
1754 assert_eq!(
1755 admit(&mut g, &to(9, change(4, AckPolicy::Disk, true)), &shutdown),
1756 Admission::GateOnly
1757 );
1758 assert_applied_through(&g, 4);
1759 assert_eq!(
1760 admit(&mut g, &to(9, change(4, AckPolicy::Ram, false)), &shutdown),
1761 Admission::GateOnly,
1762 "stale, and still the gate's"
1763 );
1764 assert_eq!(g.active_policy, AckPolicy::Disk, "the stale one ignored");
1765 }
1766
1767 #[test]
1772 fn a_change_is_held_until_the_old_policy_clears_its_entry() {
1773 let (mut g, metrics, _forced) = gate_with_one_replica(AckPolicy::DiskAndRam, 10);
1774 assert_held_until(&mut g, &change(5, AckPolicy::Disk, false), || {
1775 replica_holds(&metrics, 5)
1776 });
1777 assert_eq!(g.active_policy, AckPolicy::Disk);
1778 admit_now(&mut g, &reply(8));
1779 }
1780
1781 #[test]
1785 fn two_pending_changes_are_never_merged() {
1786 let (mut g, metrics, _forced) = gate_with_one_replica(AckPolicy::Disk, 10);
1787 admit_now(&mut g, &change(3, AckPolicy::DiskAndRam, false));
1788 assert_held_until(&mut g, &reply(4), || replica_holds(&metrics, 4));
1789 assert_held_until(&mut g, &change(6, AckPolicy::Disk, false), || {
1790 replica_holds(&metrics, 6)
1791 });
1792 assert_eq!(g.active_policy, AckPolicy::Disk);
1793 admit_now(&mut g, &reply(7));
1794 }
1795
1796 struct Primary {
1801 gate: DurabilityGate,
1802 journal: DurableWireSeqCursor,
1803 metrics: Arc<ReplicationMetrics>,
1804 active: Arc<AtomicBool>,
1805 presence: Arc<ReplicaPresence>,
1806 forced: ForcedPolicyWriter,
1807 }
1808
1809 impl Primary {
1810 fn new(policy: AckPolicy, pos: u64, connected: bool, grace: Option<Duration>) -> Self {
1814 Self::starting(GateStart::settled(policy), pos, connected, grace)
1815 }
1816
1817 fn starting(start: GateStart, pos: u64, connected: bool, grace: Option<Duration>) -> Self {
1819 let metrics = Arc::new(ReplicationMetrics::default());
1820 let active = Arc::new(AtomicBool::new(false));
1821 let presence = Arc::new(ReplicaPresence::new());
1822 let journal = DurableWireSeqCursor::detached(WireSeq::new(pos));
1823 let (forced_writer, forced) = forced_policy_cell();
1824 let inputs = GateInputs {
1825 journal_persisted_wire_seq: journal.clone(),
1826 replication_metrics: Some(Arc::clone(&metrics)),
1827 replica_active: Some([Arc::clone(&active), Arc::new(AtomicBool::new(false))]),
1828 utilization: Arc::new(StageUtilization::new()),
1829 wait: WaitStrategy::SpinThenYield,
1830 degraded_release: grace.map(|grace| DegradedRelease {
1831 presence: Arc::clone(&presence),
1832 grace,
1833 }),
1834 };
1835 let p = Self {
1836 gate: DurabilityGate::new(inputs, GatePolicy { start, forced }, "test"),
1837 journal,
1838 metrics,
1839 active,
1840 presence,
1841 forced: forced_writer,
1842 };
1843 if connected {
1844 p.replica_streams(pos);
1845 }
1846 p
1847 }
1848
1849 fn replica_leaves(&self) {
1851 self.active.store(false, Ordering::Release);
1852 self.presence.lower();
1853 }
1854
1855 fn replica_streams(&self, pos: u64) {
1858 drop(self.presence.lift());
1859 replica_persists(&self.metrics, pos);
1860 self.active.store(true, Ordering::Release);
1861 }
1862
1863 fn journal_at(&self, pos: u64) {
1864 self.journal.store(WireSeq::new(pos));
1865 }
1866
1867 fn degraded_acks(&self) -> u64 {
1868 self.gate
1869 .inputs
1870 .utilization
1871 .degraded_acks
1872 .load(Ordering::Relaxed)
1873 }
1874
1875 fn admit_within(
1879 &mut self,
1880 slot: &OutputSlot<u8, u8>,
1881 patience: Duration,
1882 ) -> Option<Admission> {
1883 let shutdown = AtomicBool::new(false);
1884 std::thread::scope(|s| {
1885 let gate = &mut self.gate;
1886 let admitting = s.spawn(|| admit(gate, slot, &shutdown));
1887 let deadline = Instant::now() + patience;
1888 while !admitting.is_finished() && Instant::now() < deadline {
1889 std::thread::sleep(Duration::from_millis(1));
1890 }
1891 shutdown.store(true, Ordering::Relaxed);
1892 match admitting.join().expect("the admitting thread panicked") {
1893 Admission::Shutdown => None,
1894 admitted => Some(admitted),
1895 }
1896 })
1897 }
1898
1899 fn release(&mut self, wire_seq: u64, patience: Duration) -> Option<Backing> {
1902 self.release_slot(&reply(wire_seq), patience)
1903 }
1904
1905 fn release_slot(
1908 &mut self,
1909 slot: &OutputSlot<u8, u8>,
1910 patience: Duration,
1911 ) -> Option<Backing> {
1912 match self.admit_within(slot, patience)? {
1913 Admission::Reply { backing, .. } => Some(backing),
1914 other => panic!("a reply slot admitted as {other:?}"),
1915 }
1916 }
1917 }
1918
1919 const PATIENCE: Duration = Duration::from_millis(300);
1922
1923 const NO_GRACE: Option<Duration> = Some(Duration::ZERO);
1926
1927 #[test]
1928 fn no_degraded_release_before_the_grace_period() {
1929 let mut p = Primary::new(
1930 AckPolicy::DiskAndRam,
1931 5,
1932 false,
1933 Some(Duration::from_secs(60)),
1934 );
1935 assert_eq!(p.release(5, PATIENCE), None, "held for the grace period");
1936 assert_eq!(p.degraded_acks(), 0);
1937 }
1938
1939 #[test]
1940 fn a_halted_node_past_the_grace_releases_on_its_own_fsync_marked() {
1941 for policy in [AckPolicy::Ram, AckPolicy::DiskAndRam, AckPolicy::TwoDisks] {
1942 let mut p = Primary::new(policy, 5, false, NO_GRACE);
1943 assert_eq!(
1944 p.release(5, PATIENCE),
1945 Some(Backing::PrimaryOnly),
1946 "{policy}"
1947 );
1948 assert_eq!(p.gate.cached_durable_pos, 0, "the policy confirmed nothing");
1949 }
1950 }
1951
1952 #[test]
1953 fn the_grace_period_runs_from_the_loss() {
1954 let mut p = Primary::new(
1955 AckPolicy::DiskAndRam,
1956 5,
1957 true,
1958 Some(Duration::from_millis(150)),
1959 );
1960 p.journal_at(6);
1961 p.replica_leaves();
1962 let started = Instant::now();
1963 assert_eq!(
1964 p.release(6, Duration::from_secs(5)),
1965 Some(Backing::PrimaryOnly)
1966 );
1967 assert!(
1968 started.elapsed() >= Duration::from_millis(150),
1969 "released before the grace ran out: {:?}",
1970 started.elapsed()
1971 );
1972 }
1973
1974 #[test]
1978 fn an_unseen_rejoin_restarts_the_grace_period() {
1979 let grace = Duration::from_millis(1_000);
1982 let mut p = Primary::new(AckPolicy::DiskAndRam, 5, true, Some(grace));
1983 p.replica_leaves();
1984 assert!(!p.gate.degraded_release_open(), "the halt has just begun");
1985 std::thread::sleep(Duration::from_millis(700));
1986 p.replica_streams(5);
1988 p.replica_leaves();
1989 let second_loss = Instant::now();
1990 std::thread::sleep(Duration::from_millis(400));
1991 assert!(
1993 !p.gate.degraded_release_open(),
1994 "aged from the first loss: {:?} since the second",
1995 second_loss.elapsed()
1996 );
1997 std::thread::sleep(grace.saturating_sub(second_loss.elapsed()));
1998 assert!(
1999 p.gate.degraded_release_open(),
2000 "past the grace from the second loss"
2001 );
2002 }
2003
2004 #[test]
2008 fn a_missing_loss_stamp_ages_from_the_first_sighting() {
2009 let seen = Instant::now();
2010 let now = seen + Duration::from_secs(10);
2011 let lost = seen + Duration::from_secs(3);
2012 assert_eq!(
2013 halt_start(LastLoss::Unknown, None, now),
2014 now,
2015 "first sighting"
2016 );
2017 assert_eq!(halt_start(LastLoss::Unknown, Some(seen), now), seen);
2018 assert_eq!(halt_start(LastLoss::At(lost), Some(seen), now), lost);
2019 assert_eq!(halt_start(LastLoss::At(lost), None, now), lost);
2020 }
2021
2022 #[test]
2025 fn the_halt_start_is_tracked_while_halted_and_dropped_on_a_replica() {
2026 let mut p = Primary::new(AckPolicy::DiskAndRam, 5, true, NO_GRACE);
2027 assert!(!p.gate.degraded_release_open());
2028 assert_eq!(p.gate.halt_started, None);
2029
2030 let before = Instant::now();
2031 p.replica_leaves();
2032 assert!(p.gate.degraded_release_open());
2033 let started = p.gate.halt_started.expect("halted");
2034 assert!(started >= before, "the loss's own stamp");
2035 assert!(p.gate.degraded_release_open());
2036 assert_eq!(p.gate.halt_started, Some(started), "the same halt");
2037
2038 p.replica_streams(5);
2039 assert!(!p.gate.degraded_release_open());
2040 assert_eq!(p.gate.halt_started, None);
2041 assert!(!p.gate.releasing_degraded);
2042 }
2043
2044 #[test]
2046 fn the_release_follows_a_switch_to_and_from_a_policy_needing_a_replica() {
2047 let mut p = Primary::new(AckPolicy::Disk, 5, false, NO_GRACE);
2048 assert!(!p.gate.degraded_release_open(), "disk needs no replica");
2049 p.gate.switch_to(AckPolicy::TwoDisks, "test");
2050 assert!(p.gate.degraded_release_open());
2051 assert!(p.gate.releasing_degraded);
2052 p.gate.switch_to(AckPolicy::Disk, "test");
2053 assert!(!p.gate.degraded_release_open());
2054 assert!(!p.gate.releasing_degraded);
2055 }
2056
2057 #[test]
2060 fn no_degraded_release_under_disk() {
2061 let mut p = Primary::new(AckPolicy::Disk, 5, false, NO_GRACE);
2062 assert_eq!(p.release(5, PATIENCE), Some(Backing::Policy));
2063 assert_eq!(p.release(6, PATIENCE), None, "not yet on the journal");
2064 assert!(!p.gate.releasing_degraded);
2065 }
2066
2067 #[test]
2068 fn no_degraded_release_with_the_switch_off() {
2069 let mut p = Primary::new(AckPolicy::DiskAndRam, 5, false, None);
2070 assert_eq!(p.release(5, PATIENCE), None, "the stall, unchanged");
2071 assert_eq!(p.degraded_acks(), 0);
2072 }
2073
2074 #[test]
2077 fn no_degraded_release_while_a_replica_is_connected_but_behind() {
2078 let mut p = Primary::new(AckPolicy::TwoDisks, 5, true, NO_GRACE);
2079 p.journal_at(9);
2080 assert_eq!(p.release(9, PATIENCE), None, "held for the replica");
2081 }
2082
2083 #[test]
2086 fn an_unfsynced_slot_under_ram_is_held_until_the_primary_fsyncs_it() {
2087 let mut p = Primary::new(AckPolicy::Ram, 5, false, NO_GRACE);
2088 assert_eq!(
2089 p.release(6, PATIENCE),
2090 None,
2091 "the primary's disk does not hold it yet"
2092 );
2093 p.journal_at(6);
2094 assert_eq!(p.release(6, PATIENCE), Some(Backing::PrimaryOnly));
2095 }
2096
2097 #[test]
2098 fn a_full_ack_resumes_once_a_replica_streams_again() {
2099 let mut p = Primary::new(AckPolicy::DiskAndRam, 5, false, NO_GRACE);
2100 assert_eq!(p.release(5, PATIENCE), Some(Backing::PrimaryOnly));
2101 assert!(p.gate.releasing_degraded);
2102
2103 p.journal_at(7);
2104 p.replica_streams(7);
2105 assert_eq!(
2106 p.release(6, PATIENCE),
2107 Some(Backing::Policy),
2108 "the policy is met again"
2109 );
2110 assert_eq!(p.release(7, PATIENCE), Some(Backing::Policy));
2111 assert_eq!(p.gate.cached_durable_pos, 7);
2112
2113 p.journal_at(9);
2116 assert_eq!(p.release(9, PATIENCE), None);
2117 assert!(!p.gate.releasing_degraded);
2118 }
2119
2120 #[test]
2135 fn a_query_over_state_confirmed_before_the_loss_is_answered_primary_only() {
2136 let grace = Duration::from_millis(100);
2137 let mut p = Primary::new(AckPolicy::DiskAndRam, 5, true, Some(grace));
2138 assert_eq!(
2139 p.release(5, PATIENCE),
2140 Some(Backing::Policy),
2141 "before the loss the policy confirms everything through 5"
2142 );
2143 assert_eq!(p.gate.cached_durable_pos, 5);
2144
2145 p.replica_leaves();
2146 std::thread::sleep(grace);
2147 let due = p.gate.last_policy_check + POLICY_CHECK_INTERVAL;
2148 p.gate.idle_recheck(due);
2149 assert_eq!(
2150 p.gate.cached_durable_pos, 0,
2151 "with no replica streaming the evaluation pins the durable position to 0"
2152 );
2153
2154 let query = OutputSlot {
2155 wire_seq: 5,
2156 payload: OutputPayload::QueryResponse(0),
2157 is_last_in_request: true,
2158 ..OutputSlot::default()
2159 };
2160 let backing = p.release_slot(&query, PATIENCE);
2161 assert_eq!(
2162 backing,
2163 Some(Backing::PrimaryOnly),
2164 "marked, though every replica confirmed position 5 before it left"
2165 );
2166 let ends = BatchEnds::new();
2167 assert_eq!(
2168 p.gate.batch_end(Backing::PrimaryOnly, &ends),
2169 ends.primary_only
2170 );
2171 assert_eq!(p.degraded_acks(), 1, "counted in melin_degraded_acks_total");
2172 }
2173
2174 #[test]
2178 fn after_a_degraded_release_the_next_slot_is_still_evaluated_and_still_degraded() {
2179 let mut p = Primary::new(AckPolicy::DiskAndRam, 9, false, NO_GRACE);
2180 assert_eq!(p.release(5, PATIENCE), Some(Backing::PrimaryOnly));
2181 assert!(
2182 slot_needs_gate(&reply(6), p.gate.cached_durable_pos),
2183 "the next slot is not waved through on the degraded release"
2184 );
2185 assert_eq!(
2186 p.release(6, PATIENCE),
2187 Some(Backing::PrimaryOnly),
2188 "never a full BatchEnd"
2189 );
2190 assert_eq!(p.gate.cached_durable_pos, 0);
2191 }
2192
2193 #[test]
2199 fn a_change_released_degraded_is_applied_only_once_the_policy_before_it_confirms_it() {
2200 let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
2201 assert_eq!(
2202 p.admit_within(&change(4, AckPolicy::TwoDisks, false), PATIENCE),
2203 Some(Admission::GateOnly)
2204 );
2205 assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam, "not applied");
2206 assert_eq!(p.gate.applied_through, None);
2207 assert_eq!(p.gate.pending.len(), 1, "deferred");
2208 assert_eq!(p.release(5, PATIENCE), Some(Backing::PrimaryOnly));
2209
2210 drop(p.presence.lift());
2214 replica_holds(&p.metrics, 6);
2215 p.active.store(true, Ordering::Release);
2216 assert_eq!(
2217 p.release(6, PATIENCE),
2218 None,
2219 "judged under two-disks, which the replica's disk does not meet yet"
2220 );
2221 assert_eq!(p.gate.active_policy, AckPolicy::TwoDisks);
2222 assert!(p.gate.pending.is_empty());
2223 replica_persists(&p.metrics, 6);
2224 assert_eq!(p.release(6, PATIENCE), Some(Backing::Policy));
2225 }
2226
2227 #[test]
2231 fn a_pending_change_released_degraded_stays_pending_and_marks_what_follows() {
2232 let mut p = Primary::starting(
2233 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Ram, false),
2234 5,
2235 false,
2236 NO_GRACE,
2237 );
2238 assert_eq!(p.release(4, PATIENCE), Some(Backing::PrimaryOnly));
2239 assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam);
2240 assert_eq!(p.gate.pending.len(), 1);
2241
2242 p.replica_streams(5);
2243 assert_eq!(p.release(5, PATIENCE), Some(Backing::Policy));
2244 assert_eq!(p.gate.active_policy, AckPolicy::Ram, "applied in order");
2245 assert!(p.gate.pending.is_empty());
2246 }
2247
2248 #[test]
2251 fn changes_released_degraded_are_applied_one_by_one() {
2252 let mut p = Primary::new(AckPolicy::TwoDisks, 8, false, NO_GRACE);
2253 for (wire_seq, policy) in [(3, AckPolicy::DiskAndRam), (5, AckPolicy::Disk)] {
2254 assert_eq!(
2255 p.admit_within(&change(wire_seq, policy, false), PATIENCE),
2256 Some(Admission::GateOnly)
2257 );
2258 }
2259 assert_eq!(p.gate.pending.len(), 2);
2260
2261 drop(p.presence.lift());
2264 p.metrics.acked_sequence[0].store(3, Ordering::Release);
2265 replica_holds(&p.metrics, 5);
2266 p.active.store(true, Ordering::Release);
2267 assert_eq!(p.release(8, PATIENCE), Some(Backing::Policy));
2268 assert_eq!(p.gate.active_policy, AckPolicy::Disk);
2269 assert_applied_through(&p.gate, 5);
2270 }
2271
2272 #[test]
2275 fn an_unwitnessed_change_released_degraded_applies_once_on_disk() {
2276 let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
2277 assert_eq!(
2278 p.admit_within(&change(4, AckPolicy::Disk, true), PATIENCE),
2279 Some(Admission::GateOnly)
2280 );
2281 assert_eq!(p.release(5, PATIENCE), Some(Backing::Policy));
2282 assert_eq!(p.gate.active_policy, AckPolicy::Disk);
2283 }
2284
2285 #[test]
2291 fn a_pending_change_released_degraded_marks_a_slot_the_policy_then_confirms() {
2292 let mut p = Primary::starting(
2293 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Ram, false),
2294 5,
2295 false,
2296 NO_GRACE,
2297 );
2298 let shutdown = AtomicBool::new(false);
2299 let mut waits = 0;
2300 let mut before_wait = Some(|| waits += 1);
2301 #[cfg(feature = "tick-to-trade")]
2302 let mut tracker = GateCrossTracker::new(5);
2303 let backing = match p.gate.admit_pending(
2304 &shutdown,
2305 #[cfg(feature = "tick-to-trade")]
2306 &mut tracker,
2307 &mut before_wait,
2308 ) {
2309 GateOutcome::Open(backing) => backing,
2310 GateOutcome::Shutdown => panic!("no shutdown was requested"),
2311 };
2312 assert_eq!(backing, Backing::PrimaryOnly);
2313 assert_eq!(p.gate.pending.len(), 1, "left pending");
2314
2315 p.replica_streams(5);
2316 let admitted = p.gate.admit_slot(
2317 &reply(5),
2318 backing,
2319 &shutdown,
2320 #[cfg(feature = "tick-to-trade")]
2321 &mut tracker,
2322 &mut before_wait,
2323 );
2324 assert_eq!(
2325 admitted,
2326 Admission::Reply {
2327 connection_id: 0,
2328 backing: Backing::PrimaryOnly
2329 },
2330 "never a full BatchEnd behind an unapplied change"
2331 );
2332 assert_eq!(
2333 p.gate.cached_durable_pos, 5,
2334 "the slot's wait opened in full"
2335 );
2336 assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam, "not applied");
2337 assert_eq!(p.gate.pending.len(), 1);
2338 assert_eq!(waits, 1, "the flush hook ran once, before the first wait");
2339
2340 p.journal_at(6);
2341 p.replica_streams(6);
2342 assert_eq!(p.release(6, PATIENCE), Some(Backing::Policy));
2343 assert_eq!(p.gate.active_policy, AckPolicy::Ram, "applied in order");
2344 assert_applied_through(&p.gate, 3);
2345 assert!(p.gate.pending.is_empty());
2346 }
2347
2348 #[test]
2351 fn a_change_released_degraded_behind_a_forced_one_is_stale() {
2352 let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
2353 p.forced.store(forced_change(5, AckPolicy::Ram.to_byte()));
2355 assert_eq!(p.release(6, PATIENCE), Some(Backing::PrimaryOnly));
2356 assert_eq!(p.gate.active_policy, AckPolicy::Ram);
2357 assert_eq!(
2358 p.admit_within(&change(4, AckPolicy::TwoDisks, false), PATIENCE),
2359 Some(Admission::GateOnly)
2360 );
2361 assert!(p.gate.pending.is_empty(), "stale, not deferred");
2362 assert_applied_through(&p.gate, 5);
2363 assert_eq!(p.gate.active_policy, AckPolicy::Ram);
2364 }
2365
2366 #[test]
2370 fn an_unknown_policy_leaves_an_earlier_deferred_change_pending() {
2371 let mut p = Primary::new(AckPolicy::DiskAndRam, 8, false, NO_GRACE);
2372 assert_eq!(
2373 p.admit_within(&change(3, AckPolicy::TwoDisks, false), PATIENCE),
2374 Some(Admission::GateOnly)
2375 );
2376 assert_eq!(
2378 p.admit_within(&change_byte(5, AckPolicyByte(CORRUPT), false), PATIENCE),
2379 Some(Admission::GateOnly)
2380 );
2381 assert_eq!(p.gate.pending.len(), 1, "the unknown one is not deferred");
2382 assert_eq!(p.gate.last_refused, Some(WireSeq::new(5)));
2383 p.forced.store(forced_change(6, AckPolicyByte(CORRUPT)));
2385 assert_eq!(p.release(7, PATIENCE), Some(Backing::PrimaryOnly));
2386 assert_eq!(p.gate.last_refused, Some(WireSeq::new(6)));
2387 assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam);
2388 assert_eq!(p.gate.applied_through, None, "neither recorded as applied");
2389 assert_eq!(p.gate.pending.len(), 1);
2390
2391 p.replica_streams(8);
2392 assert_eq!(p.release(8, PATIENCE), Some(Backing::Policy));
2393 assert_eq!(p.gate.active_policy, AckPolicy::TwoDisks, "not made stale");
2394 assert_applied_through(&p.gate, 3);
2395 assert!(p.gate.pending.is_empty());
2396 }
2397
2398 #[test]
2402 fn a_forced_change_overtakes_a_deferred_one() {
2403 let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
2404 assert_eq!(
2405 p.admit_within(&change(4, AckPolicy::Ram, false), PATIENCE),
2406 Some(Admission::GateOnly)
2407 );
2408 assert_eq!(p.gate.pending.len(), 1, "deferred");
2409 p.forced.store(forced_change(5, AckPolicy::Disk.to_byte()));
2410 assert_eq!(p.release(6, PATIENCE), Some(Backing::Policy));
2411 assert_eq!(
2412 p.gate.active_policy,
2413 AckPolicy::Disk,
2414 "not the stale deferred `ram`"
2415 );
2416 assert_applied_through(&p.gate, 5);
2417 assert!(p.gate.pending.is_empty());
2418 }
2419
2420 #[test]
2422 fn the_terminator_follows_the_backing_and_counts_degraded_acks() {
2423 let p = Primary::new(AckPolicy::DiskAndRam, 5, false, NO_GRACE);
2424 let ends = BatchEnds::new();
2425 assert_eq!(p.gate.batch_end(Backing::Policy, &ends), ends.policy);
2426 assert_eq!(p.degraded_acks(), 0);
2427 assert_eq!(
2428 p.gate.batch_end(Backing::PrimaryOnly, &ends),
2429 ends.primary_only
2430 );
2431 assert_eq!(p.degraded_acks(), 1);
2432 assert_eq!(
2433 ends.policy[4],
2434 melin_wire_protocol::control_codec::TAG_BATCH_END
2435 );
2436 assert_eq!(
2437 ends.primary_only[4],
2438 melin_wire_protocol::control_codec::TAG_BATCH_END_DEGRADED
2439 );
2440 }
2441
2442 #[test]
2448 fn a_forced_swap_frees_an_earlier_stuck_slot_once_its_entry_is_on_disk() {
2449 let (mut g, mut forced) = build(AckPolicy::DiskAndRam, 4, None, None);
2451 let journal = g.inputs.journal_persisted_wire_seq.clone();
2452 forced.store(forced_change(6, AckPolicy::Disk.to_byte()));
2453 assert_held_until(&mut g, &reply(3), || journal.store(WireSeq::new(6)));
2455 assert_eq!(g.active_policy, AckPolicy::Disk);
2456 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
2457 assert!(
2462 degraded_nanos(&g) > 0,
2463 "degraded time accrued during the wedge"
2464 );
2465 assert_eq!(attributions(&g), (1, 0), "opened on the journal, once");
2466
2467 let cached = g.cached_durable_pos;
2468 admit_now(&mut g, &change(5, AckPolicy::TwoDisks, false));
2469 admit_now(&mut g, &change(6, AckPolicy::Disk, true));
2470 assert_eq!(g.active_policy, AckPolicy::Disk, "stale changes ignored");
2471 assert_eq!(g.cached_durable_pos, cached, "no switch, no reset");
2472
2473 journal.store(WireSeq::new(7));
2474 admit_now(&mut g, &change(7, AckPolicy::Ram, false));
2475 assert_eq!(g.active_policy, AckPolicy::Ram);
2476 }
2477
2478 #[test]
2482 fn a_forced_change_applied_in_order_is_not_reapplied() {
2483 let (mut g, metrics, mut forced) = gate_with_one_replica(AckPolicy::DiskAndRam, 10);
2484 replica_holds(&metrics, 3);
2485 forced.store(forced_change(2, AckPolicy::Disk.to_byte()));
2486 admit_now(&mut g, &change(2, AckPolicy::Disk, true));
2487 admit_now(&mut g, &change(3, AckPolicy::DiskAndRam, false));
2488 assert_held_until(&mut g, &reply(4), || replica_holds(&metrics, 4));
2489 assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
2490 }
2491
2492 #[test]
2497 fn an_unknown_policy_in_a_change_keeps_the_prior_policy() {
2498 let (mut g, mut forced) = build(AckPolicy::Disk, 4, None, None);
2499 let journal = g.inputs.journal_persisted_wire_seq.clone();
2500 admit_now(&mut g, &change_byte(4, AckPolicyByte(CORRUPT), false));
2501 assert_eq!(g.active_policy, AckPolicy::Disk);
2502 assert_eq!(g.policy, AckPolicy::Disk.to_policy());
2503 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
2504 assert_eq!(g.cached_durable_pos, 4, "no switch: cached position kept");
2505 assert_eq!(g.applied_through, None, "not recorded as applied");
2506 assert_eq!(g.last_refused, Some(WireSeq::new(4)));
2507
2508 forced.store(forced_change(6, AckPolicyByte(CORRUPT)));
2511 assert_held_until(&mut g, &reply(5), || journal.store(WireSeq::new(6)));
2512 assert_eq!(g.active_policy, AckPolicy::Disk);
2513 assert_eq!(g.applied_through, None, "not recorded as applied");
2514 assert_eq!(g.last_refused, Some(WireSeq::new(6)));
2516
2517 let mut g = gate(AckPolicy::DiskAndRam, 5);
2519 let_time_pass();
2520 g.apply_change(WireSeq::new(1), AckPolicyByte(CORRUPT), "test");
2521 assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
2522 assert_eq!(degraded_nanos(&g), 0, "no re-seed on an unknown byte");
2523 }
2524
2525 #[test]
2527 fn before_wait_runs_only_when_the_slot_waits() {
2528 let mut g = gate(AckPolicy::Disk, 5);
2529 let shutdown = AtomicBool::new(false);
2530 for (wire_seq, waits) in [(5, false), (6, true)] {
2531 let mut ran = false;
2532 if waits {
2533 let journal = g.inputs.journal_persisted_wire_seq.clone();
2535 journal.store(WireSeq::new(wire_seq));
2536 }
2537 #[cfg(feature = "tick-to-trade")]
2538 let mut tracker = GateCrossTracker::new(wire_seq);
2539 let outcome = g.admit(
2540 &reply(wire_seq),
2541 &shutdown,
2542 #[cfg(feature = "tick-to-trade")]
2543 &mut tracker,
2544 || ran = true,
2545 );
2546 assert_eq!(
2547 outcome,
2548 Admission::Reply {
2549 connection_id: 0,
2550 backing: Backing::Policy
2551 }
2552 );
2553 assert_eq!(ran, waits, "slot {wire_seq}");
2554 }
2555 }
2556
2557 #[test]
2558 fn idle_recheck_waits_out_the_check_interval() {
2559 let mut g = gate(AckPolicy::Disk, 5);
2560 let checked = g.last_policy_check;
2561 g.inputs.journal_persisted_wire_seq.store(WireSeq::new(9));
2562
2563 g.idle_recheck(checked + POLICY_CHECK_INTERVAL - Duration::from_millis(1));
2564 assert_eq!(g.cached_durable_pos, 5, "too early: no re-evaluation");
2565 assert_eq!(g.last_policy_check, checked);
2566
2567 let due = checked + POLICY_CHECK_INTERVAL;
2568 g.idle_recheck(due);
2569 assert_eq!(g.cached_durable_pos, 9);
2570 assert_eq!(g.last_policy_check, due);
2571 }
2572
2573 #[test]
2574 fn a_batch_defers_the_next_idle_recheck() {
2575 let mut g = gate(AckPolicy::Disk, 5);
2576 g.last_policy_check = Instant::now()
2578 .checked_sub(2 * POLICY_CHECK_INTERVAL)
2579 .expect("the monotonic clock is past two check intervals");
2580 g.inputs.journal_persisted_wire_seq.store(WireSeq::new(9));
2581
2582 g.after_batch();
2583 g.idle_recheck(Instant::now());
2584 assert_eq!(
2585 g.cached_durable_pos, 5,
2586 "the batch counted as the check, so the idle path waits"
2587 );
2588 }
2589}