use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use melin_app::RejectReason;
use melin_pipeline::padding::Sequence;
use melin_pipeline::spsc;
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::fence::FenceState;
pub(crate) const REFUSAL_QUEUE_CAPACITY: usize = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Verdict {
Take,
Refuse(RejectReason),
Close,
}
pub struct HaltGate {
replicas_connected: Option<Arc<AtomicU32>>,
fence_state: Arc<FenceState>,
refused: Arc<AtomicU64>,
}
impl HaltGate {
pub fn new(
replicas_connected: Option<Arc<AtomicU32>>,
fence_state: Arc<FenceState>,
refused: Arc<AtomicU64>,
) -> Self {
Self {
replicas_connected,
fence_state,
refused,
}
}
#[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(|count| count.load(Ordering::Relaxed) == 0)
{
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)]
mod tests {
use super::*;
use melin_pipeline::padding::CachePadded;
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<AtomicU32>>, fence: Arc<FenceState>) -> HaltGate {
HaltGate::new(replicas_connected, fence, Arc::new(AtomicU64::new(0)))
}
#[test]
fn standalone_never_refuses() {
let gate = gate(None, Arc::new(FenceState::new(0)));
assert_eq!(gate.verdict(), Verdict::Take);
}
#[test]
fn no_replica_refuses_with_replica_disconnected_until_one_connects() {
let count = Arc::new(AtomicU32::new(0));
let gate = gate(Some(Arc::clone(&count)), Arc::new(FenceState::new(0)));
assert_eq!(
gate.verdict(),
Verdict::Refuse(RejectReason::ReplicaDisconnected)
);
count.store(1, Ordering::Relaxed);
assert_eq!(gate.verdict(), Verdict::Take);
}
#[test]
fn a_fenced_node_closes_whatever_its_replica_count() {
let fence = Arc::new(FenceState::new(1));
let gate = gate(Some(Arc::new(AtomicU32::new(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());
}
}