use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use melin_app::RejectReason;
use melin_journal::AckPolicyChange;
use melin_pipeline::padding::Sequence;
use melin_pipeline::spsc;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
use melin_transport_core::replica_presence::{Observed, ReplicaPresence};
use crate::ack_policy::AckPolicy;
use crate::policy_control::{Origin, is_unwitnessed};
pub(crate) const REFUSAL_QUEUE_CAPACITY: usize = 4096;
pub struct DegradedRelease {
pub presence: Arc<ReplicaPresence>,
pub grace: Duration,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Verdict {
Take,
Refuse(RejectReason),
Close,
}
pub struct HaltGate {
replicas_connected: Option<Arc<ReplicaPresence>>,
fence_state: Arc<FenceState>,
refused: Arc<AtomicU64>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Replication {
Enabled,
Disabled,
}
impl Replication {
pub(crate) fn can_meet(self, policy: AckPolicy) -> bool {
match self {
Replication::Enabled => true,
Replication::Disabled => !policy.needs_replica(),
}
}
}
#[derive(Debug)]
struct Decision {
policy: AckPolicy,
unwitnessed: bool,
seen: Option<Observed>,
}
impl Decision {
fn change(&self) -> AckPolicyChange {
AckPolicyChange {
policy: self.policy.to_byte(),
unwitnessed: self.unwitnessed,
}
}
fn is_forced(&self) -> bool {
self.unwitnessed && matches!(self.seen, Some(Observed::NoneConnected(_)))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Journaled {
policy: AckPolicy,
unwitnessed: bool,
forced: bool,
input_seq: u64,
}
impl Journaled {
pub(crate) fn policy(self) -> AckPolicy {
self.policy
}
pub(crate) fn unwitnessed(self) -> bool {
self.unwitnessed
}
pub(crate) fn is_forced(self) -> bool {
self.forced
}
pub(crate) fn input_seq(self) -> u64 {
self.input_seq
}
}
impl HaltGate {
pub fn new(
replicas_connected: Option<Arc<ReplicaPresence>>,
fence_state: Arc<FenceState>,
refused: Arc<AtomicU64>,
) -> Self {
Self {
replicas_connected,
fence_state,
refused,
}
}
pub(crate) fn replication(&self) -> Replication {
match self.replicas_connected {
Some(_) => Replication::Enabled,
None => Replication::Disabled,
}
}
pub(crate) fn journal<E>(
&self,
policy: AckPolicy,
origin: Origin,
publish: impl FnOnce(AckPolicyChange) -> Result<u64, E>,
) -> Result<Journaled, E> {
let decision = self.decide(policy, origin);
let input_seq = publish(decision.change())?;
Ok(self.record_journaled(decision, input_seq))
}
fn decide(&self, policy: AckPolicy, origin: Origin) -> Decision {
let seen = self.replicas_connected.as_ref().map(|p| p.observe());
Decision {
policy,
unwitnessed: is_unwitnessed(policy, origin, seen.as_ref()),
seen,
}
}
fn record_journaled(&self, decision: Decision, input_seq: u64) -> Journaled {
let forced = decision.is_forced();
let Decision {
policy,
unwitnessed,
seen,
} = decision;
let journaled = Journaled {
policy,
unwitnessed,
forced,
input_seq,
};
let (Some(presence), Some(seen)) = (&self.replicas_connected, seen) else {
return journaled;
};
if policy.needs_replica() {
presence.clear_override(seen);
} else if let Observed::NoneConnected(since) = seen
&& forced
{
presence.latch_override(since);
}
journaled
}
#[inline]
pub fn record_refused(&self, count: u64) {
self.refused.fetch_add(count, Ordering::Relaxed);
}
#[inline]
pub fn verdict(&self) -> Verdict {
if self.fence_state.is_fenced() {
Verdict::Close
} else if self
.replicas_connected
.as_ref()
.is_some_and(|presence| presence.halted())
{
Verdict::Refuse(RejectReason::ReplicaDisconnected)
} else {
Verdict::Take
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Refusal<R> {
pub connection_id: u64,
pub input_seq: u64,
pub report: R,
}
pub fn refusal_channel<R: Copy>(
matching_progress: Arc<Sequence>,
) -> (RefusalSender<R>, RefusalQueue<R>) {
let (tx, rx) =
spsc::channel::<Option<Refusal<R>>>(REFUSAL_QUEUE_CAPACITY, WaitStrategy::SpinThenYield);
(
RefusalSender { tx },
RefusalQueue {
rx,
head: None,
matching_progress,
},
)
}
pub struct RefusalSender<R> {
tx: spsc::Producer<Option<Refusal<R>>>,
}
impl<R: Copy> RefusalSender<R> {
pub(crate) fn try_send(&mut self, refusal: Refusal<R>) -> Result<(), spsc::Full> {
self.tx
.try_push_with(|slot| *slot = Some(refusal))
.map(|_| ())
}
pub(crate) fn flush(&mut self) {
self.tx.flush();
}
}
pub struct RefusalQueue<R> {
rx: spsc::Consumer<Option<Refusal<R>>>,
head: Option<Refusal<R>>,
matching_progress: Arc<Sequence>,
}
impl<R: Copy> RefusalQueue<R> {
#[inline]
pub fn idle_bound(&mut self) -> Option<u64> {
self.peek()?;
Some(self.matching_progress.get().load(Ordering::Acquire))
}
#[inline]
pub fn sync(&mut self) -> bool {
self.rx.refresh();
self.peek().is_some()
}
#[inline]
pub fn release(&mut self, bound: u64, mut emit: impl FnMut(Refusal<R>)) {
while let Some(refusal) = self.peek() {
if refusal.input_seq > bound {
return;
}
self.head = None;
emit(refusal);
}
}
fn peek(&mut self) -> Option<Refusal<R>> {
if self.head.is_none() {
self.head = self
.rx
.try_consume_visible()
.and_then(|(_, refusal)| refusal);
}
self.head
}
}
#[cfg(test)]
pub(crate) mod test_support {
use super::*;
pub(crate) fn presence(connected: u32) -> Arc<ReplicaPresence> {
let presence = Arc::new(ReplicaPresence::new());
for _ in 0..connected {
drop(presence.lift());
}
presence
}
pub(crate) fn gate_over(replicas: Option<Arc<ReplicaPresence>>) -> HaltGate {
HaltGate::new(
replicas,
Arc::new(FenceState::new(0)),
Arc::new(AtomicU64::new(0)),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use melin_pipeline::padding::CachePadded;
use melin_transport_core::replica_presence::Lowered;
use std::sync::atomic::AtomicBool;
fn progress(value: u64) -> Arc<Sequence> {
Arc::new(CachePadded::new(AtomicU64::new(value)))
}
fn refusal(connection_id: u64, input_seq: u64) -> Refusal<u64> {
Refusal {
connection_id,
input_seq,
report: connection_id * 100,
}
}
fn gate(replicas_connected: Option<Arc<ReplicaPresence>>, fence: Arc<FenceState>) -> HaltGate {
HaltGate::new(replicas_connected, fence, Arc::new(AtomicU64::new(0)))
}
const HALTED: Verdict = Verdict::Refuse(RejectReason::ReplicaDisconnected);
#[test]
fn standalone_never_refuses() {
let gate = gate(None, Arc::new(FenceState::new(0)));
assert_eq!(gate.verdict(), Verdict::Take);
for policy in [AckPolicy::Disk, AckPolicy::TwoDisks] {
journal(&gate, policy, Origin::Runtime);
assert_eq!(gate.verdict(), Verdict::Take);
}
}
#[test]
fn no_replica_refuses_with_replica_disconnected_until_one_connects() {
let replicas = test_support::presence(0);
let gate = gate(Some(Arc::clone(&replicas)), Arc::new(FenceState::new(0)));
assert_eq!(gate.verdict(), HALTED);
drop(replicas.lift());
assert_eq!(gate.verdict(), Verdict::Take);
}
#[test]
fn a_decision_follows_the_replica_count() {
let standalone =
gate(None, Arc::new(FenceState::new(0))).decide(AckPolicy::Disk, Origin::Runtime);
assert!(standalone.unwitnessed && !standalone.is_forced());
let replicas = test_support::presence(0);
let gate = gate(Some(Arc::clone(&replicas)), Arc::new(FenceState::new(0)));
let none = gate.decide(AckPolicy::Disk, Origin::Runtime);
assert!(none.unwitnessed && none.is_forced());
drop(replicas.lift());
drop(replicas.lift());
let connected = gate.decide(AckPolicy::Disk, Origin::Runtime);
assert!(!connected.unwitnessed && !connected.is_forced());
}
fn journal(gate: &HaltGate, policy: AckPolicy, origin: Origin) -> Journaled {
let Ok(journaled) = gate.journal(policy, origin, |_| Ok::<_, std::convert::Infallible>(0));
journaled
}
#[test]
fn a_journaled_change_carries_its_policy_as_the_observation_rules() {
let standalone = gate(None, Arc::new(FenceState::new(0)));
let mut published = None;
let Ok(journaled) = standalone.journal(AckPolicy::Ram, Origin::Staged, |change| {
published = Some(change);
Ok::<_, std::convert::Infallible>(42)
});
assert_eq!(journaled.policy(), AckPolicy::Ram);
assert_eq!(
published,
Some(AckPolicyChange {
policy: AckPolicy::Ram.to_byte(),
unwitnessed: journaled.unwitnessed(),
}),
"the entry published"
);
assert_eq!(journaled.input_seq(), 42, "where it was published");
assert!(journaled.unwitnessed(), "nothing can witness it");
assert!(
!journaled.is_forced(),
"unwitnessed for want of replication"
);
}
#[test]
fn an_entry_not_published_is_not_recorded() {
let gate = gate(
Some(test_support::presence(0)),
Arc::new(FenceState::new(0)),
);
assert_eq!(
gate.journal(AckPolicy::Disk, Origin::Runtime, |_| Err("full")),
Err("full")
);
assert_eq!(gate.verdict(), HALTED, "still halted");
assert!(journal(&gate, AckPolicy::Disk, Origin::Runtime).is_forced());
assert_eq!(gate.verdict(), Verdict::Take, "published: lifted");
}
#[test]
fn the_replication_shape_is_the_presence() {
let fence = || Arc::new(FenceState::new(0));
assert_eq!(gate(None, fence()).replication(), Replication::Disabled);
assert_eq!(
gate(Some(test_support::presence(0)), fence()).replication(),
Replication::Enabled
);
}
#[test]
fn a_node_without_replication_meets_only_policies_that_need_no_replica() {
for policy in [
AckPolicy::Disk,
AckPolicy::Ram,
AckPolicy::DiskAndRam,
AckPolicy::TwoDisks,
] {
assert!(Replication::Enabled.can_meet(policy), "{policy}");
assert_eq!(
Replication::Disabled.can_meet(policy),
policy == AckPolicy::Disk,
"{policy}"
);
}
}
#[test]
fn a_forced_disk_lifts_the_halt_until_a_replica_returns() {
let replicas = test_support::presence(1);
let gate = gate(Some(Arc::clone(&replicas)), Arc::new(FenceState::new(0)));
assert_eq!(gate.verdict(), Verdict::Take);
assert_eq!(replicas.lower(), Lowered::LastHalted);
assert_eq!(gate.verdict(), HALTED, "the halt ignores the policy");
journal(&gate, AckPolicy::Disk, Origin::Runtime);
assert_eq!(gate.verdict(), Verdict::Take, "consent lifts the halt");
assert!(
gate.decide(AckPolicy::Disk, Origin::Runtime).is_forced(),
"still no witness: a later `disk` is forced too"
);
journal(&gate, AckPolicy::Disk, Origin::Runtime);
assert_eq!(gate.verdict(), Verdict::Take, "re-sent, it stays lifted");
let mut first_ack = replicas.lift();
assert_eq!(gate.verdict(), Verdict::Take);
first_ack.acked(&replicas);
assert_eq!(replicas.lower(), Lowered::LastHalted);
assert_eq!(
gate.verdict(),
HALTED,
"the override did not outlive the replica"
);
}
#[test]
fn a_replica_refused_after_authenticating_leaves_the_override() {
let replicas = test_support::presence(0);
let gate = gate(Some(Arc::clone(&replicas)), Arc::new(FenceState::new(0)));
journal(&gate, AckPolicy::Disk, Origin::Runtime);
assert_eq!(gate.verdict(), Verdict::Take);
for _ in 0..3 {
let refused = replicas.lift();
assert!(
!gate.decide(AckPolicy::Disk, Origin::Runtime).is_forced(),
"streaming, so witnessed"
);
drop(refused);
assert_eq!(replicas.lower(), Lowered::LastOverridden);
assert_eq!(gate.verdict(), Verdict::Take, "the override held");
}
replicas.lift().acked(&replicas);
assert_eq!(replicas.lower(), Lowered::LastHalted);
assert_eq!(gate.verdict(), HALTED, "a replica that acked ended it");
}
#[test]
fn a_swap_to_a_replica_backed_policy_restores_the_halt() {
for policy in [AckPolicy::Ram, AckPolicy::DiskAndRam, AckPolicy::TwoDisks] {
let gate = gate(
Some(test_support::presence(0)),
Arc::new(FenceState::new(0)),
);
journal(&gate, AckPolicy::Disk, Origin::Runtime);
assert_eq!(gate.verdict(), Verdict::Take);
journal(&gate, policy, Origin::Runtime);
assert_eq!(gate.verdict(), HALTED, "{policy}");
journal(&gate, AckPolicy::Disk, Origin::Runtime);
assert_eq!(
gate.verdict(),
Verdict::Take,
"a forced swap again: {policy}"
);
}
}
#[test]
fn a_disk_not_forced_does_not_lift_the_halt() {
let replicas = test_support::presence(1);
let gate = gate(Some(Arc::clone(&replicas)), Arc::new(FenceState::new(0)));
journal(&gate, AckPolicy::Disk, Origin::Runtime);
assert_eq!(replicas.lower(), Lowered::LastHalted);
assert_eq!(gate.verdict(), HALTED, "witnessed: no consent");
journal(&gate, AckPolicy::Disk, Origin::Staged);
assert_eq!(gate.verdict(), HALTED, "staged: never forced");
}
#[test]
fn a_fenced_node_closes_despite_the_override() {
let fence = Arc::new(FenceState::new(0));
let gate = gate(Some(test_support::presence(0)), Arc::clone(&fence));
journal(&gate, AckPolicy::Disk, Origin::Runtime);
fence.fence();
assert_eq!(gate.verdict(), Verdict::Close);
}
#[test]
fn a_fenced_node_closes_whatever_its_replica_count() {
let fence = Arc::new(FenceState::new(1));
let gate = gate(Some(test_support::presence(1)), Arc::clone(&fence));
assert_eq!(gate.verdict(), Verdict::Take);
assert_eq!(
fence.fence_if_superseded(2, &AtomicBool::new(false)),
Some(true)
);
assert_eq!(gate.verdict(), Verdict::Close);
}
#[test]
fn refusals_are_counted() {
let refused = Arc::new(AtomicU64::new(0));
let gate = HaltGate::new(None, Arc::new(FenceState::new(0)), Arc::clone(&refused));
gate.record_refused(3);
gate.record_refused(2);
assert_eq!(refused.load(Ordering::Relaxed), 5);
}
#[test]
fn a_refusal_waits_for_both_the_flush_and_a_sync() {
let (mut tx, mut rx) = refusal_channel::<u64>(progress(10));
tx.try_send(refusal(1, 0)).unwrap();
assert!(!rx.sync(), "not flushed");
tx.flush();
assert_eq!(rx.idle_bound(), None, "flushed, not yet synced");
let mut released = Vec::new();
rx.release(u64::MAX, |r| released.push(r.connection_id));
assert!(released.is_empty(), "release sees only synced refusals");
assert!(rx.sync());
assert_eq!(rx.idle_bound(), Some(10));
}
#[test]
fn release_does_not_reach_past_the_sync() {
let (mut tx, mut rx) = refusal_channel::<u64>(progress(0));
tx.try_send(refusal(1, 0)).unwrap();
tx.flush();
assert!(rx.sync());
tx.try_send(refusal(2, 0)).unwrap();
tx.flush();
let mut released = Vec::new();
rx.release(u64::MAX, |r| released.push(r.connection_id));
assert_eq!(released, [1]);
assert!(rx.sync());
rx.release(u64::MAX, |r| released.push(r.connection_id));
assert_eq!(released, [1, 2]);
assert!(!rx.sync(), "queue drained");
}
#[test]
fn release_stops_at_the_first_refusal_past_the_bound() {
let (mut tx, mut rx) = refusal_channel::<u64>(progress(0));
for (conn, seq) in [(1, 3), (2, 3), (3, 5), (4, 9)] {
tx.try_send(refusal(conn, seq)).unwrap();
}
tx.flush();
assert!(rx.sync());
let mut released = Vec::new();
rx.release(2, |r| released.push(r.connection_id));
assert!(released.is_empty(), "no event before seq 3 is answered yet");
rx.release(5, |r| released.push(r.connection_id));
assert_eq!(
released,
[1, 2, 3],
"in order, up to and including the bound"
);
rx.release(8, |r| released.push(r.connection_id));
assert_eq!(released, [1, 2, 3], "the held head is not re-emitted");
rx.release(9, |r| released.push(r.connection_id));
assert_eq!(released, [1, 2, 3, 4]);
assert!(!rx.sync(), "queue drained");
assert_eq!(rx.idle_bound(), None);
}
#[test]
fn a_full_queue_is_an_error_not_a_wait() {
let (mut tx, _rx) = refusal_channel::<u64>(progress(0));
for seq in 0..REFUSAL_QUEUE_CAPACITY as u64 {
tx.try_send(refusal(1, seq)).unwrap();
}
assert!(tx.try_send(refusal(1, 0)).is_err());
}
}