Skip to main content

melin_server_runtime/
policy_control.rs

1//! How an ack-policy change reaches the journal.
2//!
3//! The policy in force is part of the lineage: a change is an
4//! `AckPolicyChange` entry, journaled in order with the events it governs,
5//! and the response gate switches at the entry's own slot (see
6//! `durability_gate`). This module is the producing side.
7//!
8//! The input ring has a single producer: the main thread while a primary
9//! starts, then the io_uring reader or the DPDK poll thread. The admin
10//! `ACK-POLICY` command runs on its own thread, so it cannot publish an
11//! entry itself. It posts the request in [`AckPolicyControl`], a mailbox
12//! shared with the ring's producer, and waits for the producer to publish
13//! it:
14//!
15//! - **At boot and at promotion**, the main thread takes a
16//!   [`PublisherClaim`] before the pipeline serves anyone, and journals the
17//!   requested policy (the configured one, or one an operator staged) when
18//!   the lineage holds a different one or none, or, standalone, when the
19//!   change that put it there may never have been witnessed
20//!   (`PublisherClaim::journal_boot`, which consumes the claim; the rule
21//!   is `boot_entry`).
22//! - **While the node serves**, the booted claim is attached to the
23//!   producer (`BootedClaim::attach`), which checks the mailbox on its loop
24//!   (`AckPolicyPublisher::poll`) and journals each request. The admin
25//!   command replies once the entry is on the ring, or refuses if the ring
26//!   stays full past a bounded wait (`AckPolicyControl::request`).
27//! - **With no producer attached** (a replica, or a primary still
28//!   starting), a request is staged: a replica journals it when promoted,
29//!   a primary as its producer attaches, from the main thread that still
30//!   owns the ring, so ahead of any runtime request.
31//!
32//! Every runtime request is journaled, duplicates included: the entries
33//! are the audit trail of what operators asked for, and a re-sent request
34//! is how an operator forces a swap a departed replica would otherwise
35//! hold up.
36//!
37//! The one request refused up front, whatever the producer's state, is a
38//! policy that needs a replica on a node that accepts none (no
39//! `--replication-bind`: a `--standalone` node, or a replica that will be
40//! promoted without one): no replica could ever attach to meet it, so,
41//! journaled, it would hold every reply (`Replication::can_meet`). The
42//! mailbox knows the node's shape from its configuration, which fixes it
43//! for the process: a replica refuses such a policy as it would be staged,
44//! not at promotion.
45//!
46//! # Witnessed and forced changes
47//!
48//! A change normally takes effect once its entry is durable under the
49//! policy before it, so the nodes that policy relied on hold it. A primary
50//! halted under a policy that needs a replica, with none connected, can
51//! never get there. The one way out is a *forced* change: an explicit
52//! runtime `ACK-POLICY disk` while no replica is connected, journaled as
53//! unwitnessed, which the gate applies once its entry is on this node's
54//! disk. A node with replication disabled has no witness for any change,
55//! so its entries are all unwitnessed too. The rule is `is_unwitnessed`.
56//!
57//! Journaling a forced change is also the operator's consent to serve on
58//! this node's disk alone, so it lifts the halt on client writes until a
59//! replica streaming live acknowledges an entry; journaling a policy that
60//! needs a replica restores it (see `halt`). The producer records both as
61//! it journals the entry.
62
63use std::convert::Infallible;
64use std::os::fd::OwnedFd;
65use std::sync::atomic::{AtomicBool, Ordering};
66use std::sync::{Arc, Condvar, Mutex, MutexGuard};
67use std::time::{Duration, Instant};
68
69use melin_app::{AppEvent, unix_epoch_nanos};
70use melin_journal::{AckPolicyChange, JournalEvent};
71use melin_pipeline::ring;
72use melin_transport_core::lineage::LineagePolicy;
73use melin_transport_core::pipeline::InputSlot;
74use melin_transport_core::replica_presence::Observed;
75use tracing::{info, warn};
76
77use crate::ack_policy::AckPolicy;
78use crate::halt::{HaltGate, Journaled, Replication};
79
80/// How long an `ACK-POLICY` command waits for its entry to be published
81/// before it is refused. The producer checks the mailbox on every loop, so
82/// only a full input ring (a pipeline stalled behind its gate) holds a
83/// request this long; the admin connection's own timeout is longer, so the
84/// operator reads the refusal.
85const PUBLISH_WAIT: Duration = Duration::from_secs(2);
86
87/// Where a request comes from, which decides whether it may be forced.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub(crate) enum Origin {
90    /// An `ACK-POLICY` command a running primary's producer publishes:
91    /// the operator asked for this policy, now.
92    Runtime,
93    /// The policy a primary starts or is promoted under, or one staged
94    /// before its producer ran. Never forced: nobody asked for it in the
95    /// knowledge of who is connected when it is published.
96    Staged,
97}
98
99/// Whether a change to `policy` is journaled unwitnessed: applied once its
100/// entry is on this node's disk, rather than once the nodes the previous
101/// policy relied on hold it.
102///
103/// `seen` is who could witness it: the halt gate's look at the replicas
104/// streaming (`HaltGate::decide`), `None` with replication disabled. Then
105/// every change (nothing can witness one). With no replica streaming, an
106/// explicit runtime `disk` only: the operator's consent to serve on this
107/// node's disk alone. Any other change waits.
108pub(crate) fn is_unwitnessed(policy: AckPolicy, origin: Origin, seen: Option<&Observed>) -> bool {
109    match seen {
110        None => true,
111        Some(Observed::NoneConnected(_)) => origin == Origin::Runtime && policy == AckPolicy::Disk,
112        Some(Observed::Connected(_)) => false,
113    }
114}
115
116/// Why a primary journals the policy it requests as its tenure starts
117/// ([`boot_entry`]), for the log line.
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119enum BootEntry {
120    /// The lineage holds another policy, or none (a new history, or a
121    /// lineage written before policy changes were journaled).
122    Differs,
123    /// The lineage already names the requested policy, but the change
124    /// that put it there may still be waiting for its witness, and this
125    /// node, standalone, starts serving under it at once: the entry,
126    /// unwitnessed, records that the policy took effect without one.
127    TakesEffectUnwitnessed,
128}
129
130/// Whether a primary starting a tenure on `lineage` journals `requested`,
131/// and why; `None` when the lineage already holds it.
132///
133/// With replication enabled, only when the lineage names another policy
134/// or none: a newest change still waiting for its witness is re-applied
135/// in order by the gate (see `durability_gate::initial_gate_policy`).
136///
137/// With replication disabled, also when the lineage names `requested`
138/// through a change that may never have been witnessed: the standalone
139/// gate serves under `requested` at once, so without an entry nothing in
140/// the journal would mark those acknowledgements unwitnessed. Whether the
141/// change was witnessed before the restart is not on disk (the lineage
142/// records the transition, not its outcome), so this reads it
143/// conservatively: every transition journaled witnessed counts as still
144/// waiting. One journaled unwitnessed does not (the journal already marks
145/// it), nor does the lineage's first change (a primary journals it as the
146/// policy its gate starts under, so it had nothing to wait for); and once
147/// this entry is folded the transition is unwitnessed, so a later
148/// standalone restart journals nothing more.
149fn boot_entry(
150    lineage: Option<LineagePolicy>,
151    requested: AckPolicy,
152    replication: Replication,
153) -> Option<BootEntry> {
154    let Some(lineage) = lineage.filter(|l| l.policy() == requested.to_byte()) else {
155        return Some(BootEntry::Differs);
156    };
157    let may_await_witness = lineage.previous.is_some() && !lineage.change.unwitnessed;
158    match replication {
159        Replication::Disabled if may_await_witness => Some(BootEntry::TakesEffectUnwitnessed),
160        Replication::Disabled | Replication::Enabled => None,
161    }
162}
163
164/// The input-ring slot that journals `change`.
165fn change_slot<E: AppEvent>(change: AckPolicyChange) -> InputSlot<E> {
166    InputSlot::node_event(JournalEvent::AckPolicyChange(change), unix_epoch_nanos())
167}
168
169/// Journal `policy` as a staged change (`Origin::Staged`, never forced):
170/// the policy a primary starts or is promoted under, or one an operator
171/// staged before its producer attached. From the main thread, while it is
172/// still the input ring's only producer, so the entry lands ahead of any
173/// runtime request. Journaled through `halt`, as the producer does for a
174/// runtime request: a staged policy that needs a replica drops any halt
175/// override. Not waited on: the gate switches at the entry's slot, in
176/// order, once the policy before it has cleared it. Returns what was
177/// journaled, for the caller's log line.
178fn publish_staged<E: AppEvent>(
179    producer: &mut ring::Producer<InputSlot<E>>,
180    halt: &HaltGate,
181    policy: AckPolicy,
182) -> Journaled {
183    // A blocking publish: it waits for room rather than failing.
184    let Ok(journaled) = halt.journal(policy, Origin::Staged, |change| {
185        Ok::<_, Infallible>(producer.publish(change_slot(change)))
186    });
187    journaled
188}
189
190/// Log a change being journaled, at the level its risk calls for.
191fn log_journaled(journaled: Journaled, origin: Origin) {
192    let policy = journaled.policy();
193    if journaled.is_forced() {
194        warn!(
195            policy = %policy,
196            input_seq = journaled.input_seq(),
197            "ack policy change forced with no replica connected: client writes are taken again \
198             until a replica streaming live acknowledges an entry, the change takes effect once on this node's disk, and a \
199             replica that was offline does not hold it — if this node fails before that replica \
200             rejoins, it may auto-promote over events acknowledged under the new policy"
201        );
202    } else {
203        info!(
204            policy = %policy,
205            unwitnessed = journaled.unwitnessed(),
206            origin = ?origin,
207            input_seq = journaled.input_seq(),
208            "ack policy change journaled"
209        );
210    }
211}
212
213/// What became of an `ACK-POLICY` request.
214#[derive(Clone, Copy, Debug, PartialEq, Eq)]
215pub(crate) enum RequestOutcome {
216    /// Journaled: the entry is on the input ring.
217    Published,
218    /// No producer runs on this node (a replica, or a primary still
219    /// starting): the policy is kept, and journaled when one does.
220    Staged,
221    /// Not journaled, for this reason: nothing changed.
222    Refused(Refused),
223}
224
225/// Why an `ACK-POLICY` request was refused. Each calls for something
226/// different from the operator, so the admin reply names it.
227#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228pub(crate) enum Refused {
229    /// The input ring stayed full past the bounded wait: the pipeline is
230    /// stalled behind its gate. Retrying may succeed once it drains.
231    RingFull,
232    /// The producer stopped (the node is shutting down or stepping down)
233    /// before it published the entry.
234    ProducerStopped,
235    /// Another request was already waiting for the producer: two are
236    /// never merged.
237    AnotherWaiting,
238    /// The policy needs a replica, and this node accepts none
239    /// ([`Replication::can_meet`]): it could never be met.
240    NoReplicaCanAttach,
241}
242
243/// A runtime request waiting for the producer.
244#[derive(Clone, Copy, Debug)]
245struct Request {
246    policy: AckPolicy,
247    /// Its place among requests: published in order, so a requester waits
248    /// for the published count to reach it.
249    ticket: u64,
250}
251
252/// The input ring's producer, as the mailbox sees it.
253enum Producer {
254    /// None runs: requests are staged.
255    Detached,
256    /// One runs. `wake` brings it back round its loop when parked (the
257    /// io_uring reader's eventfd); `None` for one that never parks (the
258    /// DPDK poll thread).
259    Attached { wake: Option<Arc<OwnedFd>> },
260}
261
262/// The mailbox's state. Behind one mutex: it is touched by the admin
263/// thread per command, by the producer only when a request waits, never
264/// on the producer's steady-state path (that reads [`AckPolicyControl::pending`]),
265/// and by auto-promotion's fallback ([`AckPolicyControl::requested`]).
266struct State {
267    /// Whether a producer is attached, and how to wake it.
268    producer: Producer,
269    /// The request waiting for the producer, if any.
270    request: Option<Request>,
271    /// The last ticket handed out, and the last one published.
272    issued: u64,
273    published: u64,
274    /// The requested policy: the one the next boot or promotion requests,
275    /// i.e. the configured one, then each one staged or journaled.
276    requested: AckPolicy,
277}
278
279/// The mailbox between the admin `ACK-POLICY` command and the input ring's
280/// producer — see the module docs. One per process, shared by both roles:
281/// a replica stages into it, and its promoted primary journals from it.
282pub struct AckPolicyControl {
283    state: Mutex<State>,
284    /// Signalled when a request is published or withdrawn, or the
285    /// producer detaches.
286    changed: Condvar,
287    /// Set while a request waits for the producer: the one load the
288    /// producer's loop pays.
289    pending: AtomicBool,
290    publish_wait: Duration,
291    /// Whether this node accepts replicas, in either role: the shape
292    /// [`Self::request`] refuses a policy it could never meet on.
293    replication: Replication,
294}
295
296impl AckPolicyControl {
297    /// A mailbox starting from the configured policy, on a node of the
298    /// configured shape (`ServerConfig::replication`, the one its primary
299    /// pipeline's halt gate is built on).
300    pub(crate) fn new(configured: AckPolicy, replication: Replication) -> Arc<Self> {
301        Self::with_publish_wait(configured, replication, PUBLISH_WAIT)
302    }
303
304    /// As [`Self::new`], with the bound a request waits for its entry.
305    fn with_publish_wait(
306        configured: AckPolicy,
307        replication: Replication,
308        publish_wait: Duration,
309    ) -> Arc<Self> {
310        Arc::new(Self {
311            state: Mutex::new(State {
312                producer: Producer::Detached,
313                request: None,
314                issued: 0,
315                published: 0,
316                requested: configured,
317            }),
318            changed: Condvar::new(),
319            pending: AtomicBool::new(false),
320            publish_wait,
321            replication,
322        })
323    }
324
325    /// The requested policy ([`State::requested`]), for a reader off the
326    /// producer's path: auto-promotion's fallback, used only while a
327    /// replica's lineage holds no change. Takes the state's lock, which
328    /// only the admin thread and a producer publishing a request contend.
329    pub(crate) fn requested(&self) -> AckPolicy {
330        self.lock().requested
331    }
332
333    fn lock(&self) -> MutexGuard<'_, State> {
334        // Every critical section leaves the state consistent before it can
335        // panic (none calls out of this module), so a poisoned lock holds
336        // valid state.
337        self.state
338            .lock()
339            .unwrap_or_else(|poisoned| poisoned.into_inner())
340    }
341
342    /// Journal `policy` (the admin `ACK-POLICY` command): post it to the
343    /// attached producer and wait, at most the publish bound, until its
344    /// entry is on the input ring; or stage it while no producer runs.
345    /// Refused first, in either case, if this node could never meet it
346    /// ([`Refused::NoReplicaCanAttach`]): nothing is journaled or staged.
347    ///
348    /// Called by one thread, the admin listener's, which serves one
349    /// command at a time, so no other request is waiting: a request is
350    /// published or withdrawn before this returns.
351    pub(crate) fn request(&self, policy: AckPolicy) -> RequestOutcome {
352        if !self.replication.can_meet(policy) {
353            return RequestOutcome::Refused(Refused::NoReplicaCanAttach);
354        }
355        let deadline = Instant::now() + self.publish_wait;
356        let mut state = self.lock();
357        let Producer::Attached { wake } = &state.producer else {
358            state.requested = policy;
359            return RequestOutcome::Staged;
360        };
361        if state.request.is_some() {
362            // A second requester: two requests are never merged.
363            return RequestOutcome::Refused(Refused::AnotherWaiting);
364        }
365        let wake = wake.clone();
366        state.issued += 1;
367        let ticket = state.issued;
368        state.request = Some(Request { policy, ticket });
369        self.pending.store(true, Ordering::Release);
370        if let Some(fd) = wake {
371            crate::reader::wake_reader(&fd);
372        }
373        loop {
374            if state.published >= ticket {
375                return RequestOutcome::Published;
376            }
377            if !state
378                .request
379                .is_some_and(|waiting| waiting.ticket == ticket)
380            {
381                // Gone unpublished: only the producer's drop withdraws a
382                // request other than its own requester.
383                return RequestOutcome::Refused(Refused::ProducerStopped);
384            }
385            let Some(left) = deadline.checked_duration_since(Instant::now()) else {
386                // Withdrawn under the lock the producer publishes under,
387                // so it is either on the ring (checked above) or never
388                // will be.
389                state.request = None;
390                self.pending.store(false, Ordering::Release);
391                self.changed.notify_all();
392                return RequestOutcome::Refused(Refused::RingFull);
393            };
394            state = self.wait(state, left);
395        }
396    }
397
398    fn wait<'a>(&self, state: MutexGuard<'a, State>, left: Duration) -> MutexGuard<'a, State> {
399        self.changed
400            .wait_timeout(state, left)
401            .unwrap_or_else(|poisoned| poisoned.into_inner())
402            .0
403    }
404
405    /// Claim the producer's side for a primary tenure: the policy to
406    /// request as it starts, and the right to attach its producer. Called
407    /// once, by the primary's main thread, before it journals that policy.
408    pub(crate) fn claim(self: &Arc<Self>) -> PublisherClaim {
409        PublisherClaim {
410            control: Arc::clone(self),
411            policy: self.requested(),
412        }
413    }
414}
415
416/// The policy a primary requests as it starts, and, once its boot entry
417/// is decided (`PublisherClaim::journal_boot`), the token its input ring's
418/// producer attaches with. Used by the main thread while it still owns
419/// the ring's producer.
420///
421/// The tenure's two steps are a typestate: `journal_boot` consumes the
422/// claim and returns the [`BootedClaim`] that alone can attach, so no
423/// primary path, on either transport, can attach a producer without
424/// having journaled its boot entry (or decided the lineage already holds
425/// it), nor journal it twice or after attaching.
426pub struct PublisherClaim {
427    control: Arc<AckPolicyControl>,
428    policy: AckPolicy,
429}
430
431impl PublisherClaim {
432    /// The policy to journal as the primary starts.
433    pub(crate) fn policy(&self) -> AckPolicy {
434        self.policy
435    }
436
437    /// Journal the policy this tenure requests when [`boot_entry`] says
438    /// the lineage needs it — see [`publish_staged`]. After a promotion's
439    /// epoch bump, ahead of `on_primary` and of any client. Returns the
440    /// claim its producer attaches with.
441    ///
442    /// Whether this node replicates is read from `halt`, the gate the
443    /// entry is journaled through, so the decision and the entry's
444    /// witnessing agree on "standalone?".
445    #[must_use = "the booted claim is what attaches the producer"]
446    pub(crate) fn journal_boot<E: AppEvent>(
447        self,
448        lineage: Option<LineagePolicy>,
449        producer: &mut ring::Producer<InputSlot<E>>,
450        halt: &HaltGate,
451    ) -> BootedClaim {
452        if let Some(reason) = boot_entry(lineage, self.policy, halt.replication()) {
453            let journaled = publish_staged(producer, halt, self.policy);
454            info!(
455                policy = %self.policy,
456                unwitnessed = journaled.unwitnessed(),
457                input_seq = journaled.input_seq(),
458                lineage = ?lineage.map(|p| p.policy().0),
459                reason = ?reason,
460                "journaling the requested ack policy"
461            );
462        }
463        BootedClaim {
464            control: self.control,
465            policy: self.policy,
466        }
467    }
468}
469
470/// A [`PublisherClaim`] whose boot entry is decided: the token the input
471/// ring's producer attaches with (`BootedClaim::attach`).
472pub struct BootedClaim {
473    control: Arc<AckPolicyControl>,
474    /// The policy the boot entry journaled, or the lineage already held.
475    policy: AckPolicy,
476}
477
478impl BootedClaim {
479    /// Attach the input ring's producer. `wake` is the handle that brings
480    /// a parked producer back round its loop (the io_uring reader's
481    /// eventfd), `None` for one that never parks (the DPDK poll thread).
482    ///
483    /// Called by the main thread before it hands `producer` over to the
484    /// producer's thread: a policy staged since the claim (while the
485    /// primary started) is journaled here, on `producer`, so it lands
486    /// ahead of every runtime request the attached producer publishes. One
487    /// equal to the claim's own was journaled by the boot entry, or is the
488    /// lineage's already.
489    pub(crate) fn attach<E: AppEvent>(
490        self,
491        producer: &mut ring::Producer<InputSlot<E>>,
492        halt: &HaltGate,
493        wake: Option<Arc<OwnedFd>>,
494    ) -> AckPolicyPublisher {
495        let control = self.control;
496        let staged = {
497            let mut state = control.lock();
498            state.producer = Producer::Attached { wake };
499            state.requested
500        };
501        // Outside the lock: a full ring blocks this publish, and the admin
502        // thread must not block on the lock behind it. Nothing else can
503        // publish meanwhile: a runtime request waits for this thread's
504        // first poll.
505        if staged != self.policy {
506            log_journaled(publish_staged(producer, halt, staged), Origin::Staged);
507        }
508        AckPolicyPublisher { control }
509    }
510}
511
512/// The input ring producer's end of the mailbox: journals each request.
513/// Detaches when dropped, so requests made after the producer stopped are
514/// refused rather than left waiting.
515pub struct AckPolicyPublisher {
516    control: Arc<AckPolicyControl>,
517}
518
519impl AckPolicyPublisher {
520    /// Journal the waiting request, if any. One acquire load when none
521    /// waits. On a full ring the request stays, for the next call.
522    ///
523    /// `halt` is the producer's own halt gate, read only when a request
524    /// waits: who is streaming decides whether the entry is unwitnessed
525    /// ([`is_unwitnessed`]), and the journaled change then lifts or
526    /// restores the halt ([`HaltGate::journal`]). Taking the gate
527    /// rather than a reading of it is what keeps the two in step: the
528    /// producer cannot journal a forced change without lifting the halt.
529    #[inline]
530    pub(crate) fn poll<E: AppEvent>(
531        &mut self,
532        producer: &mut ring::Producer<InputSlot<E>>,
533        halt: &HaltGate,
534    ) {
535        if self.control.pending.load(Ordering::Acquire) {
536            self.publish(producer, halt);
537        }
538    }
539
540    #[cold]
541    fn publish<E: AppEvent>(
542        &mut self,
543        producer: &mut ring::Producer<InputSlot<E>>,
544        halt: &HaltGate,
545    ) {
546        let control = &*self.control;
547        let mut state = control.lock();
548        let Some(request) = state.request else {
549            // Withdrawn since the flag was read.
550            return;
551        };
552        // Recorded against the halt as it is published, before the
553        // requester is released, so a write it sends once told `OK` meets
554        // the halt the change leaves.
555        let Ok(journaled) = halt.journal(request.policy, Origin::Runtime, |change| {
556            producer.try_publish(change_slot(change))
557        }) else {
558            // Full: the requester's bounded wait decides when to give up.
559            return;
560        };
561        state.request = None;
562        state.published = request.ticket;
563        state.requested = request.policy;
564        control.pending.store(false, Ordering::Release);
565        control.changed.notify_all();
566        drop(state);
567        log_journaled(journaled, Origin::Runtime);
568    }
569}
570
571impl Drop for AckPolicyPublisher {
572    fn drop(&mut self) {
573        let mut state = self.control.lock();
574        state.producer = Producer::Detached;
575        // A request still waiting is refused by its requester, who sees it
576        // gone unpublished.
577        state.request = None;
578        self.control.pending.store(false, Ordering::Release);
579        self.control.changed.notify_all();
580    }
581}
582
583/// Mailbox fixtures for the crate's tests.
584#[cfg(test)]
585pub(crate) mod test_support {
586    use super::*;
587    use melin_journal::AckPolicyByte;
588    use melin_pipeline::ring::{Consumer, DisruptorBuilder};
589    use melin_pipeline::wait::WaitStrategy;
590    use melin_transport_core::WireSeq;
591
592    /// A replicating node's mailbox whose requests wait at most
593    /// `publish_wait`.
594    pub(crate) fn control_with_publish_wait(
595        configured: AckPolicy,
596        publish_wait: Duration,
597    ) -> Arc<AckPolicyControl> {
598        AckPolicyControl::with_publish_wait(configured, Replication::Enabled, publish_wait)
599    }
600
601    /// A replicating node's mailbox: every policy can be requested.
602    pub(crate) fn replicated(configured: AckPolicy) -> Arc<AckPolicyControl> {
603        AckPolicyControl::new(configured, Replication::Enabled)
604    }
605
606    /// An input ring of `capacity` slots with its one consumer.
607    pub(crate) fn ring<E: AppEvent>(
608        capacity: usize,
609    ) -> (ring::Producer<InputSlot<E>>, Consumer<InputSlot<E>>) {
610        let (producer, mut consumers) = DisruptorBuilder::<InputSlot<E>>::new(capacity)
611            .add_consumer()
612            .build(WaitStrategy::SpinThenYield);
613        (producer, consumers.pop().expect("one consumer"))
614    }
615
616    /// A lineage policy: `policy` in force since the transition at
617    /// `wire_seq` from `previous`.
618    pub(crate) fn lineage(
619        policy: AckPolicyByte,
620        unwitnessed: bool,
621        wire_seq: u64,
622        previous: Option<AckPolicyByte>,
623    ) -> Option<LineagePolicy> {
624        Some(LineagePolicy {
625            change: AckPolicyChange {
626                policy,
627                unwitnessed,
628            },
629            wire_seq: WireSeq::new(wire_seq),
630            previous,
631        })
632    }
633
634    /// A lineage whose only change, its first entry, put `policy` in
635    /// force: nothing waits for a witness.
636    pub(crate) fn first_change(policy: AckPolicy) -> Option<LineagePolicy> {
637        lineage(policy.to_byte(), false, 1, None)
638    }
639
640    /// A claim on `control` booted on a lineage that already holds its
641    /// policy, so it journals nothing: for a test that only needs a
642    /// producer to attach.
643    pub(crate) fn booted(control: &Arc<AckPolicyControl>) -> BootedClaim {
644        let claim = control.claim();
645        let lineage = first_change(claim.policy());
646        // The smallest ring there is: nothing is published on it.
647        let (mut producer, _consumer) = ring::<counter_server::CounterEvent>(2);
648        claim.journal_boot(
649            lineage,
650            &mut producer,
651            &crate::halt::test_support::gate_over(None),
652        )
653    }
654
655    /// Attach a claim on `control` to `producer`, under a standalone halt
656    /// gate: for a test that only needs a producer attached.
657    pub(crate) fn attach<E: AppEvent>(
658        control: &Arc<AckPolicyControl>,
659        producer: &mut ring::Producer<InputSlot<E>>,
660    ) -> AckPolicyPublisher {
661        booted(control).attach(producer, &crate::halt::test_support::gate_over(None), None)
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use super::test_support::{
668        attach, booted, control_with_publish_wait, first_change, lineage, replicated, ring,
669    };
670    use super::*;
671    use crate::halt::Verdict;
672    use crate::halt::test_support::{gate_over, presence};
673    use counter_server::CounterEvent as TestEvent;
674    use melin_pipeline::ring::Consumer;
675    use melin_transport_core::WireSeq;
676    use melin_transport_core::replica_presence::Lowered;
677
678    const ALL: [AckPolicy; 4] = [
679        AckPolicy::Disk,
680        AckPolicy::Ram,
681        AckPolicy::DiskAndRam,
682        AckPolicy::TwoDisks,
683    ];
684
685    type Slot = InputSlot<TestEvent>;
686
687    /// Every change on the ring, in order.
688    fn changes(consumer: &mut Consumer<Slot>) -> Vec<AckPolicyChange> {
689        let mut out = Vec::new();
690        while let Some((_, slot)) = consumer.try_consume() {
691            match slot.event {
692                JournalEvent::AckPolicyChange(change) => {
693                    assert_eq!(
694                        (slot.connection_id, slot.key_hash, slot.sequence),
695                        (0, 0, 0),
696                        "a node event"
697                    );
698                    out.push(change);
699                }
700                other => panic!("not a policy change: {other:?}"),
701            }
702        }
703        out
704    }
705
706    fn change(policy: AckPolicy, unwitnessed: bool) -> AckPolicyChange {
707        AckPolicyChange {
708            policy: policy.to_byte(),
709            unwitnessed,
710        }
711    }
712
713    /// Only an explicit runtime `disk` with no replica connected is
714    /// forced; with replication disabled every change is unwitnessed; with
715    /// a replica connected none is.
716    #[test]
717    fn the_forced_rule() {
718        let none_connected = presence(0).observe();
719        let connected = presence(1).observe();
720        for policy in ALL {
721            for origin in [Origin::Runtime, Origin::Staged] {
722                assert!(is_unwitnessed(policy, origin, None));
723                assert!(!is_unwitnessed(policy, origin, Some(&connected)));
724                assert_eq!(
725                    is_unwitnessed(policy, origin, Some(&none_connected)),
726                    origin == Origin::Runtime && policy == AckPolicy::Disk,
727                    "{policy} from {origin:?}"
728                );
729            }
730        }
731    }
732
733    /// The halt gate of a replicated primary with no replica connected:
734    /// halted, and a runtime `disk` journaled through it is forced.
735    fn halted_gate() -> HaltGate {
736        gate_over(Some(presence(0)))
737    }
738
739    /// The boot entry journaled when the lineage differs: the requested
740    /// policy, as the node's own event, witnessed unless replication is
741    /// disabled (a boot entry is never forced, so a `disk` lifts no halt).
742    /// A replicating node whose lineage already holds the requested policy
743    /// journals none; the standalone exception (a waiting change) is pinned
744    /// by `the_boot_decision`.
745    #[test]
746    fn the_boot_entry_when_the_lineage_differs() {
747        let disk = first_change(AckPolicy::Disk);
748        let (mut producer, mut consumer) = ring::<TestEvent>(8);
749        let halt = halted_gate();
750        let boot = |requested, lineage, producer: &mut ring::Producer<Slot>, halt: &HaltGate| {
751            drop(
752                replicated(requested)
753                    .claim()
754                    .journal_boot(lineage, producer, halt),
755            );
756        };
757
758        boot(AckPolicy::Disk, disk, &mut producer, &halt);
759        assert!(changes(&mut consumer).is_empty(), "nothing to change");
760        boot(AckPolicy::DiskAndRam, disk, &mut producer, &halt);
761        assert_eq!(
762            changes(&mut consumer),
763            [change(AckPolicy::DiskAndRam, false)]
764        );
765        boot(AckPolicy::Disk, None, &mut producer, &halt);
766        assert_eq!(
767            changes(&mut consumer),
768            [change(AckPolicy::Disk, false)],
769            "a lineage holding none gets one, never forced"
770        );
771        assert_ne!(halt.verdict(), Verdict::Take, "a boot `disk` is no consent");
772
773        let standalone = gate_over(None);
774        boot(
775            AckPolicy::Disk,
776            first_change(AckPolicy::DiskAndRam),
777            &mut producer,
778            &standalone,
779        );
780        assert_eq!(
781            changes(&mut consumer),
782            [change(AckPolicy::Disk, true)],
783            "standalone: unwitnessed"
784        );
785    }
786
787    /// The decision alone: a lineage naming another policy, or none, always
788    /// gets the requested one. One naming it gets nothing while
789    /// replicating (the gate re-applies a waiting change in order); with
790    /// replication disabled it gets an entry when the transition to it was
791    /// journaled witnessed (it may never have been witnessed: the
792    /// conservative reading), and none when that transition is already
793    /// marked unwitnessed, or is the lineage's first change.
794    #[test]
795    fn the_boot_decision() {
796        use Replication::{Disabled, Enabled};
797        let disk = AckPolicy::Disk.to_byte();
798        let disk_and_ram = Some(AckPolicy::DiskAndRam.to_byte());
799        let waiting = lineage(disk, false, 3, disk_and_ram);
800        let marked = lineage(disk, true, 3, disk_and_ram);
801        for replication in [Enabled, Disabled] {
802            for lineage in [None, first_change(AckPolicy::DiskAndRam), waiting, marked] {
803                assert_eq!(
804                    boot_entry(lineage, AckPolicy::Ram, replication),
805                    Some(BootEntry::Differs),
806                    "{lineage:?} under {replication:?}"
807                );
808            }
809            assert_eq!(
810                boot_entry(first_change(AckPolicy::Disk), AckPolicy::Disk, replication),
811                None,
812                "the first change under {replication:?}"
813            );
814            assert_eq!(
815                boot_entry(marked, AckPolicy::Disk, replication),
816                None,
817                "already unwitnessed under {replication:?}"
818            );
819        }
820        assert_eq!(
821            boot_entry(waiting, AckPolicy::Disk, Enabled),
822            None,
823            "replicated: the gate re-applies it"
824        );
825        assert_eq!(
826            boot_entry(waiting, AckPolicy::Disk, Disabled),
827            Some(BootEntry::TakesEffectUnwitnessed)
828        );
829        // A repeat of the policy keeps the transition, witnessed unless one
830        // of the run was not: the fold the restart reads.
831        let repeated =
832            LineagePolicy::after(waiting, WireSeq::new(4), change(AckPolicy::Disk, false));
833        assert_eq!(
834            boot_entry(Some(repeated), AckPolicy::Disk, Disabled),
835            Some(BootEntry::TakesEffectUnwitnessed)
836        );
837    }
838
839    /// Standalone, on a lineage whose newest change is still waiting for
840    /// its witness under the requested policy, the boot journals that
841    /// policy unwitnessed; folded into the lineage, it marks the
842    /// transition, so the next standalone boot journals nothing.
843    #[test]
844    fn a_standalone_boot_marks_a_waiting_change_unwitnessed_once() {
845        let (mut producer, mut consumer) = ring::<TestEvent>(8);
846        let standalone = gate_over(None);
847        let waiting = lineage(
848            AckPolicy::Disk.to_byte(),
849            false,
850            3,
851            Some(AckPolicy::DiskAndRam.to_byte()),
852        );
853        drop(
854            AckPolicyControl::new(AckPolicy::Disk, Replication::Disabled)
855                .claim()
856                .journal_boot(waiting, &mut producer, &standalone),
857        );
858        let journaled = changes(&mut consumer);
859        assert_eq!(journaled, [change(AckPolicy::Disk, true)]);
860
861        let folded = LineagePolicy::after(waiting, WireSeq::new(5), journaled[0]);
862        assert_eq!(
863            (folded.wire_seq, folded.change.unwitnessed),
864            (WireSeq::new(3), true),
865            "the same transition, now marked"
866        );
867        drop(
868            AckPolicyControl::new(AckPolicy::Disk, Replication::Disabled)
869                .claim()
870                .journal_boot(Some(folded), &mut producer, &standalone),
871        );
872        assert!(changes(&mut consumer).is_empty(), "nothing more");
873
874        // Replicated, the same waiting lineage journals nothing.
875        drop(replicated(AckPolicy::Disk).claim().journal_boot(
876            waiting,
877            &mut producer,
878            &halted_gate(),
879        ));
880        assert!(changes(&mut consumer).is_empty(), "the gate re-applies it");
881    }
882
883    /// A boot entry for a policy that needs a replica restores a halt an
884    /// earlier forced swap lifted, as a runtime one does.
885    #[test]
886    fn a_boot_entry_that_needs_a_replica_restores_the_halt() {
887        let control = replicated(AckPolicy::Disk);
888        let (mut producer, mut consumer) = ring::<TestEvent>(8);
889        let halt = halted_gate();
890        let mut publisher = booted(&control).attach(&mut producer, &halt, None);
891        let outcome = request_while_polling(
892            &control,
893            &mut publisher,
894            &mut producer,
895            AckPolicy::Disk,
896            &halt,
897        );
898        assert_eq!(outcome, RequestOutcome::Published);
899        assert_eq!(halt.verdict(), Verdict::Take, "forced: lifted");
900        drop(publisher);
901
902        drop(replicated(AckPolicy::TwoDisks).claim().journal_boot(
903            first_change(AckPolicy::Disk),
904            &mut producer,
905            &halt,
906        ));
907        assert_ne!(halt.verdict(), Verdict::Take, "restored");
908        assert_eq!(
909            changes(&mut consumer),
910            [
911                change(AckPolicy::Disk, true),
912                change(AckPolicy::TwoDisks, false)
913            ]
914        );
915    }
916
917    /// With no producer attached a request is staged: the next claim
918    /// requests it, and so does auto-promotion's fallback.
919    #[test]
920    fn a_request_with_no_producer_is_staged_for_the_next_claim() {
921        let control = replicated(AckPolicy::DiskAndRam);
922        assert_eq!(control.claim().policy(), AckPolicy::DiskAndRam);
923        assert_eq!(control.request(AckPolicy::Ram), RequestOutcome::Staged);
924        assert_eq!(control.requested(), AckPolicy::Ram);
925        assert_eq!(control.claim().policy(), AckPolicy::Ram);
926    }
927
928    /// On a node that accepts no replica, a policy that needs one is
929    /// refused whether a producer runs or not: nothing is staged, nothing
930    /// is put on the ring, the requested policy is unchanged and no
931    /// request is left waiting. `disk` is still taken, staged or
932    /// journaled (unwitnessed: nothing can witness it).
933    #[test]
934    fn a_node_without_replication_refuses_a_policy_that_needs_a_replica() {
935        const REFUSED: RequestOutcome = RequestOutcome::Refused(Refused::NoReplicaCanAttach);
936        let control = AckPolicyControl::new(AckPolicy::Disk, Replication::Disabled);
937        for policy in [AckPolicy::Ram, AckPolicy::DiskAndRam, AckPolicy::TwoDisks] {
938            assert_eq!(control.request(policy), REFUSED, "staging {policy}");
939        }
940        assert_eq!(control.requested(), AckPolicy::Disk, "nothing staged");
941        assert_eq!(control.request(AckPolicy::Disk), RequestOutcome::Staged);
942
943        let (mut producer, mut consumer) = ring::<TestEvent>(8);
944        let standalone = gate_over(None);
945        let mut publisher = booted(&control).attach(&mut producer, &standalone, None);
946        for policy in [AckPolicy::Ram, AckPolicy::DiskAndRam, AckPolicy::TwoDisks] {
947            let outcome =
948                request_while_polling(&control, &mut publisher, &mut producer, policy, &standalone);
949            assert_eq!(outcome, REFUSED, "journaling {policy}");
950            assert!(!control.pending.load(Ordering::Acquire), "nothing waits");
951        }
952        assert!(changes(&mut consumer).is_empty(), "nothing journaled");
953        assert_eq!(control.requested(), AckPolicy::Disk);
954        let outcome = request_while_polling(
955            &control,
956            &mut publisher,
957            &mut producer,
958            AckPolicy::Disk,
959            &standalone,
960        );
961        assert_eq!(outcome, RequestOutcome::Published);
962        assert_eq!(changes(&mut consumer), [change(AckPolicy::Disk, true)]);
963    }
964
965    /// Run `request` on a helper thread while this one plays the producer:
966    /// polling `publisher` on `producer` until the request returns.
967    fn request_while_polling(
968        control: &Arc<AckPolicyControl>,
969        publisher: &mut AckPolicyPublisher,
970        producer: &mut ring::Producer<Slot>,
971        policy: AckPolicy,
972        halt: &HaltGate,
973    ) -> RequestOutcome {
974        std::thread::scope(|s| {
975            let requesting = s.spawn(|| control.request(policy));
976            while !requesting.is_finished() {
977                publisher.poll(producer, halt);
978                std::thread::yield_now();
979            }
980            requesting.join().expect("the request panicked")
981        })
982    }
983
984    /// An attached producer journals each request, in order, duplicates
985    /// included, and the requester returns once the entry is on the ring.
986    /// The forced decision is the producer's, at publish time. (The halt a
987    /// forced swap lifts is the gate's: `halt.rs`.)
988    #[test]
989    fn an_attached_producer_journals_every_request() {
990        let control = replicated(AckPolicy::DiskAndRam);
991        let (mut producer, mut consumer) = ring(8);
992        let replicas = presence(1);
993        let halt = gate_over(Some(Arc::clone(&replicas)));
994        let mut publisher = booted(&control).attach(&mut producer, &halt, None);
995        let mut journal = |policy| {
996            let outcome =
997                request_while_polling(&control, &mut publisher, &mut producer, policy, &halt);
998            assert_eq!(outcome, RequestOutcome::Published);
999        };
1000
1001        journal(AckPolicy::Disk);
1002        journal(AckPolicy::Disk);
1003        assert_eq!(replicas.lower(), Lowered::LastHalted);
1004        journal(AckPolicy::Disk);
1005        journal(AckPolicy::TwoDisks);
1006        assert_eq!(
1007            changes(&mut consumer),
1008            [
1009                change(AckPolicy::Disk, false),
1010                change(AckPolicy::Disk, false),
1011                change(AckPolicy::Disk, true),
1012                change(AckPolicy::TwoDisks, false),
1013            ]
1014        );
1015        assert_eq!(control.claim().policy(), AckPolicy::TwoDisks);
1016    }
1017
1018    /// Nothing waits: polling publishes nothing and leaves the halt alone.
1019    #[test]
1020    fn polling_with_nothing_requested_publishes_nothing() {
1021        let control = replicated(AckPolicy::Disk);
1022        let (mut producer, mut consumer) = ring(8);
1023        let halt = halted_gate();
1024        let mut publisher = booted(&control).attach(&mut producer, &halt, None);
1025        publisher.poll(&mut producer, &halt);
1026        assert!(changes(&mut consumer).is_empty());
1027        assert_ne!(halt.verdict(), Verdict::Take);
1028    }
1029
1030    /// A ring that stays full past the bound refuses the request, which
1031    /// is then withdrawn: it is not published once the ring drains, and
1032    /// the requested policy is unchanged.
1033    #[test]
1034    fn a_full_ring_refuses_the_request_and_withdraws_it() {
1035        let control = control_with_publish_wait(AckPolicy::DiskAndRam, Duration::from_millis(50));
1036        let (mut producer, mut consumer) = ring(2);
1037        for _ in 0..2 {
1038            producer
1039                .try_publish(change_slot(change(AckPolicy::Ram, false)))
1040                .expect("room");
1041        }
1042        // A `disk` that would be forced: refused, it lifts nothing.
1043        let halt = halted_gate();
1044        let mut publisher = booted(&control).attach(&mut producer, &halt, None);
1045        let outcome = request_while_polling(
1046            &control,
1047            &mut publisher,
1048            &mut producer,
1049            AckPolicy::Disk,
1050            &halt,
1051        );
1052        assert_eq!(outcome, RequestOutcome::Refused(Refused::RingFull));
1053        assert_eq!(changes(&mut consumer).len(), 2, "only what filled the ring");
1054        publisher.poll(&mut producer, &halt);
1055        assert!(changes(&mut consumer).is_empty(), "withdrawn");
1056        assert_eq!(control.requested(), AckPolicy::DiskAndRam);
1057        assert_ne!(halt.verdict(), Verdict::Take, "still halted");
1058    }
1059
1060    /// A producer that stops (shutdown) refuses the request waiting on it,
1061    /// without the full bound, and later requests are staged.
1062    #[test]
1063    fn a_detached_producer_refuses_the_waiting_request() {
1064        let control = control_with_publish_wait(AckPolicy::Disk, Duration::from_secs(60));
1065        let (mut producer, _consumer) = ring::<TestEvent>(8);
1066        let publisher = attach(&control, &mut producer);
1067        let started = Instant::now();
1068        let outcome = std::thread::scope(|s| {
1069            let requesting = s.spawn(|| control.request(AckPolicy::Ram));
1070            while !control.pending.load(Ordering::Acquire) {
1071                std::thread::yield_now();
1072            }
1073            drop(publisher);
1074            requesting.join().expect("the request panicked")
1075        });
1076        assert_eq!(outcome, RequestOutcome::Refused(Refused::ProducerStopped));
1077        assert!(started.elapsed() < Duration::from_secs(30), "not the bound");
1078        assert!(!control.pending.load(Ordering::Acquire));
1079        assert_eq!(control.request(AckPolicy::Ram), RequestOutcome::Staged);
1080    }
1081
1082    /// A producer replaced while a request waits (a tenure ending, the
1083    /// next one attaching) refuses it as stopped: the request went with
1084    /// the old producer, unpublished, and the new one never sees it.
1085    #[test]
1086    fn a_producer_replaced_while_a_request_waits_refuses_it() {
1087        let control = control_with_publish_wait(AckPolicy::Disk, Duration::from_secs(60));
1088        let (mut producer, mut consumer) = ring::<TestEvent>(8);
1089        let publisher = attach(&control, &mut producer);
1090        let started = Instant::now();
1091        let outcome = std::thread::scope(|s| {
1092            let requesting = s.spawn(|| control.request(AckPolicy::Ram));
1093            while !control.pending.load(Ordering::Acquire) {
1094                std::thread::yield_now();
1095            }
1096            drop(publisher);
1097            let mut next = attach(&control, &mut producer);
1098            let outcome = requesting.join().expect("the request panicked");
1099            next.poll(&mut producer, &gate_over(None));
1100            outcome
1101        });
1102        assert_eq!(outcome, RequestOutcome::Refused(Refused::ProducerStopped));
1103        assert!(started.elapsed() < Duration::from_secs(30), "not the bound");
1104        assert!(changes(&mut consumer).is_empty(), "never published");
1105    }
1106
1107    /// A second request while one waits is refused, not merged into it,
1108    /// and the first is still published.
1109    #[test]
1110    fn a_second_waiting_request_is_refused() {
1111        let control = control_with_publish_wait(AckPolicy::DiskAndRam, Duration::from_secs(60));
1112        let (mut producer, mut consumer) = ring(8);
1113        let halt = halted_gate();
1114        let mut publisher = booted(&control).attach(&mut producer, &halt, None);
1115        std::thread::scope(|s| {
1116            let first = s.spawn(|| control.request(AckPolicy::TwoDisks));
1117            while !control.pending.load(Ordering::Acquire) {
1118                std::thread::yield_now();
1119            }
1120            assert_eq!(
1121                control.request(AckPolicy::Disk),
1122                RequestOutcome::Refused(Refused::AnotherWaiting)
1123            );
1124            publisher.poll(&mut producer, &halt);
1125            assert_eq!(first.join().expect("panicked"), RequestOutcome::Published);
1126        });
1127        assert_eq!(changes(&mut consumer), [change(AckPolicy::TwoDisks, false)]);
1128    }
1129
1130    /// A request staged during bring-up (between the claim and the attach,
1131    /// a primary still starting) is journaled as the producer attaches,
1132    /// after the boot entry and ahead of every runtime request, and never
1133    /// forced, so it lifts no halt; a runtime `disk` after it is forced.
1134    #[test]
1135    fn a_request_staged_during_bring_up_is_journaled_at_attach_in_order() {
1136        let control = replicated(AckPolicy::DiskAndRam);
1137        assert_eq!(control.request(AckPolicy::TwoDisks), RequestOutcome::Staged);
1138        let claim = control.claim();
1139        let (mut producer, mut consumer) = ring(8);
1140        let halt = halted_gate();
1141        let booted = claim.journal_boot(first_change(AckPolicy::DiskAndRam), &mut producer, &halt);
1142        assert_eq!(control.request(AckPolicy::Disk), RequestOutcome::Staged);
1143        let mut publisher = booted.attach(&mut producer, &halt, None);
1144        assert_ne!(
1145            halt.verdict(),
1146            Verdict::Take,
1147            "a staged `disk` is no consent"
1148        );
1149        let outcome = request_while_polling(
1150            &control,
1151            &mut publisher,
1152            &mut producer,
1153            AckPolicy::Disk,
1154            &halt,
1155        );
1156        assert_eq!(outcome, RequestOutcome::Published);
1157        assert_eq!(
1158            changes(&mut consumer),
1159            [
1160                change(AckPolicy::TwoDisks, false),
1161                change(AckPolicy::Disk, false),
1162                change(AckPolicy::Disk, true),
1163            ]
1164        );
1165        assert_eq!(halt.verdict(), Verdict::Take, "the forced swap came last");
1166    }
1167
1168    /// A policy staged during bring-up that ends up equal to the claim's
1169    /// own is not journaled again at attach: the boot entry (or the
1170    /// lineage) already holds it.
1171    #[test]
1172    fn a_staged_policy_equal_to_the_claims_is_not_journaled_at_attach() {
1173        let control = replicated(AckPolicy::DiskAndRam);
1174        let claim = control.claim();
1175        assert_eq!(control.request(AckPolicy::TwoDisks), RequestOutcome::Staged);
1176        assert_eq!(
1177            control.request(AckPolicy::DiskAndRam),
1178            RequestOutcome::Staged
1179        );
1180        let (mut producer, mut consumer) = ring::<TestEvent>(8);
1181        let halt = halted_gate();
1182        let _publisher = claim
1183            .journal_boot(first_change(AckPolicy::DiskAndRam), &mut producer, &halt)
1184            .attach(&mut producer, &halt, None);
1185        assert!(changes(&mut consumer).is_empty());
1186    }
1187
1188    /// A parked producer is woken through its handle when a request is
1189    /// posted.
1190    #[test]
1191    fn a_request_wakes_a_parked_producer() {
1192        use std::os::fd::{AsRawFd, FromRawFd};
1193        // SAFETY: a fresh nonblocking eventfd, owned at once.
1194        let fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK) };
1195        assert!(fd >= 0);
1196        // SAFETY: `fd` is fresh and owned by nothing else.
1197        let fd = Arc::new(unsafe { OwnedFd::from_raw_fd(fd) });
1198        let control = control_with_publish_wait(AckPolicy::DiskAndRam, Duration::from_millis(10));
1199        let (mut producer, _consumer) = ring::<TestEvent>(8);
1200        let _publisher =
1201            booted(&control).attach(&mut producer, &gate_over(None), Some(Arc::clone(&fd)));
1202        assert_eq!(
1203            control.request(AckPolicy::Disk),
1204            RequestOutcome::Refused(Refused::RingFull),
1205            "never polled, so the bound runs out"
1206        );
1207        let mut count = 0u64;
1208        // SAFETY: reads 8 bytes into a live `u64`.
1209        let read = unsafe {
1210            libc::read(
1211                fd.as_raw_fd(),
1212                (&mut count as *mut u64).cast(),
1213                std::mem::size_of::<u64>(),
1214            )
1215        };
1216        assert_eq!((read, count), (8, 1), "the eventfd was written once");
1217    }
1218}