use std::convert::Infallible;
use std::os::fd::OwnedFd;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard};
use std::time::{Duration, Instant};
use melin_app::{AppEvent, unix_epoch_nanos};
use melin_journal::{AckPolicyChange, JournalEvent};
use melin_pipeline::ring;
use melin_transport_core::lineage::LineagePolicy;
use melin_transport_core::pipeline::InputSlot;
use melin_transport_core::replica_presence::Observed;
use tracing::{info, warn};
use crate::ack_policy::AckPolicy;
use crate::halt::{HaltGate, Journaled, Replication};
const PUBLISH_WAIT: Duration = Duration::from_secs(2);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Origin {
Runtime,
Staged,
}
pub(crate) fn is_unwitnessed(policy: AckPolicy, origin: Origin, seen: Option<&Observed>) -> bool {
match seen {
None => true,
Some(Observed::NoneConnected(_)) => origin == Origin::Runtime && policy == AckPolicy::Disk,
Some(Observed::Connected(_)) => false,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum BootEntry {
Differs,
TakesEffectUnwitnessed,
}
fn boot_entry(
lineage: Option<LineagePolicy>,
requested: AckPolicy,
replication: Replication,
) -> Option<BootEntry> {
let Some(lineage) = lineage.filter(|l| l.policy() == requested.to_byte()) else {
return Some(BootEntry::Differs);
};
let may_await_witness = lineage.previous.is_some() && !lineage.change.unwitnessed;
match replication {
Replication::Disabled if may_await_witness => Some(BootEntry::TakesEffectUnwitnessed),
Replication::Disabled | Replication::Enabled => None,
}
}
fn change_slot<E: AppEvent>(change: AckPolicyChange) -> InputSlot<E> {
InputSlot::node_event(JournalEvent::AckPolicyChange(change), unix_epoch_nanos())
}
fn publish_staged<E: AppEvent>(
producer: &mut ring::Producer<InputSlot<E>>,
halt: &HaltGate,
policy: AckPolicy,
) -> Journaled {
let Ok(journaled) = halt.journal(policy, Origin::Staged, |change| {
Ok::<_, Infallible>(producer.publish(change_slot(change)))
});
journaled
}
fn log_journaled(journaled: Journaled, origin: Origin) {
let policy = journaled.policy();
if journaled.is_forced() {
warn!(
policy = %policy,
input_seq = journaled.input_seq(),
"ack policy change forced with no replica connected: client writes are taken again \
until a replica streaming live acknowledges an entry, the change takes effect once on this node's disk, and a \
replica that was offline does not hold it — if this node fails before that replica \
rejoins, it may auto-promote over events acknowledged under the new policy"
);
} else {
info!(
policy = %policy,
unwitnessed = journaled.unwitnessed(),
origin = ?origin,
input_seq = journaled.input_seq(),
"ack policy change journaled"
);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RequestOutcome {
Published,
Staged,
Refused(Refused),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Refused {
RingFull,
ProducerStopped,
AnotherWaiting,
NoReplicaCanAttach,
}
#[derive(Clone, Copy, Debug)]
struct Request {
policy: AckPolicy,
ticket: u64,
}
enum Producer {
Detached,
Attached { wake: Option<Arc<OwnedFd>> },
}
struct State {
producer: Producer,
request: Option<Request>,
issued: u64,
published: u64,
requested: AckPolicy,
}
pub struct AckPolicyControl {
state: Mutex<State>,
changed: Condvar,
pending: AtomicBool,
publish_wait: Duration,
replication: Replication,
}
impl AckPolicyControl {
pub(crate) fn new(configured: AckPolicy, replication: Replication) -> Arc<Self> {
Self::with_publish_wait(configured, replication, PUBLISH_WAIT)
}
fn with_publish_wait(
configured: AckPolicy,
replication: Replication,
publish_wait: Duration,
) -> Arc<Self> {
Arc::new(Self {
state: Mutex::new(State {
producer: Producer::Detached,
request: None,
issued: 0,
published: 0,
requested: configured,
}),
changed: Condvar::new(),
pending: AtomicBool::new(false),
publish_wait,
replication,
})
}
pub(crate) fn requested(&self) -> AckPolicy {
self.lock().requested
}
fn lock(&self) -> MutexGuard<'_, State> {
self.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(crate) fn request(&self, policy: AckPolicy) -> RequestOutcome {
if !self.replication.can_meet(policy) {
return RequestOutcome::Refused(Refused::NoReplicaCanAttach);
}
let deadline = Instant::now() + self.publish_wait;
let mut state = self.lock();
let Producer::Attached { wake } = &state.producer else {
state.requested = policy;
return RequestOutcome::Staged;
};
if state.request.is_some() {
return RequestOutcome::Refused(Refused::AnotherWaiting);
}
let wake = wake.clone();
state.issued += 1;
let ticket = state.issued;
state.request = Some(Request { policy, ticket });
self.pending.store(true, Ordering::Release);
if let Some(fd) = wake {
crate::reader::wake_reader(&fd);
}
loop {
if state.published >= ticket {
return RequestOutcome::Published;
}
if !state
.request
.is_some_and(|waiting| waiting.ticket == ticket)
{
return RequestOutcome::Refused(Refused::ProducerStopped);
}
let Some(left) = deadline.checked_duration_since(Instant::now()) else {
state.request = None;
self.pending.store(false, Ordering::Release);
self.changed.notify_all();
return RequestOutcome::Refused(Refused::RingFull);
};
state = self.wait(state, left);
}
}
fn wait<'a>(&self, state: MutexGuard<'a, State>, left: Duration) -> MutexGuard<'a, State> {
self.changed
.wait_timeout(state, left)
.unwrap_or_else(|poisoned| poisoned.into_inner())
.0
}
pub(crate) fn claim(self: &Arc<Self>) -> PublisherClaim {
PublisherClaim {
control: Arc::clone(self),
policy: self.requested(),
}
}
}
pub struct PublisherClaim {
control: Arc<AckPolicyControl>,
policy: AckPolicy,
}
impl PublisherClaim {
pub(crate) fn policy(&self) -> AckPolicy {
self.policy
}
#[must_use = "the booted claim is what attaches the producer"]
pub(crate) fn journal_boot<E: AppEvent>(
self,
lineage: Option<LineagePolicy>,
producer: &mut ring::Producer<InputSlot<E>>,
halt: &HaltGate,
) -> BootedClaim {
if let Some(reason) = boot_entry(lineage, self.policy, halt.replication()) {
let journaled = publish_staged(producer, halt, self.policy);
info!(
policy = %self.policy,
unwitnessed = journaled.unwitnessed(),
input_seq = journaled.input_seq(),
lineage = ?lineage.map(|p| p.policy().0),
reason = ?reason,
"journaling the requested ack policy"
);
}
BootedClaim {
control: self.control,
policy: self.policy,
}
}
}
pub struct BootedClaim {
control: Arc<AckPolicyControl>,
policy: AckPolicy,
}
impl BootedClaim {
pub(crate) fn attach<E: AppEvent>(
self,
producer: &mut ring::Producer<InputSlot<E>>,
halt: &HaltGate,
wake: Option<Arc<OwnedFd>>,
) -> AckPolicyPublisher {
let control = self.control;
let staged = {
let mut state = control.lock();
state.producer = Producer::Attached { wake };
state.requested
};
if staged != self.policy {
log_journaled(publish_staged(producer, halt, staged), Origin::Staged);
}
AckPolicyPublisher { control }
}
}
pub struct AckPolicyPublisher {
control: Arc<AckPolicyControl>,
}
impl AckPolicyPublisher {
#[inline]
pub(crate) fn poll<E: AppEvent>(
&mut self,
producer: &mut ring::Producer<InputSlot<E>>,
halt: &HaltGate,
) {
if self.control.pending.load(Ordering::Acquire) {
self.publish(producer, halt);
}
}
#[cold]
fn publish<E: AppEvent>(
&mut self,
producer: &mut ring::Producer<InputSlot<E>>,
halt: &HaltGate,
) {
let control = &*self.control;
let mut state = control.lock();
let Some(request) = state.request else {
return;
};
let Ok(journaled) = halt.journal(request.policy, Origin::Runtime, |change| {
producer.try_publish(change_slot(change))
}) else {
return;
};
state.request = None;
state.published = request.ticket;
state.requested = request.policy;
control.pending.store(false, Ordering::Release);
control.changed.notify_all();
drop(state);
log_journaled(journaled, Origin::Runtime);
}
}
impl Drop for AckPolicyPublisher {
fn drop(&mut self) {
let mut state = self.control.lock();
state.producer = Producer::Detached;
state.request = None;
self.control.pending.store(false, Ordering::Release);
self.control.changed.notify_all();
}
}
#[cfg(test)]
pub(crate) mod test_support {
use super::*;
use melin_journal::AckPolicyByte;
use melin_pipeline::ring::{Consumer, DisruptorBuilder};
use melin_pipeline::wait::WaitStrategy;
use melin_transport_core::WireSeq;
pub(crate) fn control_with_publish_wait(
configured: AckPolicy,
publish_wait: Duration,
) -> Arc<AckPolicyControl> {
AckPolicyControl::with_publish_wait(configured, Replication::Enabled, publish_wait)
}
pub(crate) fn replicated(configured: AckPolicy) -> Arc<AckPolicyControl> {
AckPolicyControl::new(configured, Replication::Enabled)
}
pub(crate) fn ring<E: AppEvent>(
capacity: usize,
) -> (ring::Producer<InputSlot<E>>, Consumer<InputSlot<E>>) {
let (producer, mut consumers) = DisruptorBuilder::<InputSlot<E>>::new(capacity)
.add_consumer()
.build(WaitStrategy::SpinThenYield);
(producer, consumers.pop().expect("one consumer"))
}
pub(crate) fn lineage(
policy: AckPolicyByte,
unwitnessed: bool,
wire_seq: u64,
previous: Option<AckPolicyByte>,
) -> Option<LineagePolicy> {
Some(LineagePolicy {
change: AckPolicyChange {
policy,
unwitnessed,
},
wire_seq: WireSeq::new(wire_seq),
previous,
})
}
pub(crate) fn first_change(policy: AckPolicy) -> Option<LineagePolicy> {
lineage(policy.to_byte(), false, 1, None)
}
pub(crate) fn booted(control: &Arc<AckPolicyControl>) -> BootedClaim {
let claim = control.claim();
let lineage = first_change(claim.policy());
let (mut producer, _consumer) = ring::<counter_server::CounterEvent>(2);
claim.journal_boot(
lineage,
&mut producer,
&crate::halt::test_support::gate_over(None),
)
}
pub(crate) fn attach<E: AppEvent>(
control: &Arc<AckPolicyControl>,
producer: &mut ring::Producer<InputSlot<E>>,
) -> AckPolicyPublisher {
booted(control).attach(producer, &crate::halt::test_support::gate_over(None), None)
}
}
#[cfg(test)]
mod tests {
use super::test_support::{
attach, booted, control_with_publish_wait, first_change, lineage, replicated, ring,
};
use super::*;
use crate::halt::Verdict;
use crate::halt::test_support::{gate_over, presence};
use counter_server::CounterEvent as TestEvent;
use melin_pipeline::ring::Consumer;
use melin_transport_core::WireSeq;
use melin_transport_core::replica_presence::Lowered;
const ALL: [AckPolicy; 4] = [
AckPolicy::Disk,
AckPolicy::Ram,
AckPolicy::DiskAndRam,
AckPolicy::TwoDisks,
];
type Slot = InputSlot<TestEvent>;
fn changes(consumer: &mut Consumer<Slot>) -> Vec<AckPolicyChange> {
let mut out = Vec::new();
while let Some((_, slot)) = consumer.try_consume() {
match slot.event {
JournalEvent::AckPolicyChange(change) => {
assert_eq!(
(slot.connection_id, slot.key_hash, slot.sequence),
(0, 0, 0),
"a node event"
);
out.push(change);
}
other => panic!("not a policy change: {other:?}"),
}
}
out
}
fn change(policy: AckPolicy, unwitnessed: bool) -> AckPolicyChange {
AckPolicyChange {
policy: policy.to_byte(),
unwitnessed,
}
}
#[test]
fn the_forced_rule() {
let none_connected = presence(0).observe();
let connected = presence(1).observe();
for policy in ALL {
for origin in [Origin::Runtime, Origin::Staged] {
assert!(is_unwitnessed(policy, origin, None));
assert!(!is_unwitnessed(policy, origin, Some(&connected)));
assert_eq!(
is_unwitnessed(policy, origin, Some(&none_connected)),
origin == Origin::Runtime && policy == AckPolicy::Disk,
"{policy} from {origin:?}"
);
}
}
}
fn halted_gate() -> HaltGate {
gate_over(Some(presence(0)))
}
#[test]
fn the_boot_entry_when_the_lineage_differs() {
let disk = first_change(AckPolicy::Disk);
let (mut producer, mut consumer) = ring::<TestEvent>(8);
let halt = halted_gate();
let boot = |requested, lineage, producer: &mut ring::Producer<Slot>, halt: &HaltGate| {
drop(
replicated(requested)
.claim()
.journal_boot(lineage, producer, halt),
);
};
boot(AckPolicy::Disk, disk, &mut producer, &halt);
assert!(changes(&mut consumer).is_empty(), "nothing to change");
boot(AckPolicy::DiskAndRam, disk, &mut producer, &halt);
assert_eq!(
changes(&mut consumer),
[change(AckPolicy::DiskAndRam, false)]
);
boot(AckPolicy::Disk, None, &mut producer, &halt);
assert_eq!(
changes(&mut consumer),
[change(AckPolicy::Disk, false)],
"a lineage holding none gets one, never forced"
);
assert_ne!(halt.verdict(), Verdict::Take, "a boot `disk` is no consent");
let standalone = gate_over(None);
boot(
AckPolicy::Disk,
first_change(AckPolicy::DiskAndRam),
&mut producer,
&standalone,
);
assert_eq!(
changes(&mut consumer),
[change(AckPolicy::Disk, true)],
"standalone: unwitnessed"
);
}
#[test]
fn the_boot_decision() {
use Replication::{Disabled, Enabled};
let disk = AckPolicy::Disk.to_byte();
let disk_and_ram = Some(AckPolicy::DiskAndRam.to_byte());
let waiting = lineage(disk, false, 3, disk_and_ram);
let marked = lineage(disk, true, 3, disk_and_ram);
for replication in [Enabled, Disabled] {
for lineage in [None, first_change(AckPolicy::DiskAndRam), waiting, marked] {
assert_eq!(
boot_entry(lineage, AckPolicy::Ram, replication),
Some(BootEntry::Differs),
"{lineage:?} under {replication:?}"
);
}
assert_eq!(
boot_entry(first_change(AckPolicy::Disk), AckPolicy::Disk, replication),
None,
"the first change under {replication:?}"
);
assert_eq!(
boot_entry(marked, AckPolicy::Disk, replication),
None,
"already unwitnessed under {replication:?}"
);
}
assert_eq!(
boot_entry(waiting, AckPolicy::Disk, Enabled),
None,
"replicated: the gate re-applies it"
);
assert_eq!(
boot_entry(waiting, AckPolicy::Disk, Disabled),
Some(BootEntry::TakesEffectUnwitnessed)
);
let repeated =
LineagePolicy::after(waiting, WireSeq::new(4), change(AckPolicy::Disk, false));
assert_eq!(
boot_entry(Some(repeated), AckPolicy::Disk, Disabled),
Some(BootEntry::TakesEffectUnwitnessed)
);
}
#[test]
fn a_standalone_boot_marks_a_waiting_change_unwitnessed_once() {
let (mut producer, mut consumer) = ring::<TestEvent>(8);
let standalone = gate_over(None);
let waiting = lineage(
AckPolicy::Disk.to_byte(),
false,
3,
Some(AckPolicy::DiskAndRam.to_byte()),
);
drop(
AckPolicyControl::new(AckPolicy::Disk, Replication::Disabled)
.claim()
.journal_boot(waiting, &mut producer, &standalone),
);
let journaled = changes(&mut consumer);
assert_eq!(journaled, [change(AckPolicy::Disk, true)]);
let folded = LineagePolicy::after(waiting, WireSeq::new(5), journaled[0]);
assert_eq!(
(folded.wire_seq, folded.change.unwitnessed),
(WireSeq::new(3), true),
"the same transition, now marked"
);
drop(
AckPolicyControl::new(AckPolicy::Disk, Replication::Disabled)
.claim()
.journal_boot(Some(folded), &mut producer, &standalone),
);
assert!(changes(&mut consumer).is_empty(), "nothing more");
drop(replicated(AckPolicy::Disk).claim().journal_boot(
waiting,
&mut producer,
&halted_gate(),
));
assert!(changes(&mut consumer).is_empty(), "the gate re-applies it");
}
#[test]
fn a_boot_entry_that_needs_a_replica_restores_the_halt() {
let control = replicated(AckPolicy::Disk);
let (mut producer, mut consumer) = ring::<TestEvent>(8);
let halt = halted_gate();
let mut publisher = booted(&control).attach(&mut producer, &halt, None);
let outcome = request_while_polling(
&control,
&mut publisher,
&mut producer,
AckPolicy::Disk,
&halt,
);
assert_eq!(outcome, RequestOutcome::Published);
assert_eq!(halt.verdict(), Verdict::Take, "forced: lifted");
drop(publisher);
drop(replicated(AckPolicy::TwoDisks).claim().journal_boot(
first_change(AckPolicy::Disk),
&mut producer,
&halt,
));
assert_ne!(halt.verdict(), Verdict::Take, "restored");
assert_eq!(
changes(&mut consumer),
[
change(AckPolicy::Disk, true),
change(AckPolicy::TwoDisks, false)
]
);
}
#[test]
fn a_request_with_no_producer_is_staged_for_the_next_claim() {
let control = replicated(AckPolicy::DiskAndRam);
assert_eq!(control.claim().policy(), AckPolicy::DiskAndRam);
assert_eq!(control.request(AckPolicy::Ram), RequestOutcome::Staged);
assert_eq!(control.requested(), AckPolicy::Ram);
assert_eq!(control.claim().policy(), AckPolicy::Ram);
}
#[test]
fn a_node_without_replication_refuses_a_policy_that_needs_a_replica() {
const REFUSED: RequestOutcome = RequestOutcome::Refused(Refused::NoReplicaCanAttach);
let control = AckPolicyControl::new(AckPolicy::Disk, Replication::Disabled);
for policy in [AckPolicy::Ram, AckPolicy::DiskAndRam, AckPolicy::TwoDisks] {
assert_eq!(control.request(policy), REFUSED, "staging {policy}");
}
assert_eq!(control.requested(), AckPolicy::Disk, "nothing staged");
assert_eq!(control.request(AckPolicy::Disk), RequestOutcome::Staged);
let (mut producer, mut consumer) = ring::<TestEvent>(8);
let standalone = gate_over(None);
let mut publisher = booted(&control).attach(&mut producer, &standalone, None);
for policy in [AckPolicy::Ram, AckPolicy::DiskAndRam, AckPolicy::TwoDisks] {
let outcome =
request_while_polling(&control, &mut publisher, &mut producer, policy, &standalone);
assert_eq!(outcome, REFUSED, "journaling {policy}");
assert!(!control.pending.load(Ordering::Acquire), "nothing waits");
}
assert!(changes(&mut consumer).is_empty(), "nothing journaled");
assert_eq!(control.requested(), AckPolicy::Disk);
let outcome = request_while_polling(
&control,
&mut publisher,
&mut producer,
AckPolicy::Disk,
&standalone,
);
assert_eq!(outcome, RequestOutcome::Published);
assert_eq!(changes(&mut consumer), [change(AckPolicy::Disk, true)]);
}
fn request_while_polling(
control: &Arc<AckPolicyControl>,
publisher: &mut AckPolicyPublisher,
producer: &mut ring::Producer<Slot>,
policy: AckPolicy,
halt: &HaltGate,
) -> RequestOutcome {
std::thread::scope(|s| {
let requesting = s.spawn(|| control.request(policy));
while !requesting.is_finished() {
publisher.poll(producer, halt);
std::thread::yield_now();
}
requesting.join().expect("the request panicked")
})
}
#[test]
fn an_attached_producer_journals_every_request() {
let control = replicated(AckPolicy::DiskAndRam);
let (mut producer, mut consumer) = ring(8);
let replicas = presence(1);
let halt = gate_over(Some(Arc::clone(&replicas)));
let mut publisher = booted(&control).attach(&mut producer, &halt, None);
let mut journal = |policy| {
let outcome =
request_while_polling(&control, &mut publisher, &mut producer, policy, &halt);
assert_eq!(outcome, RequestOutcome::Published);
};
journal(AckPolicy::Disk);
journal(AckPolicy::Disk);
assert_eq!(replicas.lower(), Lowered::LastHalted);
journal(AckPolicy::Disk);
journal(AckPolicy::TwoDisks);
assert_eq!(
changes(&mut consumer),
[
change(AckPolicy::Disk, false),
change(AckPolicy::Disk, false),
change(AckPolicy::Disk, true),
change(AckPolicy::TwoDisks, false),
]
);
assert_eq!(control.claim().policy(), AckPolicy::TwoDisks);
}
#[test]
fn polling_with_nothing_requested_publishes_nothing() {
let control = replicated(AckPolicy::Disk);
let (mut producer, mut consumer) = ring(8);
let halt = halted_gate();
let mut publisher = booted(&control).attach(&mut producer, &halt, None);
publisher.poll(&mut producer, &halt);
assert!(changes(&mut consumer).is_empty());
assert_ne!(halt.verdict(), Verdict::Take);
}
#[test]
fn a_full_ring_refuses_the_request_and_withdraws_it() {
let control = control_with_publish_wait(AckPolicy::DiskAndRam, Duration::from_millis(50));
let (mut producer, mut consumer) = ring(2);
for _ in 0..2 {
producer
.try_publish(change_slot(change(AckPolicy::Ram, false)))
.expect("room");
}
let halt = halted_gate();
let mut publisher = booted(&control).attach(&mut producer, &halt, None);
let outcome = request_while_polling(
&control,
&mut publisher,
&mut producer,
AckPolicy::Disk,
&halt,
);
assert_eq!(outcome, RequestOutcome::Refused(Refused::RingFull));
assert_eq!(changes(&mut consumer).len(), 2, "only what filled the ring");
publisher.poll(&mut producer, &halt);
assert!(changes(&mut consumer).is_empty(), "withdrawn");
assert_eq!(control.requested(), AckPolicy::DiskAndRam);
assert_ne!(halt.verdict(), Verdict::Take, "still halted");
}
#[test]
fn a_detached_producer_refuses_the_waiting_request() {
let control = control_with_publish_wait(AckPolicy::Disk, Duration::from_secs(60));
let (mut producer, _consumer) = ring::<TestEvent>(8);
let publisher = attach(&control, &mut producer);
let started = Instant::now();
let outcome = std::thread::scope(|s| {
let requesting = s.spawn(|| control.request(AckPolicy::Ram));
while !control.pending.load(Ordering::Acquire) {
std::thread::yield_now();
}
drop(publisher);
requesting.join().expect("the request panicked")
});
assert_eq!(outcome, RequestOutcome::Refused(Refused::ProducerStopped));
assert!(started.elapsed() < Duration::from_secs(30), "not the bound");
assert!(!control.pending.load(Ordering::Acquire));
assert_eq!(control.request(AckPolicy::Ram), RequestOutcome::Staged);
}
#[test]
fn a_producer_replaced_while_a_request_waits_refuses_it() {
let control = control_with_publish_wait(AckPolicy::Disk, Duration::from_secs(60));
let (mut producer, mut consumer) = ring::<TestEvent>(8);
let publisher = attach(&control, &mut producer);
let started = Instant::now();
let outcome = std::thread::scope(|s| {
let requesting = s.spawn(|| control.request(AckPolicy::Ram));
while !control.pending.load(Ordering::Acquire) {
std::thread::yield_now();
}
drop(publisher);
let mut next = attach(&control, &mut producer);
let outcome = requesting.join().expect("the request panicked");
next.poll(&mut producer, &gate_over(None));
outcome
});
assert_eq!(outcome, RequestOutcome::Refused(Refused::ProducerStopped));
assert!(started.elapsed() < Duration::from_secs(30), "not the bound");
assert!(changes(&mut consumer).is_empty(), "never published");
}
#[test]
fn a_second_waiting_request_is_refused() {
let control = control_with_publish_wait(AckPolicy::DiskAndRam, Duration::from_secs(60));
let (mut producer, mut consumer) = ring(8);
let halt = halted_gate();
let mut publisher = booted(&control).attach(&mut producer, &halt, None);
std::thread::scope(|s| {
let first = s.spawn(|| control.request(AckPolicy::TwoDisks));
while !control.pending.load(Ordering::Acquire) {
std::thread::yield_now();
}
assert_eq!(
control.request(AckPolicy::Disk),
RequestOutcome::Refused(Refused::AnotherWaiting)
);
publisher.poll(&mut producer, &halt);
assert_eq!(first.join().expect("panicked"), RequestOutcome::Published);
});
assert_eq!(changes(&mut consumer), [change(AckPolicy::TwoDisks, false)]);
}
#[test]
fn a_request_staged_during_bring_up_is_journaled_at_attach_in_order() {
let control = replicated(AckPolicy::DiskAndRam);
assert_eq!(control.request(AckPolicy::TwoDisks), RequestOutcome::Staged);
let claim = control.claim();
let (mut producer, mut consumer) = ring(8);
let halt = halted_gate();
let booted = claim.journal_boot(first_change(AckPolicy::DiskAndRam), &mut producer, &halt);
assert_eq!(control.request(AckPolicy::Disk), RequestOutcome::Staged);
let mut publisher = booted.attach(&mut producer, &halt, None);
assert_ne!(
halt.verdict(),
Verdict::Take,
"a staged `disk` is no consent"
);
let outcome = request_while_polling(
&control,
&mut publisher,
&mut producer,
AckPolicy::Disk,
&halt,
);
assert_eq!(outcome, RequestOutcome::Published);
assert_eq!(
changes(&mut consumer),
[
change(AckPolicy::TwoDisks, false),
change(AckPolicy::Disk, false),
change(AckPolicy::Disk, true),
]
);
assert_eq!(halt.verdict(), Verdict::Take, "the forced swap came last");
}
#[test]
fn a_staged_policy_equal_to_the_claims_is_not_journaled_at_attach() {
let control = replicated(AckPolicy::DiskAndRam);
let claim = control.claim();
assert_eq!(control.request(AckPolicy::TwoDisks), RequestOutcome::Staged);
assert_eq!(
control.request(AckPolicy::DiskAndRam),
RequestOutcome::Staged
);
let (mut producer, mut consumer) = ring::<TestEvent>(8);
let halt = halted_gate();
let _publisher = claim
.journal_boot(first_change(AckPolicy::DiskAndRam), &mut producer, &halt)
.attach(&mut producer, &halt, None);
assert!(changes(&mut consumer).is_empty());
}
#[test]
fn a_request_wakes_a_parked_producer() {
use std::os::fd::{AsRawFd, FromRawFd};
let fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK) };
assert!(fd >= 0);
let fd = Arc::new(unsafe { OwnedFd::from_raw_fd(fd) });
let control = control_with_publish_wait(AckPolicy::DiskAndRam, Duration::from_millis(10));
let (mut producer, _consumer) = ring::<TestEvent>(8);
let _publisher =
booted(&control).attach(&mut producer, &gate_over(None), Some(Arc::clone(&fd)));
assert_eq!(
control.request(AckPolicy::Disk),
RequestOutcome::Refused(Refused::RingFull),
"never polled, so the bound runs out"
);
let mut count = 0u64;
let read = unsafe {
libc::read(
fd.as_raw_fd(),
(&mut count as *mut u64).cast(),
std::mem::size_of::<u64>(),
)
};
assert_eq!((read, count), (8, 1), "the eventfd was written once");
}
}