use std::collections::{HashMap, HashSet};
use std::os::unix::io::RawFd;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::time::{Duration, Instant};
use io_uring::{IoUring, opcode, types};
use tracing::{debug, error};
use melin_pipeline::ring;
use crate::durability_policy::{CursorView, DurabilityMode, EvalStatus, Policy};
use crate::replication::ReplicationMetrics;
use melin_app::Application;
use melin_app::amortized_timer::AmortizedTimer;
use melin_transport_core::pipeline::{OutputPayload, OutputSlot, StageUtilization};
#[cfg(feature = "latency-trace")]
use melin_transport_core::trace;
use melin_transport_core::{DurableWireSeqCursor, WireSeq};
use melin_wire_protocol::control::TransportResponse;
use melin_wire_protocol::control_codec;
const MAX_BATCH: usize = 1024;
const MAX_RESPONSE_BUF: usize = 512;
const RING_SIZE: u32 = 4096;
const MAX_SEND_BUF: usize = 64 * 1024;
pub use crate::ControlEvent;
pub type ResponseEncoderArc<A> = Arc<
dyn melin_app::encoder::ResponseEncoder<
Report = <A as Application>::Report,
Query = <A as Application>::QueryResponse,
>,
>;
pub struct Response<A: Application> {
pub journal_persisted_wire_seq: DurableWireSeqCursor,
pub durability_mode: Arc<std::sync::atomic::AtomicU8>,
pub replication_metrics: Option<Arc<ReplicationMetrics>>,
pub replica_active: Option<[Arc<AtomicBool>; 2]>,
pub heartbeat_interval: Option<Duration>,
pub busy_spin: bool,
pub utilization: Arc<StageUtilization>,
pub encoder: ResponseEncoderArc<A>,
pub fence_state: Arc<melin_transport_core::fence::FenceState>,
}
struct ConnectionEntry {
fd: RawFd,
_owner: Box<dyn Send>,
send_buf: Vec<u8>,
last_send: Instant,
}
pub fn run<A: Application>(
mut consumer: ring::Consumer<OutputSlot<A::Report, A::QueryResponse>>,
control_rx: mpsc::Receiver<ControlEvent>,
config: Response<A>,
shutdown: &AtomicBool,
) {
let Response {
journal_persisted_wire_seq,
durability_mode,
replication_metrics,
replica_active,
heartbeat_interval,
busy_spin,
utilization,
encoder,
fence_state,
} = config;
let mut active_mode =
DurabilityMode::from_u8(durability_mode.load(std::sync::atomic::Ordering::Relaxed))
.unwrap_or_else(|| {
tracing::error!(
"durability_mode atomic held a corrupted byte at startup; defaulting to hybrid"
);
DurabilityMode::Hybrid
});
let mut policy = active_mode.to_policy();
let mut ring =
IoUring::new(RING_SIZE).expect("failed to create io_uring instance for response stage");
let mut connections: HashMap<u64, ConnectionEntry> = HashMap::with_capacity(256);
let mut batch = [OutputSlot::<A::Report, A::QueryResponse>::default(); MAX_BATCH];
let mut encode_buf = [0u8; MAX_RESPONSE_BUF];
let mut cached_durable_pos: u64;
let startup_now = Instant::now();
let mut last_policy_check = startup_now;
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);
let mut degraded_logger;
{
let journal_pos = journal_persisted_wire_seq.load();
let metrics_ref = replication_metrics.as_deref();
let active_ref = replica_active.as_ref();
let status = evaluate_durability(&policy, journal_pos, metrics_ref, active_ref);
cached_durable_pos = status.durable_pos;
utilization
.policy_degraded
.store(status.degraded, Ordering::Relaxed);
degraded_logger = if status.degraded {
DegradationLogger::new_starting_degraded(startup_now, &policy)
} else {
DegradationLogger::new(startup_now)
};
}
#[cfg(feature = "latency-trace")]
let mut spsc_rec =
trace::register_stage("response: SPSC wakeup (matching publish → response consume)");
#[cfg(feature = "latency-trace")]
let mut dispatch_rec = trace::register_stage("response: dispatch (consume → socket write)");
#[cfg(feature = "latency-trace")]
let mut server_e2e_rec = trace::register_stage("server e2e (reader recv → response flush)");
#[cfg(feature = "tick-to-trade")]
let mut journal_wait_rec =
trace::register_stage("response: journal-wait (match_complete → journal cursor crossed)");
#[cfg(feature = "tick-to-trade")]
let mut replica_wait_rec = trace::register_stage(
"response: replica-wait (match_complete → replication cursor crossed)",
);
#[cfg(feature = "tick-to-trade")]
let mut encode_rec = trace::register_stage("response: encode (per-kind wire encoding)");
#[cfg(feature = "tick-to-trade")]
let mut egress_rec = trace::register_stage("response: egress (flush_sends elapsed)");
let mut dirty_connections: HashSet<u64> = HashSet::new();
let mut to_remove: Vec<u64> = Vec::new();
let mut cqes: Vec<(u64, i32)> = Vec::with_capacity(RING_SIZE as usize);
let heartbeat_wire_frame = {
let mut buf = [0u8; 8];
let written =
control_codec::encode_transport_response(&TransportResponse::Heartbeat, &mut buf)
.expect("heartbeat encodes");
buf[..written].to_vec()
};
let mut last_heartbeat_scan = Instant::now();
let mut idle_spins: u32 = 0;
let mut busy_count: u64 = 0;
let mut idle_count: u64 = 0;
let mut gate_accrual_timer = AmortizedTimer::new();
loop {
let observed_byte = durability_mode.load(Ordering::Relaxed);
if observed_byte != active_mode.as_u8() {
match DurabilityMode::from_u8(observed_byte) {
Some(next) => {
tracing::info!(
prev = active_mode.as_str(),
next = next.as_str(),
"durability mode swapped at runtime"
);
active_mode = next;
policy = active_mode.to_policy();
cached_durable_pos = 0;
degraded_logger.reseed(&utilization, Instant::now());
}
None => {
tracing::error!(
byte = observed_byte,
"durability_mode atomic held a corrupted byte; retaining prior mode"
);
}
}
}
if shutdown.load(Ordering::Relaxed) {
let flush = !fence_state.is_fenced();
if flush && !dirty_connections.is_empty() {
flush_sends(
&mut ring,
&mut connections,
&dirty_connections,
&mut to_remove,
&mut cqes,
);
dirty_connections.clear();
}
utilization.busy.store(busy_count, Ordering::Relaxed);
utilization.idle.store(idle_count, Ordering::Relaxed);
#[cfg(feature = "pipeline-stats")]
print_utilization("response", busy_count, idle_count);
return;
}
while let Ok(event) = control_rx.try_recv() {
match event {
ControlEvent::Connected {
connection_id,
fd,
writer,
} => {
let owner: Box<dyn Send> = Box::new(writer);
connections.insert(
connection_id,
ConnectionEntry {
fd,
_owner: owner,
send_buf: Vec::with_capacity(4096),
last_send: Instant::now(),
},
);
}
ControlEvent::Disconnected { connection_id } => {
connections.remove(&connection_id);
dirty_connections.remove(&connection_id);
}
}
}
let count = consumer.consume_batch(&mut batch, MAX_BATCH);
if count == 0 {
if !dirty_connections.is_empty() {
#[cfg(feature = "tick-to-trade")]
let egress_start = trace::mono_trace_ns();
flush_sends(
&mut ring,
&mut connections,
&dirty_connections,
&mut to_remove,
&mut cqes,
);
#[cfg(feature = "tick-to-trade")]
egress_rec.record_elapsed(egress_start, trace::mono_trace_ns());
for conn_id in to_remove.drain(..) {
connections.remove(&conn_id);
}
dirty_connections.clear();
}
if let Some(interval) = heartbeat_interval {
let now = Instant::now();
if now.duration_since(last_heartbeat_scan) >= Duration::from_secs(1) {
last_heartbeat_scan = now;
for (&conn_id, entry) in connections.iter_mut() {
if now.duration_since(entry.last_send) >= interval {
entry.send_buf.extend_from_slice(&heartbeat_wire_frame);
dirty_connections.insert(conn_id);
entry.last_send = now;
}
}
if !dirty_connections.is_empty() {
flush_sends(
&mut ring,
&mut connections,
&dirty_connections,
&mut to_remove,
&mut cqes,
);
for conn_id in to_remove.drain(..) {
connections.remove(&conn_id);
}
dirty_connections.clear();
}
}
}
{
let now_ts = Instant::now();
if now_ts.duration_since(last_policy_check) >= POLICY_CHECK_INTERVAL {
last_policy_check = now_ts;
let journal_pos = journal_persisted_wire_seq.load();
let metrics_ref = replication_metrics.as_deref();
let active_ref = replica_active.as_ref();
let status = evaluate_durability(&policy, journal_pos, metrics_ref, active_ref);
degraded_logger.tick(
&policy,
&utilization,
status.degraded,
now_ts,
DEGRADED_LOG_INTERVAL,
);
cached_durable_pos = status.durable_pos;
}
}
idle_count += 1;
if idle_count.is_multiple_of(1024) {
utilization.busy.store(busy_count, Ordering::Relaxed);
utilization.idle.store(idle_count, Ordering::Relaxed);
}
if busy_spin || idle_spins < 1000 {
idle_spins = idle_spins.wrapping_add(1);
std::hint::spin_loop();
} else {
std::thread::yield_now();
}
continue;
}
idle_spins = 0;
busy_count += 1;
#[cfg(feature = "latency-trace")]
let consume_ts = trace::mono_trace_ns();
#[cfg(feature = "tick-to-trade")]
let mut gate_tracker;
{
let needed = batch[..count]
.iter()
.map(|s| s.wire_seq)
.max()
.expect("non-empty batch");
#[cfg(feature = "tick-to-trade")]
{
gate_tracker = GateCrossTracker::new(needed);
}
let needs_gate = batch[..count].iter().any(|s| !s.durability_bypass);
if needs_gate && cached_durable_pos < needed {
loop {
let observed_byte = durability_mode.load(Ordering::Relaxed);
if observed_byte != active_mode.as_u8()
&& let Some(next) = DurabilityMode::from_u8(observed_byte)
{
tracing::info!(
prev = active_mode.as_str(),
next = next.as_str(),
"durability mode swapped during gate wait"
);
active_mode = next;
policy = active_mode.to_policy();
degraded_logger.reseed(&utilization, Instant::now());
}
let journal_pos = journal_persisted_wire_seq.load();
let metrics_ref = replication_metrics.as_deref();
let active_ref = replica_active.as_ref();
let repl_min = connected_persisted_min(metrics_ref, active_ref);
#[cfg(feature = "tick-to-trade")]
gate_tracker.observe(journal_pos.get(), repl_min, trace::mono_trace_ns());
let status = evaluate_durability(&policy, journal_pos, metrics_ref, active_ref);
cached_durable_pos = status.durable_pos;
utilization
.policy_degraded
.store(status.degraded, Ordering::Relaxed);
if gate_accrual_timer
.tick(GATE_ACCRUAL_INTERVAL, true)
.is_some()
{
degraded_logger.tick(
&policy,
&utilization,
status.degraded,
Instant::now(),
DEGRADED_LOG_INTERVAL,
);
}
if cached_durable_pos >= needed {
if journal_pos.get() <= repl_min {
utilization.gate_journal.fetch_add(1, Ordering::Relaxed);
} else {
utilization.gate_replication.fetch_add(1, Ordering::Relaxed);
}
break;
}
std::hint::spin_loop();
}
}
}
let batch_now = Instant::now();
let degraded_now = utilization.policy_degraded.load(Ordering::Relaxed);
degraded_logger.tick(
&policy,
&utilization,
degraded_now,
batch_now,
DEGRADED_LOG_INTERVAL,
);
last_policy_check = batch_now;
for slot in &batch[..count] {
#[cfg(feature = "latency-trace")]
spsc_rec.record_elapsed(slot.match_complete_ts, consume_ts);
#[cfg(feature = "tick-to-trade")]
if let Some(ts) = gate_tracker.journal_crossed() {
journal_wait_rec.record_elapsed(slot.match_complete_ts, ts);
}
#[cfg(feature = "tick-to-trade")]
if let Some(ts) = gate_tracker.replica_crossed() {
replica_wait_rec.record_elapsed(slot.match_complete_ts, ts);
}
if let Some(entry) = connections.get_mut(&slot.connection_id) {
let payload_result: Option<Result<usize, &'static str>> = match slot.payload {
OutputPayload::Report(ref report) => {
Some(encoder.encode_report(report, &mut encode_buf))
}
OutputPayload::QueryResponse(ref q) => {
Some(encoder.encode_query(q, &mut encode_buf))
}
OutputPayload::EngineError => Some(
control_codec::encode_transport_response(
&TransportResponse::EngineError,
&mut encode_buf,
)
.map_err(|_| "encode error"),
),
OutputPayload::BatchEnd => None,
};
let payload_handled = if let Some(result) = payload_result {
#[cfg(feature = "tick-to-trade")]
let encode_start = trace::mono_trace_ns();
let outcome = append_frame(
result,
slot.connection_id,
entry,
&encode_buf,
batch_now,
&mut dirty_connections,
&mut to_remove,
);
#[cfg(feature = "tick-to-trade")]
encode_rec.record_elapsed(encode_start, trace::mono_trace_ns());
outcome
} else {
AppendOutcome::Continue
};
if matches!(payload_handled, AppendOutcome::Continue) && slot.is_last_in_request {
let result = control_codec::encode_transport_response(
&TransportResponse::BatchEnd,
&mut encode_buf,
)
.map_err(|_| "encode error");
let outcome = append_frame(
result,
slot.connection_id,
entry,
&encode_buf,
batch_now,
&mut dirty_connections,
&mut to_remove,
);
#[cfg(feature = "latency-trace")]
if matches!(outcome, AppendOutcome::Continue) {
server_e2e_rec.record_elapsed(slot.recv_ts, trace::mono_trace_ns());
}
let _ = outcome;
}
}
}
for conn_id in to_remove.drain(..) {
connections.remove(&conn_id);
dirty_connections.remove(&conn_id);
}
#[cfg(feature = "latency-trace")]
dispatch_rec.record_elapsed(consume_ts, trace::mono_trace_ns());
}
}
#[derive(Clone, Copy)]
enum AppendOutcome {
Continue,
ConnectionDropped,
}
#[allow(clippy::too_many_arguments)]
fn append_frame(
result: Result<usize, &'static str>,
connection_id: u64,
entry: &mut ConnectionEntry,
encode_buf: &[u8],
batch_now: Instant,
dirty_connections: &mut HashSet<u64>,
to_remove: &mut Vec<u64>,
) -> AppendOutcome {
let written = match result {
Ok(n) => n,
Err(reason) => {
tracing::error!(connection_id, reason, "encode error");
return AppendOutcome::Continue;
}
};
if entry.send_buf.len() + written > MAX_SEND_BUF {
debug!(
connection_id,
send_buf_len = entry.send_buf.len(),
"send buffer exceeded limit, dropping connection"
);
to_remove.push(connection_id);
return AppendOutcome::ConnectionDropped;
}
entry.send_buf.extend_from_slice(&encode_buf[..written]);
entry.last_send = batch_now;
dirty_connections.insert(connection_id);
AppendOutcome::Continue
}
fn flush_sends(
ring: &mut IoUring,
connections: &mut HashMap<u64, ConnectionEntry>,
dirty: &HashSet<u64>,
to_remove: &mut Vec<u64>,
cqes: &mut Vec<(u64, i32)>,
) {
let mut pending: usize = 0;
for &conn_id in dirty {
if let Some(entry) = connections.get(&conn_id) {
if entry.send_buf.is_empty() {
continue;
}
let sqe = opcode::Send::new(
types::Fd(entry.fd),
entry.send_buf.as_ptr(),
entry.send_buf.len() as u32,
)
.build()
.user_data(conn_id);
unsafe {
ring.submission()
.push(&sqe)
.expect("io_uring SQ full — increase RING_SIZE");
}
pending += 1;
}
}
if pending == 0 {
return;
}
if let Err(e) = ring.submit_and_wait(pending) {
error!(error = %e, "io_uring submit_and_wait failed in response stage");
return;
}
cqes.clear();
cqes.extend(ring.completion().map(|cqe| (cqe.user_data(), cqe.result())));
for &(conn_id, result) in cqes.iter() {
if result < 0 {
debug!(
connection_id = conn_id,
error = result,
"send error, dropping connection"
);
to_remove.push(conn_id);
continue;
}
let sent = result as usize;
if let Some(entry) = connections.get_mut(&conn_id) {
if sent >= entry.send_buf.len() {
entry.send_buf.clear();
} else {
entry.send_buf.drain(..sent);
retry_send(ring, entry, conn_id, to_remove);
}
}
}
}
fn retry_send(
ring: &mut IoUring,
entry: &mut ConnectionEntry,
conn_id: u64,
to_remove: &mut Vec<u64>,
) {
while !entry.send_buf.is_empty() {
let sqe = opcode::Send::new(
types::Fd(entry.fd),
entry.send_buf.as_ptr(),
entry.send_buf.len() as u32,
)
.build()
.user_data(conn_id);
unsafe {
ring.submission()
.push(&sqe)
.expect("io_uring SQ full during send retry");
}
if let Err(e) = ring.submit_and_wait(1) {
debug!(connection_id = conn_id, error = %e, "send retry failed");
to_remove.push(conn_id);
return;
}
if let Some(cqe) = ring.completion().next() {
let result = cqe.result();
if result <= 0 {
debug!(
connection_id = conn_id,
error = result,
"send retry error, dropping connection"
);
to_remove.push(conn_id);
return;
}
let sent = result as usize;
if sent >= entry.send_buf.len() {
entry.send_buf.clear();
} else {
entry.send_buf.drain(..sent);
}
}
}
}
#[inline]
pub(crate) fn evaluate_durability(
policy: &Policy,
journal_pos: WireSeq,
metrics: Option<&ReplicationMetrics>,
replica_active: Option<&[Arc<AtomicBool>; 2]>,
) -> EvalStatus {
let mut nodes: [[u64; 2]; 3] = [[0, 0]; 3];
nodes[0] = [u64::MAX, journal_pos.get()];
let mut len = 1;
if let (Some(m), Some(active)) = (metrics, replica_active) {
for (i, slot_active) in active.iter().enumerate() {
if !slot_active.load(Ordering::Acquire) {
continue;
}
let in_mem = m.in_memory_sequence[i].load(Ordering::Acquire);
let persisted = m.acked_sequence[i].load(Ordering::Acquire);
nodes[len] = [in_mem, persisted];
len += 1;
}
}
policy.evaluate_with_status(&CursorView::new(&nodes[..len]))
}
const DEGRADED_FLAP_HOLD: Duration = Duration::from_secs(1);
pub(crate) struct DegradationLogger {
pending_state: bool,
pending_since: Instant,
pending_logged: bool,
last_log: Option<Instant>,
last_tick: Instant,
}
impl DegradationLogger {
pub(crate) fn new(now: Instant) -> Self {
Self {
pending_state: false,
pending_since: now,
pending_logged: true, last_log: None,
last_tick: now,
}
}
pub(crate) fn new_starting_degraded(now: Instant, policy: &Policy) -> Self {
tracing::warn!(
policy = %policy,
"durability policy starts in degraded mode — fewer connected nodes than the target count"
);
Self {
pending_state: true,
pending_since: now,
pending_logged: true,
last_log: Some(now),
last_tick: now,
}
}
#[inline]
fn accrue(&mut self, utilization: &StageUtilization, prev_degraded: bool, now: Instant) {
if prev_degraded {
let elapsed = now.duration_since(self.last_tick);
utilization
.policy_degraded_nanos
.fetch_add(elapsed.as_nanos() as u64, Ordering::Relaxed);
}
self.last_tick = now;
}
pub(crate) fn reseed(&mut self, utilization: &StageUtilization, now: Instant) {
let prev_state = self.pending_state;
self.accrue(utilization, prev_state, now);
*self = Self::new(now);
}
pub(crate) fn tick(
&mut self,
policy: &Policy,
utilization: &StageUtilization,
degraded_now: bool,
now: Instant,
heartbeat_interval: Duration,
) {
let prev_state = self.pending_state;
self.accrue(utilization, prev_state, now);
utilization
.policy_degraded
.store(degraded_now, Ordering::Relaxed);
if degraded_now != self.pending_state {
self.pending_state = degraded_now;
self.pending_since = now;
self.pending_logged = false;
return;
}
if !self.pending_logged && now.duration_since(self.pending_since) >= DEGRADED_FLAP_HOLD {
if degraded_now {
tracing::warn!(
policy = %policy,
"durability policy operating in degraded mode — fewer connected nodes than the target count, gate clamped to surviving cluster"
);
} else {
tracing::info!(
policy = %policy,
"durability policy returned to target shape"
);
}
self.pending_logged = true;
self.last_log = Some(now);
return;
}
if degraded_now
&& self.pending_logged
&& self
.last_log
.is_none_or(|t| now.duration_since(t) >= heartbeat_interval)
{
tracing::warn!(
policy = %policy,
"durability policy still degraded — fewer connected nodes than the target count"
);
self.last_log = Some(now);
}
}
}
#[inline]
pub(crate) fn connected_persisted_min(
metrics: Option<&ReplicationMetrics>,
replica_active: Option<&[Arc<AtomicBool>; 2]>,
) -> u64 {
let (Some(m), Some(active)) = (metrics, replica_active) else {
return u64::MAX;
};
let mut min = u64::MAX;
for (i, slot_active) in active.iter().enumerate() {
if !slot_active.load(Ordering::Acquire) {
continue;
}
let v = m.acked_sequence[i].load(Ordering::Acquire);
if v < min {
min = v;
}
}
min
}
#[cfg(feature = "tick-to-trade")]
pub(crate) struct GateCrossTracker {
needed: u64,
journal_crossed_ts: Option<trace::MonoTraceInstant>,
replica_crossed_ts: Option<trace::MonoTraceInstant>,
journal_was_below: bool,
replica_was_below: bool,
first: bool,
}
#[cfg(feature = "tick-to-trade")]
impl GateCrossTracker {
pub(crate) fn new(needed: u64) -> Self {
Self {
needed,
journal_crossed_ts: None,
replica_crossed_ts: None,
journal_was_below: false,
replica_was_below: false,
first: true,
}
}
pub(crate) fn observe(
&mut self,
journal_pos: u64,
repl_min: u64,
now_ns: trace::MonoTraceInstant,
) {
if self.first {
self.journal_was_below = journal_pos < self.needed;
self.replica_was_below = repl_min < self.needed;
self.first = false;
}
if self.journal_was_below && self.journal_crossed_ts.is_none() && journal_pos >= self.needed
{
self.journal_crossed_ts = Some(now_ns);
}
if self.replica_was_below && self.replica_crossed_ts.is_none() && repl_min >= self.needed {
self.replica_crossed_ts = Some(now_ns);
}
}
pub(crate) fn journal_crossed(&self) -> Option<trace::MonoTraceInstant> {
self.journal_crossed_ts
}
pub(crate) fn replica_crossed(&self) -> Option<trace::MonoTraceInstant> {
self.replica_crossed_ts
}
}
#[cfg(feature = "pipeline-stats")]
fn print_utilization(stage: &str, busy: u64, idle: u64) {
let total = busy + idle;
if total == 0 {
tracing::info!(stage, "no iterations recorded");
return;
}
let pct = (busy as f64 / total as f64) * 100.0;
tracing::info!(
stage,
pct_busy = format_args!("{pct:.2}%"),
busy,
idle,
total,
"pipeline utilization",
);
}
#[cfg(test)]
mod tests {
#[cfg(feature = "tick-to-trade")]
use super::GateCrossTracker;
use super::{DegradationLogger, WireSeq, connected_persisted_min, evaluate_durability};
use crate::durability_policy::{Clause, Level, Policy};
use crate::replication::ReplicationMetrics;
fn parse(s: &str) -> Result<Policy, String> {
let mut clauses = Vec::new();
for raw in s.split("&&") {
let token = raw.trim();
let (lvl, rhs) = token
.split_once(">=")
.ok_or_else(|| format!("clause `{token}` missing `>=`"))?;
let level = match lvl.trim() {
"persisted" => Level::Persisted,
"in_memory" => Level::InMemory,
other => return Err(format!("unknown level `{other}`")),
};
let count: u8 = rhs.trim().parse().map_err(|e| format!("bad count: {e}"))?;
clauses.push(Clause { count, level });
}
Policy::new(clauses).map_err(|e| e.to_string())
}
use melin_transport_core::pipeline::StageUtilization;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
fn metrics(slot0: (u64, u64), slot1: (u64, u64)) -> Arc<ReplicationMetrics> {
let m = Arc::new(ReplicationMetrics::default());
m.in_memory_sequence[0].store(slot0.0, Ordering::Relaxed);
m.acked_sequence[0].store(slot0.1, Ordering::Relaxed);
m.in_memory_sequence[1].store(slot1.0, Ordering::Relaxed);
m.acked_sequence[1].store(slot1.1, Ordering::Relaxed);
m
}
fn flags(slot0_active: bool, slot1_active: bool) -> [Arc<AtomicBool>; 2] {
[
Arc::new(AtomicBool::new(slot0_active)),
Arc::new(AtomicBool::new(slot1_active)),
]
}
fn both_active() -> [Arc<AtomicBool>; 2] {
flags(true, true)
}
#[test]
fn standalone_persisted_one_gates_on_journal() {
let p = parse("persisted>=1").unwrap();
assert_eq!(
evaluate_durability(&p, WireSeq::new(500), None, None).durable_pos,
500
);
}
#[test]
fn standalone_strict_persisted_two_never_opens() {
let p = parse("persisted>=2").unwrap();
let r = evaluate_durability(&p, WireSeq::new(500), None, None);
assert_eq!(r.durable_pos, 0);
assert!(
r.degraded,
"policy structurally unsatisfiable on this shape → degraded",
);
}
#[test]
fn quorum_both_replicas_ahead_of_journal() {
let p = parse("persisted>=2").unwrap();
let m = metrics((100, 100), (120, 120));
let a = both_active();
assert_eq!(
evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a)).durable_pos,
100
);
}
#[test]
fn quorum_journal_ahead_of_both_replicas() {
let p = parse("persisted>=2").unwrap();
let m = metrics((100, 100), (120, 120));
let a = both_active();
assert_eq!(
evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a)).durable_pos,
120
);
}
#[test]
fn quorum_journal_between_slow_and_fast_replica() {
let p = parse("persisted>=2").unwrap();
let m = metrics((50, 50), (200, 200));
let a = both_active();
assert_eq!(
evaluate_durability(&p, WireSeq::new(150), Some(&m), Some(&a)).durable_pos,
150
);
}
#[test]
fn single_replica_strict_persisted_two_requires_both_survivors() {
let p = parse("persisted>=2").unwrap();
let m = metrics((100, 100), (999, 999)); let a = flags(true, false);
assert_eq!(
evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a)).durable_pos,
50
);
assert_eq!(
evaluate_durability(&p, WireSeq::new(200), Some(&m), Some(&a)).durable_pos,
100
);
}
#[test]
fn single_replica_persisted_two_requires_both_survivors() {
let p = parse("persisted>=2").unwrap();
let m = metrics((100, 100), (999, 999));
let a = flags(true, false);
let r = evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a));
assert_eq!(r.durable_pos, 50);
assert!(!r.degraded);
}
#[test]
fn both_replicas_disconnected_strict_stalls() {
let p = parse("persisted>=2").unwrap();
let m = metrics((999, 999), (999, 999));
let a = flags(false, false);
assert_eq!(
evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a)).durable_pos,
0
);
}
#[test]
fn both_replicas_disconnected_strict_stalls_and_flags_degraded() {
let p = parse("persisted>=2").unwrap();
let m = metrics((999, 999), (999, 999));
let a = flags(false, false);
let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
assert_eq!(r.durable_pos, 0);
assert!(r.degraded);
}
#[test]
fn persisted_one_and_in_memory_two() {
let p = parse("persisted>=1 && in_memory>=2").unwrap();
let m = metrics((80, 20), (999, 999));
let a = flags(true, false);
assert_eq!(
evaluate_durability(&p, WireSeq::new(50), Some(&m), Some(&a)).durable_pos,
50
);
}
#[test]
fn journal_at_zero_with_replicas_persisted_one() {
let p = parse("persisted>=1").unwrap();
let m = metrics((100, 100), (200, 200));
let a = both_active();
assert_eq!(
evaluate_durability(&p, WireSeq::new(0), Some(&m), Some(&a)).durable_pos,
200
);
}
#[test]
fn attribution_min_skips_disconnected_slots() {
let m = metrics((150, 100), (999, 999));
let a = flags(true, false);
assert_eq!(connected_persisted_min(Some(&m), Some(&a)), 100);
}
#[test]
fn attribution_min_returns_max_when_standalone() {
assert_eq!(connected_persisted_min(None, None), u64::MAX);
}
#[test]
fn fresh_cluster_zero_cursors_included_in_view() {
let p = parse("persisted>=2").unwrap();
let m = metrics((0, 0), (0, 0));
let a = both_active();
let r = evaluate_durability(&p, WireSeq::new(0), Some(&m), Some(&a));
assert_eq!(r.durable_pos, 0);
assert!(
!r.degraded,
"all 3 nodes present, view meets clause target — should not flag degraded"
);
}
#[test]
fn attribution_min_takes_smaller_when_both_connected() {
let m = metrics((150, 100), (180, 80));
let a = both_active();
assert_eq!(connected_persisted_min(Some(&m), Some(&a)), 80);
}
#[test]
fn race_b1_post_seed_gate_doesnt_freeze_on_reconnect() {
let p = parse("persisted>=2").unwrap();
let m = metrics((480, 480), (999, 999));
let a = flags(true, false);
let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
assert_eq!(
r.durable_pos, 480,
"post-seed reconnect should produce a coherent gate position equal to the slower node, not freeze at 0"
);
}
#[test]
fn race_b1_pre_seed_freeze_is_what_the_fix_avoids() {
let p = parse("persisted>=2").unwrap();
let m = metrics((0, 0), (999, 999));
let a = flags(true, false);
let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
assert_eq!(
r.durable_pos, 0,
"the gate behaviour under (active=true, cursor=0) — if the senders ever fail to seed before flipping active, this is the freeze the operator would see"
);
}
#[test]
fn race_b2_disconnect_window_doesnt_open_gate_spuriously() {
let p = parse("persisted>=2").unwrap();
let m = metrics((0, 0), (999, 999));
let a = flags(true, false);
let r = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a));
assert_eq!(r.durable_pos, 0);
let a_disconnected = flags(false, false);
let r_after = evaluate_durability(&p, WireSeq::new(500), Some(&m), Some(&a_disconnected));
assert_eq!(r_after.durable_pos, 0);
assert!(
r_after.degraded,
"post-disconnect view of size 1 cannot meet persisted>=2 → degraded"
);
}
#[test]
fn race_invariant_zero_cursor_never_opens_gate_above_slower_node() {
let p = parse("persisted>=2").unwrap();
let m = metrics((0, 0), (999, 999));
let a = flags(true, false);
for primary_pos in [0, 1, 100, 500, 1_000_000_000_u64] {
let r = evaluate_durability(&p, WireSeq::new(primary_pos), Some(&m), Some(&a));
assert_eq!(
r.durable_pos, 0,
"race-window observation must not open the gate above 0 for any primary position (got {} for primary_pos={primary_pos})",
r.durable_pos
);
}
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_records_journal_when_strictly_below() {
let mut t = GateCrossTracker::new(10);
t.observe(5, 100, 1_000);
t.observe(15, 100, 2_000);
assert_eq!(t.journal_crossed(), Some(2_000));
assert_eq!(t.replica_crossed(), None);
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_records_replica_when_strictly_below() {
let mut t = GateCrossTracker::new(10);
t.observe(50, 5, 1_000);
t.observe(50, 12, 2_000);
assert_eq!(t.journal_crossed(), None);
assert_eq!(t.replica_crossed(), Some(2_000));
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_records_both_when_both_below() {
let mut t = GateCrossTracker::new(100);
t.observe(50, 60, 1_000); t.observe(105, 60, 2_000); t.observe(105, 110, 3_000); assert_eq!(t.journal_crossed(), Some(2_000));
assert_eq!(t.replica_crossed(), Some(3_000));
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_skips_cursor_already_past_at_entry() {
let mut t = GateCrossTracker::new(10);
t.observe(50, 100, 1_000);
t.observe(60, 110, 2_000);
assert_eq!(t.journal_crossed(), None);
assert_eq!(t.replica_crossed(), None);
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_first_observation_only_for_cross_decision() {
let mut t = GateCrossTracker::new(10);
t.observe(50, 5, 1_000); t.observe(20, 12, 2_000); assert_eq!(t.journal_crossed(), None);
assert_eq!(t.replica_crossed(), Some(2_000));
}
#[cfg(feature = "tick-to-trade")]
#[test]
fn gate_cross_tracker_holds_first_cross_only() {
let mut t = GateCrossTracker::new(10);
t.observe(5, 100, 1_000);
t.observe(15, 100, 2_000); t.observe(25, 100, 3_000); assert_eq!(t.journal_crossed(), Some(2_000));
}
fn logger_test_policy() -> crate::durability_policy::Policy {
parse("persisted>=2").unwrap()
}
fn drive_logger(
logger: &mut DegradationLogger,
utilization: &StageUtilization,
policy: &crate::durability_policy::Policy,
start: Instant,
states: &[bool],
step: Duration,
) -> bool {
for (i, &state) in states.iter().enumerate() {
let now = start + step.checked_mul(i as u32).unwrap_or(Duration::ZERO);
logger.tick(policy, utilization, state, now, Duration::from_secs(5));
}
utilization.policy_degraded.load(Ordering::Relaxed)
}
#[test]
fn logger_gauge_tracks_state_immediately() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let now = Instant::now();
let mut logger = DegradationLogger::new(now);
logger.tick(&p, &utilization, true, now, Duration::from_secs(5));
assert!(utilization.policy_degraded.load(Ordering::Relaxed));
logger.tick(
&p,
&utilization,
false,
now + Duration::from_millis(50),
Duration::from_secs(5),
);
assert!(!utilization.policy_degraded.load(Ordering::Relaxed));
}
#[test]
fn logger_starting_degraded_marks_initial_state_logged() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let now = Instant::now();
let mut logger = DegradationLogger::new_starting_degraded(now, &p);
logger.tick(
&p,
&utilization,
true,
now + Duration::from_millis(10),
Duration::from_secs(5),
);
assert!(utilization.policy_degraded.load(Ordering::Relaxed));
}
#[test]
fn logger_accumulates_degraded_seconds_only_while_degraded() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let start = Instant::now();
let mut logger = DegradationLogger::new(start);
let sec = Duration::from_secs(1);
logger.tick(&p, &utilization, true, start, Duration::from_secs(5));
assert_eq!(utilization.policy_degraded_nanos.load(Ordering::Relaxed), 0);
logger.tick(&p, &utilization, true, start + sec, Duration::from_secs(5));
logger.tick(
&p,
&utilization,
false,
start + 2 * sec,
Duration::from_secs(5),
);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
2 * sec.as_nanos() as u64
);
logger.tick(
&p,
&utilization,
false,
start + 3 * sec,
Duration::from_secs(5),
);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
2 * sec.as_nanos() as u64
);
}
#[test]
fn logger_starting_degraded_accrues_from_construction() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let start = Instant::now();
let mut logger = DegradationLogger::new_starting_degraded(start, &p);
logger.tick(
&p,
&utilization,
true,
start + Duration::from_secs(1),
Duration::from_secs(5),
);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
Duration::from_secs(1).as_nanos() as u64
);
}
#[test]
fn logger_reseed_flushes_in_flight_degraded_accrual() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let start = Instant::now();
let sec = Duration::from_secs(1);
let mut logger = DegradationLogger::new_starting_degraded(start, &p);
logger.tick(&p, &utilization, true, start, Duration::from_secs(5));
assert_eq!(utilization.policy_degraded_nanos.load(Ordering::Relaxed), 0);
logger.reseed(&utilization, start + 30 * sec);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
30 * sec.as_nanos() as u64
);
logger.tick(
&p,
&utilization,
false,
start + 31 * sec,
Duration::from_secs(5),
);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
30 * sec.as_nanos() as u64
);
}
#[test]
fn logger_starting_degraded_then_first_tick_healthy_counts_construction_interval() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let start = Instant::now();
let mut logger = DegradationLogger::new_starting_degraded(start, &p);
logger.tick(
&p,
&utilization,
false,
start + Duration::from_secs(2),
Duration::from_secs(5),
);
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
Duration::from_secs(2).as_nanos() as u64
);
}
#[test]
fn logger_counter_matches_total_degraded_time_across_flaps() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let start = Instant::now();
let step = Duration::from_millis(500);
let mut logger = DegradationLogger::new(start);
let states = [false, true, true, false, true, false];
for (i, &s) in states.iter().enumerate() {
logger.tick(
&p,
&utilization,
s,
start + step * i as u32,
Duration::from_secs(5),
);
}
assert_eq!(
utilization.policy_degraded_nanos.load(Ordering::Relaxed),
(3 * step).as_nanos() as u64
);
}
#[test]
fn logger_handles_rapid_flap_without_panic() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let now = Instant::now();
let mut logger = DegradationLogger::new(now);
let states: Vec<bool> = (0..100u32).map(|i| i.is_multiple_of(2)).collect();
let final_state = drive_logger(
&mut logger,
&utilization,
&p,
now,
&states,
Duration::from_millis(100),
);
assert!(!final_state);
}
#[test]
fn logger_sustained_degraded_eventually_settles() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let now = Instant::now();
let mut logger = DegradationLogger::new(now);
let final_state = drive_logger(
&mut logger,
&utilization,
&p,
now,
&[true; 5],
Duration::from_millis(500),
);
assert!(final_state);
}
#[test]
fn logger_recovery_to_healthy_settles_gauge() {
let p = logger_test_policy();
let utilization = StageUtilization::new();
let now = Instant::now();
let mut logger = DegradationLogger::new_starting_degraded(now, &p);
let mut states = vec![true; 5]; states.extend(vec![false; 5]); let final_state = drive_logger(
&mut logger,
&utilization,
&p,
now,
&states,
Duration::from_millis(500),
);
assert!(!final_state);
}
}