Skip to main content

epics_libcom_rs/runtime/readiness/
mod.rs

1//! Socket readiness for the targets with no mio selector — a poll thread, an
2//! armed set, and an `AsyncRead`/`AsyncWrite` adapter over a non-blocking fd.
3//!
4//! # Why this exists at all
5//!
6//! `mio` has no selector for either embedded triple. Measured against
7//! mio 1.2.3 with `-Zbuild-std`: `armv7-rtems-eabihf` and `x86_64-wrs-vxworks`
8//! both fail at `error[E0583]: file not found for module 'selector'`, because
9//! `mio/src/sys/unix/mod.rs` maps `target_os` to a selector file and neither
10//! target appears in any of its lists. Forcing mio's own escape hatches
11//! (`mio_unsupported_force_poll_poll`, `mio_unsupported_force_waker_pipe`)
12//! gets the two down to 8 and 9 errors respectively, all of them missing
13//! `libc` items (`accept4`, `SOCK_NONBLOCK`, `POLLRDNORM`, `sin_len`) — so the
14//! path through mio is a set of PRs across three upstream crates, not a build
15//! flag. Until those land this module is the readiness layer.
16//!
17//! It is deliberately **not** a mio clone. It carries exactly what a PVA
18//! server connection needs: level-triggered read/write readiness on sockets,
19//! one waker per direction, and nothing else — no edge triggering, no
20//! `Interest` bitsets, no cross-thread registration of foreign event sources.
21//!
22//! # The armed set is owned by the poll thread, and nothing else
23//!
24//! > **MUST** an armed interest is one-shot: the poll thread removes
25//! > `(fd, direction)` from the armed set *before* it wakes that direction's
26//! > waker, and only the task that then observed `WouldBlock` may arm it
27//! > again.
28//! > **MUST NOT** any path remove an fd from the armed set other than
29//! > `StreamInner`'s `Drop` and the one-shot removal above — and no fd may
30//! > be closed while the kernel is still watching it, which is why the poll
31//! > thread is the one that closes it.
32//!
33//! The first half is what keeps the poll thread from spinning: a
34//! level-triggered backend reports a readable socket on every wait until the
35//! bytes are consumed, so an interest that survived its own wake-up would
36//! re-report immediately and burn the CPU that a reactor exists to save. The
37//! second half is what keeps it correct: an fd left in a `select` set past
38//! `close(2)` is `EBADF` from the next call — at entry on Linux, on every
39//! rescan under the BSD `kern_select` libbsd carries — and a number the kernel
40//! may since have handed to a different socket. So dropping the last handle to
41//! a stream does not close its socket. It hands the socket to the poll thread,
42//! which closes it in the same `wait` that takes the fd out of the kernel's
43//! sets — after the removal and before it blocks — and a socket the kernel was
44//! never told about is closed on the spot, there being nothing to remove
45//! first. The order holds by construction rather than by convention: nothing
46//! else holds the socket once the last handle is gone, and the delete and the
47//! close travel in one changelist.
48//!
49//! One-shot arming is also what makes registering *after* a `WouldBlock` race-
50//! free. Between the failed read and the arm, the peer may have sent the byte
51//! that would have woken us; a level-triggered backend still reports the
52//! socket ready on the very next wait, so the wake-up cannot be lost. An
53//! edge-triggered backend would need the arm to precede the read attempt, and
54//! that is the ordering this module does not use. `EV_CLEAR` is therefore
55//! banned for socket interests in the kqueue backend; the one place it appears
56//! there is the `EVFILT_USER` wake, which carries no readiness to lose.
57//!
58//! # Waiting is (changes in, ready set out)
59//!
60//! This is libevent's `eventop` split — `add` and `del` accumulate, `dispatch`
61//! submits — which is what pvxs runs under on every platform it supports, and
62//! what this module now follows. `Backend::wait` takes the changes since the
63//! last call rather than the armed set, so a registration placed once stays in
64//! the kernel until this module deletes it and a wait with nothing to change
65//! submits an empty changelist. The cost of a wait is then the number of
66//! *transitions*, where the whole-set resubmission it replaces charged the
67//! number of armed fds on every call whether or not any of them had moved. On
68//! `armv7-rtems-eabihf` that is worth 3.7x to `kqueue` at 112 held
69//! connections and nothing at all to `select`, which re-registers every
70//! watched fd inside the kernel on every call because `select(2)` has nowhere
71//! to remember one between calls — a cost no changelist can reach. The kqueue
72//! backend's doc carries the table.
73//!
74//! The one-shot rule above is what produces most of those transitions:
75//! delivering `(fd, direction)` queues that direction's delete, and the task's
76//! re-arm after its next `WouldBlock` queues the add. Two changes per
77//! readiness event, and on a busy connection the only two.
78//!
79//! `Change`es accumulate under the same lock as the armed set, so the kernel
80//! hears about a transition in the order the armed set recorded it. That is
81//! what makes the awkward case — an fd closed and handed straight back out to
82//! the next connection — fall out of the design rather than need handling, and
83//! it is why both of libevent's `changelist` rules come along:
84//!
85//! - An add over a pending delete stays an add and never collapses to a
86//!   no-op, because that delete may name an fd that has since closed and come
87//!   back as a different socket under the same number.
88//! - A delete of an interest the kernel was never told about *is* a no-op.
89//!   `Direction::registered` is what tells the two apart, and dropping the
90//!   pair is what keeps kqueue from reporting a spurious event for an
91//!   add-then-delete that never left this process.
92//!
93//! A delete that misses in the kernel is ordinary rather than exceptional on
94//! `kqueue`: that backend closes the socket before the `kevent` call carrying
95//! the delete, because the same call also blocks and a close after it would
96//! wait for the next wake-up; `close(2)` drops the knotes itself, so the
97//! delete then finds nothing, and the backend ignores that. `select` closes
98//! after clearing the bits and before blocking, which is the order its
99//! kernel-side scan needs.
100//!
101//! New registrations arriving while the thread is blocked in the syscall are
102//! what `Backend::interrupt` is for; it is the only reason this module needs
103//! a self-pipe (`select`) or a user event (`kqueue`).
104
105// RTEMS-EXEC-MODEL-ALLOW(7): the seven poller tests park a `tokio::spawn`ed
106// task on a `ReadyStream` half, so the property under test is that the poller
107// wakes a tokio task — the tokio flavor is the subject, not an accident. They
108// run on the driver `#[tokio::test]` builds and pass in the exec-backend suite
109// (measured: `EPICS_RS_BUILD_EXEC_BACKEND=thread cargo nextest run
110// -p epics-libcom-rs -E 'test(/runtime::readiness::/)'`, 22/22).
111
112use std::collections::HashMap;
113use std::io;
114use std::mem::ManuallyDrop;
115use std::net::TcpStream;
116use std::os::fd::{AsRawFd, RawFd};
117use std::sync::atomic::{AtomicBool, Ordering};
118use std::sync::{Arc, Mutex};
119use std::task::Waker;
120
121#[cfg(target_os = "rtems")]
122mod kqueue;
123mod select;
124
125/// Which backend a poller waits on, decided once at construction.
126///
127/// Both arms are compiled on RTEMS rather than `cfg`-ed apart, so a type error
128/// in either fails the RTEMS gate; the choice between them is a runtime one
129/// because the fact it turns on — the BSP's own RTEMS version — is only
130/// knowable at runtime. See `rtems_kqueue_usable`.
131enum ActiveBackend {
132    Select(select::SelectBackend),
133    #[cfg(target_os = "rtems")]
134    Kqueue(kqueue::KqueueBackend),
135}
136
137impl ActiveBackend {
138    /// Choose, open, and say which — on RTEMS, where there is a choice.
139    ///
140    /// The line is an `eprintln!` from the construction path for the same
141    /// reason [`report_lock_protocol`](crate::runtime::sync::report_lock_protocol)
142    /// is one: the RTEMS target has no iocsh to ask afterwards, and a
143    /// diagnostic that reports which guarantee the process got must not
144    /// itself depend on a `tracing` subscriber being installed and unfiltered.
145    /// It is printed after the backend opened, so it never names one that
146    /// failed.
147    ///
148    /// Reporting the *reason* beside the choice is what makes the line
149    /// actionable — "select (RTEMS 6.0, EPICS_RTEMS_KQUEUE unset)" names the
150    /// variable to set — and [`rtems_kqueue_usable`] returns both from one
151    /// call so the reason cannot drift from the rule that was applied.
152    #[cfg(target_os = "rtems")]
153    fn new(poller: &str) -> io::Result<Self> {
154        let (usable, why) = rtems_kqueue_usable();
155        let (backend, name) = if usable {
156            (Self::Kqueue(kqueue::KqueueBackend::new()?), "kqueue")
157        } else {
158            (Self::Select(select::SelectBackend::new()?), "select")
159        };
160        eprintln!("epics-rs: readiness poller {poller}: {name} backend ({why})");
161        Ok(backend)
162    }
163
164    /// No choice to report: `select` is the only backend off RTEMS.
165    #[cfg(not(target_os = "rtems"))]
166    fn new(_poller: &str) -> io::Result<Self> {
167        Ok(Self::Select(select::SelectBackend::new()?))
168    }
169}
170
171impl Backend for ActiveBackend {
172    fn wait(
173        &self,
174        changes: &[Change],
175        closing: &mut Vec<TcpStream>,
176        ready: &mut Vec<(RawFd, Interest)>,
177    ) -> io::Result<()> {
178        match self {
179            Self::Select(b) => b.wait(changes, closing, ready),
180            #[cfg(target_os = "rtems")]
181            Self::Kqueue(b) => b.wait(changes, closing, ready),
182        }
183    }
184
185    fn interrupt(&self) -> io::Result<()> {
186        match self {
187            Self::Select(b) => b.interrupt(),
188            #[cfg(target_os = "rtems")]
189            Self::Kqueue(b) => b.interrupt(),
190        }
191    }
192}
193
194#[cfg(target_os = "rtems")]
195unsafe extern "C" {
196    /// `rtems/version.h:90,97`. Defined in `librtemscpu.a` on both prefixes
197    /// `scripts/rtems-bsp.sh` builds (`nm --defined-only`: `T
198    /// rtems_version_major`), and absent from `libc`.
199    fn rtems_version_major() -> libc::c_int;
200    fn rtems_version_minor() -> libc::c_int;
201}
202
203/// pvxs's rule, applied to the BSP this image actually booted on.
204///
205/// pvxs gates kqueue at RTEMS 6.3 (`> 6.2`) — epics-base/pvxs#197 — because
206/// the RTEMS 5-era workaround below that version steers libevent onto a `poll`
207/// backend that never blocks on this BSP. The version is read at runtime, not
208/// at build time, because a branch build reports its series' development
209/// version (7.0.0 on main, 6.0.0 on the 6 branch) and only the running system
210/// knows which one it is.
211///
212/// `EPICS_RTEMS_KQUEUE` overrides the rule in both directions. A series-6
213/// prefix built by `scripts/rtems-bsp.sh` carries the libbsd and kernel fixes
214/// the rule is a proxy for — the script asserts they are in the tree it builds
215/// — yet reports 6.0.0, so that prefix's `epics-rs-env.sh` sets
216/// `EPICS_RTEMS_KQUEUE=1` for exactly that case. It is read here in the
217/// *target* process, so what carries it is the boot command line
218/// (`epics_rtems_boot::boot_args`); `scripts/embedded-image.sh` bakes the
219/// build environment's value into the image for that reason.
220///
221/// Returns the reason beside the answer so the line
222/// [`ActiveBackend::new`] prints names the rule that was actually applied.
223#[cfg(target_os = "rtems")]
224fn rtems_kqueue_usable() -> (bool, String) {
225    if let Some(raw) = super::env::get("EPICS_RTEMS_KQUEUE") {
226        if let Some(forced) = parse_override(&raw) {
227            return (forced, format!("EPICS_RTEMS_KQUEUE={}", raw.trim()));
228        }
229        // Not a third answer (see `parse_override`), but the operator set
230        // something and is owed the reason it did nothing.
231        eprintln!(
232            "epics-rs: EPICS_RTEMS_KQUEUE={} is neither 1/yes/true nor 0/no/false; \
233             the RTEMS version rule decides",
234            raw.trim()
235        );
236    }
237    // SAFETY: both are argument-free integer returns from librtemscpu.
238    let (major, minor) = unsafe { (rtems_version_major(), rtems_version_minor()) };
239    let usable = major > 6 || (major == 6 && minor > 2);
240    (
241        usable,
242        format!("RTEMS {major}.{minor}, EPICS_RTEMS_KQUEUE unset"),
243    )
244}
245
246/// The override's spellings. `scripts/rtems-bsp.sh` writes `1`; a value that
247/// is none of these is not a third answer, so it falls through to the version
248/// rule rather than being read as one direction or the other.
249#[cfg(target_os = "rtems")]
250fn parse_override(raw: &str) -> Option<bool> {
251    match raw.trim().to_ascii_lowercase().as_str() {
252        "1" | "yes" | "true" => Some(true),
253        "0" | "no" | "false" => Some(false),
254        _ => None,
255    }
256}
257
258/// The direction a task is waiting on. One waker per direction per fd — a PVA
259/// connection has one reader and one writer, and they are different tasks.
260#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
261pub enum Interest {
262    Read,
263    Write,
264}
265
266/// The largest fd this module can wait on, as a bit index into the `select`
267/// fd set.
268///
269/// `select` addresses fds by bit position, so the cost of a high fd is a
270/// larger bitmap and nothing else. 4096 covers both embedded triples with
271/// room: `x86_64-wrs-vxworks`'s limit is the VSB's `_WRS_CONFIG_FD_SET_SIZE`,
272/// 2048 in the SDK measured here, and RTEMS sizes its own set from
273/// `CONFIGURE_MAXIMUM_FILE_DESCRIPTORS`. An fd at or above this is refused at
274/// registration with `EMFILE` rather than silently dropped, because a dropped
275/// interest is a connection that hangs forever.
276pub const FD_CAPACITY: usize = 4096;
277
278/// One thing to tell the kernel about one direction on one fd — libevent's
279/// `EV_CHANGE_ADD` / `EV_CHANGE_DEL`, narrowed to the one direction it names.
280#[derive(Clone, Copy, Debug, PartialEq, Eq)]
281struct Change {
282    fd: RawFd,
283    interest: Interest,
284    action: Action,
285}
286
287/// Which way a [`Change`] goes.
288#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289enum Action {
290    /// Start watching `(fd, interest)`, or re-affirm a watch already placed.
291    Arm,
292    /// Stop watching it.
293    Disarm,
294}
295
296/// Refuse an fd this module cannot address, rather than dropping it silently.
297///
298/// Shared by both backends because both have the same limit for the same
299/// reason: [`FD_CAPACITY`] is the `select` bitmap's width, and a poller whose
300/// two backends disagreed about which fds it accepts would fail differently
301/// depending on which one the target picked.
302fn check_fd(fd: RawFd) -> io::Result<()> {
303    if fd < 0 || (fd as usize) >= FD_CAPACITY {
304        return Err(io::Error::new(
305            io::ErrorKind::InvalidInput,
306            format!("fd {fd} is beyond the readiness poller's {FD_CAPACITY}-fd capacity"),
307        ));
308    }
309    Ok(())
310}
311
312/// What a backend must do. Two calls, both from the poll thread except
313/// [`Backend::interrupt`], which is called from whichever thread arms an
314/// interest.
315trait Backend: Send + Sync + 'static {
316    /// Apply `changes`, close every socket in `closing`, then wait until at
317    /// least one watched interest is ready and push the ready ones into
318    /// `ready`. Returns without error and with an empty `ready` when only
319    /// [`Backend::interrupt`] fired.
320    ///
321    /// > **MUST** every change is applied and every socket in `closing` is
322    /// > closed before the call can block, so a `wait` that fails with
323    /// > [`io::ErrorKind::Interrupted`] has still taken both.
324    ///
325    /// That is what lets [`poll_loop`] treat `Interrupted` as "go round
326    /// again" without re-queueing anything: a change leaves the armed set
327    /// exactly once, and a backend that deferred one past a blocking point
328    /// would strand the task that asked for it with no second chance to ask.
329    /// `select` satisfies it by keeping its fd sets in this process; `kqueue`
330    /// by submitting the changelist in the same `kevent` call, which applies
331    /// it before the wait begins.
332    ///
333    /// The sockets in `closing` are the ones whose deletes are in `changes`.
334    /// `select` closes them after the bits are cleared, which is the order its
335    /// scan needs; `kqueue` closes them before the `kevent` that carries the
336    /// deletes, which then miss harmlessly, because that call is also the one
337    /// that blocks.
338    fn wait(
339        &self,
340        changes: &[Change],
341        closing: &mut Vec<TcpStream>,
342        ready: &mut Vec<(RawFd, Interest)>,
343    ) -> io::Result<()>;
344
345    /// Break a concurrent [`Backend::wait`] out of its syscall.
346    fn interrupt(&self) -> io::Result<()>;
347}
348
349/// One direction on one fd: who to wake, what the kernel has been told, and
350/// what it has yet to be told.
351#[derive(Default)]
352struct Direction {
353    waker: Option<Waker>,
354    /// Whether the kernel is currently watching this direction — libevent's
355    /// `event_change::old_events`. It is the field that makes deleting
356    /// something never added a no-op instead of a syscall that fails.
357    registered: bool,
358    /// The change no backend has been handed yet.
359    pending: Option<Action>,
360}
361
362impl Direction {
363    /// Nobody to wake, nothing in the kernel, nothing left to say.
364    fn is_idle(&self) -> bool {
365        self.waker.is_none() && !self.registered && self.pending.is_none()
366    }
367}
368
369#[derive(Default)]
370struct Slot {
371    read: Direction,
372    write: Direction,
373    /// The socket, once its last handle is dropped, held until the delete the
374    /// kernel is owed for it goes out — see [`Inner::disarm_all`].
375    closing: Option<TcpStream>,
376}
377
378impl Slot {
379    fn direction(&mut self, interest: Interest) -> &mut Direction {
380        match interest {
381            Interest::Read => &mut self.read,
382            Interest::Write => &mut self.write,
383        }
384    }
385
386    fn has_waker(&self) -> bool {
387        self.read.waker.is_some() || self.write.waker.is_some()
388    }
389
390    fn is_dirty(&self) -> bool {
391        self.read.pending.is_some() || self.write.pending.is_some()
392    }
393
394    fn is_idle(&self) -> bool {
395        self.read.is_idle() && self.write.is_idle() && self.closing.is_none()
396    }
397}
398
399/// The armed set and the changes the kernel has not been told about yet, under
400/// one lock so the two cannot disagree about the order of a transition.
401#[derive(Default)]
402struct Armed {
403    slots: HashMap<RawFd, Slot>,
404    /// The fds carrying at least one pending change, in the order they came to
405    /// carry one. An fd is pushed on the clean-to-dirty edge, so it lands here
406    /// once per drain rather than once per change; an entry left behind by a
407    /// slot that went idle before the drain is skipped there, which is cheaper
408    /// than searching this vector to remove it.
409    dirty: Vec<RawFd>,
410}
411
412struct Inner {
413    armed: Mutex<Armed>,
414    shutdown: AtomicBool,
415    backend: ActiveBackend,
416}
417
418impl Inner {
419    /// Arm `(fd, interest)` with `waker`, replacing whatever waker that
420    /// direction held.
421    fn arm(&self, fd: RawFd, interest: Interest, waker: &Waker) -> io::Result<()> {
422        if self.shutdown.load(Ordering::Acquire) {
423            return Err(io::Error::new(
424                io::ErrorKind::Other,
425                "readiness poller is shut down",
426            ));
427        }
428        {
429            let mut armed = self.armed.lock().expect("readiness armed set poisoned");
430            let Armed { slots, dirty } = &mut *armed;
431            let slot = slots.entry(fd).or_default();
432            let was_dirty = slot.is_dirty();
433            let cell = slot.direction(interest);
434            match &cell.waker {
435                // `will_wake` is the common case on a re-poll of the same
436                // task: keep the waker we have rather than cloning again.
437                Some(existing) if existing.will_wake(waker) => {}
438                _ => cell.waker = Some(waker.clone()),
439            }
440            // An add replaces a pending delete and is never cancelled into a
441            // no-op, which is libevent's rule and for libevent's reason: that
442            // delete may have named an fd this number no longer stands for,
443            // and the kernel dropped the registration with the `close`.
444            cell.pending = Some(Action::Arm);
445            if !was_dirty {
446                dirty.push(fd);
447            }
448        }
449        self.backend.interrupt()
450    }
451
452    /// Take a socket back: drop every interest on it and close it once the
453    /// kernel is no longer watching it. The one path that removes an fd
454    /// besides the one-shot removal in the poll loop — see the module
455    /// invariant.
456    ///
457    /// The close is the poll thread's when the kernel holds a registration for
458    /// the fd — the socket rides in the slot until the delete goes out — and
459    /// immediate when it does not: an fd the kernel was never told about, or
460    /// one on a poller whose thread is gone. `shutdown` is read under the
461    /// armed lock because [`Inner::retire`] sets it under the same lock, so
462    /// the two cannot both miss the socket.
463    fn disarm_all(&self, socket: TcpStream) {
464        let fd = socket.as_raw_fd();
465        let close_now = {
466            let mut armed = self.armed.lock().expect("readiness armed set poisoned");
467            if self.shutdown.load(Ordering::Acquire) {
468                Some(socket)
469            } else {
470                let Armed { slots, dirty } = &mut *armed;
471                match slots.get_mut(&fd) {
472                    None => Some(socket),
473                    Some(slot) => {
474                        let was_dirty = slot.is_dirty();
475                        for interest in [Interest::Read, Interest::Write] {
476                            let cell = slot.direction(interest);
477                            cell.waker = None;
478                            cell.pending = cell.registered.then_some(Action::Disarm);
479                        }
480                        if slot.is_dirty() {
481                            slot.closing = Some(socket);
482                            if !was_dirty {
483                                dirty.push(fd);
484                            }
485                            None
486                        } else {
487                            // Nothing registered — an add the poll thread
488                            // never drained was cancelled just above — so
489                            // there is nothing to remove before the close.
490                            slots.remove(&fd);
491                            Some(socket)
492                        }
493                    }
494                }
495            }
496        };
497        drop(close_now);
498        // The armed set is no longer rebuilt from scratch on every wait, so
499        // this delete — and the close behind it — reaches the kernel only
500        // when the poll thread next drains. Waking it now is what holds that
501        // window to the wait already in flight rather than to whenever some
502        // other fd is next armed.
503        let _ = self.backend.interrupt();
504    }
505
506    /// Take the waker for one ready interest and stop watching that direction
507    /// — the one-shot half of the module invariant, kernel side included.
508    fn take_ready(&self, fd: RawFd, interest: Interest) -> Option<Waker> {
509        let mut armed = self.armed.lock().expect("readiness armed set poisoned");
510        let Armed { slots, dirty } = &mut *armed;
511        let slot = slots.get_mut(&fd)?;
512        let was_dirty = slot.is_dirty();
513        let cell = slot.direction(interest);
514        let waker = cell.waker.take();
515        cell.pending = cell.registered.then_some(Action::Disarm);
516        if slot.is_idle() {
517            slots.remove(&fd);
518        } else if slot.is_dirty() && !was_dirty {
519            dirty.push(fd);
520        }
521        waker
522    }
523
524    /// How many fds hold at least one waker. See [`Poller::armed_fds`].
525    fn armed_fds(&self) -> usize {
526        self.armed
527            .lock()
528            .expect("readiness armed set poisoned")
529            .slots
530            .values()
531            .filter(|slot| slot.has_waker())
532            .count()
533    }
534
535    /// Move the pending changes into `out`, the sockets whose deletes they
536    /// carry into `closing`, and record that the kernel is about to be told
537    /// about them.
538    ///
539    /// `registered` is committed here rather than after the syscall because of
540    /// the [`Backend::wait`] MUST: the only failure that leaves this poller
541    /// running is `Interrupted`, and that one happens after the changelist is
542    /// in.
543    fn drain_changes(&self, out: &mut Vec<Change>, closing: &mut Vec<TcpStream>) {
544        out.clear();
545        debug_assert!(
546            closing.is_empty(),
547            "the backend owes every socket it was handed a close before it blocks"
548        );
549        let mut armed = self.armed.lock().expect("readiness armed set poisoned");
550        let Armed { slots, dirty } = &mut *armed;
551        for fd in dirty.drain(..) {
552            let Some(slot) = slots.get_mut(&fd) else {
553                continue;
554            };
555            for interest in [Interest::Read, Interest::Write] {
556                let cell = slot.direction(interest);
557                let Some(action) = cell.pending.take() else {
558                    continue;
559                };
560                cell.registered = action == Action::Arm;
561                out.push(Change {
562                    fd,
563                    interest,
564                    action,
565                });
566            }
567            // Both directions' deletes are in `out` now — `disarm_all` queued
568            // them together with the socket — so the socket goes with them.
569            if let Some(socket) = slot.closing.take() {
570                closing.push(socket);
571            }
572            if slot.is_idle() {
573                slots.remove(&fd);
574            }
575        }
576    }
577
578    /// The poll thread's last act, on either exit: shut the poller, wake every
579    /// parked task, and close every socket held for a delete that will now
580    /// never go out — the kernel stops watching when the thread stops
581    /// waiting.
582    ///
583    /// `shutdown` is stored under the armed lock so that a concurrent
584    /// [`Inner::disarm_all`] either sees it set and closes its socket itself,
585    /// or has stored the socket in a slot this sweep takes. A task woken here
586    /// re-polls its socket, sees `WouldBlock` and re-arms, and it is the
587    /// refused re-arm that hands it the error it unwinds on.
588    fn retire(&self) {
589        let mut armed = self.armed.lock().expect("readiness armed set poisoned");
590        self.shutdown.store(true, Ordering::Release);
591        let Armed { slots, dirty } = &mut *armed;
592        dirty.clear();
593        for (_, slot) in slots.drain() {
594            let Slot {
595                read,
596                write,
597                closing,
598            } = slot;
599            for cell in [read, write] {
600                if let Some(w) = cell.waker {
601                    w.wake();
602                }
603            }
604            drop(closing);
605        }
606    }
607}
608
609/// A readiness poller and the thread that owns its armed set.
610///
611/// One per server. [`Poller::shutdown`] stops the thread; dropping the last
612/// handle does the same.
613pub struct Poller {
614    inner: Arc<Inner>,
615}
616
617impl Poller {
618    /// Start a poller and its thread.
619    ///
620    /// `name` is the thread name, which is what an `epicsThreadShowAll`
621    /// equivalent prints; keep it short.
622    ///
623    /// `priority` is the caller's, not this module's: the poll thread is the
624    /// reactor of whatever server owns it, and a server's band is set against
625    /// the *other* servers in the IOC — pvxs runs its TCP reactor at
626    /// `CAServerLow-2`, CA's rsrv its own ladder from `caservertask.c`. A
627    /// constant here would put every server's reactor in one band and silently
628    /// undo that ordering.
629    pub fn new(
630        name: &str,
631        priority: crate::runtime::task::ThreadPriority,
632    ) -> io::Result<Arc<Self>> {
633        let inner = Arc::new(Inner {
634            armed: Mutex::new(Armed::default()),
635            shutdown: AtomicBool::new(false),
636            backend: ActiveBackend::new(name)?,
637        });
638        let thread_inner = Arc::clone(&inner);
639        crate::runtime::task::spawn_dedicated_thread(
640            name.to_string(),
641            priority,
642            crate::runtime::task::StackSizeClass::Small,
643            move || poll_loop(&thread_inner),
644        )?;
645        Ok(Arc::new(Self { inner }))
646    }
647
648    /// Wrap an accepted socket so the protocol code can read and write it
649    /// through this poller.
650    pub fn stream(self: &Arc<Self>, stream: std::net::TcpStream) -> io::Result<ReadyStream> {
651        stream.set_nonblocking(true)?;
652        let fd = stream.as_raw_fd();
653        if fd < 0 || fd as usize >= FD_CAPACITY {
654            return Err(io::Error::new(
655                io::ErrorKind::Other,
656                format!("fd {fd} is beyond the readiness poller's {FD_CAPACITY}-fd capacity"),
657            ));
658        }
659        Ok(ReadyStream {
660            inner: Arc::new(StreamInner {
661                poller: Arc::clone(self),
662                stream: ManuallyDrop::new(stream),
663            }),
664        })
665    }
666
667    /// Stop the poll thread. Idempotent.
668    pub fn shutdown(&self) {
669        self.inner.shutdown.store(true, Ordering::Release);
670        let _ = self.inner.backend.interrupt();
671    }
672
673    /// How many fds currently hold at least one armed interest. Test and
674    /// report surface; not a control input.
675    ///
676    /// An fd whose last waker has been taken but whose kernel registration is
677    /// still queued for deletion is not one of them. The count is of tasks
678    /// parked here, which is the question a report is asking.
679    pub fn armed_fds(&self) -> usize {
680        self.inner.armed_fds()
681    }
682}
683
684impl Drop for Poller {
685    fn drop(&mut self) {
686        self.shutdown();
687    }
688}
689
690fn poll_loop(inner: &Arc<Inner>) {
691    let mut ready: Vec<(RawFd, Interest)> = Vec::new();
692    let mut changes: Vec<Change> = Vec::new();
693    let mut closing: Vec<TcpStream> = Vec::new();
694    while !inner.shutdown.load(Ordering::Acquire) {
695        inner.drain_changes(&mut changes, &mut closing);
696        ready.clear();
697        match inner.backend.wait(&changes, &mut closing, &mut ready) {
698            Ok(()) => {}
699            // The changes are not re-queued, and need not be: `Backend::wait`
700            // owes them and the closes to the kernel before it may block, so
701            // the interrupted call is one that already took them.
702            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
703            Err(e) => {
704                // A backend that cannot wait cannot serve anyone; `retire`
705                // below is what tells the parked tasks.
706                //
707                // `eprintln!` for the reason `ActiveBackend::new` uses it: a
708                // poller dying is a diagnostic that must not depend on a
709                // `tracing` subscriber being installed.
710                eprintln!("epics-rs: readiness poller cannot wait, shutting down: {e}");
711                break;
712            }
713        }
714        for &(fd, interest) in &ready {
715            if let Some(waker) = inner.take_ready(fd, interest) {
716                waker.wake();
717            }
718        }
719    }
720    inner.retire();
721    // A wait that failed may have refused its `closing`; those sockets close
722    // with the vector, after the sweep, when the kernel is watching nothing.
723}
724
725/// The socket and the poller it is registered with — the state every handle
726/// to one connection shares.
727///
728/// Dropping the last handle does not close the socket. It hands the socket to
729/// the poller ([`Inner::disarm_all`]), which closes it once the kernel is no
730/// longer watching the fd — the module invariant, held by construction rather
731/// than by field order.
732struct StreamInner {
733    poller: Arc<Poller>,
734    /// Taken in `Drop`, once, to hand to the poller.
735    stream: ManuallyDrop<TcpStream>,
736}
737
738impl Drop for StreamInner {
739    fn drop(&mut self) {
740        // SAFETY: taken exactly once, here, and `self.stream` is never read
741        // again — `drop` is the last code to see `self`.
742        let socket = unsafe { ManuallyDrop::take(&mut self.stream) };
743        self.poller.inner.disarm_all(socket);
744    }
745}
746
747impl StreamInner {
748    fn arm(&self, interest: Interest, waker: &Waker) -> io::Result<()> {
749        self.poller
750            .inner
751            .arm(self.stream.as_raw_fd(), interest, waker)
752    }
753
754    fn poll_read(
755        &self,
756        cx: &mut std::task::Context<'_>,
757        buf: &mut tokio::io::ReadBuf<'_>,
758    ) -> std::task::Poll<io::Result<()>> {
759        use std::io::Read;
760        // SAFETY-adjacent, but not `unsafe`: read into the uninitialised tail
761        // is avoided by going through `initialize_unfilled`, which is what
762        // costs a memset and what keeps this file free of raw buffers.
763        let dst = buf.initialize_unfilled();
764        match (&*self.stream).read(dst) {
765            Ok(n) => {
766                buf.advance(n);
767                std::task::Poll::Ready(Ok(()))
768            }
769            Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
770                match self.arm(Interest::Read, cx.waker()) {
771                    Ok(()) => std::task::Poll::Pending,
772                    Err(e) => std::task::Poll::Ready(Err(e)),
773                }
774            }
775            Err(e) => std::task::Poll::Ready(Err(e)),
776        }
777    }
778
779    fn poll_write(
780        &self,
781        cx: &mut std::task::Context<'_>,
782        src: &[u8],
783    ) -> std::task::Poll<io::Result<usize>> {
784        use std::io::Write;
785        match (&*self.stream).write(src) {
786            Ok(n) => std::task::Poll::Ready(Ok(n)),
787            Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
788                match self.arm(Interest::Write, cx.waker()) {
789                    Ok(()) => std::task::Poll::Pending,
790                    Err(e) => std::task::Poll::Ready(Err(e)),
791                }
792            }
793            Err(e) => std::task::Poll::Ready(Err(e)),
794        }
795    }
796
797    fn poll_shutdown(&self) -> std::task::Poll<io::Result<()>> {
798        match self.stream.shutdown(std::net::Shutdown::Write) {
799            Ok(()) => std::task::Poll::Ready(Ok(())),
800            // The peer closing first makes our half-close a no-op, not a
801            // failure the connection loop should report.
802            Err(e) if e.kind() == io::ErrorKind::NotConnected => std::task::Poll::Ready(Ok(())),
803            Err(e) => std::task::Poll::Ready(Err(e)),
804        }
805    }
806}
807
808/// A non-blocking `TcpStream` that reads and writes through a [`Poller`].
809pub struct ReadyStream {
810    inner: Arc<StreamInner>,
811}
812
813impl ReadyStream {
814    /// Two handles to the one socket, as a connection protocol takes them: a
815    /// reader and a writer that are polled by different tasks.
816    ///
817    /// Splitting costs nothing at the poller: read and write are already
818    /// separate interests on the same fd, each with its own waker, so the two
819    /// halves park independently on the one registration they share. There is
820    /// no lock between them either — the halves call `read`/`write` through
821    /// `&TcpStream`, and the kernel already serialises those per direction.
822    pub fn split(self) -> (ReadHalf, WriteHalf) {
823        (
824            ReadHalf {
825                inner: Arc::clone(&self.inner),
826            },
827            WriteHalf { inner: self.inner },
828        )
829    }
830
831    /// The wrapped socket, for the options a driver sets after accept
832    /// (`SO_SNDBUF`, keepalive).
833    pub fn socket(&self) -> &TcpStream {
834        &self.inner.stream
835    }
836}
837
838/// The read half of a split [`ReadyStream`].
839pub struct ReadHalf {
840    inner: Arc<StreamInner>,
841}
842
843/// The write half of a split [`ReadyStream`].
844pub struct WriteHalf {
845    inner: Arc<StreamInner>,
846}
847
848impl tokio::io::AsyncRead for ReadyStream {
849    fn poll_read(
850        self: std::pin::Pin<&mut Self>,
851        cx: &mut std::task::Context<'_>,
852        buf: &mut tokio::io::ReadBuf<'_>,
853    ) -> std::task::Poll<io::Result<()>> {
854        self.inner.poll_read(cx, buf)
855    }
856}
857
858impl tokio::io::AsyncRead for ReadHalf {
859    fn poll_read(
860        self: std::pin::Pin<&mut Self>,
861        cx: &mut std::task::Context<'_>,
862        buf: &mut tokio::io::ReadBuf<'_>,
863    ) -> std::task::Poll<io::Result<()>> {
864        self.inner.poll_read(cx, buf)
865    }
866}
867
868impl tokio::io::AsyncWrite for ReadyStream {
869    fn poll_write(
870        self: std::pin::Pin<&mut Self>,
871        cx: &mut std::task::Context<'_>,
872        src: &[u8],
873    ) -> std::task::Poll<io::Result<usize>> {
874        self.inner.poll_write(cx, src)
875    }
876
877    fn poll_flush(
878        self: std::pin::Pin<&mut Self>,
879        _cx: &mut std::task::Context<'_>,
880    ) -> std::task::Poll<io::Result<()>> {
881        // A TCP socket has no userspace buffer of ours to flush: every
882        // accepted byte is already in the kernel's send buffer.
883        std::task::Poll::Ready(Ok(()))
884    }
885
886    fn poll_shutdown(
887        self: std::pin::Pin<&mut Self>,
888        _cx: &mut std::task::Context<'_>,
889    ) -> std::task::Poll<io::Result<()>> {
890        self.inner.poll_shutdown()
891    }
892}
893
894impl tokio::io::AsyncWrite for WriteHalf {
895    fn poll_write(
896        self: std::pin::Pin<&mut Self>,
897        cx: &mut std::task::Context<'_>,
898        src: &[u8],
899    ) -> std::task::Poll<io::Result<usize>> {
900        self.inner.poll_write(cx, src)
901    }
902
903    fn poll_flush(
904        self: std::pin::Pin<&mut Self>,
905        _cx: &mut std::task::Context<'_>,
906    ) -> std::task::Poll<io::Result<()>> {
907        std::task::Poll::Ready(Ok(()))
908    }
909
910    fn poll_shutdown(
911        self: std::pin::Pin<&mut Self>,
912        _cx: &mut std::task::Context<'_>,
913    ) -> std::task::Poll<io::Result<()>> {
914        self.inner.poll_shutdown()
915    }
916}
917
918#[cfg(test)]
919mod tests {
920    use super::*;
921    use crate::runtime::task::ThreadPriority;
922    use std::io::{Read, Write};
923    use std::net::{TcpListener, TcpStream};
924    use tokio::io::{AsyncReadExt, AsyncWriteExt};
925
926    /// A connected pair over loopback: (accepted server side, client side).
927    fn pair() -> (TcpStream, TcpStream) {
928        let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind");
929        let addr = listener.local_addr().expect("local_addr");
930        let client = TcpStream::connect(addr).expect("connect");
931        let (server, _) = listener.accept().expect("accept");
932        (server, client)
933    }
934
935    /// Whether `fd` is still open in this process. Sound as a test probe
936    /// because nextest runs each test in a process of its own, so a number
937    /// this test closed is reused only by this test.
938    fn is_open(fd: RawFd) -> bool {
939        // SAFETY: `F_GETFD` takes no pointer and touches nothing.
940        unsafe { libc::fcntl(fd, libc::F_GETFD) != -1 }
941    }
942
943    /// Two round trips, not one: the second is what fails if the delete the
944    /// first delivery queued outlives the re-arm that follows it, or if the
945    /// re-arm is skipped because the kernel is thought to hold a registration
946    /// it no longer has.
947    #[tokio::test]
948    async fn a_read_parks_until_the_peer_writes_and_then_delivers() {
949        let poller = Poller::new("RDT1", ThreadPriority::CaServerHigh).expect("poller");
950        let (server, mut client) = pair();
951        let mut stream = poller.stream(server).expect("stream");
952
953        let read = tokio::spawn(async move {
954            let mut both = [0u8; 10];
955            // Deliberately started before any byte exists: this must park in
956            // the poller, not spin or return zero.
957            stream.read_exact(&mut both[..5]).await.expect("first");
958            stream.read_exact(&mut both[5..]).await.expect("second");
959            both
960        });
961
962        for payload in [b"hello", b"again"] {
963            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
964            client.write_all(payload).expect("peer write");
965        }
966
967        let got = tokio::time::timeout(std::time::Duration::from_secs(5), read)
968            .await
969            .expect("read completed")
970            .expect("join");
971        assert_eq!(&got, b"helloagain");
972    }
973
974    #[tokio::test]
975    async fn b_write_parks_when_the_send_buffer_fills_and_resumes_when_it_drains() {
976        const BYTES: usize = 32 * 1024 * 1024;
977        let poller = Poller::new("RDT2", ThreadPriority::CaServerHigh).expect("poller");
978        let (server, mut client) = pair();
979        let mut stream = poller.stream(server).expect("stream");
980
981        // Past any auto-tuned SO_SNDBUF plus the peer's receive buffer, so the
982        // write cannot finish while nobody reads.
983        let payload = vec![0x5au8; BYTES];
984        let write = tokio::spawn(async move {
985            stream.write_all(&payload).await.expect("write_all");
986            stream.flush().await.expect("flush");
987        });
988
989        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
990        assert!(
991            !write.is_finished(),
992            "the write must park on the poller while the peer reads nothing"
993        );
994
995        let drained = tokio::task::spawn_blocking(move || {
996            let mut sink = vec![0u8; 64 * 1024];
997            let mut total = 0usize;
998            while total < BYTES {
999                match client.read(&mut sink) {
1000                    Ok(0) => break,
1001                    Ok(n) => total += n,
1002                    Err(e) => panic!("peer read: {e}"),
1003                }
1004            }
1005            total
1006        });
1007
1008        tokio::time::timeout(std::time::Duration::from_secs(30), write)
1009            .await
1010            .expect("write completed once the peer drained")
1011            .expect("join");
1012        assert_eq!(drained.await.expect("join"), BYTES);
1013    }
1014
1015    /// The invariant end to end: the drop takes the fd out of the armed set
1016    /// at once, and the socket closes only once the poll thread has drained
1017    /// the delete — so the close is the poll thread's, never the dropping
1018    /// task's.
1019    #[tokio::test]
1020    async fn c_dropping_the_stream_disarms_the_fd_and_the_poll_thread_closes_it() {
1021        let poller = Poller::new("RDT3", ThreadPriority::CaServerHigh).expect("poller");
1022        let (server, _client) = pair();
1023        let mut stream = poller.stream(server).expect("stream");
1024        let fd = stream.socket().as_raw_fd();
1025
1026        // Park a read so the fd is genuinely armed.
1027        let mut buf = [0u8; 1];
1028        let armed =
1029            tokio::time::timeout(std::time::Duration::from_millis(100), stream.read(&mut buf))
1030                .await;
1031        assert!(armed.is_err(), "read must park with no byte available");
1032        assert_eq!(poller.armed_fds(), 1);
1033
1034        drop(stream);
1035        assert_eq!(
1036            poller.armed_fds(),
1037            0,
1038            "StreamInner::drop must clear the armed set at once"
1039        );
1040        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
1041        while is_open(fd) {
1042            assert!(
1043                std::time::Instant::now() < deadline,
1044                "the poll thread must close the socket once its delete is out"
1045            );
1046            tokio::time::sleep(std::time::Duration::from_millis(5)).await;
1047        }
1048    }
1049
1050    #[tokio::test]
1051    async fn d_an_fd_past_capacity_is_refused_rather_than_dropped() {
1052        let poller = Poller::new("RDT4", ThreadPriority::CaServerHigh).expect("poller");
1053        let (server, _client) = pair();
1054        // The real guard is in `Poller::stream`; drive it by asking about a
1055        // number no process will hand out, which is the same branch.
1056        let too_high = FD_CAPACITY as RawFd + 1;
1057        assert!(too_high as usize >= FD_CAPACITY);
1058        drop(server);
1059        let err = poller
1060            .inner
1061            .backend
1062            .wait(
1063                &[Change {
1064                    fd: too_high,
1065                    interest: Interest::Read,
1066                    action: Action::Arm,
1067                }],
1068                &mut Vec::new(),
1069                &mut Vec::new(),
1070            )
1071            .expect_err("a fd past capacity must be an error, not a silent skip");
1072        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1073    }
1074
1075    #[tokio::test]
1076    async fn f_the_two_halves_share_one_registration_until_the_last_drops() {
1077        let poller = Poller::new("RDT6", ThreadPriority::CaServerHigh).expect("poller");
1078        let (server, mut client) = pair();
1079        let (mut read_half, mut write_half) = poller.stream(server).expect("stream").split();
1080
1081        // Park the read half; the fd is armed for read from here on.
1082        let mut buf = [0u8; 5];
1083        let parked = tokio::time::timeout(
1084            std::time::Duration::from_millis(100),
1085            read_half.read(&mut buf),
1086        )
1087        .await;
1088        assert!(parked.is_err(), "the read half must park with no byte");
1089        assert_eq!(poller.armed_fds(), 1);
1090
1091        // The write half drives the same fd while that read is parked — the
1092        // two directions are independent interests, not a shared lock.
1093        write_half.write_all(b"ping").await.expect("write half");
1094        let mut echo = [0u8; 4];
1095        client.read_exact(&mut echo).expect("peer read");
1096        assert_eq!(&echo, b"ping");
1097
1098        // The registration belongs to neither half alone: only the last one
1099        // out may take the fd out of the armed set, because only then is the
1100        // socket about to close.
1101        drop(write_half);
1102        assert_eq!(
1103            poller.armed_fds(),
1104            1,
1105            "the surviving half still holds the registration"
1106        );
1107        drop(read_half);
1108        assert_eq!(
1109            poller.armed_fds(),
1110            0,
1111            "the last half dropping disarms the fd before the close"
1112        );
1113    }
1114
1115    #[tokio::test]
1116    async fn e_shutdown_stops_the_poll_thread_and_further_arming_fails() {
1117        let poller = Poller::new("RDT5", ThreadPriority::CaServerHigh).expect("poller");
1118        let (server, _client) = pair();
1119        let stream = poller.stream(server).expect("stream");
1120        poller.shutdown();
1121
1122        let err = stream
1123            .inner
1124            .arm(Interest::Read, std::task::Waker::noop())
1125            .expect_err("arming a shut-down poller must fail");
1126        assert_eq!(err.kind(), io::ErrorKind::Other);
1127    }
1128
1129    /// A poller whose backend cannot wait is dead, and the tasks parked on it
1130    /// must find out. Waking them is not enough: a woken task re-polls its
1131    /// socket, sees `WouldBlock` again, and re-arms — and if the poller still
1132    /// accepts the arm it parks there for good, on a thread that has exited.
1133    /// The fatal path must shut the poller first, so the re-arm is refused
1134    /// and the read completes with an error the connection can act on.
1135    ///
1136    /// The backend is made to fail through the one input it refuses: a
1137    /// change naming an fd past capacity, which `Inner::arm` does not check
1138    /// (`Poller::stream` does) and `Backend::wait` does.
1139    #[tokio::test]
1140    async fn l_a_poller_that_cannot_wait_fails_its_parked_reads() {
1141        let poller = Poller::new("RDT7", ThreadPriority::CaServerHigh).expect("poller");
1142        let (server, _client) = pair();
1143        let mut stream = poller.stream(server).expect("stream");
1144
1145        let read = tokio::spawn(async move {
1146            let mut buf = [0u8; 1];
1147            stream.read(&mut buf).await
1148        });
1149        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1150        assert_eq!(poller.armed_fds(), 1, "the read is parked on the poller");
1151
1152        poller
1153            .inner
1154            .arm(
1155                FD_CAPACITY as RawFd + 1,
1156                Interest::Read,
1157                std::task::Waker::noop(),
1158            )
1159            .expect("the armed set takes it; the backend is what refuses");
1160
1161        let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), read)
1162            .await
1163            .expect("a read parked on a dead poller must not park forever")
1164            .expect("join");
1165        let err = outcome.expect_err("the read fails rather than pretending to wait");
1166        assert_eq!(err.kind(), io::ErrorKind::Other, "{err}");
1167    }
1168
1169    /// The armed set and its changelist, without a poll thread to race the
1170    /// assertions. `Inner::drain_changes` is the poll thread's first act on
1171    /// every turn, so calling it by hand is calling the same code.
1172    fn bare_inner() -> Arc<Inner> {
1173        Arc::new(Inner {
1174            armed: Mutex::new(Armed::default()),
1175            shutdown: AtomicBool::new(false),
1176            backend: ActiveBackend::new("RDTC").expect("backend"),
1177        })
1178    }
1179
1180    fn arm(inner: &Inner, fd: RawFd, interest: Interest) {
1181        inner
1182            .arm(fd, interest, std::task::Waker::noop())
1183            .expect("arm");
1184    }
1185
1186    fn change(fd: RawFd, interest: Interest, action: Action) -> Change {
1187        Change {
1188            fd,
1189            interest,
1190            action,
1191        }
1192    }
1193
1194    /// libevent's first changelist rule, and the one that matters under fd
1195    /// reuse: the add wins over the pending delete, and it is still an add.
1196    #[test]
1197    fn g_an_add_over_a_pending_delete_stays_an_add() {
1198        let inner = bare_inner();
1199        let mut out = Vec::new();
1200
1201        let mut closing = Vec::new();
1202
1203        arm(&inner, 7, Interest::Read);
1204        inner.drain_changes(&mut out, &mut closing);
1205        assert_eq!(out, [change(7, Interest::Read, Action::Arm)]);
1206
1207        // Delivery queues the one-shot delete; the task re-arms before the
1208        // poll thread gets back round to draining it.
1209        assert!(inner.take_ready(7, Interest::Read).is_some());
1210        arm(&inner, 7, Interest::Read);
1211        inner.drain_changes(&mut out, &mut closing);
1212        assert_eq!(
1213            out,
1214            [change(7, Interest::Read, Action::Arm)],
1215            "the re-arm replaces the delete, and does not cancel itself with it"
1216        );
1217    }
1218
1219    /// libevent's second rule: a delete of something the kernel was never told
1220    /// about is dropped rather than submitted — and with nothing to remove
1221    /// first, the socket closes on the spot.
1222    #[test]
1223    fn h_a_delete_of_an_unregistered_interest_is_cancelled() {
1224        let inner = bare_inner();
1225        let mut out = Vec::new();
1226        let mut closing = Vec::new();
1227        let (server, _client) = pair();
1228        let fd = server.as_raw_fd();
1229
1230        arm(&inner, fd, Interest::Read);
1231        // The connection died before the poll thread ever drained the add.
1232        inner.disarm_all(server);
1233        assert!(
1234            !is_open(fd),
1235            "nothing in the kernel to remove: closed at once"
1236        );
1237        inner.drain_changes(&mut out, &mut closing);
1238        assert!(out.is_empty(), "nothing to undo in the kernel: {out:?}");
1239        assert!(
1240            closing.is_empty(),
1241            "nothing left for the poll thread to close"
1242        );
1243        assert_eq!(inner.armed.lock().expect("armed").slots.len(), 0);
1244    }
1245
1246    /// The other side of that rule, and the one-shot invariant's kernel half:
1247    /// what the kernel does hold is deleted, exactly once — and the socket
1248    /// stays open until that delete is out, riding in the same drain.
1249    #[test]
1250    fn i_a_delete_of_a_registered_interest_is_emitted_once() {
1251        let inner = bare_inner();
1252        let mut out = Vec::new();
1253        let mut closing = Vec::new();
1254        let (server, _client) = pair();
1255        let fd = server.as_raw_fd();
1256
1257        arm(&inner, fd, Interest::Write);
1258        inner.drain_changes(&mut out, &mut closing);
1259        inner.disarm_all(server);
1260        assert!(is_open(fd), "registered: the close waits for the delete");
1261        inner.drain_changes(&mut out, &mut closing);
1262        assert_eq!(out, [change(fd, Interest::Write, Action::Disarm)]);
1263        assert_eq!(closing.len(), 1, "the socket rides with its delete");
1264        assert!(is_open(fd), "and is still open until the backend takes it");
1265        // What a backend does once the delete is applied.
1266        closing.clear();
1267        assert!(!is_open(fd));
1268
1269        inner.drain_changes(&mut out, &mut closing);
1270        assert!(
1271            out.is_empty(),
1272            "a drained change is not re-emitted: {out:?}"
1273        );
1274        assert_eq!(inner.armed.lock().expect("armed").slots.len(), 0);
1275    }
1276
1277    /// Boundary: the poller is already shut down when the socket comes back.
1278    /// No poll thread will ever drain the delete, so the close is immediate,
1279    /// and the slot does not keep a socket nobody will take.
1280    #[test]
1281    fn o_a_socket_given_back_to_a_shut_down_poller_closes_at_once() {
1282        let inner = bare_inner();
1283        let mut out = Vec::new();
1284        let mut closing = Vec::new();
1285        let (server, _client) = pair();
1286        let fd = server.as_raw_fd();
1287
1288        arm(&inner, fd, Interest::Read);
1289        inner.drain_changes(&mut out, &mut closing);
1290        inner.retire();
1291        inner.disarm_all(server);
1292        assert!(!is_open(fd), "no thread left to close it later");
1293        assert_eq!(inner.armed.lock().expect("armed").slots.len(), 0);
1294    }
1295
1296    /// The poll thread's exit sweep: a parked task is woken, its next arm is
1297    /// refused, and a socket held for a delete that will never go out is
1298    /// closed — on both exits, since both end in `retire`.
1299    #[test]
1300    fn p_retire_wakes_the_parked_and_closes_what_it_holds() {
1301        struct Flag(AtomicBool);
1302        impl std::task::Wake for Flag {
1303            fn wake(self: Arc<Self>) {
1304                self.0.store(true, Ordering::Release);
1305            }
1306        }
1307        let inner = bare_inner();
1308        let mut out = Vec::new();
1309        let mut closing = Vec::new();
1310        let (parked, _c1) = pair();
1311        let parked_fd = parked.as_raw_fd();
1312        let (dropped, _c2) = pair();
1313        let dropped_fd = dropped.as_raw_fd();
1314
1315        let woken = Arc::new(Flag(AtomicBool::new(false)));
1316        let waker = Waker::from(Arc::clone(&woken));
1317        inner.arm(parked_fd, Interest::Read, &waker).expect("arm");
1318        arm(&inner, dropped_fd, Interest::Read);
1319        inner.drain_changes(&mut out, &mut closing);
1320        inner.disarm_all(dropped);
1321        assert!(
1322            is_open(dropped_fd),
1323            "held for a delete the thread now never sends"
1324        );
1325
1326        inner.retire();
1327        assert!(woken.0.load(Ordering::Acquire), "the parked task is woken");
1328        assert!(
1329            !is_open(dropped_fd),
1330            "the held socket is closed by the sweep"
1331        );
1332        assert!(is_open(parked_fd), "a socket still owned by a task is not");
1333        assert!(
1334            inner.arm(parked_fd, Interest::Read, &waker).is_err(),
1335            "the woken task's re-arm is refused, which is its error"
1336        );
1337        inner.disarm_all(parked);
1338        assert!(!is_open(parked_fd), "and its own drop closes at once");
1339    }
1340
1341    /// The whole point of the changelist: holding connections costs nothing
1342    /// per wait once they are registered. This is the assertion the old
1343    /// whole-armed-set design could not have made.
1344    #[test]
1345    fn j_a_quiet_armed_set_submits_no_changes() {
1346        let inner = bare_inner();
1347        let mut out = Vec::new();
1348
1349        let mut closing = Vec::new();
1350
1351        for fd in 3..11 {
1352            arm(&inner, fd, Interest::Read);
1353        }
1354        inner.drain_changes(&mut out, &mut closing);
1355        assert_eq!(out.len(), 8, "eight arms, eight changes: {out:?}");
1356
1357        inner.drain_changes(&mut out, &mut closing);
1358        assert!(
1359            out.is_empty(),
1360            "eight registrations already placed cost nothing to keep: {out:?}"
1361        );
1362        assert_eq!(inner.armed_fds(), 8);
1363    }
1364
1365    /// Both directions of one fd are one entry in the dirty list and two
1366    /// changes out of it — the boundary the per-fd change record exists to
1367    /// get right.
1368    #[test]
1369    fn k_both_directions_of_one_fd_drain_together() {
1370        let inner = bare_inner();
1371        let mut out = Vec::new();
1372
1373        let mut closing = Vec::new();
1374        let (server, _client) = pair();
1375        let fd = server.as_raw_fd();
1376
1377        arm(&inner, fd, Interest::Read);
1378        arm(&inner, fd, Interest::Write);
1379        assert_eq!(inner.armed.lock().expect("armed").dirty, [fd]);
1380        inner.drain_changes(&mut out, &mut closing);
1381        out.sort_by_key(|c| c.interest == Interest::Write);
1382        assert_eq!(
1383            out,
1384            [
1385                change(fd, Interest::Read, Action::Arm),
1386                change(fd, Interest::Write, Action::Arm)
1387            ]
1388        );
1389
1390        inner.disarm_all(server);
1391        inner.drain_changes(&mut out, &mut closing);
1392        out.sort_by_key(|c| c.interest == Interest::Write);
1393        assert_eq!(
1394            out,
1395            [
1396                change(fd, Interest::Read, Action::Disarm),
1397                change(fd, Interest::Write, Action::Disarm)
1398            ]
1399        );
1400        assert_eq!(closing.len(), 1, "one socket behind the two deletes");
1401    }
1402}