use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use melin_app::amortized_timer::AmortizedTimer;
use melin_journal::{AckPolicyByte, AckPolicyChange};
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::lineage::{ForcedPolicyReader, LineagePolicy};
use melin_transport_core::pipeline::{OutputPayload, OutputSlot, RequestId, StageUtilization};
use melin_transport_core::replica_presence::LastLoss;
#[cfg(feature = "tick-to-trade")]
use melin_transport_core::trace;
use melin_transport_core::{DurableWireSeqCursor, WireSeq};
use melin_wire_protocol::control::TransportResponse;
use melin_wire_protocol::control_codec::{self, BATCH_END_FRAME_LEN};
use crate::ack_policy::{AckPolicy, Blocker, Policy};
use crate::halt::{DegradedRelease, Replication};
use crate::replication::ReplicationMetrics;
use crate::response::{DegradationLogger, evaluate_durability, evaluate_gate, slot_needs_gate};
#[cfg(feature = "tick-to-trade")]
use crate::response::{GateCrossTracker, policy_replica_cursor};
const DEGRADED_LOG_INTERVAL: Duration = Duration::from_secs(5);
const POLICY_CHECK_INTERVAL: Duration = Duration::from_secs(1);
const GATE_ACCRUAL_INTERVAL: Duration = Duration::from_millis(10);
pub(crate) const DEFAULT_DEGRADED_ACK_GRACE_MS: u64 = 2_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Backing {
Policy,
PrimaryOnly,
}
impl Backing {
fn min(self, other: Self) -> Self {
match (self, other) {
(Self::Policy, Self::Policy) => Self::Policy,
(Self::PrimaryOnly, _) | (_, Self::PrimaryOnly) => Self::PrimaryOnly,
}
}
}
pub(crate) struct BatchEnds {
policy: [u8; BATCH_END_FRAME_LEN],
primary_only: [u8; BATCH_END_FRAME_LEN],
}
impl BatchEnds {
pub(crate) fn new() -> Self {
let encode = |response| {
let mut buf = [0u8; BATCH_END_FRAME_LEN];
let written = control_codec::encode_transport_response(&response, &mut buf)
.expect("a batch terminator encodes into `BATCH_END_FRAME_LEN` bytes");
assert_eq!(
written, BATCH_END_FRAME_LEN,
"a batch terminator fills its frame"
);
buf
};
Self {
policy: encode(TransportResponse::BatchEnd { id: RequestId(0) }),
primary_only: encode(TransportResponse::BatchEndDegraded { id: RequestId(0) }),
}
}
#[inline]
fn policy(&mut self, id: RequestId) -> &[u8] {
control_codec::write_terminal_id(&mut self.policy, id);
&self.policy
}
#[inline]
fn primary_only(&mut self, id: RequestId) -> &[u8] {
control_codec::write_terminal_id(&mut self.primary_only, id);
&self.primary_only
}
}
#[must_use]
enum GateOutcome {
Open(Backing),
Shutdown,
}
pub(crate) trait GateWait {
fn before_wait(&mut self);
fn while_waiting(&mut self);
}
struct Hook<'h, H: GateWait> {
hook: &'h mut H,
started: bool,
}
impl<'h, H: GateWait> Hook<'h, H> {
fn new(hook: &'h mut H) -> Self {
Self {
hook,
started: false,
}
}
#[inline]
fn starting(&mut self) {
if !self.started {
self.started = true;
self.hook.before_wait();
}
}
}
#[must_use]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Admission {
Reply {
connection_id: u64,
backing: Backing,
},
GateOnly,
Shutdown,
}
pub struct GatePolicy {
pub start: GateStart,
pub forced: ForcedPolicyReader,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GateStart {
pub(crate) in_force: AckPolicy,
pub(crate) pending: Option<PendingChange>,
}
impl GateStart {
pub fn settled(policy: AckPolicy) -> Self {
Self {
in_force: policy,
pending: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PendingChange {
pub(crate) wire_seq: WireSeq,
pub(crate) policy: AckPolicy,
pub(crate) unwitnessed: bool,
}
pub(crate) fn initial_gate_policy(
lineage: Option<LineagePolicy>,
requested: AckPolicy,
replication: Replication,
) -> Result<GateStart, String> {
let Some(lineage) = lineage else {
return Ok(GateStart::settled(requested));
};
let newest = known_lineage_policy(lineage.policy())?;
match replication {
Replication::Disabled => return Ok(GateStart::settled(requested)),
Replication::Enabled => {}
}
let Some(previous) = lineage.previous else {
return Ok(GateStart::settled(newest));
};
Ok(GateStart {
in_force: known_lineage_policy(previous)?,
pending: Some(PendingChange {
wire_seq: lineage.wire_seq,
policy: newest,
unwitnessed: lineage.change.unwitnessed,
}),
})
}
fn known_lineage_policy(byte: AckPolicyByte) -> Result<AckPolicy, String> {
AckPolicy::from_byte(byte).ok_or_else(|| {
format!(
"the journal's ack policy (byte {}) is unknown to this build; \
refusing to serve under it",
byte.0
)
})
}
pub(crate) struct GateInputs {
pub journal_persisted_wire_seq: DurableWireSeqCursor,
pub replication_metrics: Option<Arc<ReplicationMetrics>>,
pub replica_active: Option<[Arc<AtomicBool>; 2]>,
pub utilization: Arc<StageUtilization>,
pub wait: WaitStrategy,
pub degraded_release: Option<DegradedRelease>,
}
pub(crate) struct DurabilityGate {
inputs: GateInputs,
stage: &'static str,
forced: ForcedPolicyReader,
applied_through: Option<WireSeq>,
last_refused: Option<WireSeq>,
pending: VecDeque<PendingChange>,
active_policy: AckPolicy,
needs_replica: bool,
policy: Policy,
cached_durable_pos: u64,
degraded_logger: DegradationLogger,
last_policy_check: Instant,
accrual_timer: AmortizedTimer,
halt_started: Option<Instant>,
releasing_degraded: bool,
}
impl DurabilityGate {
pub(crate) fn new(inputs: GateInputs, policy: GatePolicy, stage: &'static str) -> Self {
let GatePolicy {
start:
GateStart {
in_force: active_policy,
pending,
},
forced,
} = policy;
inputs
.utilization
.ack_policy_in_force
.store(Some(active_policy.to_byte()));
let policy = active_policy.to_policy();
let now = Instant::now();
let journal_pos = inputs.journal_persisted_wire_seq.load();
let status = evaluate_durability(
&policy,
journal_pos,
inputs.replication_metrics.as_deref(),
inputs.replica_active.as_ref(),
);
inputs
.utilization
.policy_degraded
.store(status.degraded, Ordering::Relaxed);
let degraded_logger = if status.degraded {
DegradationLogger::new_starting_degraded(now, &policy)
} else {
DegradationLogger::new(now)
};
if let Some(pending) = pending {
tracing::info!(
stage,
in_force = active_policy.as_str(),
pending = pending.policy.as_str(),
wire_seq = pending.wire_seq.get(),
unwitnessed = pending.unwitnessed,
"the lineage's newest ack-policy change is applied again before any reply"
);
}
let mut gate = Self {
inputs,
stage,
forced,
applied_through: None,
last_refused: None,
pending: pending.into_iter().collect(),
active_policy,
needs_replica: active_policy.needs_replica(),
policy,
cached_durable_pos: status.durable_pos,
degraded_logger,
last_policy_check: now,
accrual_timer: AmortizedTimer::new(),
halt_started: None,
releasing_degraded: false,
};
gate.settle_pending_if_durable(journal_pos);
gate
}
fn settle_pending_if_durable(&mut self, journal_pos: WireSeq) {
while let Some(&pending) = self.pending.front() {
if self.cached_durable_pos < pending.wire_seq.get()
&& !(pending.unwitnessed && journal_pos >= pending.wire_seq)
{
return;
}
self.apply_front_pending();
}
}
fn apply_front_pending(&mut self) {
let Some(pending) = self.pending.pop_front() else {
return;
};
let cause = if pending.unwitnessed {
"pending unwitnessed change, on disk"
} else {
"pending change, durable under the policy before it"
};
self.apply_change(pending.wire_seq, pending.policy.to_byte(), cause);
}
#[inline]
pub(crate) fn batch_end<'e>(
&self,
backing: Backing,
ends: &'e mut BatchEnds,
id: RequestId,
) -> &'e [u8] {
match backing {
Backing::Policy => ends.policy(id),
Backing::PrimaryOnly => {
self.inputs
.utilization
.degraded_acks
.fetch_add(1, Ordering::Relaxed);
ends.primary_only(id)
}
}
}
fn degraded_release_open(&mut self) -> bool {
let Some(release) = self.inputs.degraded_release.as_ref() else {
return false;
};
let grace = release.grace;
let Some(loss) = release.presence.last_loss() else {
self.halt_started = None;
if self.releasing_degraded {
self.log_degraded_release(false, true, grace);
}
return false;
};
let now = Instant::now();
let started = halt_start(loss, self.halt_started, now);
self.halt_started = Some(started);
let open = self.needs_replica && now.saturating_duration_since(started) >= grace;
if open != self.releasing_degraded {
self.log_degraded_release(open, false, grace);
}
open
}
#[cold]
fn log_degraded_release(&mut self, open: bool, replica_connected: bool, grace: Duration) {
self.releasing_degraded = open;
if open {
tracing::warn!(
stage = self.stage,
policy = self.active_policy.as_str(),
grace_ms = grace.as_millis() as u64,
"no replica for the grace period: replies held for the ack policy are now \
released once this node's own journal holds them, marked as backed by the \
primary alone (BatchEndDegraded)"
);
} else {
tracing::warn!(
stage = self.stage,
policy = self.active_policy.as_str(),
replica_connected,
"degraded release stopped: held replies wait for the ack policy again"
);
}
}
fn switch_to(&mut self, next: AckPolicy, cause: &'static str) {
if next == self.active_policy {
return;
}
tracing::info!(
stage = self.stage,
prev = self.active_policy.as_str(),
next = next.as_str(),
cause,
"ack policy switched"
);
self.active_policy = next;
self.needs_replica = next.needs_replica();
self.policy = next.to_policy();
self.cached_durable_pos = 0;
self.degraded_logger
.reseed(&self.inputs.utilization, Instant::now());
self.inputs
.utilization
.ack_policy_in_force
.store(Some(next.to_byte()));
}
fn apply_change(&mut self, wire_seq: WireSeq, policy: AckPolicyByte, cause: &'static str) {
if self.is_stale(wire_seq) {
return;
}
let Some(next) = AckPolicy::from_byte(policy) else {
self.refuse_unknown(wire_seq, policy);
return;
};
self.applied_through = Some(wire_seq);
self.switch_to(next, cause);
}
fn is_stale(&self, wire_seq: WireSeq) -> bool {
self.applied_through
.is_some_and(|through| wire_seq <= through)
}
#[cold]
fn refuse_unknown(&mut self, wire_seq: WireSeq, policy: AckPolicyByte) {
if self.last_refused == Some(wire_seq) {
return;
}
self.last_refused = Some(wire_seq);
tracing::error!(
stage = self.stage,
byte = policy.0,
wire_seq = wire_seq.get(),
"journaled ack-policy change names an unknown policy; retaining prior policy"
);
}
#[inline]
fn observe_forced(&mut self, journal_pos: WireSeq) {
self.settle_pending_if_durable(journal_pos);
if let Some(forced) = self.forced.load()
&& journal_pos >= forced.wire_seq
{
self.apply_change(forced.wire_seq, forced.policy, "forced change");
}
}
#[inline]
pub(crate) fn admit<R: Copy, Q: Copy>(
&mut self,
slot: &OutputSlot<R, Q>,
shutdown: &AtomicBool,
#[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
hook: &mut impl GateWait,
) -> Admission {
let mut hook = Hook::new(hook);
match self.admit_pending(
shutdown,
#[cfg(feature = "tick-to-trade")]
tracker,
&mut hook,
) {
GateOutcome::Shutdown => Admission::Shutdown,
GateOutcome::Open(backing) => self.admit_slot(
slot,
backing,
shutdown,
#[cfg(feature = "tick-to-trade")]
tracker,
&mut hook,
),
}
}
#[inline]
fn admit_pending(
&mut self,
shutdown: &AtomicBool,
#[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
hook: &mut Hook<'_, impl GateWait>,
) -> GateOutcome {
while let Some(&pending) = self.pending.front() {
if pending.wire_seq.get() > self.cached_durable_pos {
hook.starting();
match self.wait_durable(
pending.wire_seq.get(),
shutdown,
#[cfg(feature = "tick-to-trade")]
tracker,
hook.hook,
) {
GateOutcome::Shutdown => return GateOutcome::Shutdown,
GateOutcome::Open(Backing::PrimaryOnly) => {
return GateOutcome::Open(Backing::PrimaryOnly);
}
GateOutcome::Open(Backing::Policy) => {}
}
if self.pending.front() != Some(&pending) {
continue;
}
}
self.apply_front_pending();
}
GateOutcome::Open(Backing::Policy)
}
#[inline]
fn admit_slot<R: Copy, Q: Copy>(
&mut self,
slot: &OutputSlot<R, Q>,
mut backing: Backing,
shutdown: &AtomicBool,
#[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
hook: &mut Hook<'_, impl GateWait>,
) -> Admission {
let change = match slot.payload {
OutputPayload::AckPolicyChange(change) => Some(change),
OutputPayload::Report(_)
| OutputPayload::QueryResponse(_)
| OutputPayload::BatchEnd => None,
};
if slot_needs_gate(slot, self.cached_durable_pos) {
hook.starting();
match self.wait_durable(
slot.wire_seq,
shutdown,
#[cfg(feature = "tick-to-trade")]
tracker,
hook.hook,
) {
GateOutcome::Shutdown => return Admission::Shutdown,
GateOutcome::Open(released) => backing = backing.min(released),
}
}
let Some(change) = change else {
return Admission::Reply {
connection_id: slot.connection_id,
backing,
};
};
let wire_seq = WireSeq::new(slot.wire_seq);
match backing {
Backing::Policy => {
let cause = if change.unwitnessed {
"unwitnessed change, in order"
} else {
"journaled change"
};
self.apply_change(wire_seq, change.policy, cause);
}
Backing::PrimaryOnly => self.defer_change(wire_seq, change),
}
Admission::GateOnly
}
fn defer_change(&mut self, wire_seq: WireSeq, change: AckPolicyChange) {
if self.is_stale(wire_seq) {
return;
}
let Some(policy) = AckPolicy::from_byte(change.policy) else {
self.refuse_unknown(wire_seq, change.policy);
return;
};
tracing::info!(
stage = self.stage,
policy = policy.as_str(),
wire_seq = wire_seq.get(),
"ack-policy change released on this node's disk alone: applied once durable under \
the policy before it"
);
self.pending.push_back(PendingChange {
wire_seq,
policy,
unwitnessed: change.unwitnessed,
});
}
#[inline]
fn wait_durable(
&mut self,
needed: u64,
shutdown: &AtomicBool,
#[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
hook: &mut impl GateWait,
) -> GateOutcome {
let mut gate_waiter = self.inputs.wait.waiter();
loop {
if shutdown.load(Ordering::Relaxed) {
return GateOutcome::Shutdown;
}
let journal_pos = self.inputs.journal_persisted_wire_seq.load();
self.observe_forced(journal_pos);
let metrics_ref = self.inputs.replication_metrics.as_deref();
let active_ref = self.inputs.replica_active.as_ref();
#[cfg(feature = "tick-to-trade")]
tracker.observe(
journal_pos.get(),
policy_replica_cursor(&self.policy, journal_pos, metrics_ref, active_ref),
trace::mono_trace_ns(),
);
let (status, blocker) =
evaluate_gate(&self.policy, needed, journal_pos, metrics_ref, active_ref);
self.cached_durable_pos = status.durable_pos;
self.inputs
.utilization
.policy_degraded
.store(status.degraded, Ordering::Relaxed);
if self
.accrual_timer
.tick(GATE_ACCRUAL_INTERVAL, gate_waiter.spinning())
.is_some()
{
self.degraded_logger.tick(
&self.policy,
&self.inputs.utilization,
status.degraded,
Instant::now(),
DEGRADED_LOG_INTERVAL,
);
}
if self.cached_durable_pos >= needed {
match blocker {
Some(Blocker::Journal) => {
self.inputs
.utilization
.gate_journal
.fetch_add(1, Ordering::Relaxed);
}
Some(Blocker::Replication) => {
self.inputs
.utilization
.gate_replication
.fetch_add(1, Ordering::Relaxed);
}
None => {}
}
return GateOutcome::Open(Backing::Policy);
}
if journal_pos.get() >= needed && self.degraded_release_open() {
return GateOutcome::Open(Backing::PrimaryOnly);
}
hook.while_waiting();
gate_waiter.idle();
}
}
#[inline]
pub(crate) fn idle_recheck(&mut self, now: Instant) {
if now.duration_since(self.last_policy_check) < POLICY_CHECK_INTERVAL {
return;
}
self.last_policy_check = now;
let journal_pos = self.inputs.journal_persisted_wire_seq.load();
let status = evaluate_durability(
&self.policy,
journal_pos,
self.inputs.replication_metrics.as_deref(),
self.inputs.replica_active.as_ref(),
);
self.degraded_logger.tick(
&self.policy,
&self.inputs.utilization,
status.degraded,
now,
DEGRADED_LOG_INTERVAL,
);
self.cached_durable_pos = status.durable_pos;
self.settle_pending_if_durable(journal_pos);
self.degraded_release_open();
}
#[inline]
pub(crate) fn after_batch(&mut self) {
let ticked_at = Instant::now();
let degraded_now = self
.inputs
.utilization
.policy_degraded
.load(Ordering::Relaxed);
self.degraded_logger.tick(
&self.policy,
&self.inputs.utilization,
degraded_now,
ticked_at,
DEGRADED_LOG_INTERVAL,
);
self.last_policy_check = ticked_at;
}
}
fn halt_start(loss: LastLoss, seen: Option<Instant>, now: Instant) -> Instant {
match loss {
LastLoss::At(at) => at,
LastLoss::Unknown => seen.unwrap_or(now),
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use melin_transport_core::WireSeq;
use melin_transport_core::lineage::{ForcedPolicy, ForcedPolicyWriter, forced_policy_cell};
use melin_transport_core::replica_presence::ReplicaPresence;
use super::*;
use crate::policy_control::test_support::lineage;
const CORRUPT: u8 = 0xEE;
fn build(
initial: AckPolicy,
journal_pos: u64,
replication_metrics: Option<Arc<ReplicationMetrics>>,
replica_active: Option<[Arc<AtomicBool>; 2]>,
) -> (DurabilityGate, ForcedPolicyWriter) {
build_from(
GateStart::settled(initial),
journal_pos,
replication_metrics,
replica_active,
)
}
fn build_from(
start: GateStart,
journal_pos: u64,
replication_metrics: Option<Arc<ReplicationMetrics>>,
replica_active: Option<[Arc<AtomicBool>; 2]>,
) -> (DurabilityGate, ForcedPolicyWriter) {
let (forced_writer, forced) = forced_policy_cell();
let inputs = GateInputs {
journal_persisted_wire_seq: DurableWireSeqCursor::detached(WireSeq::new(journal_pos)),
replication_metrics,
replica_active,
utilization: Arc::new(StageUtilization::new()),
wait: WaitStrategy::SpinThenYield,
degraded_release: None,
};
let policy = GatePolicy { start, forced };
(DurabilityGate::new(inputs, policy, "test"), forced_writer)
}
fn pending_start(
in_force: AckPolicy,
wire_seq: u64,
policy: AckPolicy,
unwitnessed: bool,
) -> GateStart {
GateStart {
in_force,
pending: Some(PendingChange {
wire_seq: WireSeq::new(wire_seq),
policy,
unwitnessed,
}),
}
}
fn forced_change(wire_seq: u64, policy: AckPolicyByte) -> ForcedPolicy {
ForcedPolicy {
wire_seq: WireSeq::new(wire_seq),
policy,
}
}
#[track_caller]
fn assert_applied_through(gate: &DurabilityGate, wire_seq: u64) {
assert_eq!(gate.applied_through, Some(WireSeq::new(wire_seq)));
}
fn gate(policy: AckPolicy, journal_pos: u64) -> DurabilityGate {
build(policy, journal_pos, None, None).0
}
fn gate_with_replicas(policy: AckPolicy, pos: u64) -> DurabilityGate {
let metrics = ReplicationMetrics::default();
for slot in 0..2 {
metrics.acked_sequence[slot].store(pos, Ordering::Relaxed);
metrics.in_memory_sequence[slot].store(pos, Ordering::Relaxed);
}
build(
policy,
pos,
Some(Arc::new(metrics)),
Some([
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(true)),
]),
)
.0
}
fn gate_with_one_replica(
initial: AckPolicy,
journal_pos: u64,
) -> (DurabilityGate, Arc<ReplicationMetrics>, ForcedPolicyWriter) {
gate_with_one_replica_from(GateStart::settled(initial), journal_pos)
}
fn gate_with_one_replica_from(
start: GateStart,
journal_pos: u64,
) -> (DurabilityGate, Arc<ReplicationMetrics>, ForcedPolicyWriter) {
let metrics = Arc::new(ReplicationMetrics::default());
let (gate, forced) = build_from(
start,
journal_pos,
Some(Arc::clone(&metrics)),
Some([
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(false)),
]),
);
(gate, metrics, forced)
}
fn replica_holds(metrics: &ReplicationMetrics, pos: u64) {
metrics.in_memory_sequence[0].store(pos, Ordering::Release);
}
fn replica_persists(metrics: &ReplicationMetrics, pos: u64) {
replica_holds(metrics, pos);
metrics.acked_sequence[0].store(pos, Ordering::Release);
}
fn reply(wire_seq: u64) -> OutputSlot<u8, u8> {
OutputSlot {
wire_seq,
payload: OutputPayload::Report(0),
is_last_in_request: true,
..OutputSlot::default()
}
}
fn change(wire_seq: u64, policy: AckPolicy, unwitnessed: bool) -> OutputSlot<u8, u8> {
change_byte(wire_seq, policy.to_byte(), unwitnessed)
}
fn change_byte(wire_seq: u64, policy: AckPolicyByte, unwitnessed: bool) -> OutputSlot<u8, u8> {
OutputSlot {
wire_seq,
payload: OutputPayload::AckPolicyChange(melin_journal::AckPolicyChange {
policy,
unwitnessed,
}),
..OutputSlot::default()
}
}
struct NoHook;
impl GateWait for NoHook {
fn before_wait(&mut self) {}
fn while_waiting(&mut self) {}
}
struct Recording<F: FnMut()> {
calls: Vec<&'static str>,
during: F,
}
impl<F: FnMut()> Recording<F> {
fn new(during: F) -> Self {
Self {
calls: Vec::new(),
during,
}
}
}
impl<F: FnMut()> GateWait for Recording<F> {
fn before_wait(&mut self) {
self.calls.push("before");
}
fn while_waiting(&mut self) {
self.calls.push("during");
(self.during)();
}
}
fn admit(
gate: &mut DurabilityGate,
slot: &OutputSlot<u8, u8>,
shutdown: &AtomicBool,
) -> Admission {
#[cfg(feature = "tick-to-trade")]
let mut tracker = GateCrossTracker::new(slot.wire_seq);
gate.admit(
slot,
shutdown,
#[cfg(feature = "tick-to-trade")]
&mut tracker,
&mut NoHook,
)
}
const HOLD: Duration = Duration::from_millis(50);
const OPEN_WITHIN: Duration = Duration::from_secs(10);
fn admit_watched(
gate: &mut DurabilityGate,
slot: &OutputSlot<u8, u8>,
hold: Option<Duration>,
release: impl FnOnce(),
) -> (bool, bool) {
let shutdown = AtomicBool::new(false);
std::thread::scope(|s| {
let admitting = s.spawn(|| admit(gate, slot, &shutdown));
let held = hold.is_some_and(|hold| {
std::thread::sleep(hold);
!admitting.is_finished()
});
release();
let deadline = Instant::now() + OPEN_WITHIN;
while !admitting.is_finished() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(1));
}
shutdown.store(true, Ordering::Relaxed);
let outcome = admitting.join().expect("the admitting thread panicked");
(held, outcome != Admission::Shutdown)
})
}
#[track_caller]
fn admit_now(gate: &mut DurabilityGate, slot: &OutputSlot<u8, u8>) {
let (_, opened) = admit_watched(gate, slot, None, || {});
assert!(opened, "slot {} did not open", slot.wire_seq);
}
#[track_caller]
fn assert_held_until(
gate: &mut DurabilityGate,
slot: &OutputSlot<u8, u8>,
release: impl FnOnce(),
) {
let (held, opened) = admit_watched(gate, slot, Some(HOLD), release);
assert!(
held,
"slot {} was admitted before its release",
slot.wire_seq
);
assert!(
opened,
"slot {} stayed held after its release",
slot.wire_seq
);
}
fn in_force(gate: &DurabilityGate) -> Option<AckPolicy> {
gate.inputs
.utilization
.ack_policy_in_force
.load()
.map(|byte| AckPolicy::from_byte(byte).expect("a known policy"))
}
fn wait(gate: &mut DurabilityGate, needed: u64, shutdown: &AtomicBool) -> GateOutcome {
#[cfg(feature = "tick-to-trade")]
let mut tracker = GateCrossTracker::new(needed);
gate.wait_durable(
needed,
shutdown,
#[cfg(feature = "tick-to-trade")]
&mut tracker,
&mut NoHook,
)
}
fn degraded_nanos(gate: &DurabilityGate) -> u64 {
gate.inputs
.utilization
.policy_degraded_nanos
.load(Ordering::Relaxed)
}
fn attributions(gate: &DurabilityGate) -> (u64, u64) {
let u = &gate.inputs.utilization;
(
u.gate_journal.load(Ordering::Relaxed),
u.gate_replication.load(Ordering::Relaxed),
)
}
fn let_time_pass() {
std::thread::sleep(Duration::from_millis(2));
}
#[test]
fn startup_resolves_the_policy_and_its_durable_position() {
let g = gate(AckPolicy::Disk, 5);
assert_eq!(g.active_policy, AckPolicy::Disk);
assert_eq!(g.cached_durable_pos, 5);
assert!(!g.inputs.utilization.policy_degraded.load(Ordering::Relaxed));
let g = gate(AckPolicy::DiskAndRam, 5);
assert_eq!(g.cached_durable_pos, 0, "an unsatisfiable policy pins 0");
assert!(g.inputs.utilization.policy_degraded.load(Ordering::Relaxed));
}
#[test]
fn a_switch_resets_the_cached_position_and_reseeds_the_logger() {
let mut g = gate(AckPolicy::Disk, 5);
g.apply_change(WireSeq::new(1), AckPolicy::TwoDisks.to_byte(), "test");
assert_eq!(g.active_policy, AckPolicy::TwoDisks);
assert_eq!(g.policy, AckPolicy::TwoDisks.to_policy());
assert_eq!(g.cached_durable_pos, 0);
assert_eq!(in_force(&g), Some(AckPolicy::TwoDisks));
let mut g = gate(AckPolicy::DiskAndRam, 5);
let_time_pass();
g.apply_change(WireSeq::new(1), AckPolicy::Disk.to_byte(), "test");
assert_eq!(g.active_policy, AckPolicy::Disk);
assert!(
degraded_nanos(&g) > 0,
"the re-seed flushes the degraded interval before the switch"
);
}
#[test]
fn a_change_to_the_policy_in_force_is_applied_without_a_switch() {
let mut g = gate(AckPolicy::Disk, 5);
g.apply_change(WireSeq::new(3), AckPolicy::Disk.to_byte(), "test");
assert_eq!(g.active_policy, AckPolicy::Disk);
assert_eq!(g.cached_durable_pos, 5, "no switch: cached position kept");
assert_applied_through(&g, 3);
g.apply_change(WireSeq::new(2), AckPolicy::TwoDisks.to_byte(), "test");
assert_eq!(g.active_policy, AckPolicy::Disk, "stale behind it");
let mut g = gate(AckPolicy::DiskAndRam, 5);
let_time_pass();
g.apply_change(WireSeq::new(1), AckPolicy::DiskAndRam.to_byte(), "test");
assert_eq!(degraded_nanos(&g), 0, "no re-seed, so nothing flushed");
}
#[test]
fn the_initial_policy_is_the_lineages_with_its_newest_change_pending() {
let disk = AckPolicy::Disk.to_byte();
let two_disks = AckPolicy::TwoDisks.to_byte();
let corrupt = AckPolicyByte(CORRUPT);
use Replication::{Disabled, Enabled};
for replication in [Enabled, Disabled] {
assert_eq!(
initial_gate_policy(None, AckPolicy::Disk, replication),
Ok(GateStart::settled(AckPolicy::Disk))
);
let refused = initial_gate_policy(
lineage(corrupt, false, 3, None),
AckPolicy::Disk,
replication,
)
.expect_err("an unknown byte is refused");
assert!(refused.contains("238"), "names the byte: {refused}");
assert_eq!(
initial_gate_policy(
lineage(disk, true, 9, Some(two_disks)),
AckPolicy::Ram,
replication,
),
Ok(match replication {
Enabled => pending_start(AckPolicy::TwoDisks, 9, AckPolicy::Disk, true),
Disabled => GateStart::settled(AckPolicy::Ram),
}),
"replication {replication:?}"
);
}
assert_eq!(
initial_gate_policy(lineage(two_disks, false, 1, None), AckPolicy::Disk, Enabled),
Ok(GateStart::settled(AckPolicy::TwoDisks)),
"the lineage's first change: nothing to apply again"
);
assert_eq!(
initial_gate_policy(
lineage(disk, false, 4, Some(two_disks)),
AckPolicy::Disk,
Enabled
),
Ok(pending_start(
AckPolicy::TwoDisks,
4,
AckPolicy::Disk,
false
)),
"requested equal to the newest: still pending"
);
let refused = initial_gate_policy(
lineage(disk, false, 4, Some(corrupt)),
AckPolicy::Disk,
Enabled,
)
.expect_err("an unknown previous byte is refused");
assert!(refused.contains("238"), "names the byte: {refused}");
}
#[test]
fn a_pending_change_is_held_until_durable_under_the_policy_before_it() {
let (mut g, metrics, _forced) = gate_with_one_replica_from(
pending_start(AckPolicy::DiskAndRam, 5, AckPolicy::Disk, false),
10,
);
assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam));
replica_holds(&metrics, 4);
assert_held_until(&mut g, &reply(8), || replica_holds(&metrics, 5));
assert_eq!(g.active_policy, AckPolicy::Disk);
assert_eq!(in_force(&g), Some(AckPolicy::Disk));
assert!(g.pending.is_empty(), "consumed");
assert_applied_through(&g, 5);
admit_now(&mut g, &reply(9));
}
#[test]
fn an_unwitnessed_pending_change_is_applied_once_on_disk() {
let (g, _) = build_from(
pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Disk, true),
4,
None,
None,
);
assert_eq!(g.active_policy, AckPolicy::Disk);
assert_eq!(in_force(&g), Some(AckPolicy::Disk));
assert!(g.pending.is_empty());
let (mut g, _) = build_from(
pending_start(AckPolicy::DiskAndRam, 6, AckPolicy::Disk, true),
4,
None,
None,
);
assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam));
let journal = g.inputs.journal_persisted_wire_seq.clone();
assert_held_until(&mut g, &reply(7), || journal.store(WireSeq::new(7)));
assert_eq!(g.active_policy, AckPolicy::Disk);
assert_applied_through(&g, 6);
}
#[test]
fn a_pending_change_already_durable_is_applied_at_the_start() {
let metrics = ReplicationMetrics::default();
for slot in 0..2 {
metrics.in_memory_sequence[slot].store(6, Ordering::Relaxed);
}
let (g, _) = build_from(
pending_start(AckPolicy::Ram, 3, AckPolicy::Disk, false),
6,
Some(Arc::new(metrics)),
Some([
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(true)),
]),
);
assert_eq!(g.active_policy, AckPolicy::Disk);
assert!(g.pending.is_empty());
}
#[test]
fn a_boot_change_after_a_pending_one_is_applied_after_it() {
let (mut g, metrics, _forced) = gate_with_one_replica_from(
pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Disk, false),
10,
);
assert_held_until(&mut g, &change(4, AckPolicy::TwoDisks, false), || {
replica_holds(&metrics, 3)
});
assert_eq!(g.active_policy, AckPolicy::TwoDisks);
replica_holds(&metrics, 5);
assert_held_until(&mut g, &reply(5), || replica_persists(&metrics, 5));
}
#[test]
fn the_idle_recheck_applies_a_confirmed_pending_change() {
let (mut g, metrics, _forced) = gate_with_one_replica_from(
pending_start(AckPolicy::DiskAndRam, 5, AckPolicy::Disk, false),
10,
);
let checked = g.last_policy_check;
replica_holds(&metrics, 4);
g.idle_recheck(checked + POLICY_CHECK_INTERVAL);
assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam), "not yet held");
assert!(!g.pending.is_empty());
replica_holds(&metrics, 5);
g.idle_recheck(checked + 2 * POLICY_CHECK_INTERVAL);
assert_eq!(in_force(&g), Some(AckPolicy::Disk));
assert!(g.pending.is_empty());
}
#[test]
fn a_forced_change_overtakes_a_pending_one() {
let (mut g, mut forced) = build_from(
pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Ram, false),
4,
None,
None,
);
let journal = g.inputs.journal_persisted_wire_seq.clone();
forced.store(forced_change(5, AckPolicy::Disk.to_byte()));
assert_held_until(&mut g, &reply(6), || journal.store(WireSeq::new(6)));
assert_eq!(
g.active_policy,
AckPolicy::Disk,
"not the stale pending `ram`"
);
assert_applied_through(&g, 5);
assert!(g.pending.is_empty());
}
#[test]
fn shutdown_releases_a_wait_on_the_pending_change() {
let (mut g, _) = build_from(
pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Disk, false),
4,
None,
None,
);
let shutdown = AtomicBool::new(true);
assert_eq!(admit(&mut g, &reply(4), &shutdown), Admission::Shutdown);
assert!(!g.pending.is_empty());
assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
}
#[test]
fn an_open_gate_attributes_exactly_once() {
let mut g = gate(AckPolicy::Disk, 5);
let shutdown = AtomicBool::new(false);
assert!(matches!(
wait(&mut g, 3, &shutdown),
GateOutcome::Open(Backing::Policy)
));
assert_eq!(attributions(&g), (1, 0), "disk opens on the journal");
assert_eq!(g.cached_durable_pos, 5);
}
#[test]
fn shutdown_releases_a_wedged_gate_without_attribution() {
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let mut g = gate(AckPolicy::DiskAndRam, 5);
let shutdown = AtomicBool::new(true);
let outcome = wait(&mut g, 3, &shutdown);
let _ = tx.send((matches!(outcome, GateOutcome::Shutdown), attributions(&g)));
});
let (was_shutdown, attributed) = rx
.recv_timeout(Duration::from_secs(10))
.expect("the wedged gate ignored shutdown");
assert!(was_shutdown);
assert_eq!(attributed, (0, 0));
}
#[test]
fn shutdown_wins_over_a_gate_ready_to_open() {
let mut g = gate_with_replicas(AckPolicy::Disk, 5);
let shutdown = AtomicBool::new(true);
assert!(matches!(wait(&mut g, 3, &shutdown), GateOutcome::Shutdown));
assert_eq!(attributions(&g), (0, 0));
}
#[test]
fn a_change_applies_from_its_own_slot_on() {
let (mut g, metrics, _forced) = gate_with_one_replica(AckPolicy::Disk, 10);
assert_eq!(in_force(&g), Some(AckPolicy::Disk));
admit_now(&mut g, &reply(4));
admit_now(&mut g, &change(5, AckPolicy::DiskAndRam, false));
assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
assert_eq!(in_force(&g), Some(AckPolicy::DiskAndRam));
assert_held_until(&mut g, &reply(6), || replica_holds(&metrics, 6));
}
#[test]
fn a_change_slot_goes_to_no_connection() {
let mut g = gate(AckPolicy::Disk, 10);
let shutdown = AtomicBool::new(false);
let to = |connection_id, slot: OutputSlot<u8, u8>| OutputSlot {
connection_id,
..slot
};
assert_eq!(
admit(&mut g, &to(9, reply(3)), &shutdown),
Admission::Reply {
connection_id: 9,
backing: Backing::Policy
}
);
assert_eq!(
admit(&mut g, &to(9, change(4, AckPolicy::Disk, true)), &shutdown),
Admission::GateOnly
);
assert_applied_through(&g, 4);
assert_eq!(
admit(&mut g, &to(9, change(4, AckPolicy::Ram, false)), &shutdown),
Admission::GateOnly,
"stale, and still the gate's"
);
assert_eq!(g.active_policy, AckPolicy::Disk, "the stale one ignored");
}
#[test]
fn a_change_is_held_until_the_old_policy_clears_its_entry() {
let (mut g, metrics, _forced) = gate_with_one_replica(AckPolicy::DiskAndRam, 10);
assert_held_until(&mut g, &change(5, AckPolicy::Disk, false), || {
replica_holds(&metrics, 5)
});
assert_eq!(g.active_policy, AckPolicy::Disk);
admit_now(&mut g, &reply(8));
}
#[test]
fn two_pending_changes_are_never_merged() {
let (mut g, metrics, _forced) = gate_with_one_replica(AckPolicy::Disk, 10);
admit_now(&mut g, &change(3, AckPolicy::DiskAndRam, false));
assert_held_until(&mut g, &reply(4), || replica_holds(&metrics, 4));
assert_held_until(&mut g, &change(6, AckPolicy::Disk, false), || {
replica_holds(&metrics, 6)
});
assert_eq!(g.active_policy, AckPolicy::Disk);
admit_now(&mut g, &reply(7));
}
struct Primary {
gate: DurabilityGate,
journal: DurableWireSeqCursor,
metrics: Arc<ReplicationMetrics>,
active: Arc<AtomicBool>,
presence: Arc<ReplicaPresence>,
forced: ForcedPolicyWriter,
}
impl Primary {
fn new(policy: AckPolicy, pos: u64, connected: bool, grace: Option<Duration>) -> Self {
Self::starting(GateStart::settled(policy), pos, connected, grace)
}
fn starting(start: GateStart, pos: u64, connected: bool, grace: Option<Duration>) -> Self {
let metrics = Arc::new(ReplicationMetrics::default());
let active = Arc::new(AtomicBool::new(false));
let presence = Arc::new(ReplicaPresence::new());
let journal = DurableWireSeqCursor::detached(WireSeq::new(pos));
let (forced_writer, forced) = forced_policy_cell();
let inputs = GateInputs {
journal_persisted_wire_seq: journal.clone(),
replication_metrics: Some(Arc::clone(&metrics)),
replica_active: Some([Arc::clone(&active), Arc::new(AtomicBool::new(false))]),
utilization: Arc::new(StageUtilization::new()),
wait: WaitStrategy::SpinThenYield,
degraded_release: grace.map(|grace| DegradedRelease {
presence: Arc::clone(&presence),
grace,
}),
};
let p = Self {
gate: DurabilityGate::new(inputs, GatePolicy { start, forced }, "test"),
journal,
metrics,
active,
presence,
forced: forced_writer,
};
if connected {
p.replica_streams(pos);
}
p
}
fn replica_leaves(&self) {
self.active.store(false, Ordering::Release);
self.presence.lower();
}
fn replica_streams(&self, pos: u64) {
drop(self.presence.lift());
replica_persists(&self.metrics, pos);
self.active.store(true, Ordering::Release);
}
fn journal_at(&self, pos: u64) {
self.journal.store(WireSeq::new(pos));
}
fn degraded_acks(&self) -> u64 {
self.gate
.inputs
.utilization
.degraded_acks
.load(Ordering::Relaxed)
}
fn admit_within(
&mut self,
slot: &OutputSlot<u8, u8>,
patience: Duration,
) -> Option<Admission> {
let shutdown = AtomicBool::new(false);
std::thread::scope(|s| {
let gate = &mut self.gate;
let admitting = s.spawn(|| admit(gate, slot, &shutdown));
let deadline = Instant::now() + patience;
while !admitting.is_finished() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(1));
}
shutdown.store(true, Ordering::Relaxed);
match admitting.join().expect("the admitting thread panicked") {
Admission::Shutdown => None,
admitted => Some(admitted),
}
})
}
fn release(&mut self, wire_seq: u64, patience: Duration) -> Option<Backing> {
self.release_slot(&reply(wire_seq), patience)
}
fn release_slot(
&mut self,
slot: &OutputSlot<u8, u8>,
patience: Duration,
) -> Option<Backing> {
match self.admit_within(slot, patience)? {
Admission::Reply { backing, .. } => Some(backing),
other => panic!("a reply slot admitted as {other:?}"),
}
}
}
const PATIENCE: Duration = Duration::from_millis(300);
const NO_GRACE: Option<Duration> = Some(Duration::ZERO);
#[test]
fn no_degraded_release_before_the_grace_period() {
let mut p = Primary::new(
AckPolicy::DiskAndRam,
5,
false,
Some(Duration::from_secs(60)),
);
assert_eq!(p.release(5, PATIENCE), None, "held for the grace period");
assert_eq!(p.degraded_acks(), 0);
}
#[test]
fn a_halted_node_past_the_grace_releases_on_its_own_fsync_marked() {
for policy in [AckPolicy::Ram, AckPolicy::DiskAndRam, AckPolicy::TwoDisks] {
let mut p = Primary::new(policy, 5, false, NO_GRACE);
assert_eq!(
p.release(5, PATIENCE),
Some(Backing::PrimaryOnly),
"{policy}"
);
assert_eq!(p.gate.cached_durable_pos, 0, "the policy confirmed nothing");
}
}
#[test]
fn the_grace_period_runs_from_the_loss() {
let mut p = Primary::new(
AckPolicy::DiskAndRam,
5,
true,
Some(Duration::from_millis(150)),
);
p.journal_at(6);
p.replica_leaves();
let started = Instant::now();
assert_eq!(
p.release(6, Duration::from_secs(5)),
Some(Backing::PrimaryOnly)
);
assert!(
started.elapsed() >= Duration::from_millis(150),
"released before the grace ran out: {:?}",
started.elapsed()
);
}
#[test]
fn an_unseen_rejoin_restarts_the_grace_period() {
let grace = Duration::from_millis(1_000);
let mut p = Primary::new(AckPolicy::DiskAndRam, 5, true, Some(grace));
p.replica_leaves();
assert!(!p.gate.degraded_release_open(), "the halt has just begun");
std::thread::sleep(Duration::from_millis(700));
p.replica_streams(5);
p.replica_leaves();
let second_loss = Instant::now();
std::thread::sleep(Duration::from_millis(400));
assert!(
!p.gate.degraded_release_open(),
"aged from the first loss: {:?} since the second",
second_loss.elapsed()
);
std::thread::sleep(grace.saturating_sub(second_loss.elapsed()));
assert!(
p.gate.degraded_release_open(),
"past the grace from the second loss"
);
}
#[test]
fn a_missing_loss_stamp_ages_from_the_first_sighting() {
let seen = Instant::now();
let now = seen + Duration::from_secs(10);
let lost = seen + Duration::from_secs(3);
assert_eq!(
halt_start(LastLoss::Unknown, None, now),
now,
"first sighting"
);
assert_eq!(halt_start(LastLoss::Unknown, Some(seen), now), seen);
assert_eq!(halt_start(LastLoss::At(lost), Some(seen), now), lost);
assert_eq!(halt_start(LastLoss::At(lost), None, now), lost);
}
#[test]
fn the_halt_start_is_tracked_while_halted_and_dropped_on_a_replica() {
let mut p = Primary::new(AckPolicy::DiskAndRam, 5, true, NO_GRACE);
assert!(!p.gate.degraded_release_open());
assert_eq!(p.gate.halt_started, None);
let before = Instant::now();
p.replica_leaves();
assert!(p.gate.degraded_release_open());
let started = p.gate.halt_started.expect("halted");
assert!(started >= before, "the loss's own stamp");
assert!(p.gate.degraded_release_open());
assert_eq!(p.gate.halt_started, Some(started), "the same halt");
p.replica_streams(5);
assert!(!p.gate.degraded_release_open());
assert_eq!(p.gate.halt_started, None);
assert!(!p.gate.releasing_degraded);
}
#[test]
fn the_release_follows_a_switch_to_and_from_a_policy_needing_a_replica() {
let mut p = Primary::new(AckPolicy::Disk, 5, false, NO_GRACE);
assert!(!p.gate.degraded_release_open(), "disk needs no replica");
p.gate.switch_to(AckPolicy::TwoDisks, "test");
assert!(p.gate.degraded_release_open());
assert!(p.gate.releasing_degraded);
p.gate.switch_to(AckPolicy::Disk, "test");
assert!(!p.gate.degraded_release_open());
assert!(!p.gate.releasing_degraded);
}
#[test]
fn no_degraded_release_under_disk() {
let mut p = Primary::new(AckPolicy::Disk, 5, false, NO_GRACE);
assert_eq!(p.release(5, PATIENCE), Some(Backing::Policy));
assert_eq!(p.release(6, PATIENCE), None, "not yet on the journal");
assert!(!p.gate.releasing_degraded);
}
#[test]
fn no_degraded_release_with_the_switch_off() {
let mut p = Primary::new(AckPolicy::DiskAndRam, 5, false, None);
assert_eq!(p.release(5, PATIENCE), None, "the stall, unchanged");
assert_eq!(p.degraded_acks(), 0);
}
#[test]
fn no_degraded_release_while_a_replica_is_connected_but_behind() {
let mut p = Primary::new(AckPolicy::TwoDisks, 5, true, NO_GRACE);
p.journal_at(9);
assert_eq!(p.release(9, PATIENCE), None, "held for the replica");
}
#[test]
fn an_unfsynced_slot_under_ram_is_held_until_the_primary_fsyncs_it() {
let mut p = Primary::new(AckPolicy::Ram, 5, false, NO_GRACE);
assert_eq!(
p.release(6, PATIENCE),
None,
"the primary's disk does not hold it yet"
);
p.journal_at(6);
assert_eq!(p.release(6, PATIENCE), Some(Backing::PrimaryOnly));
}
#[test]
fn a_full_ack_resumes_once_a_replica_streams_again() {
let mut p = Primary::new(AckPolicy::DiskAndRam, 5, false, NO_GRACE);
assert_eq!(p.release(5, PATIENCE), Some(Backing::PrimaryOnly));
assert!(p.gate.releasing_degraded);
p.journal_at(7);
p.replica_streams(7);
assert_eq!(
p.release(6, PATIENCE),
Some(Backing::Policy),
"the policy is met again"
);
assert_eq!(p.release(7, PATIENCE), Some(Backing::Policy));
assert_eq!(p.gate.cached_durable_pos, 7);
p.journal_at(9);
assert_eq!(p.release(9, PATIENCE), None);
assert!(!p.gate.releasing_degraded);
}
#[test]
fn a_query_over_state_confirmed_before_the_loss_is_answered_primary_only() {
let grace = Duration::from_millis(100);
let mut p = Primary::new(AckPolicy::DiskAndRam, 5, true, Some(grace));
assert_eq!(
p.release(5, PATIENCE),
Some(Backing::Policy),
"before the loss the policy confirms everything through 5"
);
assert_eq!(p.gate.cached_durable_pos, 5);
p.replica_leaves();
std::thread::sleep(grace);
let due = p.gate.last_policy_check + POLICY_CHECK_INTERVAL;
p.gate.idle_recheck(due);
assert_eq!(
p.gate.cached_durable_pos, 0,
"with no replica streaming the evaluation pins the durable position to 0"
);
let query = OutputSlot {
wire_seq: 5,
payload: OutputPayload::QueryResponse(0),
is_last_in_request: true,
..OutputSlot::default()
};
let backing = p.release_slot(&query, PATIENCE);
assert_eq!(
backing,
Some(Backing::PrimaryOnly),
"marked, though every replica confirmed position 5 before it left"
);
let mut ends = BatchEnds::new();
assert_eq!(
p.gate
.batch_end(Backing::PrimaryOnly, &mut ends, RequestId(5)),
encoded(TransportResponse::BatchEndDegraded { id: RequestId(5) })
);
assert_eq!(p.degraded_acks(), 1, "counted in melin_degraded_acks_total");
}
fn encoded(response: TransportResponse) -> Vec<u8> {
let mut buf = [0u8; BATCH_END_FRAME_LEN];
let n = control_codec::encode_transport_response(&response, &mut buf).unwrap();
buf[..n].to_vec()
}
#[test]
fn after_a_degraded_release_the_next_slot_is_still_evaluated_and_still_degraded() {
let mut p = Primary::new(AckPolicy::DiskAndRam, 9, false, NO_GRACE);
assert_eq!(p.release(5, PATIENCE), Some(Backing::PrimaryOnly));
assert!(
slot_needs_gate(&reply(6), p.gate.cached_durable_pos),
"the next slot is not waved through on the degraded release"
);
assert_eq!(
p.release(6, PATIENCE),
Some(Backing::PrimaryOnly),
"never a full BatchEnd"
);
assert_eq!(p.gate.cached_durable_pos, 0);
}
#[test]
fn a_change_released_degraded_is_applied_only_once_the_policy_before_it_confirms_it() {
let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
assert_eq!(
p.admit_within(&change(4, AckPolicy::TwoDisks, false), PATIENCE),
Some(Admission::GateOnly)
);
assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam, "not applied");
assert_eq!(p.gate.applied_through, None);
assert_eq!(p.gate.pending.len(), 1, "deferred");
assert_eq!(p.release(5, PATIENCE), Some(Backing::PrimaryOnly));
drop(p.presence.lift());
replica_holds(&p.metrics, 6);
p.active.store(true, Ordering::Release);
assert_eq!(
p.release(6, PATIENCE),
None,
"judged under two-disks, which the replica's disk does not meet yet"
);
assert_eq!(p.gate.active_policy, AckPolicy::TwoDisks);
assert!(p.gate.pending.is_empty());
replica_persists(&p.metrics, 6);
assert_eq!(p.release(6, PATIENCE), Some(Backing::Policy));
}
#[test]
fn a_pending_change_released_degraded_stays_pending_and_marks_what_follows() {
let mut p = Primary::starting(
pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Ram, false),
5,
false,
NO_GRACE,
);
assert_eq!(p.release(4, PATIENCE), Some(Backing::PrimaryOnly));
assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam);
assert_eq!(p.gate.pending.len(), 1);
p.replica_streams(5);
assert_eq!(p.release(5, PATIENCE), Some(Backing::Policy));
assert_eq!(p.gate.active_policy, AckPolicy::Ram, "applied in order");
assert!(p.gate.pending.is_empty());
}
#[test]
fn changes_released_degraded_are_applied_one_by_one() {
let mut p = Primary::new(AckPolicy::TwoDisks, 8, false, NO_GRACE);
for (wire_seq, policy) in [(3, AckPolicy::DiskAndRam), (5, AckPolicy::Disk)] {
assert_eq!(
p.admit_within(&change(wire_seq, policy, false), PATIENCE),
Some(Admission::GateOnly)
);
}
assert_eq!(p.gate.pending.len(), 2);
drop(p.presence.lift());
p.metrics.acked_sequence[0].store(3, Ordering::Release);
replica_holds(&p.metrics, 5);
p.active.store(true, Ordering::Release);
assert_eq!(p.release(8, PATIENCE), Some(Backing::Policy));
assert_eq!(p.gate.active_policy, AckPolicy::Disk);
assert_applied_through(&p.gate, 5);
}
#[test]
fn an_unwitnessed_change_released_degraded_applies_once_on_disk() {
let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
assert_eq!(
p.admit_within(&change(4, AckPolicy::Disk, true), PATIENCE),
Some(Admission::GateOnly)
);
assert_eq!(p.release(5, PATIENCE), Some(Backing::Policy));
assert_eq!(p.gate.active_policy, AckPolicy::Disk);
}
#[test]
fn a_pending_change_released_degraded_marks_a_slot_the_policy_then_confirms() {
let mut p = Primary::starting(
pending_start(AckPolicy::DiskAndRam, 3, AckPolicy::Ram, false),
5,
false,
NO_GRACE,
);
let shutdown = AtomicBool::new(false);
let mut recording = Recording::new(|| {});
let mut hook = Hook::new(&mut recording);
#[cfg(feature = "tick-to-trade")]
let mut tracker = GateCrossTracker::new(5);
let backing = match p.gate.admit_pending(
&shutdown,
#[cfg(feature = "tick-to-trade")]
&mut tracker,
&mut hook,
) {
GateOutcome::Open(backing) => backing,
GateOutcome::Shutdown => panic!("no shutdown was requested"),
};
assert_eq!(backing, Backing::PrimaryOnly);
assert_eq!(p.gate.pending.len(), 1, "left pending");
p.replica_streams(5);
let admitted = p.gate.admit_slot(
&reply(5),
backing,
&shutdown,
#[cfg(feature = "tick-to-trade")]
&mut tracker,
&mut hook,
);
assert_eq!(
admitted,
Admission::Reply {
connection_id: 0,
backing: Backing::PrimaryOnly
},
"never a full BatchEnd behind an unapplied change"
);
assert_eq!(
p.gate.cached_durable_pos, 5,
"the slot's wait opened in full"
);
assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam, "not applied");
assert_eq!(p.gate.pending.len(), 1);
assert_eq!(
recording.calls.iter().filter(|&&c| c == "before").count(),
1,
"the flush hook ran once, before the first wait"
);
assert_eq!(recording.calls.first(), Some(&"before"));
p.journal_at(6);
p.replica_streams(6);
assert_eq!(p.release(6, PATIENCE), Some(Backing::Policy));
assert_eq!(p.gate.active_policy, AckPolicy::Ram, "applied in order");
assert_applied_through(&p.gate, 3);
assert!(p.gate.pending.is_empty());
}
#[test]
fn a_change_released_degraded_behind_a_forced_one_is_stale() {
let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
p.forced.store(forced_change(5, AckPolicy::Ram.to_byte()));
assert_eq!(p.release(6, PATIENCE), Some(Backing::PrimaryOnly));
assert_eq!(p.gate.active_policy, AckPolicy::Ram);
assert_eq!(
p.admit_within(&change(4, AckPolicy::TwoDisks, false), PATIENCE),
Some(Admission::GateOnly)
);
assert!(p.gate.pending.is_empty(), "stale, not deferred");
assert_applied_through(&p.gate, 5);
assert_eq!(p.gate.active_policy, AckPolicy::Ram);
}
#[test]
fn an_unknown_policy_leaves_an_earlier_deferred_change_pending() {
let mut p = Primary::new(AckPolicy::DiskAndRam, 8, false, NO_GRACE);
assert_eq!(
p.admit_within(&change(3, AckPolicy::TwoDisks, false), PATIENCE),
Some(Admission::GateOnly)
);
assert_eq!(
p.admit_within(&change_byte(5, AckPolicyByte(CORRUPT), false), PATIENCE),
Some(Admission::GateOnly)
);
assert_eq!(p.gate.pending.len(), 1, "the unknown one is not deferred");
assert_eq!(p.gate.last_refused, Some(WireSeq::new(5)));
p.forced.store(forced_change(6, AckPolicyByte(CORRUPT)));
assert_eq!(p.release(7, PATIENCE), Some(Backing::PrimaryOnly));
assert_eq!(p.gate.last_refused, Some(WireSeq::new(6)));
assert_eq!(p.gate.active_policy, AckPolicy::DiskAndRam);
assert_eq!(p.gate.applied_through, None, "neither recorded as applied");
assert_eq!(p.gate.pending.len(), 1);
p.replica_streams(8);
assert_eq!(p.release(8, PATIENCE), Some(Backing::Policy));
assert_eq!(p.gate.active_policy, AckPolicy::TwoDisks, "not made stale");
assert_applied_through(&p.gate, 3);
assert!(p.gate.pending.is_empty());
}
#[test]
fn a_forced_change_overtakes_a_deferred_one() {
let mut p = Primary::new(AckPolicy::DiskAndRam, 6, false, NO_GRACE);
assert_eq!(
p.admit_within(&change(4, AckPolicy::Ram, false), PATIENCE),
Some(Admission::GateOnly)
);
assert_eq!(p.gate.pending.len(), 1, "deferred");
p.forced.store(forced_change(5, AckPolicy::Disk.to_byte()));
assert_eq!(p.release(6, PATIENCE), Some(Backing::Policy));
assert_eq!(
p.gate.active_policy,
AckPolicy::Disk,
"not the stale deferred `ram`"
);
assert_applied_through(&p.gate, 5);
assert!(p.gate.pending.is_empty());
}
#[test]
fn the_terminator_follows_the_backing_and_counts_degraded_acks() {
let p = Primary::new(AckPolicy::DiskAndRam, 5, false, NO_GRACE);
let mut ends = BatchEnds::new();
for id in [RequestId(1), RequestId(u64::MAX), RequestId(0)] {
assert_eq!(
p.gate.batch_end(Backing::Policy, &mut ends, id),
encoded(TransportResponse::BatchEnd { id })
);
}
assert_eq!(p.degraded_acks(), 0);
assert_eq!(
p.gate
.batch_end(Backing::PrimaryOnly, &mut ends, RequestId(7)),
encoded(TransportResponse::BatchEndDegraded { id: RequestId(7) })
);
assert_eq!(p.degraded_acks(), 1);
assert_eq!(
ends.policy(RequestId(3)),
encoded(TransportResponse::BatchEnd { id: RequestId(3) })
);
}
#[test]
fn a_forced_swap_frees_an_earlier_stuck_slot_once_its_entry_is_on_disk() {
let (mut g, mut forced) = build(AckPolicy::DiskAndRam, 4, None, None);
let journal = g.inputs.journal_persisted_wire_seq.clone();
forced.store(forced_change(6, AckPolicy::Disk.to_byte()));
assert_held_until(&mut g, &reply(3), || journal.store(WireSeq::new(6)));
assert_eq!(g.active_policy, AckPolicy::Disk);
assert_eq!(in_force(&g), Some(AckPolicy::Disk));
assert!(
degraded_nanos(&g) > 0,
"degraded time accrued during the wedge"
);
assert_eq!(attributions(&g), (1, 0), "opened on the journal, once");
let cached = g.cached_durable_pos;
admit_now(&mut g, &change(5, AckPolicy::TwoDisks, false));
admit_now(&mut g, &change(6, AckPolicy::Disk, true));
assert_eq!(g.active_policy, AckPolicy::Disk, "stale changes ignored");
assert_eq!(g.cached_durable_pos, cached, "no switch, no reset");
journal.store(WireSeq::new(7));
admit_now(&mut g, &change(7, AckPolicy::Ram, false));
assert_eq!(g.active_policy, AckPolicy::Ram);
}
#[test]
fn a_forced_change_applied_in_order_is_not_reapplied() {
let (mut g, metrics, mut forced) = gate_with_one_replica(AckPolicy::DiskAndRam, 10);
replica_holds(&metrics, 3);
forced.store(forced_change(2, AckPolicy::Disk.to_byte()));
admit_now(&mut g, &change(2, AckPolicy::Disk, true));
admit_now(&mut g, &change(3, AckPolicy::DiskAndRam, false));
assert_held_until(&mut g, &reply(4), || replica_holds(&metrics, 4));
assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
}
#[test]
fn an_unknown_policy_in_a_change_keeps_the_prior_policy() {
let (mut g, mut forced) = build(AckPolicy::Disk, 4, None, None);
let journal = g.inputs.journal_persisted_wire_seq.clone();
admit_now(&mut g, &change_byte(4, AckPolicyByte(CORRUPT), false));
assert_eq!(g.active_policy, AckPolicy::Disk);
assert_eq!(g.policy, AckPolicy::Disk.to_policy());
assert_eq!(in_force(&g), Some(AckPolicy::Disk));
assert_eq!(g.cached_durable_pos, 4, "no switch: cached position kept");
assert_eq!(g.applied_through, None, "not recorded as applied");
assert_eq!(g.last_refused, Some(WireSeq::new(4)));
forced.store(forced_change(6, AckPolicyByte(CORRUPT)));
assert_held_until(&mut g, &reply(5), || journal.store(WireSeq::new(6)));
assert_eq!(g.active_policy, AckPolicy::Disk);
assert_eq!(g.applied_through, None, "not recorded as applied");
assert_eq!(g.last_refused, Some(WireSeq::new(6)));
let mut g = gate(AckPolicy::DiskAndRam, 5);
let_time_pass();
g.apply_change(WireSeq::new(1), AckPolicyByte(CORRUPT), "test");
assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
assert_eq!(degraded_nanos(&g), 0, "no re-seed on an unknown byte");
}
fn admit_recorded(
g: &mut DurabilityGate,
wire_seq: u64,
during: impl FnMut(),
) -> Vec<&'static str> {
let shutdown = AtomicBool::new(false);
let mut hook = Recording::new(during);
#[cfg(feature = "tick-to-trade")]
let mut tracker = GateCrossTracker::new(wire_seq);
let outcome = g.admit(
&reply(wire_seq),
&shutdown,
#[cfg(feature = "tick-to-trade")]
&mut tracker,
&mut hook,
);
assert_eq!(
outcome,
Admission::Reply {
connection_id: 0,
backing: Backing::Policy
}
);
hook.calls
}
#[test]
fn before_wait_runs_only_when_the_slot_waits() {
let mut g = gate(AckPolicy::Disk, 5);
assert!(
admit_recorded(&mut g, 5, || {}).is_empty(),
"slot 5 is durable: no hook at all"
);
g.inputs.journal_persisted_wire_seq.store(WireSeq::new(6));
assert_eq!(admit_recorded(&mut g, 6, || {}), ["before"]);
}
#[test]
fn while_waiting_runs_during_a_wait_and_never_on_a_slot_that_does_not_wait() {
let mut g = gate(AckPolicy::Disk, 5);
let journal = g.inputs.journal_persisted_wire_seq.clone();
let mut turns = 0;
let calls = admit_recorded(&mut g, 6, || {
turns += 1;
if turns == 3 {
journal.store(WireSeq::new(6));
}
});
assert_eq!(calls, ["before", "during", "during", "during"]);
assert!(
admit_recorded(&mut g, 6, || panic!("slot 6 is durable now")).is_empty(),
"a slot that does not wait runs no hook"
);
}
#[test]
fn idle_recheck_waits_out_the_check_interval() {
let mut g = gate(AckPolicy::Disk, 5);
let checked = g.last_policy_check;
g.inputs.journal_persisted_wire_seq.store(WireSeq::new(9));
g.idle_recheck(checked + POLICY_CHECK_INTERVAL - Duration::from_millis(1));
assert_eq!(g.cached_durable_pos, 5, "too early: no re-evaluation");
assert_eq!(g.last_policy_check, checked);
let due = checked + POLICY_CHECK_INTERVAL;
g.idle_recheck(due);
assert_eq!(g.cached_durable_pos, 9);
assert_eq!(g.last_policy_check, due);
}
#[test]
fn a_batch_defers_the_next_idle_recheck() {
let mut g = gate(AckPolicy::Disk, 5);
g.last_policy_check = Instant::now()
.checked_sub(2 * POLICY_CHECK_INTERVAL)
.expect("the monotonic clock is past two check intervals");
g.inputs.journal_persisted_wire_seq.store(WireSeq::new(9));
g.after_batch();
g.idle_recheck(Instant::now());
assert_eq!(
g.cached_durable_pos, 5,
"the batch counted as the check, so the idle path waits"
);
}
}