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, RequestId, 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::{self, BATCH_END_FRAME_LEN};
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 {
153 policy: [u8; BATCH_END_FRAME_LEN],
155 primary_only: [u8; BATCH_END_FRAME_LEN],
157}
158
159impl BatchEnds {
160 pub(crate) fn new() -> Self {
161 let encode = |response| {
162 let mut buf = [0u8; BATCH_END_FRAME_LEN];
163 let written = control_codec::encode_transport_response(&response, &mut buf)
164 .expect("a batch terminator encodes into `BATCH_END_FRAME_LEN` bytes");
165 assert_eq!(
166 written, BATCH_END_FRAME_LEN,
167 "a batch terminator fills its frame"
168 );
169 buf
170 };
171 Self {
172 policy: encode(TransportResponse::BatchEnd { id: RequestId(0) }),
173 primary_only: encode(TransportResponse::BatchEndDegraded { id: RequestId(0) }),
174 }
175 }
176
177 #[inline]
180 fn policy(&mut self, id: RequestId) -> &[u8] {
181 control_codec::write_terminal_id(&mut self.policy, id);
182 &self.policy
183 }
184
185 #[inline]
187 fn primary_only(&mut self, id: RequestId) -> &[u8] {
188 control_codec::write_terminal_id(&mut self.primary_only, id);
189 &self.primary_only
190 }
191}
192
193#[must_use]
195enum GateOutcome {
196 Open(Backing),
198 Shutdown,
202}
203
204pub(crate) trait GateWait {
213 fn before_wait(&mut self);
216
217 fn while_waiting(&mut self);
222}
223
224struct Hook<'h, H: GateWait> {
228 hook: &'h mut H,
229 started: bool,
230}
231
232impl<'h, H: GateWait> Hook<'h, H> {
233 fn new(hook: &'h mut H) -> Self {
234 Self {
235 hook,
236 started: false,
237 }
238 }
239
240 #[inline]
243 fn starting(&mut self) {
244 if !self.started {
245 self.started = true;
246 self.hook.before_wait();
247 }
248 }
249}
250
251#[must_use]
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub(crate) enum Admission {
256 Reply {
260 connection_id: u64,
261 backing: Backing,
262 },
263 GateOnly,
268 Shutdown,
271}
272
273pub struct GatePolicy {
278 pub start: GateStart,
281 pub forced: ForcedPolicyReader,
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub struct GateStart {
302 pub(crate) in_force: AckPolicy,
304 pub(crate) pending: Option<PendingChange>,
306}
307
308impl GateStart {
309 pub fn settled(policy: AckPolicy) -> Self {
313 Self {
314 in_force: policy,
315 pending: None,
316 }
317 }
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub(crate) struct PendingChange {
324 pub(crate) wire_seq: WireSeq,
326 pub(crate) policy: AckPolicy,
328 pub(crate) unwitnessed: bool,
332}
333
334pub(crate) fn initial_gate_policy(
369 lineage: Option<LineagePolicy>,
370 requested: AckPolicy,
371 replication: Replication,
372) -> Result<GateStart, String> {
373 let Some(lineage) = lineage else {
374 return Ok(GateStart::settled(requested));
375 };
376 let newest = known_lineage_policy(lineage.policy())?;
377 match replication {
378 Replication::Disabled => return Ok(GateStart::settled(requested)),
379 Replication::Enabled => {}
380 }
381 let Some(previous) = lineage.previous else {
382 return Ok(GateStart::settled(newest));
383 };
384 Ok(GateStart {
385 in_force: known_lineage_policy(previous)?,
386 pending: Some(PendingChange {
387 wire_seq: lineage.wire_seq,
388 policy: newest,
389 unwitnessed: lineage.change.unwitnessed,
390 }),
391 })
392}
393
394fn known_lineage_policy(byte: AckPolicyByte) -> Result<AckPolicy, String> {
396 AckPolicy::from_byte(byte).ok_or_else(|| {
397 format!(
398 "the journal's ack policy (byte {}) is unknown to this build; \
399 refusing to serve under it",
400 byte.0
401 )
402 })
403}
404
405pub(crate) struct GateInputs {
409 pub journal_persisted_wire_seq: DurableWireSeqCursor,
412 pub replication_metrics: Option<Arc<ReplicationMetrics>>,
416 pub replica_active: Option<[Arc<AtomicBool>; 2]>,
423 pub utilization: Arc<StageUtilization>,
427 pub wait: WaitStrategy,
429 pub degraded_release: Option<DegradedRelease>,
432}
433
434pub(crate) struct DurabilityGate {
439 inputs: GateInputs,
440 stage: &'static str,
442 forced: ForcedPolicyReader,
444 applied_through: Option<WireSeq>,
449 last_refused: Option<WireSeq>,
456 pending: VecDeque<PendingChange>,
464 active_policy: AckPolicy,
465 needs_replica: bool,
469 policy: Policy,
470 cached_durable_pos: u64,
474 degraded_logger: DegradationLogger,
475 last_policy_check: Instant,
478 accrual_timer: AmortizedTimer,
485 halt_started: Option<Instant>,
489 releasing_degraded: bool,
492}
493
494impl DurabilityGate {
495 pub(crate) fn new(inputs: GateInputs, policy: GatePolicy, stage: &'static str) -> Self {
502 let GatePolicy {
503 start:
504 GateStart {
505 in_force: active_policy,
506 pending,
507 },
508 forced,
509 } = policy;
510 inputs
511 .utilization
512 .ack_policy_in_force
513 .store(Some(active_policy.to_byte()));
514 let policy = active_policy.to_policy();
515 let now = Instant::now();
516 let journal_pos = inputs.journal_persisted_wire_seq.load();
517 let status = evaluate_durability(
518 &policy,
519 journal_pos,
520 inputs.replication_metrics.as_deref(),
521 inputs.replica_active.as_ref(),
522 );
523 inputs
524 .utilization
525 .policy_degraded
526 .store(status.degraded, Ordering::Relaxed);
527 let degraded_logger = if status.degraded {
528 DegradationLogger::new_starting_degraded(now, &policy)
529 } else {
530 DegradationLogger::new(now)
531 };
532 if let Some(pending) = pending {
533 tracing::info!(
534 stage,
535 in_force = active_policy.as_str(),
536 pending = pending.policy.as_str(),
537 wire_seq = pending.wire_seq.get(),
538 unwitnessed = pending.unwitnessed,
539 "the lineage's newest ack-policy change is applied again before any reply"
540 );
541 }
542 let mut gate = Self {
543 inputs,
544 stage,
545 forced,
546 applied_through: None,
547 last_refused: None,
548 pending: pending.into_iter().collect(),
549 active_policy,
550 needs_replica: active_policy.needs_replica(),
551 policy,
552 cached_durable_pos: status.durable_pos,
553 degraded_logger,
554 last_policy_check: now,
555 accrual_timer: AmortizedTimer::new(),
556 halt_started: None,
557 releasing_degraded: false,
558 };
559 gate.settle_pending_if_durable(journal_pos);
560 gate
561 }
562
563 fn settle_pending_if_durable(&mut self, journal_pos: WireSeq) {
576 while let Some(&pending) = self.pending.front() {
577 if self.cached_durable_pos < pending.wire_seq.get()
578 && !(pending.unwitnessed && journal_pos >= pending.wire_seq)
579 {
580 return;
581 }
582 self.apply_front_pending();
583 }
584 }
585
586 fn apply_front_pending(&mut self) {
590 let Some(pending) = self.pending.pop_front() else {
591 return;
592 };
593 let cause = if pending.unwitnessed {
594 "pending unwitnessed change, on disk"
595 } else {
596 "pending change, durable under the policy before it"
597 };
598 self.apply_change(pending.wire_seq, pending.policy.to_byte(), cause);
599 }
600
601 #[inline]
605 pub(crate) fn batch_end<'e>(
606 &self,
607 backing: Backing,
608 ends: &'e mut BatchEnds,
609 id: RequestId,
610 ) -> &'e [u8] {
611 match backing {
612 Backing::Policy => ends.policy(id),
613 Backing::PrimaryOnly => {
614 self.inputs
615 .utilization
616 .degraded_acks
617 .fetch_add(1, Ordering::Relaxed);
618 ends.primary_only(id)
619 }
620 }
621 }
622
623 fn degraded_release_open(&mut self) -> bool {
634 let Some(release) = self.inputs.degraded_release.as_ref() else {
635 return false;
636 };
637 let grace = release.grace;
638 let Some(loss) = release.presence.last_loss() else {
639 self.halt_started = None;
640 if self.releasing_degraded {
641 self.log_degraded_release(false, true, grace);
642 }
643 return false;
644 };
645 let now = Instant::now();
646 let started = halt_start(loss, self.halt_started, now);
647 self.halt_started = Some(started);
648 let open = self.needs_replica && now.saturating_duration_since(started) >= grace;
652 if open != self.releasing_degraded {
653 self.log_degraded_release(open, false, grace);
654 }
655 open
656 }
657
658 #[cold]
660 fn log_degraded_release(&mut self, open: bool, replica_connected: bool, grace: Duration) {
661 self.releasing_degraded = open;
662 if open {
663 tracing::warn!(
664 stage = self.stage,
665 policy = self.active_policy.as_str(),
666 grace_ms = grace.as_millis() as u64,
667 "no replica for the grace period: replies held for the ack policy are now \
668 released once this node's own journal holds them, marked as backed by the \
669 primary alone (BatchEndDegraded)"
670 );
671 } else {
672 tracing::warn!(
673 stage = self.stage,
674 policy = self.active_policy.as_str(),
675 replica_connected,
676 "degraded release stopped: held replies wait for the ack policy again"
677 );
678 }
679 }
680
681 fn switch_to(&mut self, next: AckPolicy, cause: &'static str) {
696 if next == self.active_policy {
697 return;
698 }
699 tracing::info!(
700 stage = self.stage,
701 prev = self.active_policy.as_str(),
702 next = next.as_str(),
703 cause,
704 "ack policy switched"
705 );
706 self.active_policy = next;
707 self.needs_replica = next.needs_replica();
708 self.policy = next.to_policy();
709 self.cached_durable_pos = 0;
710 self.degraded_logger
711 .reseed(&self.inputs.utilization, Instant::now());
712 self.inputs
713 .utilization
714 .ack_policy_in_force
715 .store(Some(next.to_byte()));
716 }
717
718 fn apply_change(&mut self, wire_seq: WireSeq, policy: AckPolicyByte, cause: &'static str) {
723 if self.is_stale(wire_seq) {
724 return;
725 }
726 let Some(next) = AckPolicy::from_byte(policy) else {
727 self.refuse_unknown(wire_seq, policy);
728 return;
729 };
730 self.applied_through = Some(wire_seq);
731 self.switch_to(next, cause);
732 }
733
734 fn is_stale(&self, wire_seq: WireSeq) -> bool {
737 self.applied_through
738 .is_some_and(|through| wire_seq <= through)
739 }
740
741 #[cold]
750 fn refuse_unknown(&mut self, wire_seq: WireSeq, policy: AckPolicyByte) {
751 if self.last_refused == Some(wire_seq) {
752 return;
753 }
754 self.last_refused = Some(wire_seq);
755 tracing::error!(
756 stage = self.stage,
757 byte = policy.0,
758 wire_seq = wire_seq.get(),
759 "journaled ack-policy change names an unknown policy; retaining prior policy"
760 );
761 }
762
763 #[inline]
773 fn observe_forced(&mut self, journal_pos: WireSeq) {
774 self.settle_pending_if_durable(journal_pos);
775 if let Some(forced) = self.forced.load()
776 && journal_pos >= forced.wire_seq
777 {
778 self.apply_change(forced.wire_seq, forced.policy, "forced change");
779 }
780 }
781
782 #[inline]
811 pub(crate) fn admit<R: Copy, Q: Copy>(
812 &mut self,
813 slot: &OutputSlot<R, Q>,
814 shutdown: &AtomicBool,
815 #[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
816 hook: &mut impl GateWait,
817 ) -> Admission {
818 let mut hook = Hook::new(hook);
819 match self.admit_pending(
820 shutdown,
821 #[cfg(feature = "tick-to-trade")]
822 tracker,
823 &mut hook,
824 ) {
825 GateOutcome::Shutdown => Admission::Shutdown,
826 GateOutcome::Open(backing) => self.admit_slot(
827 slot,
828 backing,
829 shutdown,
830 #[cfg(feature = "tick-to-trade")]
831 tracker,
832 &mut hook,
833 ),
834 }
835 }
836
837 #[inline]
844 fn admit_pending(
845 &mut self,
846 shutdown: &AtomicBool,
847 #[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
848 hook: &mut Hook<'_, impl GateWait>,
849 ) -> GateOutcome {
850 while let Some(&pending) = self.pending.front() {
851 if pending.wire_seq.get() > self.cached_durable_pos {
852 hook.starting();
853 match self.wait_durable(
854 pending.wire_seq.get(),
855 shutdown,
856 #[cfg(feature = "tick-to-trade")]
857 tracker,
858 hook.hook,
859 ) {
860 GateOutcome::Shutdown => return GateOutcome::Shutdown,
861 GateOutcome::Open(Backing::PrimaryOnly) => {
862 return GateOutcome::Open(Backing::PrimaryOnly);
863 }
864 GateOutcome::Open(Backing::Policy) => {}
865 }
866 if self.pending.front() != Some(&pending) {
869 continue;
870 }
871 }
872 self.apply_front_pending();
878 }
879 GateOutcome::Open(Backing::Policy)
880 }
881
882 #[inline]
887 fn admit_slot<R: Copy, Q: Copy>(
888 &mut self,
889 slot: &OutputSlot<R, Q>,
890 mut backing: Backing,
891 shutdown: &AtomicBool,
892 #[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
893 hook: &mut Hook<'_, impl GateWait>,
894 ) -> Admission {
895 let change = match slot.payload {
896 OutputPayload::AckPolicyChange(change) => Some(change),
897 OutputPayload::Report(_)
898 | OutputPayload::QueryResponse(_)
899 | OutputPayload::BatchEnd => None,
900 };
901 if slot_needs_gate(slot, self.cached_durable_pos) {
902 hook.starting();
903 match self.wait_durable(
904 slot.wire_seq,
905 shutdown,
906 #[cfg(feature = "tick-to-trade")]
907 tracker,
908 hook.hook,
909 ) {
910 GateOutcome::Shutdown => return Admission::Shutdown,
911 GateOutcome::Open(released) => backing = backing.min(released),
912 }
913 }
914 let Some(change) = change else {
915 return Admission::Reply {
916 connection_id: slot.connection_id,
917 backing,
918 };
919 };
920 let wire_seq = WireSeq::new(slot.wire_seq);
921 match backing {
922 Backing::Policy => {
923 let cause = if change.unwitnessed {
924 "unwitnessed change, in order"
925 } else {
926 "journaled change"
927 };
928 self.apply_change(wire_seq, change.policy, cause);
929 }
930 Backing::PrimaryOnly => self.defer_change(wire_seq, change),
931 }
932 Admission::GateOnly
933 }
934
935 fn defer_change(&mut self, wire_seq: WireSeq, change: AckPolicyChange) {
941 if self.is_stale(wire_seq) {
942 return;
943 }
944 let Some(policy) = AckPolicy::from_byte(change.policy) else {
945 self.refuse_unknown(wire_seq, change.policy);
946 return;
947 };
948 tracing::info!(
949 stage = self.stage,
950 policy = policy.as_str(),
951 wire_seq = wire_seq.get(),
952 "ack-policy change released on this node's disk alone: applied once durable under \
953 the policy before it"
954 );
955 self.pending.push_back(PendingChange {
956 wire_seq,
957 policy,
958 unwitnessed: change.unwitnessed,
959 });
960 }
961
962 #[inline]
986 fn wait_durable(
987 &mut self,
988 needed: u64,
989 shutdown: &AtomicBool,
990 #[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
991 hook: &mut impl GateWait,
992 ) -> GateOutcome {
993 let mut gate_waiter = self.inputs.wait.waiter();
994 loop {
995 if shutdown.load(Ordering::Relaxed) {
996 return GateOutcome::Shutdown;
997 }
998
999 let journal_pos = self.inputs.journal_persisted_wire_seq.load();
1000 self.observe_forced(journal_pos);
1001 let metrics_ref = self.inputs.replication_metrics.as_deref();
1002 let active_ref = self.inputs.replica_active.as_ref();
1003
1004 #[cfg(feature = "tick-to-trade")]
1014 tracker.observe(
1015 journal_pos.get(),
1016 policy_replica_cursor(&self.policy, journal_pos, metrics_ref, active_ref),
1024 trace::mono_trace_ns(),
1025 );
1026
1027 let (status, blocker) =
1028 evaluate_gate(&self.policy, needed, journal_pos, metrics_ref, active_ref);
1029 self.cached_durable_pos = status.durable_pos;
1030 self.inputs
1031 .utilization
1032 .policy_degraded
1033 .store(status.degraded, Ordering::Relaxed);
1034
1035 if self
1044 .accrual_timer
1045 .tick(GATE_ACCRUAL_INTERVAL, gate_waiter.spinning())
1046 .is_some()
1047 {
1048 self.degraded_logger.tick(
1049 &self.policy,
1050 &self.inputs.utilization,
1051 status.degraded,
1052 Instant::now(),
1053 DEGRADED_LOG_INTERVAL,
1054 );
1055 }
1056
1057 if self.cached_durable_pos >= needed {
1058 match blocker {
1069 Some(Blocker::Journal) => {
1070 self.inputs
1071 .utilization
1072 .gate_journal
1073 .fetch_add(1, Ordering::Relaxed);
1074 }
1075 Some(Blocker::Replication) => {
1076 self.inputs
1077 .utilization
1078 .gate_replication
1079 .fetch_add(1, Ordering::Relaxed);
1080 }
1081 None => {}
1082 }
1083 return GateOutcome::Open(Backing::Policy);
1084 }
1085
1086 if journal_pos.get() >= needed && self.degraded_release_open() {
1092 return GateOutcome::Open(Backing::PrimaryOnly);
1093 }
1094 hook.while_waiting();
1095 gate_waiter.idle();
1096 }
1097 }
1098
1099 #[inline]
1110 pub(crate) fn idle_recheck(&mut self, now: Instant) {
1111 if now.duration_since(self.last_policy_check) < POLICY_CHECK_INTERVAL {
1112 return;
1113 }
1114 self.last_policy_check = now;
1115 let journal_pos = self.inputs.journal_persisted_wire_seq.load();
1116 let status = evaluate_durability(
1117 &self.policy,
1118 journal_pos,
1119 self.inputs.replication_metrics.as_deref(),
1120 self.inputs.replica_active.as_ref(),
1121 );
1122 self.degraded_logger.tick(
1123 &self.policy,
1124 &self.inputs.utilization,
1125 status.degraded,
1126 now,
1127 DEGRADED_LOG_INTERVAL,
1128 );
1129 self.cached_durable_pos = status.durable_pos;
1130 self.settle_pending_if_durable(journal_pos);
1131 self.degraded_release_open();
1136 }
1137
1138 #[inline]
1150 pub(crate) fn after_batch(&mut self) {
1151 let ticked_at = Instant::now();
1152 let degraded_now = self
1153 .inputs
1154 .utilization
1155 .policy_degraded
1156 .load(Ordering::Relaxed);
1157 self.degraded_logger.tick(
1158 &self.policy,
1159 &self.inputs.utilization,
1160 degraded_now,
1161 ticked_at,
1162 DEGRADED_LOG_INTERVAL,
1163 );
1164 self.last_policy_check = ticked_at;
1165 }
1166}
1167
1168fn halt_start(loss: LastLoss, seen: Option<Instant>, now: Instant) -> Instant {
1176 match loss {
1177 LastLoss::At(at) => at,
1178 LastLoss::Unknown => seen.unwrap_or(now),
1179 }
1180}
1181
1182#[cfg(test)]
1183mod tests {
1184 use std::sync::mpsc;
1185
1186 use melin_transport_core::WireSeq;
1187 use melin_transport_core::lineage::{ForcedPolicy, ForcedPolicyWriter, forced_policy_cell};
1188 use melin_transport_core::replica_presence::ReplicaPresence;
1189
1190 use super::*;
1191 use crate::policy_control::test_support::lineage;
1192
1193 const CORRUPT: u8 = 0xEE;
1196
1197 fn build(
1200 initial: AckPolicy,
1201 journal_pos: u64,
1202 replication_metrics: Option<Arc<ReplicationMetrics>>,
1203 replica_active: Option<[Arc<AtomicBool>; 2]>,
1204 ) -> (DurabilityGate, ForcedPolicyWriter) {
1205 build_from(
1206 GateStart::settled(initial),
1207 journal_pos,
1208 replication_metrics,
1209 replica_active,
1210 )
1211 }
1212
1213 fn build_from(
1215 start: GateStart,
1216 journal_pos: u64,
1217 replication_metrics: Option<Arc<ReplicationMetrics>>,
1218 replica_active: Option<[Arc<AtomicBool>; 2]>,
1219 ) -> (DurabilityGate, ForcedPolicyWriter) {
1220 let (forced_writer, forced) = forced_policy_cell();
1221 let inputs = GateInputs {
1222 journal_persisted_wire_seq: DurableWireSeqCursor::detached(WireSeq::new(journal_pos)),
1223 replication_metrics,
1224 replica_active,
1225 utilization: Arc::new(StageUtilization::new()),
1226 wait: WaitStrategy::SpinThenYield,
1227 degraded_release: None,
1228 };
1229 let policy = GatePolicy { start, forced };
1230 (DurabilityGate::new(inputs, policy, "test"), forced_writer)
1231 }
1232
1233 fn pending_start(
1236 in_force: AckPolicy,
1237 wire_seq: u64,
1238 policy: AckPolicy,
1239 unwitnessed: bool,
1240 ) -> GateStart {
1241 GateStart {
1242 in_force,
1243 pending: Some(PendingChange {
1244 wire_seq: WireSeq::new(wire_seq),
1245 policy,
1246 unwitnessed,
1247 }),
1248 }
1249 }
1250
1251 fn forced_change(wire_seq: u64, policy: AckPolicyByte) -> ForcedPolicy {
1253 ForcedPolicy {
1254 wire_seq: WireSeq::new(wire_seq),
1255 policy,
1256 }
1257 }
1258
1259 #[track_caller]
1261 fn assert_applied_through(gate: &DurabilityGate, wire_seq: u64) {
1262 assert_eq!(gate.applied_through, Some(WireSeq::new(wire_seq)));
1263 }
1264
1265 fn gate(policy: AckPolicy, journal_pos: u64) -> DurabilityGate {
1269 build(policy, journal_pos, None, None).0
1270 }
1271
1272 fn gate_with_replicas(policy: AckPolicy, pos: u64) -> DurabilityGate {
1275 let metrics = ReplicationMetrics::default();
1276 for slot in 0..2 {
1277 metrics.acked_sequence[slot].store(pos, Ordering::Relaxed);
1278 metrics.in_memory_sequence[slot].store(pos, Ordering::Relaxed);
1279 }
1280 build(
1281 policy,
1282 pos,
1283 Some(Arc::new(metrics)),
1284 Some([
1285 Arc::new(AtomicBool::new(true)),
1286 Arc::new(AtomicBool::new(true)),
1287 ]),
1288 )
1289 .0
1290 }
1291
1292 fn gate_with_one_replica(
1297 initial: AckPolicy,
1298 journal_pos: u64,
1299 ) -> (DurabilityGate, Arc<ReplicationMetrics>, ForcedPolicyWriter) {
1300 gate_with_one_replica_from(GateStart::settled(initial), journal_pos)
1301 }
1302
1303 fn gate_with_one_replica_from(
1305 start: GateStart,
1306 journal_pos: u64,
1307 ) -> (DurabilityGate, Arc<ReplicationMetrics>, ForcedPolicyWriter) {
1308 let metrics = Arc::new(ReplicationMetrics::default());
1309 let (gate, forced) = build_from(
1310 start,
1311 journal_pos,
1312 Some(Arc::clone(&metrics)),
1313 Some([
1314 Arc::new(AtomicBool::new(true)),
1315 Arc::new(AtomicBool::new(false)),
1316 ]),
1317 );
1318 (gate, metrics, forced)
1319 }
1320
1321 fn replica_holds(metrics: &ReplicationMetrics, pos: u64) {
1324 metrics.in_memory_sequence[0].store(pos, Ordering::Release);
1325 }
1326
1327 fn replica_persists(metrics: &ReplicationMetrics, pos: u64) {
1330 replica_holds(metrics, pos);
1331 metrics.acked_sequence[0].store(pos, Ordering::Release);
1332 }
1333
1334 fn reply(wire_seq: u64) -> OutputSlot<u8, u8> {
1336 OutputSlot {
1337 wire_seq,
1338 payload: OutputPayload::Report(0),
1339 is_last_in_request: true,
1340 ..OutputSlot::default()
1341 }
1342 }
1343
1344 fn change(wire_seq: u64, policy: AckPolicy, unwitnessed: bool) -> OutputSlot<u8, u8> {
1346 change_byte(wire_seq, policy.to_byte(), unwitnessed)
1347 }
1348
1349 fn change_byte(wire_seq: u64, policy: AckPolicyByte, unwitnessed: bool) -> OutputSlot<u8, u8> {
1350 OutputSlot {
1351 wire_seq,
1352 payload: OutputPayload::AckPolicyChange(melin_journal::AckPolicyChange {
1353 policy,
1354 unwitnessed,
1355 }),
1356 ..OutputSlot::default()
1357 }
1358 }
1359
1360 struct NoHook;
1362
1363 impl GateWait for NoHook {
1364 fn before_wait(&mut self) {}
1365 fn while_waiting(&mut self) {}
1366 }
1367
1368 struct Recording<F: FnMut()> {
1371 calls: Vec<&'static str>,
1372 during: F,
1373 }
1374
1375 impl<F: FnMut()> Recording<F> {
1376 fn new(during: F) -> Self {
1377 Self {
1378 calls: Vec::new(),
1379 during,
1380 }
1381 }
1382 }
1383
1384 impl<F: FnMut()> GateWait for Recording<F> {
1385 fn before_wait(&mut self) {
1386 self.calls.push("before");
1387 }
1388 fn while_waiting(&mut self) {
1389 self.calls.push("during");
1390 (self.during)();
1391 }
1392 }
1393
1394 fn admit(
1395 gate: &mut DurabilityGate,
1396 slot: &OutputSlot<u8, u8>,
1397 shutdown: &AtomicBool,
1398 ) -> Admission {
1399 #[cfg(feature = "tick-to-trade")]
1400 let mut tracker = GateCrossTracker::new(slot.wire_seq);
1401 gate.admit(
1402 slot,
1403 shutdown,
1404 #[cfg(feature = "tick-to-trade")]
1405 &mut tracker,
1406 &mut NoHook,
1407 )
1408 }
1409
1410 const HOLD: Duration = Duration::from_millis(50);
1413
1414 const OPEN_WITHIN: Duration = Duration::from_secs(10);
1416
1417 fn admit_watched(
1423 gate: &mut DurabilityGate,
1424 slot: &OutputSlot<u8, u8>,
1425 hold: Option<Duration>,
1426 release: impl FnOnce(),
1427 ) -> (bool, bool) {
1428 let shutdown = AtomicBool::new(false);
1429 std::thread::scope(|s| {
1430 let admitting = s.spawn(|| admit(gate, slot, &shutdown));
1431 let held = hold.is_some_and(|hold| {
1432 std::thread::sleep(hold);
1433 !admitting.is_finished()
1434 });
1435 release();
1436 let deadline = Instant::now() + OPEN_WITHIN;
1437 while !admitting.is_finished() && Instant::now() < deadline {
1438 std::thread::sleep(Duration::from_millis(1));
1439 }
1440 shutdown.store(true, Ordering::Relaxed);
1441 let outcome = admitting.join().expect("the admitting thread panicked");
1442 (held, outcome != Admission::Shutdown)
1443 })
1444 }
1445
1446 #[track_caller]
1448 fn admit_now(gate: &mut DurabilityGate, slot: &OutputSlot<u8, u8>) {
1449 let (_, opened) = admit_watched(gate, slot, None, || {});
1450 assert!(opened, "slot {} did not open", slot.wire_seq);
1451 }
1452
1453 #[track_caller]
1456 fn assert_held_until(
1457 gate: &mut DurabilityGate,
1458 slot: &OutputSlot<u8, u8>,
1459 release: impl FnOnce(),
1460 ) {
1461 let (held, opened) = admit_watched(gate, slot, Some(HOLD), release);
1462 assert!(
1463 held,
1464 "slot {} was admitted before its release",
1465 slot.wire_seq
1466 );
1467 assert!(
1468 opened,
1469 "slot {} stayed held after its release",
1470 slot.wire_seq
1471 );
1472 }
1473
1474 fn in_force(gate: &DurabilityGate) -> Option<AckPolicy> {
1476 gate.inputs
1477 .utilization
1478 .ack_policy_in_force
1479 .load()
1480 .map(|byte| AckPolicy::from_byte(byte).expect("a known policy"))
1481 }
1482
1483 fn wait(gate: &mut DurabilityGate, needed: u64, shutdown: &AtomicBool) -> GateOutcome {
1484 #[cfg(feature = "tick-to-trade")]
1485 let mut tracker = GateCrossTracker::new(needed);
1486 gate.wait_durable(
1487 needed,
1488 shutdown,
1489 #[cfg(feature = "tick-to-trade")]
1490 &mut tracker,
1491 &mut NoHook,
1492 )
1493 }
1494
1495 fn degraded_nanos(gate: &DurabilityGate) -> u64 {
1496 gate.inputs
1497 .utilization
1498 .policy_degraded_nanos
1499 .load(Ordering::Relaxed)
1500 }
1501
1502 fn attributions(gate: &DurabilityGate) -> (u64, u64) {
1503 let u = &gate.inputs.utilization;
1504 (
1505 u.gate_journal.load(Ordering::Relaxed),
1506 u.gate_replication.load(Ordering::Relaxed),
1507 )
1508 }
1509
1510 fn let_time_pass() {
1512 std::thread::sleep(Duration::from_millis(2));
1513 }
1514
1515 #[test]
1516 fn startup_resolves_the_policy_and_its_durable_position() {
1517 let g = gate(AckPolicy::Disk, 5);
1518 assert_eq!(g.active_policy, AckPolicy::Disk);
1519 assert_eq!(g.cached_durable_pos, 5);
1520 assert!(!g.inputs.utilization.policy_degraded.load(Ordering::Relaxed));
1521
1522 let g = gate(AckPolicy::DiskAndRam, 5);
1523 assert_eq!(g.cached_durable_pos, 0, "an unsatisfiable policy pins 0");
1524 assert!(g.inputs.utilization.policy_degraded.load(Ordering::Relaxed));
1525 }
1526
1527 #[test]
1531 fn a_switch_resets_the_cached_position_and_reseeds_the_logger() {
1532 let mut g = gate(AckPolicy::Disk, 5);
1533 g.apply_change(WireSeq::new(1), AckPolicy::TwoDisks.to_byte(), "test");
1534 assert_eq!(g.active_policy, AckPolicy::TwoDisks);
1535 assert_eq!(g.policy, AckPolicy::TwoDisks.to_policy());
1536 assert_eq!(g.cached_durable_pos, 0);
1537 assert_eq!(in_force(&g), Some(AckPolicy::TwoDisks));
1538
1539 let mut g = gate(AckPolicy::DiskAndRam, 5);
1540 let_time_pass();
1541 g.apply_change(WireSeq::new(1), AckPolicy::Disk.to_byte(), "test");
1542 assert_eq!(g.active_policy, AckPolicy::Disk);
1543 assert!(
1544 degraded_nanos(&g) > 0,
1545 "the re-seed flushes the degraded interval before the switch"
1546 );
1547 }
1548
1549 #[test]
1553 fn a_change_to_the_policy_in_force_is_applied_without_a_switch() {
1554 let mut g = gate(AckPolicy::Disk, 5);
1555 g.apply_change(WireSeq::new(3), AckPolicy::Disk.to_byte(), "test");
1556 assert_eq!(g.active_policy, AckPolicy::Disk);
1557 assert_eq!(g.cached_durable_pos, 5, "no switch: cached position kept");
1558 assert_applied_through(&g, 3);
1559 g.apply_change(WireSeq::new(2), AckPolicy::TwoDisks.to_byte(), "test");
1560 assert_eq!(g.active_policy, AckPolicy::Disk, "stale behind it");
1561
1562 let mut g = gate(AckPolicy::DiskAndRam, 5);
1563 let_time_pass();
1564 g.apply_change(WireSeq::new(1), AckPolicy::DiskAndRam.to_byte(), "test");
1565 assert_eq!(degraded_nanos(&g), 0, "no re-seed, so nothing flushed");
1566 }
1567
1568 #[test]
1576 fn the_initial_policy_is_the_lineages_with_its_newest_change_pending() {
1577 let disk = AckPolicy::Disk.to_byte();
1578 let two_disks = AckPolicy::TwoDisks.to_byte();
1579 let corrupt = AckPolicyByte(CORRUPT);
1580 use Replication::{Disabled, Enabled};
1581 for replication in [Enabled, Disabled] {
1582 assert_eq!(
1583 initial_gate_policy(None, AckPolicy::Disk, replication),
1584 Ok(GateStart::settled(AckPolicy::Disk))
1585 );
1586 let refused = initial_gate_policy(
1587 lineage(corrupt, false, 3, None),
1588 AckPolicy::Disk,
1589 replication,
1590 )
1591 .expect_err("an unknown byte is refused");
1592 assert!(refused.contains("238"), "names the byte: {refused}");
1593 assert_eq!(
1594 initial_gate_policy(
1595 lineage(disk, true, 9, Some(two_disks)),
1596 AckPolicy::Ram,
1597 replication,
1598 ),
1599 Ok(match replication {
1600 Enabled => pending_start(AckPolicy::TwoDisks, 9, AckPolicy::Disk, true),
1601 Disabled => GateStart::settled(AckPolicy::Ram),
1602 }),
1603 "replication {replication:?}"
1604 );
1605 }
1606 assert_eq!(
1607 initial_gate_policy(lineage(two_disks, false, 1, None), AckPolicy::Disk, Enabled),
1608 Ok(GateStart::settled(AckPolicy::TwoDisks)),
1609 "the lineage's first change: nothing to apply again"
1610 );
1611 assert_eq!(
1612 initial_gate_policy(
1613 lineage(disk, false, 4, Some(two_disks)),
1614 AckPolicy::Disk,
1615 Enabled
1616 ),
1617 Ok(pending_start(
1618 AckPolicy::TwoDisks,
1619 4,
1620 AckPolicy::Disk,
1621 false
1622 )),
1623 "requested equal to the newest: still pending"
1624 );
1625 let refused = initial_gate_policy(
1626 lineage(disk, false, 4, Some(corrupt)),
1627 AckPolicy::Disk,
1628 Enabled,
1629 )
1630 .expect_err("an unknown previous byte is refused");
1631 assert!(refused.contains("238"), "names the byte: {refused}");
1632 }
1633
1634 #[test]
1641 fn a_pending_change_is_held_until_durable_under_the_policy_before_it() {
1642 let (mut g, metrics, _forced) = gate_with_one_replica_from(
1643 pending_start(AckPolicy::DiskAndRam, 5, AckPolicy::Disk, false),
1644 10,
1645 );
1646 assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam));
1647 replica_holds(&metrics, 4);
1648 assert_held_until(&mut g, &reply(8), || replica_holds(&metrics, 5));
1649 assert_eq!(g.active_policy, AckPolicy::Disk);
1650 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
1651 assert!(g.pending.is_empty(), "consumed");
1652 assert_applied_through(&g, 5);
1653 admit_now(&mut g, &reply(9));
1654 }
1655
1656 #[test]
1661 fn an_unwitnessed_pending_change_is_applied_once_on_disk() {
1662 let (g, _) = build_from(
1664 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Disk, true),
1665 4,
1666 None,
1667 None,
1668 );
1669 assert_eq!(g.active_policy, AckPolicy::Disk);
1670 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
1671 assert!(g.pending.is_empty());
1672
1673 let (mut g, _) = build_from(
1676 pending_start(AckPolicy::DiskAndRam, 6, AckPolicy::Disk, true),
1677 4,
1678 None,
1679 None,
1680 );
1681 assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam));
1682 let journal = g.inputs.journal_persisted_wire_seq.clone();
1683 assert_held_until(&mut g, &reply(7), || journal.store(WireSeq::new(7)));
1684 assert_eq!(g.active_policy, AckPolicy::Disk);
1685 assert_applied_through(&g, 6);
1686 }
1687
1688 #[test]
1691 fn a_pending_change_already_durable_is_applied_at_the_start() {
1692 let metrics = ReplicationMetrics::default();
1693 for slot in 0..2 {
1694 metrics.in_memory_sequence[slot].store(6, Ordering::Relaxed);
1695 }
1696 let (g, _) = build_from(
1697 pending_start(AckPolicy::Ram, 3, AckPolicy::Disk, false),
1698 6,
1699 Some(Arc::new(metrics)),
1700 Some([
1701 Arc::new(AtomicBool::new(true)),
1702 Arc::new(AtomicBool::new(true)),
1703 ]),
1704 );
1705 assert_eq!(g.active_policy, AckPolicy::Disk);
1706 assert!(g.pending.is_empty());
1707 }
1708
1709 #[test]
1714 fn a_boot_change_after_a_pending_one_is_applied_after_it() {
1715 let (mut g, metrics, _forced) = gate_with_one_replica_from(
1716 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Disk, false),
1717 10,
1718 );
1719 assert_held_until(&mut g, &change(4, AckPolicy::TwoDisks, false), || {
1720 replica_holds(&metrics, 3)
1721 });
1722 assert_eq!(g.active_policy, AckPolicy::TwoDisks);
1723 replica_holds(&metrics, 5);
1725 assert_held_until(&mut g, &reply(5), || replica_persists(&metrics, 5));
1726 }
1727
1728 #[test]
1732 fn the_idle_recheck_applies_a_confirmed_pending_change() {
1733 let (mut g, metrics, _forced) = gate_with_one_replica_from(
1734 pending_start(AckPolicy::DiskAndRam, 5, AckPolicy::Disk, false),
1735 10,
1736 );
1737 let checked = g.last_policy_check;
1738 replica_holds(&metrics, 4);
1739 g.idle_recheck(checked + POLICY_CHECK_INTERVAL);
1740 assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam), "not yet held");
1741 assert!(!g.pending.is_empty());
1742
1743 replica_holds(&metrics, 5);
1744 g.idle_recheck(checked + 2 * POLICY_CHECK_INTERVAL);
1745 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
1746 assert!(g.pending.is_empty());
1747 }
1748
1749 #[test]
1753 fn a_forced_change_overtakes_a_pending_one() {
1754 let (mut g, mut forced) = build_from(
1755 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Ram, false),
1756 4,
1757 None,
1758 None,
1759 );
1760 let journal = g.inputs.journal_persisted_wire_seq.clone();
1761 forced.store(forced_change(5, AckPolicy::Disk.to_byte()));
1762 assert_held_until(&mut g, &reply(6), || journal.store(WireSeq::new(6)));
1763 assert_eq!(
1764 g.active_policy,
1765 AckPolicy::Disk,
1766 "not the stale pending `ram`"
1767 );
1768 assert_applied_through(&g, 5);
1769 assert!(g.pending.is_empty());
1770 }
1771
1772 #[test]
1774 fn shutdown_releases_a_wait_on_the_pending_change() {
1775 let (mut g, _) = build_from(
1776 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Disk, false),
1777 4,
1778 None,
1779 None,
1780 );
1781 let shutdown = AtomicBool::new(true);
1782 assert_eq!(admit(&mut g, &reply(4), &shutdown), Admission::Shutdown);
1783 assert!(!g.pending.is_empty());
1784 assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
1785 }
1786
1787 #[test]
1788 fn an_open_gate_attributes_exactly_once() {
1789 let mut g = gate(AckPolicy::Disk, 5);
1790 let shutdown = AtomicBool::new(false);
1791 assert!(matches!(
1792 wait(&mut g, 3, &shutdown),
1793 GateOutcome::Open(Backing::Policy)
1794 ));
1795 assert_eq!(attributions(&g), (1, 0), "disk opens on the journal");
1796 assert_eq!(g.cached_durable_pos, 5);
1797 }
1798
1799 #[test]
1804 fn shutdown_releases_a_wedged_gate_without_attribution() {
1805 let (tx, rx) = mpsc::channel();
1806 std::thread::spawn(move || {
1807 let mut g = gate(AckPolicy::DiskAndRam, 5);
1808 let shutdown = AtomicBool::new(true);
1809 let outcome = wait(&mut g, 3, &shutdown);
1810 let _ = tx.send((matches!(outcome, GateOutcome::Shutdown), attributions(&g)));
1813 });
1814 let (was_shutdown, attributed) = rx
1815 .recv_timeout(Duration::from_secs(10))
1816 .expect("the wedged gate ignored shutdown");
1817 assert!(was_shutdown);
1818 assert_eq!(attributed, (0, 0));
1819 }
1820
1821 #[test]
1825 fn shutdown_wins_over_a_gate_ready_to_open() {
1826 let mut g = gate_with_replicas(AckPolicy::Disk, 5);
1827 let shutdown = AtomicBool::new(true);
1828 assert!(matches!(wait(&mut g, 3, &shutdown), GateOutcome::Shutdown));
1829 assert_eq!(attributions(&g), (0, 0));
1830 }
1831
1832 #[test]
1836 fn a_change_applies_from_its_own_slot_on() {
1837 let (mut g, metrics, _forced) = gate_with_one_replica(AckPolicy::Disk, 10);
1838 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
1839 admit_now(&mut g, &reply(4));
1840 admit_now(&mut g, &change(5, AckPolicy::DiskAndRam, false));
1841 assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
1842 assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam));
1843 assert_held_until(&mut g, &reply(6), || replica_holds(&metrics, 6));
1845 }
1846
1847 #[test]
1850 fn a_change_slot_goes_to_no_connection() {
1851 let mut g = gate(AckPolicy::Disk, 10);
1852 let shutdown = AtomicBool::new(false);
1853 let to = |connection_id, slot: OutputSlot<u8, u8>| OutputSlot {
1854 connection_id,
1855 ..slot
1856 };
1857 assert_eq!(
1858 admit(&mut g, &to(9, reply(3)), &shutdown),
1859 Admission::Reply {
1860 connection_id: 9,
1861 backing: Backing::Policy
1862 }
1863 );
1864 assert_eq!(
1865 admit(&mut g, &to(9, change(4, AckPolicy::Disk, true)), &shutdown),
1866 Admission::GateOnly
1867 );
1868 assert_applied_through(&g, 4);
1869 assert_eq!(
1870 admit(&mut g, &to(9, change(4, AckPolicy::Ram, false)), &shutdown),
1871 Admission::GateOnly,
1872 "stale, and still the gate's"
1873 );
1874 assert_eq!(g.active_policy, AckPolicy::Disk, "the stale one ignored");
1875 }
1876
1877 #[test]
1882 fn a_change_is_held_until_the_old_policy_clears_its_entry() {
1883 let (mut g, metrics, _forced) = gate_with_one_replica(AckPolicy::DiskAndRam, 10);
1884 assert_held_until(&mut g, &change(5, AckPolicy::Disk, false), || {
1885 replica_holds(&metrics, 5)
1886 });
1887 assert_eq!(g.active_policy, AckPolicy::Disk);
1888 admit_now(&mut g, &reply(8));
1889 }
1890
1891 #[test]
1895 fn two_pending_changes_are_never_merged() {
1896 let (mut g, metrics, _forced) = gate_with_one_replica(AckPolicy::Disk, 10);
1897 admit_now(&mut g, &change(3, AckPolicy::DiskAndRam, false));
1898 assert_held_until(&mut g, &reply(4), || replica_holds(&metrics, 4));
1899 assert_held_until(&mut g, &change(6, AckPolicy::Disk, false), || {
1900 replica_holds(&metrics, 6)
1901 });
1902 assert_eq!(g.active_policy, AckPolicy::Disk);
1903 admit_now(&mut g, &reply(7));
1904 }
1905
1906 struct Primary {
1911 gate: DurabilityGate,
1912 journal: DurableWireSeqCursor,
1913 metrics: Arc<ReplicationMetrics>,
1914 active: Arc<AtomicBool>,
1915 presence: Arc<ReplicaPresence>,
1916 forced: ForcedPolicyWriter,
1917 }
1918
1919 impl Primary {
1920 fn new(policy: AckPolicy, pos: u64, connected: bool, grace: Option<Duration>) -> Self {
1924 Self::starting(GateStart::settled(policy), pos, connected, grace)
1925 }
1926
1927 fn starting(start: GateStart, pos: u64, connected: bool, grace: Option<Duration>) -> Self {
1929 let metrics = Arc::new(ReplicationMetrics::default());
1930 let active = Arc::new(AtomicBool::new(false));
1931 let presence = Arc::new(ReplicaPresence::new());
1932 let journal = DurableWireSeqCursor::detached(WireSeq::new(pos));
1933 let (forced_writer, forced) = forced_policy_cell();
1934 let inputs = GateInputs {
1935 journal_persisted_wire_seq: journal.clone(),
1936 replication_metrics: Some(Arc::clone(&metrics)),
1937 replica_active: Some([Arc::clone(&active), Arc::new(AtomicBool::new(false))]),
1938 utilization: Arc::new(StageUtilization::new()),
1939 wait: WaitStrategy::SpinThenYield,
1940 degraded_release: grace.map(|grace| DegradedRelease {
1941 presence: Arc::clone(&presence),
1942 grace,
1943 }),
1944 };
1945 let p = Self {
1946 gate: DurabilityGate::new(inputs, GatePolicy { start, forced }, "test"),
1947 journal,
1948 metrics,
1949 active,
1950 presence,
1951 forced: forced_writer,
1952 };
1953 if connected {
1954 p.replica_streams(pos);
1955 }
1956 p
1957 }
1958
1959 fn replica_leaves(&self) {
1961 self.active.store(false, Ordering::Release);
1962 self.presence.lower();
1963 }
1964
1965 fn replica_streams(&self, pos: u64) {
1968 drop(self.presence.lift());
1969 replica_persists(&self.metrics, pos);
1970 self.active.store(true, Ordering::Release);
1971 }
1972
1973 fn journal_at(&self, pos: u64) {
1974 self.journal.store(WireSeq::new(pos));
1975 }
1976
1977 fn degraded_acks(&self) -> u64 {
1978 self.gate
1979 .inputs
1980 .utilization
1981 .degraded_acks
1982 .load(Ordering::Relaxed)
1983 }
1984
1985 fn admit_within(
1989 &mut self,
1990 slot: &OutputSlot<u8, u8>,
1991 patience: Duration,
1992 ) -> Option<Admission> {
1993 let shutdown = AtomicBool::new(false);
1994 std::thread::scope(|s| {
1995 let gate = &mut self.gate;
1996 let admitting = s.spawn(|| admit(gate, slot, &shutdown));
1997 let deadline = Instant::now() + patience;
1998 while !admitting.is_finished() && Instant::now() < deadline {
1999 std::thread::sleep(Duration::from_millis(1));
2000 }
2001 shutdown.store(true, Ordering::Relaxed);
2002 match admitting.join().expect("the admitting thread panicked") {
2003 Admission::Shutdown => None,
2004 admitted => Some(admitted),
2005 }
2006 })
2007 }
2008
2009 fn release(&mut self, wire_seq: u64, patience: Duration) -> Option<Backing> {
2012 self.release_slot(&reply(wire_seq), patience)
2013 }
2014
2015 fn release_slot(
2018 &mut self,
2019 slot: &OutputSlot<u8, u8>,
2020 patience: Duration,
2021 ) -> Option<Backing> {
2022 match self.admit_within(slot, patience)? {
2023 Admission::Reply { backing, .. } => Some(backing),
2024 other => panic!("a reply slot admitted as {other:?}"),
2025 }
2026 }
2027 }
2028
2029 const PATIENCE: Duration = Duration::from_millis(300);
2032
2033 const NO_GRACE: Option<Duration> = Some(Duration::ZERO);
2036
2037 #[test]
2038 fn no_degraded_release_before_the_grace_period() {
2039 let mut p = Primary::new(
2040 AckPolicy::DiskAndRam,
2041 5,
2042 false,
2043 Some(Duration::from_secs(60)),
2044 );
2045 assert_eq!(p.release(5, PATIENCE), None, "held for the grace period");
2046 assert_eq!(p.degraded_acks(), 0);
2047 }
2048
2049 #[test]
2050 fn a_halted_node_past_the_grace_releases_on_its_own_fsync_marked() {
2051 for policy in [AckPolicy::Ram, AckPolicy::DiskAndRam, AckPolicy::TwoDisks] {
2052 let mut p = Primary::new(policy, 5, false, NO_GRACE);
2053 assert_eq!(
2054 p.release(5, PATIENCE),
2055 Some(Backing::PrimaryOnly),
2056 "{policy}"
2057 );
2058 assert_eq!(p.gate.cached_durable_pos, 0, "the policy confirmed nothing");
2059 }
2060 }
2061
2062 #[test]
2063 fn the_grace_period_runs_from_the_loss() {
2064 let mut p = Primary::new(
2065 AckPolicy::DiskAndRam,
2066 5,
2067 true,
2068 Some(Duration::from_millis(150)),
2069 );
2070 p.journal_at(6);
2071 p.replica_leaves();
2072 let started = Instant::now();
2073 assert_eq!(
2074 p.release(6, Duration::from_secs(5)),
2075 Some(Backing::PrimaryOnly)
2076 );
2077 assert!(
2078 started.elapsed() >= Duration::from_millis(150),
2079 "released before the grace ran out: {:?}",
2080 started.elapsed()
2081 );
2082 }
2083
2084 #[test]
2088 fn an_unseen_rejoin_restarts_the_grace_period() {
2089 let grace = Duration::from_millis(1_000);
2092 let mut p = Primary::new(AckPolicy::DiskAndRam, 5, true, Some(grace));
2093 p.replica_leaves();
2094 assert!(!p.gate.degraded_release_open(), "the halt has just begun");
2095 std::thread::sleep(Duration::from_millis(700));
2096 p.replica_streams(5);
2098 p.replica_leaves();
2099 let second_loss = Instant::now();
2100 std::thread::sleep(Duration::from_millis(400));
2101 assert!(
2103 !p.gate.degraded_release_open(),
2104 "aged from the first loss: {:?} since the second",
2105 second_loss.elapsed()
2106 );
2107 std::thread::sleep(grace.saturating_sub(second_loss.elapsed()));
2108 assert!(
2109 p.gate.degraded_release_open(),
2110 "past the grace from the second loss"
2111 );
2112 }
2113
2114 #[test]
2118 fn a_missing_loss_stamp_ages_from_the_first_sighting() {
2119 let seen = Instant::now();
2120 let now = seen + Duration::from_secs(10);
2121 let lost = seen + Duration::from_secs(3);
2122 assert_eq!(
2123 halt_start(LastLoss::Unknown, None, now),
2124 now,
2125 "first sighting"
2126 );
2127 assert_eq!(halt_start(LastLoss::Unknown, Some(seen), now), seen);
2128 assert_eq!(halt_start(LastLoss::At(lost), Some(seen), now), lost);
2129 assert_eq!(halt_start(LastLoss::At(lost), None, now), lost);
2130 }
2131
2132 #[test]
2135 fn the_halt_start_is_tracked_while_halted_and_dropped_on_a_replica() {
2136 let mut p = Primary::new(AckPolicy::DiskAndRam, 5, true, NO_GRACE);
2137 assert!(!p.gate.degraded_release_open());
2138 assert_eq!(p.gate.halt_started, None);
2139
2140 let before = Instant::now();
2141 p.replica_leaves();
2142 assert!(p.gate.degraded_release_open());
2143 let started = p.gate.halt_started.expect("halted");
2144 assert!(started >= before, "the loss's own stamp");
2145 assert!(p.gate.degraded_release_open());
2146 assert_eq!(p.gate.halt_started, Some(started), "the same halt");
2147
2148 p.replica_streams(5);
2149 assert!(!p.gate.degraded_release_open());
2150 assert_eq!(p.gate.halt_started, None);
2151 assert!(!p.gate.releasing_degraded);
2152 }
2153
2154 #[test]
2156 fn the_release_follows_a_switch_to_and_from_a_policy_needing_a_replica() {
2157 let mut p = Primary::new(AckPolicy::Disk, 5, false, NO_GRACE);
2158 assert!(!p.gate.degraded_release_open(), "disk needs no replica");
2159 p.gate.switch_to(AckPolicy::TwoDisks, "test");
2160 assert!(p.gate.degraded_release_open());
2161 assert!(p.gate.releasing_degraded);
2162 p.gate.switch_to(AckPolicy::Disk, "test");
2163 assert!(!p.gate.degraded_release_open());
2164 assert!(!p.gate.releasing_degraded);
2165 }
2166
2167 #[test]
2170 fn no_degraded_release_under_disk() {
2171 let mut p = Primary::new(AckPolicy::Disk, 5, false, NO_GRACE);
2172 assert_eq!(p.release(5, PATIENCE), Some(Backing::Policy));
2173 assert_eq!(p.release(6, PATIENCE), None, "not yet on the journal");
2174 assert!(!p.gate.releasing_degraded);
2175 }
2176
2177 #[test]
2178 fn no_degraded_release_with_the_switch_off() {
2179 let mut p = Primary::new(AckPolicy::DiskAndRam, 5, false, None);
2180 assert_eq!(p.release(5, PATIENCE), None, "the stall, unchanged");
2181 assert_eq!(p.degraded_acks(), 0);
2182 }
2183
2184 #[test]
2187 fn no_degraded_release_while_a_replica_is_connected_but_behind() {
2188 let mut p = Primary::new(AckPolicy::TwoDisks, 5, true, NO_GRACE);
2189 p.journal_at(9);
2190 assert_eq!(p.release(9, PATIENCE), None, "held for the replica");
2191 }
2192
2193 #[test]
2196 fn an_unfsynced_slot_under_ram_is_held_until_the_primary_fsyncs_it() {
2197 let mut p = Primary::new(AckPolicy::Ram, 5, false, NO_GRACE);
2198 assert_eq!(
2199 p.release(6, PATIENCE),
2200 None,
2201 "the primary's disk does not hold it yet"
2202 );
2203 p.journal_at(6);
2204 assert_eq!(p.release(6, PATIENCE), Some(Backing::PrimaryOnly));
2205 }
2206
2207 #[test]
2208 fn a_full_ack_resumes_once_a_replica_streams_again() {
2209 let mut p = Primary::new(AckPolicy::DiskAndRam, 5, false, NO_GRACE);
2210 assert_eq!(p.release(5, PATIENCE), Some(Backing::PrimaryOnly));
2211 assert!(p.gate.releasing_degraded);
2212
2213 p.journal_at(7);
2214 p.replica_streams(7);
2215 assert_eq!(
2216 p.release(6, PATIENCE),
2217 Some(Backing::Policy),
2218 "the policy is met again"
2219 );
2220 assert_eq!(p.release(7, PATIENCE), Some(Backing::Policy));
2221 assert_eq!(p.gate.cached_durable_pos, 7);
2222
2223 p.journal_at(9);
2226 assert_eq!(p.release(9, PATIENCE), None);
2227 assert!(!p.gate.releasing_degraded);
2228 }
2229
2230 #[test]
2245 fn a_query_over_state_confirmed_before_the_loss_is_answered_primary_only() {
2246 let grace = Duration::from_millis(100);
2247 let mut p = Primary::new(AckPolicy::DiskAndRam, 5, true, Some(grace));
2248 assert_eq!(
2249 p.release(5, PATIENCE),
2250 Some(Backing::Policy),
2251 "before the loss the policy confirms everything through 5"
2252 );
2253 assert_eq!(p.gate.cached_durable_pos, 5);
2254
2255 p.replica_leaves();
2256 std::thread::sleep(grace);
2257 let due = p.gate.last_policy_check + POLICY_CHECK_INTERVAL;
2258 p.gate.idle_recheck(due);
2259 assert_eq!(
2260 p.gate.cached_durable_pos, 0,
2261 "with no replica streaming the evaluation pins the durable position to 0"
2262 );
2263
2264 let query = OutputSlot {
2265 wire_seq: 5,
2266 payload: OutputPayload::QueryResponse(0),
2267 is_last_in_request: true,
2268 ..OutputSlot::default()
2269 };
2270 let backing = p.release_slot(&query, PATIENCE);
2271 assert_eq!(
2272 backing,
2273 Some(Backing::PrimaryOnly),
2274 "marked, though every replica confirmed position 5 before it left"
2275 );
2276 let mut ends = BatchEnds::new();
2277 assert_eq!(
2278 p.gate
2279 .batch_end(Backing::PrimaryOnly, &mut ends, RequestId(5)),
2280 encoded(TransportResponse::BatchEndDegraded { id: RequestId(5) })
2281 );
2282 assert_eq!(p.degraded_acks(), 1, "counted in melin_degraded_acks_total");
2283 }
2284
2285 fn encoded(response: TransportResponse) -> Vec<u8> {
2287 let mut buf = [0u8; BATCH_END_FRAME_LEN];
2288 let n = control_codec::encode_transport_response(&response, &mut buf).unwrap();
2289 buf[..n].to_vec()
2290 }
2291
2292 #[test]
2296 fn after_a_degraded_release_the_next_slot_is_still_evaluated_and_still_degraded() {
2297 let mut p = Primary::new(AckPolicy::DiskAndRam, 9, false, NO_GRACE);
2298 assert_eq!(p.release(5, PATIENCE), Some(Backing::PrimaryOnly));
2299 assert!(
2300 slot_needs_gate(&reply(6), p.gate.cached_durable_pos),
2301 "the next slot is not waved through on the degraded release"
2302 );
2303 assert_eq!(
2304 p.release(6, PATIENCE),
2305 Some(Backing::PrimaryOnly),
2306 "never a full BatchEnd"
2307 );
2308 assert_eq!(p.gate.cached_durable_pos, 0);
2309 }
2310
2311 #[test]
2317 fn a_change_released_degraded_is_applied_only_once_the_policy_before_it_confirms_it() {
2318 let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
2319 assert_eq!(
2320 p.admit_within(&change(4, AckPolicy::TwoDisks, false), PATIENCE),
2321 Some(Admission::GateOnly)
2322 );
2323 assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam, "not applied");
2324 assert_eq!(p.gate.applied_through, None);
2325 assert_eq!(p.gate.pending.len(), 1, "deferred");
2326 assert_eq!(p.release(5, PATIENCE), Some(Backing::PrimaryOnly));
2327
2328 drop(p.presence.lift());
2332 replica_holds(&p.metrics, 6);
2333 p.active.store(true, Ordering::Release);
2334 assert_eq!(
2335 p.release(6, PATIENCE),
2336 None,
2337 "judged under two-disks, which the replica's disk does not meet yet"
2338 );
2339 assert_eq!(p.gate.active_policy, AckPolicy::TwoDisks);
2340 assert!(p.gate.pending.is_empty());
2341 replica_persists(&p.metrics, 6);
2342 assert_eq!(p.release(6, PATIENCE), Some(Backing::Policy));
2343 }
2344
2345 #[test]
2349 fn a_pending_change_released_degraded_stays_pending_and_marks_what_follows() {
2350 let mut p = Primary::starting(
2351 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Ram, false),
2352 5,
2353 false,
2354 NO_GRACE,
2355 );
2356 assert_eq!(p.release(4, PATIENCE), Some(Backing::PrimaryOnly));
2357 assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam);
2358 assert_eq!(p.gate.pending.len(), 1);
2359
2360 p.replica_streams(5);
2361 assert_eq!(p.release(5, PATIENCE), Some(Backing::Policy));
2362 assert_eq!(p.gate.active_policy, AckPolicy::Ram, "applied in order");
2363 assert!(p.gate.pending.is_empty());
2364 }
2365
2366 #[test]
2369 fn changes_released_degraded_are_applied_one_by_one() {
2370 let mut p = Primary::new(AckPolicy::TwoDisks, 8, false, NO_GRACE);
2371 for (wire_seq, policy) in [(3, AckPolicy::DiskAndRam), (5, AckPolicy::Disk)] {
2372 assert_eq!(
2373 p.admit_within(&change(wire_seq, policy, false), PATIENCE),
2374 Some(Admission::GateOnly)
2375 );
2376 }
2377 assert_eq!(p.gate.pending.len(), 2);
2378
2379 drop(p.presence.lift());
2382 p.metrics.acked_sequence[0].store(3, Ordering::Release);
2383 replica_holds(&p.metrics, 5);
2384 p.active.store(true, Ordering::Release);
2385 assert_eq!(p.release(8, PATIENCE), Some(Backing::Policy));
2386 assert_eq!(p.gate.active_policy, AckPolicy::Disk);
2387 assert_applied_through(&p.gate, 5);
2388 }
2389
2390 #[test]
2393 fn an_unwitnessed_change_released_degraded_applies_once_on_disk() {
2394 let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
2395 assert_eq!(
2396 p.admit_within(&change(4, AckPolicy::Disk, true), PATIENCE),
2397 Some(Admission::GateOnly)
2398 );
2399 assert_eq!(p.release(5, PATIENCE), Some(Backing::Policy));
2400 assert_eq!(p.gate.active_policy, AckPolicy::Disk);
2401 }
2402
2403 #[test]
2409 fn a_pending_change_released_degraded_marks_a_slot_the_policy_then_confirms() {
2410 let mut p = Primary::starting(
2411 pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Ram, false),
2412 5,
2413 false,
2414 NO_GRACE,
2415 );
2416 let shutdown = AtomicBool::new(false);
2417 let mut recording = Recording::new(|| {});
2418 let mut hook = Hook::new(&mut recording);
2419 #[cfg(feature = "tick-to-trade")]
2420 let mut tracker = GateCrossTracker::new(5);
2421 let backing = match p.gate.admit_pending(
2422 &shutdown,
2423 #[cfg(feature = "tick-to-trade")]
2424 &mut tracker,
2425 &mut hook,
2426 ) {
2427 GateOutcome::Open(backing) => backing,
2428 GateOutcome::Shutdown => panic!("no shutdown was requested"),
2429 };
2430 assert_eq!(backing, Backing::PrimaryOnly);
2431 assert_eq!(p.gate.pending.len(), 1, "left pending");
2432
2433 p.replica_streams(5);
2434 let admitted = p.gate.admit_slot(
2435 &reply(5),
2436 backing,
2437 &shutdown,
2438 #[cfg(feature = "tick-to-trade")]
2439 &mut tracker,
2440 &mut hook,
2441 );
2442 assert_eq!(
2443 admitted,
2444 Admission::Reply {
2445 connection_id: 0,
2446 backing: Backing::PrimaryOnly
2447 },
2448 "never a full BatchEnd behind an unapplied change"
2449 );
2450 assert_eq!(
2451 p.gate.cached_durable_pos, 5,
2452 "the slot's wait opened in full"
2453 );
2454 assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam, "not applied");
2455 assert_eq!(p.gate.pending.len(), 1);
2456 assert_eq!(
2457 recording.calls.iter().filter(|&&c| c == "before").count(),
2458 1,
2459 "the flush hook ran once, before the first wait"
2460 );
2461 assert_eq!(recording.calls.first(), Some(&"before"));
2462
2463 p.journal_at(6);
2464 p.replica_streams(6);
2465 assert_eq!(p.release(6, PATIENCE), Some(Backing::Policy));
2466 assert_eq!(p.gate.active_policy, AckPolicy::Ram, "applied in order");
2467 assert_applied_through(&p.gate, 3);
2468 assert!(p.gate.pending.is_empty());
2469 }
2470
2471 #[test]
2474 fn a_change_released_degraded_behind_a_forced_one_is_stale() {
2475 let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
2476 p.forced.store(forced_change(5, AckPolicy::Ram.to_byte()));
2478 assert_eq!(p.release(6, PATIENCE), Some(Backing::PrimaryOnly));
2479 assert_eq!(p.gate.active_policy, AckPolicy::Ram);
2480 assert_eq!(
2481 p.admit_within(&change(4, AckPolicy::TwoDisks, false), PATIENCE),
2482 Some(Admission::GateOnly)
2483 );
2484 assert!(p.gate.pending.is_empty(), "stale, not deferred");
2485 assert_applied_through(&p.gate, 5);
2486 assert_eq!(p.gate.active_policy, AckPolicy::Ram);
2487 }
2488
2489 #[test]
2493 fn an_unknown_policy_leaves_an_earlier_deferred_change_pending() {
2494 let mut p = Primary::new(AckPolicy::DiskAndRam, 8, false, NO_GRACE);
2495 assert_eq!(
2496 p.admit_within(&change(3, AckPolicy::TwoDisks, false), PATIENCE),
2497 Some(Admission::GateOnly)
2498 );
2499 assert_eq!(
2501 p.admit_within(&change_byte(5, AckPolicyByte(CORRUPT), false), PATIENCE),
2502 Some(Admission::GateOnly)
2503 );
2504 assert_eq!(p.gate.pending.len(), 1, "the unknown one is not deferred");
2505 assert_eq!(p.gate.last_refused, Some(WireSeq::new(5)));
2506 p.forced.store(forced_change(6, AckPolicyByte(CORRUPT)));
2508 assert_eq!(p.release(7, PATIENCE), Some(Backing::PrimaryOnly));
2509 assert_eq!(p.gate.last_refused, Some(WireSeq::new(6)));
2510 assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam);
2511 assert_eq!(p.gate.applied_through, None, "neither recorded as applied");
2512 assert_eq!(p.gate.pending.len(), 1);
2513
2514 p.replica_streams(8);
2515 assert_eq!(p.release(8, PATIENCE), Some(Backing::Policy));
2516 assert_eq!(p.gate.active_policy, AckPolicy::TwoDisks, "not made stale");
2517 assert_applied_through(&p.gate, 3);
2518 assert!(p.gate.pending.is_empty());
2519 }
2520
2521 #[test]
2525 fn a_forced_change_overtakes_a_deferred_one() {
2526 let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
2527 assert_eq!(
2528 p.admit_within(&change(4, AckPolicy::Ram, false), PATIENCE),
2529 Some(Admission::GateOnly)
2530 );
2531 assert_eq!(p.gate.pending.len(), 1, "deferred");
2532 p.forced.store(forced_change(5, AckPolicy::Disk.to_byte()));
2533 assert_eq!(p.release(6, PATIENCE), Some(Backing::Policy));
2534 assert_eq!(
2535 p.gate.active_policy,
2536 AckPolicy::Disk,
2537 "not the stale deferred `ram`"
2538 );
2539 assert_applied_through(&p.gate, 5);
2540 assert!(p.gate.pending.is_empty());
2541 }
2542
2543 #[test]
2545 fn the_terminator_follows_the_backing_and_counts_degraded_acks() {
2546 let p = Primary::new(AckPolicy::DiskAndRam, 5, false, NO_GRACE);
2547 let mut ends = BatchEnds::new();
2548 for id in [RequestId(1), RequestId(u64::MAX), RequestId(0)] {
2551 assert_eq!(
2552 p.gate.batch_end(Backing::Policy, &mut ends, id),
2553 encoded(TransportResponse::BatchEnd { id })
2554 );
2555 }
2556 assert_eq!(p.degraded_acks(), 0);
2557 assert_eq!(
2558 p.gate
2559 .batch_end(Backing::PrimaryOnly, &mut ends, RequestId(7)),
2560 encoded(TransportResponse::BatchEndDegraded { id: RequestId(7) })
2561 );
2562 assert_eq!(p.degraded_acks(), 1);
2563 assert_eq!(
2564 ends.policy(RequestId(3)),
2565 encoded(TransportResponse::BatchEnd { id: RequestId(3) })
2566 );
2567 }
2568
2569 #[test]
2575 fn a_forced_swap_frees_an_earlier_stuck_slot_once_its_entry_is_on_disk() {
2576 let (mut g, mut forced) = build(AckPolicy::DiskAndRam, 4, None, None);
2578 let journal = g.inputs.journal_persisted_wire_seq.clone();
2579 forced.store(forced_change(6, AckPolicy::Disk.to_byte()));
2580 assert_held_until(&mut g, &reply(3), || journal.store(WireSeq::new(6)));
2582 assert_eq!(g.active_policy, AckPolicy::Disk);
2583 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
2584 assert!(
2589 degraded_nanos(&g) > 0,
2590 "degraded time accrued during the wedge"
2591 );
2592 assert_eq!(attributions(&g), (1, 0), "opened on the journal, once");
2593
2594 let cached = g.cached_durable_pos;
2595 admit_now(&mut g, &change(5, AckPolicy::TwoDisks, false));
2596 admit_now(&mut g, &change(6, AckPolicy::Disk, true));
2597 assert_eq!(g.active_policy, AckPolicy::Disk, "stale changes ignored");
2598 assert_eq!(g.cached_durable_pos, cached, "no switch, no reset");
2599
2600 journal.store(WireSeq::new(7));
2601 admit_now(&mut g, &change(7, AckPolicy::Ram, false));
2602 assert_eq!(g.active_policy, AckPolicy::Ram);
2603 }
2604
2605 #[test]
2609 fn a_forced_change_applied_in_order_is_not_reapplied() {
2610 let (mut g, metrics, mut forced) = gate_with_one_replica(AckPolicy::DiskAndRam, 10);
2611 replica_holds(&metrics, 3);
2612 forced.store(forced_change(2, AckPolicy::Disk.to_byte()));
2613 admit_now(&mut g, &change(2, AckPolicy::Disk, true));
2614 admit_now(&mut g, &change(3, AckPolicy::DiskAndRam, false));
2615 assert_held_until(&mut g, &reply(4), || replica_holds(&metrics, 4));
2616 assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
2617 }
2618
2619 #[test]
2624 fn an_unknown_policy_in_a_change_keeps_the_prior_policy() {
2625 let (mut g, mut forced) = build(AckPolicy::Disk, 4, None, None);
2626 let journal = g.inputs.journal_persisted_wire_seq.clone();
2627 admit_now(&mut g, &change_byte(4, AckPolicyByte(CORRUPT), false));
2628 assert_eq!(g.active_policy, AckPolicy::Disk);
2629 assert_eq!(g.policy, AckPolicy::Disk.to_policy());
2630 assert_eq!(in_force(&g), Some(AckPolicy::Disk));
2631 assert_eq!(g.cached_durable_pos, 4, "no switch: cached position kept");
2632 assert_eq!(g.applied_through, None, "not recorded as applied");
2633 assert_eq!(g.last_refused, Some(WireSeq::new(4)));
2634
2635 forced.store(forced_change(6, AckPolicyByte(CORRUPT)));
2638 assert_held_until(&mut g, &reply(5), || journal.store(WireSeq::new(6)));
2639 assert_eq!(g.active_policy, AckPolicy::Disk);
2640 assert_eq!(g.applied_through, None, "not recorded as applied");
2641 assert_eq!(g.last_refused, Some(WireSeq::new(6)));
2643
2644 let mut g = gate(AckPolicy::DiskAndRam, 5);
2646 let_time_pass();
2647 g.apply_change(WireSeq::new(1), AckPolicyByte(CORRUPT), "test");
2648 assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
2649 assert_eq!(degraded_nanos(&g), 0, "no re-seed on an unknown byte");
2650 }
2651
2652 fn admit_recorded(
2655 g: &mut DurabilityGate,
2656 wire_seq: u64,
2657 during: impl FnMut(),
2658 ) -> Vec<&'static str> {
2659 let shutdown = AtomicBool::new(false);
2660 let mut hook = Recording::new(during);
2661 #[cfg(feature = "tick-to-trade")]
2662 let mut tracker = GateCrossTracker::new(wire_seq);
2663 let outcome = g.admit(
2664 &reply(wire_seq),
2665 &shutdown,
2666 #[cfg(feature = "tick-to-trade")]
2667 &mut tracker,
2668 &mut hook,
2669 );
2670 assert_eq!(
2671 outcome,
2672 Admission::Reply {
2673 connection_id: 0,
2674 backing: Backing::Policy
2675 }
2676 );
2677 hook.calls
2678 }
2679
2680 #[test]
2683 fn before_wait_runs_only_when_the_slot_waits() {
2684 let mut g = gate(AckPolicy::Disk, 5);
2685 assert!(
2686 admit_recorded(&mut g, 5, || {}).is_empty(),
2687 "slot 5 is durable: no hook at all"
2688 );
2689 g.inputs.journal_persisted_wire_seq.store(WireSeq::new(6));
2692 assert_eq!(admit_recorded(&mut g, 6, || {}), ["before"]);
2693 }
2694
2695 #[test]
2698 fn while_waiting_runs_during_a_wait_and_never_on_a_slot_that_does_not_wait() {
2699 let mut g = gate(AckPolicy::Disk, 5);
2700 let journal = g.inputs.journal_persisted_wire_seq.clone();
2701 let mut turns = 0;
2702 let calls = admit_recorded(&mut g, 6, || {
2704 turns += 1;
2705 if turns == 3 {
2706 journal.store(WireSeq::new(6));
2707 }
2708 });
2709 assert_eq!(calls, ["before", "during", "during", "during"]);
2710
2711 assert!(
2712 admit_recorded(&mut g, 6, || panic!("slot 6 is durable now")).is_empty(),
2713 "a slot that does not wait runs no hook"
2714 );
2715 }
2716
2717 #[test]
2718 fn idle_recheck_waits_out_the_check_interval() {
2719 let mut g = gate(AckPolicy::Disk, 5);
2720 let checked = g.last_policy_check;
2721 g.inputs.journal_persisted_wire_seq.store(WireSeq::new(9));
2722
2723 g.idle_recheck(checked + POLICY_CHECK_INTERVAL - Duration::from_millis(1));
2724 assert_eq!(g.cached_durable_pos, 5, "too early: no re-evaluation");
2725 assert_eq!(g.last_policy_check, checked);
2726
2727 let due = checked + POLICY_CHECK_INTERVAL;
2728 g.idle_recheck(due);
2729 assert_eq!(g.cached_durable_pos, 9);
2730 assert_eq!(g.last_policy_check, due);
2731 }
2732
2733 #[test]
2734 fn a_batch_defers_the_next_idle_recheck() {
2735 let mut g = gate(AckPolicy::Disk, 5);
2736 g.last_policy_check = Instant::now()
2738 .checked_sub(2 * POLICY_CHECK_INTERVAL)
2739 .expect("the monotonic clock is past two check intervals");
2740 g.inputs.journal_persisted_wire_seq.store(WireSeq::new(9));
2741
2742 g.after_batch();
2743 g.idle_recheck(Instant::now());
2744 assert_eq!(
2745 g.cached_durable_pos, 5,
2746 "the batch counted as the check, so the idle path waits"
2747 );
2748 }
2749}