use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::time::{Duration, Instant};
use melin_app::amortized_timer::AmortizedTimer;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::DurableWireSeqCursor;
use melin_transport_core::pipeline::{OutputSlot, StageUtilization};
#[cfg(feature = "tick-to-trade")]
use melin_transport_core::trace;
use crate::ack_policy::{AckPolicy, Blocker, Policy};
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);
#[must_use]
pub(crate) enum GateOutcome {
Open,
Shutdown,
}
pub(crate) struct GateInputs {
pub journal_persisted_wire_seq: DurableWireSeqCursor,
pub ack_policy: Arc<AtomicU8>,
pub replication_metrics: Option<Arc<ReplicationMetrics>>,
pub replica_active: Option<[Arc<AtomicBool>; 2]>,
pub utilization: Arc<StageUtilization>,
pub wait: WaitStrategy,
}
pub(crate) struct DurabilityGate {
inputs: GateInputs,
stage: &'static str,
active_policy: AckPolicy,
policy: Policy,
cached_durable_pos: u64,
degraded_logger: DegradationLogger,
last_policy_check: Instant,
accrual_timer: AmortizedTimer,
}
impl DurabilityGate {
pub(crate) fn new(inputs: GateInputs, stage: &'static str) -> Self {
let active_policy = AckPolicy::from_u8(inputs.ack_policy.load(Ordering::Relaxed))
.unwrap_or_else(|| {
tracing::error!(
stage,
"ack_policy atomic held a corrupted byte at startup; defaulting to disk+ram"
);
AckPolicy::DiskAndRam
});
let policy = active_policy.to_policy();
let now = Instant::now();
let status = evaluate_durability(
&policy,
inputs.journal_persisted_wire_seq.load(),
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)
};
Self {
inputs,
stage,
active_policy,
policy,
cached_durable_pos: status.durable_pos,
degraded_logger,
last_policy_check: now,
accrual_timer: AmortizedTimer::new(),
}
}
#[inline]
pub(crate) fn observe_policy_swap(&mut self) {
let observed_byte = self.inputs.ack_policy.load(Ordering::Relaxed);
if observed_byte == self.active_policy.as_u8() {
return;
}
match AckPolicy::from_u8(observed_byte) {
Some(next) => {
tracing::info!(
stage = self.stage,
prev = self.active_policy.as_str(),
next = next.as_str(),
"ack policy swapped at runtime"
);
self.active_policy = next;
self.policy = next.to_policy();
self.cached_durable_pos = 0;
self.degraded_logger
.reseed(&self.inputs.utilization, Instant::now());
}
None => {
tracing::error!(
stage = self.stage,
byte = observed_byte,
"ack_policy atomic held a corrupted byte; retaining prior policy"
);
}
}
}
#[inline]
pub(crate) fn needs_wait<R: Copy, Q: Copy>(&self, slot: &OutputSlot<R, Q>) -> bool {
slot_needs_gate(slot, self.cached_durable_pos)
}
#[inline]
pub(crate) fn wait_durable(
&mut self,
needed: u64,
shutdown: &AtomicBool,
#[cfg(feature = "tick-to-trade")] tracker: &mut GateCrossTracker,
) -> GateOutcome {
let mut gate_waiter = self.inputs.wait.waiter();
loop {
let observed_byte = self.inputs.ack_policy.load(Ordering::Relaxed);
if observed_byte != self.active_policy.as_u8()
&& let Some(next) = AckPolicy::from_u8(observed_byte)
{
tracing::info!(
stage = self.stage,
prev = self.active_policy.as_str(),
next = next.as_str(),
"ack policy swapped during gate wait"
);
self.active_policy = next;
self.policy = next.to_policy();
self.degraded_logger
.reseed(&self.inputs.utilization, Instant::now());
}
if shutdown.load(Ordering::Relaxed) {
return GateOutcome::Shutdown;
}
let journal_pos = self.inputs.journal_persisted_wire_seq.load();
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;
}
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 status = evaluate_durability(
&self.policy,
self.inputs.journal_persisted_wire_seq.load(),
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;
}
#[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;
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use melin_transport_core::WireSeq;
use super::*;
const CORRUPT: u8 = 0xEE;
fn gate(policy: AckPolicy, journal_pos: u64) -> DurabilityGate {
let inputs = GateInputs {
journal_persisted_wire_seq: DurableWireSeqCursor::detached(WireSeq::new(journal_pos)),
ack_policy: Arc::new(AtomicU8::new(policy.as_u8())),
replication_metrics: None,
replica_active: None,
utilization: Arc::new(StageUtilization::new()),
wait: WaitStrategy::SpinThenYield,
};
DurabilityGate::new(inputs, "test")
}
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);
}
let inputs = GateInputs {
journal_persisted_wire_seq: DurableWireSeqCursor::detached(WireSeq::new(pos)),
ack_policy: Arc::new(AtomicU8::new(policy.as_u8())),
replication_metrics: Some(Arc::new(metrics)),
replica_active: Some([
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(true)),
]),
utilization: Arc::new(StageUtilization::new()),
wait: WaitStrategy::SpinThenYield,
};
DurabilityGate::new(inputs, "test")
}
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,
)
}
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_policy_swap_resets_the_cached_position_and_reseeds_the_logger() {
let mut g = gate(AckPolicy::Disk, 5);
g.inputs
.ack_policy
.store(AckPolicy::TwoDisks.as_u8(), Ordering::Relaxed);
g.observe_policy_swap();
assert_eq!(g.active_policy, AckPolicy::TwoDisks);
assert_eq!(g.policy, AckPolicy::TwoDisks.to_policy());
assert_eq!(g.cached_durable_pos, 0);
let mut g = gate(AckPolicy::DiskAndRam, 5);
let_time_pass();
g.inputs
.ack_policy
.store(AckPolicy::Disk.as_u8(), Ordering::Relaxed);
g.observe_policy_swap();
assert_eq!(g.active_policy, AckPolicy::Disk);
assert!(
degraded_nanos(&g) > 0,
"the re-seed flushes the degraded interval before the swap"
);
}
#[test]
fn a_corrupted_byte_keeps_the_prior_policy() {
let mut g = gate(AckPolicy::DiskAndRam, 5);
let_time_pass();
g.inputs.ack_policy.store(CORRUPT, Ordering::Relaxed);
g.observe_policy_swap();
assert_eq!(g.active_policy, AckPolicy::DiskAndRam);
assert_eq!(g.policy, AckPolicy::DiskAndRam.to_policy());
assert_eq!(degraded_nanos(&g), 0, "no re-seed on a corrupted byte");
let mut g = gate(AckPolicy::Disk, 5);
g.inputs.ack_policy.store(CORRUPT, Ordering::Relaxed);
g.observe_policy_swap();
assert_eq!(g.active_policy, AckPolicy::Disk);
assert_eq!(g.cached_durable_pos, 5, "cached position kept");
}
#[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));
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_swap_during_the_wait_unwedges_the_gate() {
let mut g = gate(AckPolicy::DiskAndRam, 5);
let_time_pass();
g.inputs
.ack_policy
.store(AckPolicy::Disk.as_u8(), Ordering::Relaxed);
let shutdown = AtomicBool::new(false);
assert!(matches!(wait(&mut g, 3, &shutdown), GateOutcome::Open));
assert_eq!(g.active_policy, AckPolicy::Disk);
assert_eq!(g.cached_durable_pos, 5);
assert_eq!(attributions(&g), (1, 0));
assert!(
degraded_nanos(&g) > 0,
"the re-seed flushes the wedged interval"
);
}
#[test]
fn a_corrupted_byte_during_the_wait_is_ignored() {
let mut g = gate_with_replicas(AckPolicy::Disk, 5);
g.inputs.ack_policy.store(CORRUPT, Ordering::Relaxed);
let shutdown = AtomicBool::new(false);
assert!(matches!(wait(&mut g, 3, &shutdown), GateOutcome::Open));
assert_eq!(g.active_policy, AckPolicy::Disk);
assert_eq!(g.policy, AckPolicy::Disk.to_policy());
}
#[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"
);
}
}