use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use melin_app::RejectReason;
use melin_journal::AckPolicyChange;
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 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
}
}
}
#[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_transport_core::replica_presence::Lowered;
use std::sync::atomic::AtomicBool;
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);
}
}