Skip to main content

epics_base_rs/server/
event_queue.rs

1//! The CA server monitor event queue — a port of C `dbEvent.c`'s
2//! `event_user` / `event_que` / `evSubscrip` triple.
3//!
4//! # Why this exists as its own primitive
5//!
6//! The port used to model a monitor as a bounded `mpsc` channel plus a side
7//! "coalesce slot" holding the newest event once the channel filled. That
8//! shape cannot express C's queue, and three review rounds (R8-21, R8-22,
9//! R8-23) found separate divergences that all trace back to it:
10//!
11//! * the slot's value means "newer than everything queued", so a consumer that
12//!   finds it set has to discard the whole backlog — C instead replaces the
13//!   monitor's *last* queued entry in place and keeps the earlier distinct
14//!   ones (R8-22);
15//! * the queue was per-subscription, so C's `nDuplicates` — a field of the
16//!   *queue*, shared by every subscription attached to it — had no home, and
17//!   the EVENTS_OFF drain gate was evaluated per subscription (R8-23).
18//!
19//! So the primitive here is C's, not a patched channel:
20//!
21//! * [`EventUser`] == C `event_user`: one per CA circuit (C: one per
22//!   `db_init_events` client). Owns `flowCtrlMode` (EVENTS_OFF) and the chain
23//!   of queues.
24//! * [`EvQue`] == C `event_que`: a ring of `EVENTQUESIZE` entries shared by up
25//!   to `EVENTQUESIZE/EVENTENTRIES - 1` subscriptions (the `quota` rule,
26//!   `dbEvent.c:450-469`), carrying the queue-level `nDuplicates`.
27//! * `SubQ` == C `evSubscrip`: `npend` (queued entry count), `nreplace`, and
28//!   `pLastLog` — the entry a post *replaces* rather than appending.
29//!
30//! # Invariants (all enforced by [`EvQue`], the single owner)
31//!
32//! * MUST: `n_duplicates == Σ over subscriptions of max(0, npend - 1)`. Only
33//!   the append branch of [`EventSink::post`] raises it and only
34//!   `QueInner::remove_front` lowers it — C `db_queue_event_log`
35//!   (`dbEvent.c:837-839`) and `event_remove` (`dbEvent.c:542-558`).
36//! * MUST: `total_pending == Σ npend`, and `total_pending < size` — the `quota`
37//!   reservation is what guarantees the ring can never fill: past the replace
38//!   threshold each attached subscription can add at most one more entry, and
39//!   attachment is capped so that always fits (C asserts the free slot at
40//!   `dbEvent.c:833`).
41//! * MUST NOT: any pending event live outside `SubQ::events`. There is no side
42//!   slot; a monitor's newest event is `events.back()` — C's `*pLastLog` —
43//!   whether it got there by append or by in-place replace.
44//! * MUST: a subscription that has ever carried a value too wide for C's
45//!   `union native_value` holds at most ONE pending entry (`latest_only`).
46//!   This is what bounds the queue in **bytes**, not just in entries — see
47//!   below.
48//!
49//! # Why an entry bound is not a memory bound
50//!
51//! C's ring bounds entries: `EVENTQUESIZE` of them, `EVENTENTRIES` reserved
52//! per monitor. It gets away with that because an entry is a fixed-size
53//! `db_field_log`. For anything wider than `union native_value` — every array
54//! field, and every `DBF_STRING` field, whose declared 40 bytes exceed the
55//! union's 8 — `db_create_field_log` (`dbEvent.c:726-735`) takes the
56//! `dbfl_type_ref` branch and copies **nothing**:
57//!
58//! ```c
59//! pLog->type = dbfl_type_ref;
60//! /* don't make a copy yet, just reference the field value */
61//! pLog->u.r.field = dbChannelField(chan);
62//! /* indicate field value still owned by record */
63//! pLog->dtor = NULL;
64//! ```
65//!
66//! and `db_queue_event_log` then refuses to queue a second one
67//! (`dbEvent.c:794-800`), because a reference already queued will read the
68//! record's current value when it is finally delivered:
69//!
70//! ```c
71//! /* if we have an event on the queue and both the last
72//!  * event on the queue and the current event reference
73//!  * a record field, simply ignore duplicate events.
74//!  */
75//! if (pevent->npend > 0u
76//!         && !dbfl_has_copy(*pevent->pLastLog)
77//!         && !dbfl_has_copy(pLog)) {
78//!     db_delete_field_log(pLog);
79//!     UNLOCKEVQUE (ev_que);
80//!     return;
81//! }
82//! ```
83//!
84//! So in C a 1 MiB waveform monitor costs one ~100-byte field log, for ever.
85//!
86//! The port cannot store a reference: [`MonitorEvent`] owns its `Snapshot`, and
87//! `dbfl_has_copy` is therefore always true. Ported literally, the entry bound
88//! alone let ONE subscription hold `size - replace_threshold` = 108 whole array
89//! copies before the replace branch engaged — 108 MiB for that same waveform,
90//! per monitor, per circuit. On an embedded target that is not a slow leak but
91//! an allocation failure, and a failed allocation in Rust aborts the process
92//! where C's `freeListCalloc` would merely return NULL.
93//!
94//! Hence `SubQ::latest_only`, C's `useValque == FALSE` reached from the value
95//! rather than from the channel: the first post whose value does not satisfy
96//! [`EpicsValue::queues_by_value`](crate::types::EpicsValue::queues_by_value) latches the subscription into keep-only-the-
97//! latest, and from then on a post with an entry already pending overwrites it
98//! instead of appending. Two consequences, both C's:
99//!
100//! * the subscription's queued bytes are bounded by ONE snapshot, exactly as
101//!   C's are bounded by one field log plus the record's own field;
102//! * the client still receives the newest value, which is what C's surviving
103//!   reference would have read at delivery time.
104//!
105//! The latch is one-way because C's is: `useValque` is decided once, from the
106//! channel's declared element count, and never revisited — a waveform whose
107//! `NORD` happens to fall to 1 does not become queueable in C either.
108//!
109//! # The two decisions, each in exactly one place
110//!
111//! * **Append or replace** — [`EventSink::post`], C `db_queue_event_log`
112//!   (`dbEvent.c:812-852`): with an entry already queued for this monitor AND
113//!   (`flowCtrlMode` OR the ring within `EVENTSPERQUE` of full), overwrite
114//!   `*pLastLog` in place; otherwise append and grow the ring.
115//! * **Drain or suspend** — [`EventReader::recv`] / [`EventReader::try_recv`],
116//!   C `event_read` (`dbEvent.c:932-1014`): suspend only while `flowCtrlMode &&
117//!   nDuplicates == 0`; otherwise drain the queue to empty. Once a drain pass
118//!   starts it runs to `EVENTQEMPTY` without re-checking the gate (C's `while
119//!   (evque[getix] != EVENTQEMPTY)` loop), which is what `draining` tracks.
120//!
121//! Those two are the ONLY ways in and out of the queue: [`EvQue`] hands out no
122//! mutating method of its own, so no caller can enqueue past the replace rule
123//! or dequeue past the EVENTS_OFF gate.
124//!
125//! # Documented deviations from C
126//!
127//! * C's ring is drained in strict ring order by one event task per
128//!   `event_user`; here each subscription has its own reader task (the CA
129//!   server frames every subscription separately), so entries are taken in
130//!   per-subscription FIFO order and the *interleaving* of two subscriptions on
131//!   one circuit is not fixed. Every quantity C's queue behaviour depends on —
132//!   ring occupancy, `nDuplicates`, `quota`, per-monitor `npend`/`pLastLog` —
133//!   is shared and accounted exactly as in C.
134//! * C's early-drop (`dbEvent.c:794-800`) keeps the entry already queued and
135//!   discards the incoming one, because the queued one is a live reference.
136//!   The port's entries are owned copies, so keeping the older one would
137//!   deliver a stale value; the latest-only branch keeps the INCOMING event
138//!   instead. The queue depth, and therefore the memory, is C's either way,
139//!   and so is the value the client ends up seeing.
140//! * That branch does not raise `nreplace`, matching C: C's early-drop is not
141//!   a replacement and `dbel` reports 0 discards for a by-reference monitor.
142//!   The port counts it separately as [`EvQue::ncollapse`].
143//! * When a post replaces `*pLastLog`, C frees the displaced field log and with
144//!   it its `mask`. The port ORs the displaced mask into the survivor so a
145//!   class-narrowing consumer still learns which `DBE_*` classes changed since
146//!   its last delivery (pre-existing deliberate deviation, 446e0d4a); the
147//!   surviving *value* is C's.
148
149use std::collections::HashMap;
150use std::collections::VecDeque;
151use std::sync::Mutex;
152use std::sync::atomic::{AtomicBool, Ordering};
153
154use crate::runtime::sync::{Arc, Notify};
155use crate::server::pv::MonitorEvent;
156
157/// C `EVENTENTRIES` (`dbEvent.c:62`) — ring entries reserved per attached
158/// subscription.
159pub const EVENT_ENTRIES: usize = 4;
160
161/// C `EVENTSPERQUE` (`dbEvent.c:61`) — the ring-space threshold at or below
162/// which a post replaces the monitor's last entry instead of appending, sized
163/// by C from the Ethernet MTU. Operators scale the queue with
164/// `EPICS_CAS_MAX_EVENTS_PER_CHAN`; the default is C's 36, giving C's 144-entry
165/// ring.
166pub fn events_per_que() -> usize {
167    crate::runtime::env::get("EPICS_CAS_MAX_EVENTS_PER_CHAN")
168        .and_then(|v| v.parse::<usize>().ok())
169        .filter(|n| *n >= EVENT_ENTRIES)
170        .unwrap_or(36)
171}
172
173/// C `EVENTQUESIZE` (`dbEvent.c:63`) — total ring entries in one queue.
174pub fn event_que_size() -> usize {
175    EVENT_ENTRIES * events_per_que()
176}
177
178/// C `event_user::flowCtrlMode` (`dbEvent.c:101`) — the circuit-wide EVENTS_OFF
179/// flag. Held behind its own `Arc` so an [`EvQue`] can read it without a
180/// back-pointer to the [`EventUser`] that owns the chain.
181#[derive(Default, Debug)]
182struct FlowCtrl {
183    on: AtomicBool,
184}
185
186impl FlowCtrl {
187    fn is_on(&self) -> bool {
188        self.on.load(Ordering::Acquire)
189    }
190}
191
192/// What [`EventSink::post`] did, for the caller to account for. The queue
193/// applies every change to its own state itself (it is the single owner); this
194/// reports the part that lives outside it — the dropped-monitor-event counter.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum PostOutcome {
197    /// C append branch (`dbEvent.c:832-852`): the event took a new ring entry.
198    /// `first_event` mirrors C's `firstEventFlag` — the ring was empty before
199    /// this post.
200    Appended { first_event: bool },
201    /// C replace branch (`dbEvent.c:812-827`): the monitor already had an entry
202    /// queued and the queue is in flow control or within `EVENTSPERQUE` of full,
203    /// so `*pLastLog` was overwritten. The displaced value is never delivered —
204    /// one lost monitor event (C `nreplace`).
205    Replaced,
206    /// C early-drop branch (`dbEvent.c:794-800`): this subscription keeps only
207    /// its latest entry because it carries values too wide for C's
208    /// `union native_value`, and one was already pending. Depth did not grow.
209    /// Not counted in `nreplace` — C does not count it either.
210    Collapsed,
211    /// The subscription is gone (reader dropped, or never attached). Nothing was
212    /// queued — the `mpsc::Sender::try_send`-on-closed-channel case.
213    Closed,
214}
215
216/// C `evSubscrip` — one subscription's state inside a queue.
217struct SubQ {
218    /// C `npend` is `events.len()`; C `*pLastLog` is `events.back_mut()`. Every
219    /// pending event of this monitor lives here and nowhere else.
220    events: VecDeque<MonitorEvent>,
221    /// C `nreplace` — posts that overwrote `*pLastLog`.
222    nreplace: u64,
223    /// C `useValque == FALSE` (`dbEvent.c:493-500`), latched from the first
224    /// value this subscription carried that does not fit `union native_value`.
225    /// While set, the subscription holds at most one pending entry — the rule
226    /// that bounds the queue in bytes (see the module header).
227    latest_only: bool,
228    /// Posts absorbed by the latest-only rule. C's early-drop keeps no counter
229    /// of its own; this one exists so an operator can see that a wide-value
230    /// monitor is shedding updates rather than silently falling behind.
231    ncollapse: u64,
232    /// The producer row (`EventSink`) is gone: no further posts can arrive. The
233    /// reader still drains what is queued and then sees end-of-stream, exactly
234    /// as an `mpsc::Receiver` does when the last `Sender` drops.
235    producer_gone: bool,
236    /// The reader (`EventReader`) is gone: posts are refused from here on.
237    reader_gone: bool,
238}
239
240impl SubQ {
241    fn new() -> Self {
242        Self {
243            events: VecDeque::new(),
244            nreplace: 0,
245            latest_only: false,
246            ncollapse: 0,
247            producer_gone: false,
248            reader_gone: false,
249        }
250    }
251
252    /// C `*pevent->pLastLog = pLog` — overwrite this monitor's newest queued
253    /// entry in place. Ring occupancy and `nDuplicates` are untouched by
254    /// construction, so both callers (the flow-control replace and the
255    /// latest-only collapse) stay symmetric without touching queue counters.
256    ///
257    /// Requires `!self.events.is_empty()`; both callers test `npend > 0`.
258    fn overwrite_last(&mut self, event: MonitorEvent) {
259        let last = self
260            .events
261            .back_mut()
262            .expect("npend > 0 ⇒ this monitor has a last log");
263        let displaced = last.mask;
264        *last = event;
265        // The displaced VALUE is gone (C frees it); its event class is kept —
266        // see the module's documented deviations.
267        last.mask |= displaced;
268    }
269}
270
271/// The queue's mutable state. Everything here moves under one lock — C's
272/// `LOCKEVQUE`.
273struct QueInner {
274    /// Occupied ring entries, `Σ npend`. C derives this from `putix`/`getix`
275    /// (`ringSpace`); the port counts it because entries are taken in
276    /// per-subscription order (see the module's documented deviations).
277    total_pending: usize,
278    /// C `event_que::nDuplicates` (`dbEvent.c:80`) — entries queued beyond the
279    /// first for their monitor, summed over every subscription attached HERE.
280    /// This is what makes a duplicate on subscription B unblock the drain of
281    /// subscription A's entry under EVENTS_OFF.
282    n_duplicates: usize,
283    /// C `event_read`'s drain loop is in flight: it empties the queue in one
284    /// pass and does not re-consult `flowCtrlMode` per entry.
285    draining: bool,
286    /// Wakers registered by [`EvQue::poll_next`] callers parked on this
287    /// queue. The poll-based twin of the `Notify` waiter list: a consumer
288    /// that multiplexes MANY subscriptions from ONE task (the QSRV group
289    /// drain) parks here instead of holding a `Notified` future per queue.
290    /// Flushed — woken and cleared — by `EvQue::wake_readers`, the single
291    /// owner of reader wakeup, on every transition that can make a parked
292    /// read Ready.
293    poll_wakers: Vec<std::task::Waker>,
294    subs: HashMap<u32, SubQ>,
295    /// C `event_que::quota` (`dbEvent.c:79`) — ring entries reserved by the
296    /// subscriptions attached here, `EVENT_ENTRIES` each. A subscription may
297    /// attach while `quota < size - EVENT_ENTRIES` (`dbEvent.c:453`); this is
298    /// what caps a queue at `size / EVENT_ENTRIES - 1` monitors and guarantees
299    /// the ring cannot fill.
300    quota: usize,
301    size: usize,
302    replace_threshold: usize,
303}
304
305impl QueInner {
306    /// C `ringSpace` (`dbEvent.c:136-147`) — unused ring entries.
307    fn ring_space(&self) -> usize {
308        self.size - self.total_pending
309    }
310
311    /// C `event_remove` (`dbEvent.c:542-558`): take this monitor's oldest entry
312    /// and keep `nDuplicates` / occupancy symmetric. The ONLY removal path —
313    /// the reader, subscription teardown, and queue detach all go through it, so
314    /// no caller can move one counter without the other.
315    fn remove_front(&mut self, sid: u32) -> Option<MonitorEvent> {
316        let sub = self.subs.get_mut(&sid)?;
317        let event = sub.events.pop_front()?;
318        // C: `npend == 1` ⇒ the monitor has no last log any more; otherwise the
319        // entry just removed was one of its duplicates.
320        if !sub.events.is_empty() {
321            debug_assert!(self.n_duplicates >= 1, "nDuplicates underflow");
322            self.n_duplicates -= 1;
323        }
324        self.total_pending -= 1;
325        // C's drain loop ends at `EVENTQEMPTY`.
326        self.draining = self.total_pending > 0;
327        Some(event)
328    }
329
330    /// Drop every entry a departing subscription still holds, then release its
331    /// quota. Symmetric by construction: it drains through `remove_front`
332    /// rather than zeroing counters, so `n_duplicates` / `total_pending` cannot
333    /// drift. C does the same per entry in `event_remove`, and releases the
334    /// quota once the cancelled subscription has drained (`dbEvent.c:999-1002`).
335    fn detach(&mut self, sid: u32) {
336        while self.remove_front(sid).is_some() {}
337        if self.subs.remove(&sid).is_some() {
338            self.quota -= EVENT_ENTRIES;
339        }
340    }
341
342    /// C `db_add_event`'s attach test (`dbEvent.c:451-457`): take this queue if
343    /// it still has room for one more monitor's reservation.
344    fn try_attach(&mut self, sid: u32) -> bool {
345        if self.quota >= self.size - EVENT_ENTRIES {
346            return false;
347        }
348        self.quota += EVENT_ENTRIES;
349        self.subs.insert(sid, SubQ::new());
350        true
351    }
352
353    /// May a reader take an entry right now? C `event_read`'s gate
354    /// (`dbEvent.c:947`), plus the in-pass stickiness of its drain loop.
355    fn may_drain(&self, flow_ctrl_on: bool) -> bool {
356        self.draining || !flow_ctrl_on || self.n_duplicates > 0
357    }
358}
359
360/// C `event_que` (`dbEvent.c:69-82`) — one ring, shared by every subscription
361/// attached to it.
362///
363/// Exposes no mutating method: events enter through [`EventSink::post`] and
364/// leave through [`EventReader::recv`] / [`EventReader::try_recv`], which are
365/// the sole owners of C's two decisions. The accessors below are read-only
366/// views of C's counters for diagnostics and tests.
367pub struct EvQue {
368    flow: Arc<FlowCtrl>,
369    inner: Mutex<QueInner>,
370    /// Broadcast wakeup: a post, a flow-control change, or a producer/reader
371    /// teardown re-arms every reader on this queue. C signals the one
372    /// `ppendsem` per `event_user` for the same reason.
373    wake: Notify,
374}
375
376impl EvQue {
377    fn new(flow: Arc<FlowCtrl>) -> Self {
378        Self {
379            flow,
380            inner: Mutex::new(QueInner {
381                total_pending: 0,
382                n_duplicates: 0,
383                draining: false,
384                poll_wakers: Vec::new(),
385                subs: HashMap::new(),
386                quota: 0,
387                size: event_que_size(),
388                replace_threshold: events_per_que(),
389            }),
390            wake: Notify::new(),
391        }
392    }
393
394    fn lock(&self) -> std::sync::MutexGuard<'_, QueInner> {
395        self.inner
396            .lock()
397            .unwrap_or_else(std::sync::PoisonError::into_inner)
398    }
399
400    /// Wake every reader parked on this queue — the `Notify` waiters that
401    /// [`Self::next`] suspends on AND the [`Self::poll_next`] wakers held in
402    /// [`QueInner::poll_wakers`].
403    ///
404    /// SINGLE OWNER of reader wakeup: every transition that can make a
405    /// previously-parked read Ready (a post, a producer/reader teardown, an
406    /// EVENTS_ON release) MUST come through here and MUST NOT call
407    /// `wake.notify_waiters()` directly, so the two wake mechanisms cannot
408    /// diverge — a site that woke only the `Notify` would strand a
409    /// `poll_next` consumer forever (and on the RTEMS exec backend a task
410    /// whose waker is held nowhere live is dropped outright).
411    fn wake_readers(&self) {
412        let wakers = std::mem::take(&mut self.lock().poll_wakers);
413        self.wake.notify_waiters();
414        for waker in wakers {
415            waker.wake();
416        }
417    }
418
419    /// C `db_queue_event_log` (`dbEvent.c:776-868`).
420    fn post(&self, sid: u32, event: MonitorEvent) -> PostOutcome {
421        let outcome = {
422            let mut q = self.lock();
423            let flow_on = self.flow.is_on();
424            let ring_space = q.ring_space();
425            let size = q.size;
426            let threshold = q.replace_threshold;
427            let Some(sub) = q.subs.get_mut(&sid) else {
428                return PostOutcome::Closed;
429            };
430            if sub.reader_gone {
431                return PostOutcome::Closed;
432            }
433            let npend = sub.events.len();
434            // C `db_add_event`'s `useValque` decision (`dbEvent.c:493-500`),
435            // taken from the value because the port has no channel here. It is
436            // a latch, not a per-post test: C decides once per subscription and
437            // never revisits it.
438            if !event.snapshot.value.queues_by_value() {
439                sub.latest_only = true;
440            }
441            if sub.latest_only && npend > 0 {
442                // C `db_queue_event_log`'s early-drop (`dbEvent.c:794-800`).
443                // C keeps the queued reference and frees the incoming log; the
444                // port keeps the incoming snapshot, because its queued one is a
445                // copy that would deliver a stale value where C's reference
446                // reads the record. Depth is C's — one entry — either way.
447                sub.overwrite_last(event);
448                sub.ncollapse += 1;
449                PostOutcome::Collapsed
450            } else if npend > 0 && (flow_on || ring_space <= threshold) {
451                // C: `db_delete_field_log(*pLastLog); *pLastLog = pLog;` — the
452                // ring does not grow and the earlier distinct entries stay put.
453                sub.overwrite_last(event);
454                sub.nreplace += 1;
455                PostOutcome::Replaced
456            } else {
457                sub.events.push_back(event);
458                // C: an entry appended while the monitor already had one queued
459                // is a duplicate — the queue-level count the EVENTS_OFF gate
460                // reads (`dbEvent.c:837-839`).
461                if npend > 0 {
462                    q.n_duplicates += 1;
463                }
464                q.total_pending += 1;
465                debug_assert!(
466                    q.total_pending < size,
467                    "the quota reservation must keep the ring from filling"
468                );
469                PostOutcome::Appended {
470                    first_event: ring_space == size,
471                }
472            }
473        };
474        if outcome != PostOutcome::Closed {
475            self.wake_readers();
476        }
477        outcome
478    }
479
480    /// C `event_read` (`dbEvent.c:932-1014`), reader half.
481    async fn next(&self, sid: u32) -> Option<MonitorEvent> {
482        loop {
483            // Arm the wakeup BEFORE inspecting the queue: `notify_waiters()`
484            // stores no permit, so a post or an EVENTS_ON landing between the
485            // check and the await must find this waiter already registered.
486            let wake = self.wake.notified();
487            tokio::pin!(wake);
488            wake.as_mut().enable();
489            {
490                let mut q = self.lock();
491                let flow_on = self.flow.is_on();
492                let sub = q.subs.get(&sid)?;
493                let has_entry = !sub.events.is_empty();
494                let producer_gone = sub.producer_gone;
495                if has_entry {
496                    if q.may_drain(flow_on) {
497                        q.draining = true;
498                        return q.remove_front(sid);
499                    }
500                    // Suspended: C's event_read returns without delivering.
501                } else if producer_gone {
502                    return None;
503                }
504            }
505            wake.await;
506        }
507    }
508
509    /// Poll-based [`Self::next`]: the same gate and the same delivery, but
510    /// instead of suspending on the queue's `Notify` it registers `cx`'s
511    /// waker in [`QueInner::poll_wakers`] and returns [`Poll::Pending`](std::task::Poll::Pending).
512    ///
513    /// This is what lets ONE task await MANY subscriptions — the QSRV group
514    /// drain polls each of its member readers in turn and parks once, its
515    /// waker held by every queue it polled. Registration happens under the
516    /// queue lock and every Ready-making mutation wakes through
517    /// [`Self::wake_readers`] under the same lock, so a post landing between
518    /// the check and the `Pending` return cannot be lost.
519    ///
520    /// [`Poll::Ready`](std::task::Poll::Ready)`(None)` matches [`Self::next`]'s `None`: the
521    /// subscription is detached, or its producer is gone and the queue
522    /// drained. An entry withheld by EVENTS_OFF parks exactly where
523    /// [`Self::next`] suspends (`flowCtrlMode && nDuplicates == 0`, no drain
524    /// pass in flight) and is released by the same [`Self::wake_readers`]
525    /// call `flow_ctrl_off` makes.
526    fn poll_next(
527        &self,
528        sid: u32,
529        cx: &mut std::task::Context<'_>,
530    ) -> std::task::Poll<Option<MonitorEvent>> {
531        use std::task::Poll;
532        let mut q = self.lock();
533        let flow_on = self.flow.is_on();
534        let Some(sub) = q.subs.get(&sid) else {
535            return Poll::Ready(None);
536        };
537        let has_entry = !sub.events.is_empty();
538        let producer_gone = sub.producer_gone;
539        if has_entry {
540            if q.may_drain(flow_on) {
541                q.draining = true;
542                return Poll::Ready(q.remove_front(sid));
543            }
544            // Suspended by EVENTS_OFF: park, C's event_read returns without
545            // delivering.
546        } else if producer_gone {
547            return Poll::Ready(None);
548        }
549        if !q.poll_wakers.iter().any(|w| w.will_wake(cx.waker())) {
550            q.poll_wakers.push(cx.waker().clone());
551        }
552        Poll::Pending
553    }
554
555    /// Non-blocking [`Self::next`]: same gate, no suspension.
556    fn try_next(&self, sid: u32) -> Result<MonitorEvent, TryRecvError> {
557        let mut q = self.lock();
558        let flow_on = self.flow.is_on();
559        let Some(sub) = q.subs.get(&sid) else {
560            return Err(TryRecvError::Disconnected);
561        };
562        if sub.events.is_empty() {
563            return Err(if sub.producer_gone {
564                TryRecvError::Disconnected
565            } else {
566                TryRecvError::Empty
567            });
568        }
569        if !q.may_drain(flow_on) {
570            // Suspended by EVENTS_OFF — nothing is deliverable to this monitor.
571            return Err(TryRecvError::Empty);
572        }
573        q.draining = true;
574        q.remove_front(sid).ok_or(TryRecvError::Empty)
575    }
576
577    /// Is this subscription's reader gone (C: the monitor was cancelled)?
578    /// Producer rows are reaped on this, replacing `mpsc::Sender::is_closed`.
579    fn reader_gone(&self, sid: u32) -> bool {
580        self.lock().subs.get(&sid).is_none_or(|s| s.reader_gone)
581    }
582
583    /// The producer row for `sid` is gone: no more posts, but what is already
584    /// queued is still delivered.
585    fn close_producer(&self, sid: u32) {
586        {
587            let mut q = self.lock();
588            let Some(sub) = q.subs.get_mut(&sid) else {
589                return;
590            };
591            sub.producer_gone = true;
592            if sub.reader_gone {
593                q.detach(sid);
594            }
595        }
596        self.wake_readers();
597    }
598
599    /// The reader for `sid` is gone: its queued entries leave the ring through
600    /// the same accounting every other removal uses.
601    fn close_reader(&self, sid: u32) {
602        {
603            let mut q = self.lock();
604            let Some(sub) = q.subs.get_mut(&sid) else {
605                return;
606            };
607            sub.reader_gone = true;
608            q.detach(sid);
609        }
610        self.wake_readers();
611    }
612
613    /// C `nDuplicates` — entries queued beyond the first for their monitor,
614    /// across every subscription on this queue.
615    pub fn n_duplicates(&self) -> usize {
616        self.lock().n_duplicates
617    }
618
619    /// C `nreplace` for one monitor — posts that overwrote its last entry.
620    pub fn nreplace(&self, sid: u32) -> u64 {
621        self.lock().subs.get(&sid).map_or(0, |s| s.nreplace)
622    }
623
624    /// C `npend` for one monitor — entries queued and not yet delivered.
625    pub fn npend(&self, sid: u32) -> usize {
626        self.lock().subs.get(&sid).map_or(0, |s| s.events.len())
627    }
628
629    /// Posts this monitor absorbed under the latest-only rule — the port's
630    /// counter for C's uncounted early-drop (`dbEvent.c:794-800`).
631    pub fn ncollapse(&self, sid: u32) -> u64 {
632        self.lock().subs.get(&sid).map_or(0, |s| s.ncollapse)
633    }
634
635    /// C `useValque == FALSE` for one monitor: it has carried a value too wide
636    /// for `union native_value`, so it keeps only its latest entry. C reports
637    /// the same state as "queueing disabled" in `dbel` (`dbEvent.c:224-226`).
638    pub fn latest_only(&self, sid: u32) -> bool {
639        self.lock().subs.get(&sid).is_some_and(|s| s.latest_only)
640    }
641
642    /// C `quota` — ring entries reserved by the monitors attached here.
643    pub fn quota(&self) -> usize {
644        self.lock().quota
645    }
646
647    /// Everything `dbel` prints about one monitor's queue, read under a
648    /// single `LOCKEVQUE`.
649    ///
650    /// C takes that lock twice — once in its `level > 1` block for
651    /// `ringSpace`, again in its `level > 2` block for `nDuplicates`
652    /// (`dbEvent.c:198-233`) — so its two halves can disagree about a queue
653    /// a producer is filling between them. One snapshot cannot.
654    pub fn report(&self, sid: u32) -> QueReport {
655        let q = self.lock();
656        let sub = q.subs.get(&sid);
657        QueReport {
658            npend: sub.map_or(0, |s| s.events.len()),
659            ring_space: q.ring_space(),
660            ring_size: q.size,
661            nreplace: sub.map_or(0, |s| s.nreplace),
662            latest_only: sub.is_some_and(|s| s.latest_only),
663            n_duplicates: q.n_duplicates,
664        }
665    }
666}
667
668/// One consistent read of a monitor's queue state — the fields C's `dbel`
669/// reaches for through `pevent` and `pevent->ev_que` (`dbEvent.c:181-246`).
670#[derive(Debug, Clone, Copy, PartialEq, Eq)]
671pub struct QueReport {
672    /// C `pevent->npend` — entries queued for this monitor, undelivered.
673    pub npend: usize,
674    /// C `ringSpace(pevent->ev_que)` — unused entries in the shared ring.
675    pub ring_space: usize,
676    /// C `EVENTQUESIZE`, the constant `ringSpace` is compared against to
677    /// decide "queue empty".
678    pub ring_size: usize,
679    /// C `pevent->nreplace` — posts that overwrote this monitor's last entry.
680    pub nreplace: u64,
681    /// C `! pevent->useValque`, which `dbel` reports as "queueing disabled".
682    pub latest_only: bool,
683    /// C `pevent->ev_que->nDuplicates`, shared by every monitor on the queue.
684    pub n_duplicates: usize,
685}
686
687/// C `event_user` (`dbEvent.c:84-105`) — one per CA circuit. Owns the EVENTS_OFF
688/// flag and the chain of queues subscriptions attach to.
689pub struct EventUser {
690    flow: Arc<FlowCtrl>,
691    /// C `firstque` + `nextque`: a subscription attaches to the first queue with
692    /// spare quota, and a new queue is chained when none has
693    /// (`dbEvent.c:450-469`). This — not the client — is the sharing granularity
694    /// of `nDuplicates`.
695    ques: Mutex<Vec<Arc<EvQue>>>,
696}
697
698impl Default for EventUser {
699    fn default() -> Self {
700        Self::new()
701    }
702}
703
704impl EventUser {
705    /// C `db_init_events`.
706    pub fn new() -> Self {
707        let flow = Arc::new(FlowCtrl::default());
708        Self {
709            ques: Mutex::new(vec![Arc::new(EvQue::new(flow.clone()))]),
710            flow,
711        }
712    }
713
714    /// C `db_add_event`'s queue-selection loop (`dbEvent.c:450-469`): walk the
715    /// chain for the first queue whose `quota` still admits a monitor, and chain
716    /// a fresh one only when none does.
717    ///
718    /// The sharing this produces is not an implementation detail — it is where
719    /// `nDuplicates` lives (R8-23). A duplicate queued for any monitor on the
720    /// queue releases the EVENTS_OFF drain of every monitor on it, which
721    /// per-subscription rings cannot express.
722    fn attach_que(&self, sid: u32) -> Arc<EvQue> {
723        let mut ques = self
724            .ques
725            .lock()
726            .unwrap_or_else(std::sync::PoisonError::into_inner);
727        for que in ques.iter() {
728            if que.lock().try_attach(sid) {
729                return que.clone();
730            }
731        }
732        let que = Arc::new(EvQue::new(self.flow.clone()));
733        let attached = que.lock().try_attach(sid);
734        debug_assert!(attached, "a fresh queue must admit its first monitor");
735        ques.push(que.clone());
736        que
737    }
738
739    /// C `db_event_flow_ctrl_mode_on` — EVENTS_OFF. Posts now replace each
740    /// monitor's last queued entry in place, and a reader suspends once its
741    /// queue holds no duplicates.
742    pub fn flow_ctrl_on(&self) {
743        self.flow.on.store(true, Ordering::Release);
744    }
745
746    /// C `db_event_flow_ctrl_mode_off` — EVENTS_ON. Releases every suspended
747    /// reader on this circuit; C posts `ppendsem` for the same reason.
748    pub fn flow_ctrl_off(&self) {
749        self.flow.on.store(false, Ordering::Release);
750        let ques = self
751            .ques
752            .lock()
753            .unwrap_or_else(std::sync::PoisonError::into_inner);
754        for que in ques.iter() {
755            que.wake_readers();
756        }
757    }
758
759    pub fn is_flow_ctrl_on(&self) -> bool {
760        self.flow.is_on()
761    }
762}
763
764/// Why a non-blocking take found nothing. Mirrors `mpsc::error::TryRecvError` so
765/// consumers of the channel this replaced read unchanged.
766#[derive(Debug, Clone, Copy, PartialEq, Eq)]
767pub enum TryRecvError {
768    /// Nothing deliverable to this monitor right now — either its queue is empty
769    /// or EVENTS_OFF is suspending the drain.
770    Empty,
771    /// The producer row is gone and everything queued has been delivered.
772    Disconnected,
773}
774
775impl std::fmt::Display for TryRecvError {
776    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
777        match self {
778            Self::Empty => write!(f, "no monitor event available"),
779            Self::Disconnected => write!(f, "monitor producer gone"),
780        }
781    }
782}
783
784impl std::error::Error for TryRecvError {}
785
786/// The consumer half of one subscription — C's per-monitor view of `event_read`,
787/// and the single owner of the EVENTS_OFF drain-or-suspend decision. Both CA
788/// server monitor loops and every in-process consumer reach the gate through
789/// here, so they cannot disagree about what a pause does.
790///
791/// Dropping it cancels the subscription's queue slot (C `db_cancel_event`),
792/// releasing its ring entries and its quota.
793pub struct EventReader {
794    que: Arc<EvQue>,
795    sid: u32,
796}
797
798impl EventReader {
799    /// Await this subscription's next event, suspending exactly where C's
800    /// `event_read` does (`flowCtrlMode && nDuplicates == 0`, no drain pass in
801    /// flight). `None` = producer gone and queue drained.
802    pub async fn recv(&mut self) -> Option<MonitorEvent> {
803        self.que.next(self.sid).await
804    }
805
806    /// Non-blocking [`Self::recv`].
807    pub fn try_recv(&mut self) -> Result<MonitorEvent, TryRecvError> {
808        self.que.try_next(self.sid)
809    }
810
811    /// Poll-based [`Self::recv`]: `Poll::Ready(None)` where `recv()` returns
812    /// `None`, `Poll::Pending` with `cx`'s waker registered on the queue
813    /// where `recv()` suspends. For consumers that multiplex many
814    /// subscriptions from one task (the QSRV group drain) — the waker stays
815    /// registered until `EvQue::wake_readers` flushes it, so a caller that
816    /// polled several readers and parked is woken by whichever queue changes
817    /// first.
818    pub fn poll_recv(
819        &mut self,
820        cx: &mut std::task::Context<'_>,
821    ) -> std::task::Poll<Option<MonitorEvent>> {
822        self.que.poll_next(self.sid, cx)
823    }
824
825    /// The queue this subscription is attached to — a read-only handle for
826    /// diagnostics and tests (`n_duplicates`, `npend`, `nreplace`).
827    pub fn queue(&self) -> Arc<EvQue> {
828        self.que.clone()
829    }
830
831    /// C `npend` for this subscription.
832    pub fn npend(&self) -> usize {
833        self.que.npend(self.sid)
834    }
835}
836
837impl Drop for EventReader {
838    fn drop(&mut self) {
839        self.que.close_reader(self.sid);
840    }
841}
842
843/// The producer half of one subscription, held by the record / PV that posts to
844/// it. Dropping it is the end-of-stream signal a monitor gets when its channel
845/// goes away.
846pub struct EventSink {
847    que: Arc<EvQue>,
848    sid: u32,
849}
850
851impl EventSink {
852    /// C `db_queue_event_log` for this subscription: append, or replace this
853    /// monitor's last queued entry in place. The single owner of that decision.
854    pub fn post(&self, event: MonitorEvent) -> PostOutcome {
855        self.que.post(self.sid, event)
856    }
857
858    /// The reader is gone — the producer row can be reaped. Replaces
859    /// `mpsc::Sender::is_closed`.
860    pub fn is_closed(&self) -> bool {
861        self.que.reader_gone(self.sid)
862    }
863
864    /// This monitor's queue state, for `dbel`.
865    pub fn report(&self) -> QueReport {
866        self.que.report(self.sid)
867    }
868}
869
870impl Drop for EventSink {
871    fn drop(&mut self) {
872        self.que.close_producer(self.sid);
873    }
874}
875
876/// C `db_add_event`: attach `sid` to `user`'s queue chain and hand back the
877/// producer and consumer halves.
878pub fn attach(user: &EventUser, sid: u32) -> (EventSink, EventReader) {
879    let que = user.attach_que(sid);
880    (
881        EventSink {
882            que: que.clone(),
883            sid,
884        },
885        EventReader { que, sid },
886    )
887}
888
889#[cfg(test)]
890mod tests {
891    use super::*;
892    use crate::server::recgbl::EventMask;
893    use crate::server::snapshot::Snapshot;
894    use crate::types::EpicsValue;
895
896    fn ev(v: i32) -> MonitorEvent {
897        MonitorEvent {
898            snapshot: std::sync::Arc::new(Snapshot::new(
899                EpicsValue::Long(v),
900                0,
901                0,
902                std::time::SystemTime::UNIX_EPOCH,
903            )),
904            origin: 0,
905            mask: EventMask::VALUE,
906        }
907    }
908
909    fn val(e: &MonitorEvent) -> i32 {
910        match e.snapshot.value {
911            EpicsValue::Long(v) => v,
912            ref other => panic!("expected Long, got {other:?}"),
913        }
914    }
915
916    /// Boundary `npend == 0`: C appends (`dbEvent.c:832-852`) — there is no last
917    /// log to replace — even under flow control, and flags the ring's
918    /// empty→non-empty transition (C `firstEventFlag`).
919    #[epics_macros_rs::epics_test]
920    async fn npend_zero_appends_even_under_flow_control() {
921        let user = EventUser::new();
922        user.flow_ctrl_on();
923        let (sink, reader) = attach(&user, 1);
924        assert_eq!(
925            sink.post(ev(1)),
926            PostOutcome::Appended { first_event: true }
927        );
928        assert_eq!(reader.npend(), 1);
929        assert_eq!(
930            reader.queue().n_duplicates(),
931            0,
932            "a monitor's first entry is not a duplicate"
933        );
934    }
935
936    /// Boundary `npend > 0` with flow control ON: C replaces `*pLastLog` in
937    /// place (`dbEvent.c:812-827`). The queue does not grow and no duplicate is
938    /// created — which is exactly why the reader stays suspended.
939    #[epics_macros_rs::epics_test]
940    async fn npend_positive_under_flow_control_replaces_in_place() {
941        let user = EventUser::new();
942        user.flow_ctrl_on();
943        let (sink, mut reader) = attach(&user, 1);
944        sink.post(ev(1));
945        assert_eq!(sink.post(ev(2)), PostOutcome::Replaced);
946        assert_eq!(sink.post(ev(3)), PostOutcome::Replaced);
947        assert_eq!(reader.npend(), 1, "the queue never grew past one entry");
948        assert_eq!(reader.queue().nreplace(1), 2);
949        assert_eq!(reader.queue().n_duplicates(), 0);
950        user.flow_ctrl_off();
951        assert_eq!(
952            val(&reader.recv().await.unwrap()),
953            3,
954            "the latest value survives in the held entry"
955        );
956        assert!(matches!(reader.try_recv(), Err(TryRecvError::Empty)));
957    }
958
959    /// Boundary `npend > 0`, flow control OFF, ring space ABOVE the threshold:
960    /// C appends (`dbEvent.c:832-852`), so distinct updates each get their own
961    /// entry and each is delivered.
962    #[epics_macros_rs::epics_test]
963    async fn ring_space_above_threshold_appends_distinct_entries() {
964        let user = EventUser::new();
965        let (sink, mut reader) = attach(&user, 1);
966        for v in 1..=3 {
967            assert!(matches!(sink.post(ev(v)), PostOutcome::Appended { .. }));
968        }
969        assert_eq!(reader.npend(), 3);
970        assert_eq!(reader.queue().n_duplicates(), 2, "npend 3 ⇒ 2 duplicates");
971        let got: Vec<i32> = (0..3).map(|_| val(&reader.try_recv().unwrap())).collect();
972        assert_eq!(got, vec![1, 2, 3]);
973        assert_eq!(reader.queue().n_duplicates(), 0, "symmetric on drain");
974    }
975
976    /// R8-22 boundary — `npend > 0`, flow control OFF, ring space AT/BELOW the
977    /// threshold: C replaces ONLY the monitor's last entry (`dbEvent.c:812-827`)
978    /// and keeps every earlier distinct entry, so burst delivery is
979    /// {earlier distinct backlog…, coalesced tail}. The old primitive parked the
980    /// newest event in a side slot and the consumer then discarded the whole
981    /// backlog, delivering only the newest.
982    #[epics_macros_rs::epics_test]
983    async fn ring_space_at_threshold_replaces_only_the_last_entry() {
984        let user = EventUser::new();
985        let (sink, mut reader) = attach(&user, 1);
986        let appended = event_que_size() - events_per_que();
987        for v in 0..appended as i32 {
988            assert!(
989                matches!(sink.post(ev(v)), PostOutcome::Appended { .. }),
990                "post {v} must append while ring space is above the threshold"
991            );
992        }
993        assert_eq!(reader.npend(), appended);
994        // Ring space is now AT the threshold: every further post replaces the
995        // tail in place.
996        for v in 100..110 {
997            assert_eq!(sink.post(ev(v)), PostOutcome::Replaced);
998        }
999        assert_eq!(reader.npend(), appended, "the backlog did not grow");
1000        assert_eq!(reader.queue().nreplace(1), 10);
1001        let got: Vec<i32> = (0..appended)
1002            .map(|_| val(&reader.try_recv().unwrap()))
1003            .collect();
1004        let mut want: Vec<i32> = (0..appended as i32 - 1).collect();
1005        want.push(109);
1006        assert_eq!(
1007            got, want,
1008            "earlier distinct entries survive; only the tail coalesced"
1009        );
1010        assert!(matches!(reader.try_recv(), Err(TryRecvError::Empty)));
1011    }
1012
1013    /// The `nDuplicates` invariant survives the removal path that bypasses the
1014    /// reader: a subscription torn down with entries still queued (C
1015    /// `event_remove` per entry, `dbEvent.c:542-558`). Teardown drains through
1016    /// the same accounting the reader uses, so the counters cannot drift.
1017    #[epics_macros_rs::epics_test]
1018    async fn detach_with_queued_entries_keeps_the_duplicate_count_symmetric() {
1019        let user = EventUser::new();
1020        let (sink, reader) = attach(&user, 1);
1021        let que = reader.queue();
1022        for v in 1..=3 {
1023            sink.post(ev(v)); // npend 3 ⇒ 2 duplicates
1024        }
1025        assert_eq!(que.n_duplicates(), 2);
1026        assert_eq!(que.npend(1), 3);
1027        drop(reader);
1028        assert_eq!(que.n_duplicates(), 0, "teardown removed its duplicates");
1029        assert_eq!(que.npend(1), 0, "its entries left the ring");
1030        // The row is gone, so the producer reaps itself on the next post.
1031        assert_eq!(sink.post(ev(4)), PostOutcome::Closed);
1032        assert!(sink.is_closed());
1033    }
1034
1035    /// Producer gone with entries still queued: the reader drains them and only
1036    /// then sees end-of-stream — the `mpsc` contract every consumer was written
1037    /// against.
1038    #[epics_macros_rs::epics_test]
1039    async fn producer_drop_drains_then_ends_the_stream() {
1040        let user = EventUser::new();
1041        let (sink, mut reader) = attach(&user, 1);
1042        sink.post(ev(1));
1043        sink.post(ev(2));
1044        drop(sink);
1045        assert_eq!(val(&reader.recv().await.unwrap()), 1);
1046        assert_eq!(val(&reader.recv().await.unwrap()), 2);
1047        assert!(reader.recv().await.is_none(), "drained ⇒ end of stream");
1048        assert!(matches!(reader.try_recv(), Err(TryRecvError::Disconnected)));
1049    }
1050
1051    /// A reader suspended by EVENTS_OFF wakes on EVENTS_ON without needing a
1052    /// further post (C signals `ppendsem` from `db_event_flow_ctrl_mode_off`).
1053    /// A lost wake here would strand the monitor for good.
1054    #[epics_macros_rs::epics_test]
1055    async fn flow_ctrl_off_wakes_a_suspended_reader() {
1056        let user = Arc::new(EventUser::new());
1057        user.flow_ctrl_on();
1058        let (sink, mut reader) = attach(&user, 1);
1059        sink.post(ev(1));
1060        sink.post(ev(2)); // replaces in place: still one entry, no duplicate
1061        let u2 = user.clone();
1062        let waker = crate::runtime::task::Reactor::current()
1063            .expect("the test driver enters an executor")
1064            .spawn(async move {
1065                crate::runtime::task::yield_now().await;
1066                u2.flow_ctrl_off();
1067            });
1068        let got = crate::runtime::task::timeout(std::time::Duration::from_secs(2), reader.recv())
1069            .await
1070            .expect("EVENTS_ON must wake the suspended reader")
1071            .expect("the held entry is delivered");
1072        waker.await.unwrap();
1073        assert_eq!(val(&got), 2, "the held entry carries the latest value");
1074    }
1075
1076    /// R8-23 boundary — a duplicate on a SIBLING subscription. C's `nDuplicates`
1077    /// is a field of the queue (`dbEvent.c:80`), so `event_read`'s gate
1078    /// (`flowCtrlMode && nDuplicates == 0`, `dbEvent.c:947`) is answered by the
1079    /// queue as a whole: a duplicate queued for monitor B releases the drain of
1080    /// monitor A's entry even though A has no duplicate of its own.
1081    ///
1082    /// The per-subscription queues this replaced evaluated the gate per monitor,
1083    /// so A stayed suspended until EVENTS_ON.
1084    #[epics_macros_rs::epics_test]
1085    async fn duplicate_on_a_sibling_subscription_releases_the_events_off_drain() {
1086        let user = EventUser::new();
1087        let (sink_a, mut reader_a) = attach(&user, 1);
1088        let (sink_b, _reader_b) = attach(&user, 2);
1089        assert!(
1090            Arc::ptr_eq(&reader_a.queue(), &_reader_b.queue()),
1091            "the quota admits both monitors to one queue — C's sharing granularity"
1092        );
1093
1094        sink_a.post(ev(1)); // A: npend 1, no duplicate of its own
1095        sink_b.post(ev(10));
1096        sink_b.post(ev(11)); // B: npend 2 ⇒ the queue holds one duplicate
1097        assert_eq!(reader_a.queue().n_duplicates(), 1);
1098
1099        user.flow_ctrl_on();
1100        let got = crate::runtime::task::timeout(std::time::Duration::from_secs(2), reader_a.recv())
1101            .await
1102            .expect("a duplicate anywhere on the queue must release the drain")
1103            .expect("A's entry is delivered");
1104        assert_eq!(val(&got), 1);
1105    }
1106
1107    /// The complement of the boundary above — EVENTS_OFF with the queue holding
1108    /// NO duplicate on any subscription: every reader on it suspends, and
1109    /// EVENTS_ON releases them all.
1110    #[epics_macros_rs::epics_test]
1111    async fn flow_control_without_duplicates_suspends_every_reader_on_the_queue() {
1112        let user = EventUser::new();
1113        user.flow_ctrl_on();
1114        let (sink_a, mut reader_a) = attach(&user, 1);
1115        let (sink_b, mut reader_b) = attach(&user, 2);
1116        sink_a.post(ev(1));
1117        sink_b.post(ev(2));
1118        assert_eq!(reader_a.queue().n_duplicates(), 0);
1119        assert!(matches!(reader_a.try_recv(), Err(TryRecvError::Empty)));
1120        assert!(matches!(reader_b.try_recv(), Err(TryRecvError::Empty)));
1121        user.flow_ctrl_off();
1122        assert_eq!(val(&reader_a.try_recv().unwrap()), 1);
1123        assert_eq!(val(&reader_b.try_recv().unwrap()), 2);
1124    }
1125
1126    /// Boundary `quota == size - EVENT_ENTRIES`: C chains a new queue rather than
1127    /// overbooking the ring (`dbEvent.c:450-469`), so a circuit's monitors past
1128    /// the cap share a *different* `nDuplicates`.
1129    #[epics_macros_rs::epics_test]
1130    async fn quota_exhaustion_chains_a_second_queue() {
1131        let user = EventUser::new();
1132        let cap = event_que_size() / EVENT_ENTRIES - 1;
1133        let mut held = Vec::new();
1134        for sid in 0..cap as u32 {
1135            held.push(attach(&user, sid));
1136        }
1137        let first = held[0].1.queue();
1138        for (_, reader) in &held {
1139            assert!(Arc::ptr_eq(&reader.queue(), &first), "all within quota");
1140        }
1141        assert_eq!(first.quota(), event_que_size() - EVENT_ENTRIES);
1142
1143        let (_sink, overflow) = attach(&user, cap as u32);
1144        assert!(
1145            !Arc::ptr_eq(&overflow.queue(), &first),
1146            "the {cap}th monitor exhausts the quota; the next one chains a queue"
1147        );
1148        assert_eq!(overflow.queue().quota(), EVENT_ENTRIES);
1149    }
1150
1151    /// C releases the cancelled monitor's quota (`dbEvent.c:999-1002`), so the
1152    /// freed slot is reusable — the next attach lands back on the first queue
1153    /// instead of chaining forever.
1154    #[epics_macros_rs::epics_test]
1155    async fn detaching_a_monitor_releases_its_quota() {
1156        let user = EventUser::new();
1157        let cap = event_que_size() / EVENT_ENTRIES - 1;
1158        let mut held: Vec<_> = (0..cap as u32).map(|sid| attach(&user, sid)).collect();
1159        let first = held[0].1.queue();
1160        assert_eq!(first.quota(), event_que_size() - EVENT_ENTRIES);
1161        held.pop(); // cancel one monitor: sink and reader both go
1162        assert_eq!(first.quota(), event_que_size() - 2 * EVENT_ENTRIES);
1163        let (_sink, reader) = attach(&user, 900);
1164        assert!(
1165            Arc::ptr_eq(&reader.queue(), &first),
1166            "the released quota must be reusable"
1167        );
1168    }
1169
1170    /// Teardown symmetry on a SHARED queue: cancelling one monitor removes only
1171    /// its own duplicates from the queue-level count; its sibling's stay.
1172    #[epics_macros_rs::epics_test]
1173    async fn detaching_one_monitor_leaves_a_siblings_duplicates_counted() {
1174        let user = EventUser::new();
1175        let (sink_a, reader_a) = attach(&user, 1);
1176        let (sink_b, reader_b) = attach(&user, 2);
1177        let que = reader_a.queue();
1178        for v in 1..=3 {
1179            sink_a.post(ev(v)); // A: npend 3 ⇒ 2 duplicates
1180        }
1181        for v in 10..=11 {
1182            sink_b.post(ev(v)); // B: npend 2 ⇒ 1 duplicate
1183        }
1184        assert_eq!(que.n_duplicates(), 3);
1185        drop(reader_a);
1186        drop(sink_a);
1187        assert_eq!(que.n_duplicates(), 1, "only A's duplicates left the ring");
1188        assert_eq!(que.npend(2), 2, "B's entries are untouched");
1189        drop(reader_b);
1190        drop(sink_b);
1191        assert_eq!(que.n_duplicates(), 0);
1192        assert_eq!(que.quota(), 0, "both monitors released their reservation");
1193    }
1194
1195    // -- poll_recv: the poll-based reader the QSRV group drain multiplexes on
1196
1197    /// A waker that counts its wakes, for driving `poll_recv` without a
1198    /// runtime. `std::task::Wake` gives the `RawWaker` plumbing for free.
1199    struct CountWaker(std::sync::atomic::AtomicUsize);
1200
1201    impl std::task::Wake for CountWaker {
1202        fn wake(self: Arc<Self>) {
1203            self.0.fetch_add(1, Ordering::SeqCst);
1204        }
1205    }
1206
1207    fn count_waker() -> (Arc<CountWaker>, std::task::Waker) {
1208        let counter = Arc::new(CountWaker(std::sync::atomic::AtomicUsize::new(0)));
1209        let waker = std::task::Waker::from(counter.clone());
1210        (counter, waker)
1211    }
1212
1213    /// Boundary empty→posted: a parked `poll_recv` registers its waker, a
1214    /// post wakes it exactly through `wake_readers`, and the re-poll drains
1215    /// the entry then parks again. Also proves the waker is NOT re-woken by
1216    /// its own drain (no self-wake loop for the group drain to spin on).
1217    #[test]
1218    fn poll_recv_parks_then_delivers_on_post() {
1219        let user = EventUser::new();
1220        let (sink, mut reader) = attach(&user, 7);
1221        let (counter, waker) = count_waker();
1222        let mut cx = std::task::Context::from_waker(&waker);
1223
1224        assert!(reader.poll_recv(&mut cx).is_pending(), "empty queue parks");
1225        assert_eq!(counter.0.load(Ordering::SeqCst), 0);
1226
1227        sink.post(ev(41));
1228        assert_eq!(
1229            counter.0.load(Ordering::SeqCst),
1230            1,
1231            "the post must flush the registered waker"
1232        );
1233        match reader.poll_recv(&mut cx) {
1234            std::task::Poll::Ready(Some(event)) => assert_eq!(val(&event), 41),
1235            other => panic!("expected the posted event, got {other:?}"),
1236        }
1237        assert!(
1238            reader.poll_recv(&mut cx).is_pending(),
1239            "drained queue parks"
1240        );
1241        // Delivering must not have woken the waker again.
1242        assert_eq!(counter.0.load(Ordering::SeqCst), 1);
1243    }
1244
1245    /// Boundary producer-gone: queued entries still drain through
1246    /// `poll_recv`, then the stream ends with `Ready(None)` — same contract
1247    /// as `recv()` — and the teardown itself wakes a parked poller.
1248    #[test]
1249    fn poll_recv_drains_backlog_then_reports_disconnect() {
1250        let user = EventUser::new();
1251        let (sink, mut reader) = attach(&user, 7);
1252        let (counter, waker) = count_waker();
1253        let mut cx = std::task::Context::from_waker(&waker);
1254
1255        sink.post(ev(1));
1256        sink.post(ev(2));
1257        match reader.poll_recv(&mut cx) {
1258            std::task::Poll::Ready(Some(event)) => assert_eq!(val(&event), 1),
1259            other => panic!("expected first entry, got {other:?}"),
1260        }
1261        match reader.poll_recv(&mut cx) {
1262            std::task::Poll::Ready(Some(event)) => assert_eq!(val(&event), 2),
1263            other => panic!("expected second entry, got {other:?}"),
1264        }
1265        assert!(reader.poll_recv(&mut cx).is_pending());
1266        let woken_before = counter.0.load(Ordering::SeqCst);
1267        drop(sink);
1268        assert!(
1269            counter.0.load(Ordering::SeqCst) > woken_before,
1270            "producer teardown must wake the parked poller"
1271        );
1272        assert!(
1273            matches!(reader.poll_recv(&mut cx), std::task::Poll::Ready(None)),
1274            "producer gone + queue drained ⇒ end of stream"
1275        );
1276    }
1277
1278    /// Boundary EVENTS_OFF: an entry withheld by flow control parks
1279    /// `poll_recv` exactly where `recv()` suspends (`flowCtrlMode &&
1280    /// nDuplicates == 0`), and EVENTS_ON releases it through the same
1281    /// `wake_readers` the `Notify` waiters get.
1282    #[test]
1283    fn poll_recv_respects_events_off_and_wakes_on_events_on() {
1284        let user = EventUser::new();
1285        let (sink, mut reader) = attach(&user, 7);
1286        let (counter, waker) = count_waker();
1287        let mut cx = std::task::Context::from_waker(&waker);
1288
1289        user.flow_ctrl_on();
1290        sink.post(ev(5)); // npend 0 → appends even under flow control
1291        assert!(
1292            reader.poll_recv(&mut cx).is_pending(),
1293            "EVENTS_OFF with no duplicates suspends the poll-based reader too"
1294        );
1295        user.flow_ctrl_off();
1296        assert_eq!(
1297            counter.0.load(Ordering::SeqCst),
1298            1,
1299            "the post preceded registration (woke nobody); EVENTS_ON must wake \
1300             the parked poller"
1301        );
1302        match reader.poll_recv(&mut cx) {
1303            std::task::Poll::Ready(Some(event)) => assert_eq!(val(&event), 5),
1304            other => panic!("expected the withheld entry after EVENTS_ON, got {other:?}"),
1305        }
1306    }
1307
1308    /// A value too wide for C's `union native_value`: a 32-element waveform
1309    /// post, tagged with `v` in element 0 so delivery order is checkable.
1310    fn wide(v: i32) -> MonitorEvent {
1311        let mut arr = vec![0.0f64; 32];
1312        arr[0] = v as f64;
1313        MonitorEvent {
1314            snapshot: std::sync::Arc::new(Snapshot::new(
1315                EpicsValue::DoubleArray(arr),
1316                0,
1317                0,
1318                std::time::SystemTime::UNIX_EPOCH,
1319            )),
1320            origin: 0,
1321            mask: EventMask::VALUE,
1322        }
1323    }
1324
1325    fn wide_val(e: &MonitorEvent) -> i32 {
1326        match e.snapshot.value {
1327            EpicsValue::DoubleArray(ref arr) => arr[0] as i32,
1328            ref other => panic!("expected DoubleArray, got {other:?}"),
1329        }
1330    }
1331
1332    /// The memory bound, at the boundary that used to break it: ring space is
1333    /// ABOVE the replace threshold, which is precisely where a narrow monitor
1334    /// appends (see `ring_space_above_threshold_appends_distinct_entries`).
1335    /// A wide-value monitor must NOT append there — C's early-drop
1336    /// (`dbEvent.c:794-800`) caps it at one entry regardless of ring space,
1337    /// and that cap is what keeps 108 whole array copies from accumulating.
1338    #[epics_macros_rs::epics_test]
1339    async fn wide_value_holds_one_entry_however_much_ring_space_is_free() {
1340        let user = EventUser::new();
1341        let (sink, mut reader) = attach(&user, 1);
1342        assert_eq!(
1343            sink.post(wide(0)),
1344            PostOutcome::Appended { first_event: true },
1345            "npend == 0 appends: C has no queued log to drop against"
1346        );
1347        let posts = event_que_size() * 4;
1348        for v in 1..posts as i32 {
1349            assert_eq!(
1350                sink.post(wide(v)),
1351                PostOutcome::Collapsed,
1352                "post {v} landed on a non-empty wide-value monitor"
1353            );
1354        }
1355        assert_eq!(reader.npend(), 1, "one snapshot's worth of memory, no more");
1356        assert_eq!(reader.queue().n_duplicates(), 0);
1357        assert_eq!(
1358            reader.queue().nreplace(1),
1359            0,
1360            "C's early-drop is not a replacement and raises no nreplace"
1361        );
1362        assert_eq!(reader.queue().ncollapse(1), posts as u64 - 1);
1363        assert_eq!(
1364            wide_val(&reader.recv().await.unwrap()),
1365            posts as i32 - 1,
1366            "the client sees the newest value, as C's surviving reference would"
1367        );
1368        assert!(matches!(reader.try_recv(), Err(TryRecvError::Empty)));
1369    }
1370
1371    /// The latch is one-way, as C's `useValque` is: once a monitor has carried
1372    /// a wide value it keeps only the latest even for a narrow post. Without
1373    /// this a waveform whose element count dips to a scalar would start
1374    /// appending again — C never does, because it decided from the channel's
1375    /// declared element count.
1376    #[epics_macros_rs::epics_test]
1377    async fn wide_value_latch_is_one_way() {
1378        let user = EventUser::new();
1379        let (sink, reader) = attach(&user, 1);
1380        sink.post(wide(0));
1381        assert!(reader.queue().latest_only(1));
1382        assert_eq!(sink.post(ev(7)), PostOutcome::Collapsed);
1383        assert_eq!(sink.post(ev(8)), PostOutcome::Collapsed);
1384        assert_eq!(reader.npend(), 1);
1385        assert!(reader.queue().latest_only(1));
1386    }
1387
1388    /// The other side of the boundary: a narrow backlog already queued when the
1389    /// first wide value arrives. The wide post overwrites the tail rather than
1390    /// appending, so the queue holds at most one wide snapshot and the earlier
1391    /// distinct narrow entries still go out — the invariant is on wide entries,
1392    /// not on depth.
1393    #[epics_macros_rs::epics_test]
1394    async fn wide_post_onto_a_narrow_backlog_takes_the_tail() {
1395        let user = EventUser::new();
1396        let (sink, mut reader) = attach(&user, 1);
1397        for v in 1..=3 {
1398            assert!(matches!(sink.post(ev(v)), PostOutcome::Appended { .. }));
1399        }
1400        assert_eq!(sink.post(wide(9)), PostOutcome::Collapsed);
1401        assert_eq!(reader.npend(), 3, "depth unchanged: the tail was replaced");
1402        assert_eq!(val(&reader.try_recv().unwrap()), 1);
1403        assert_eq!(val(&reader.try_recv().unwrap()), 2);
1404        assert_eq!(
1405            wide_val(&reader.try_recv().unwrap()),
1406            9,
1407            "the third entry is the wide value that displaced ev(3)"
1408        );
1409        // A second wide post can only land on an empty or already-wide tail, so
1410        // two wide snapshots are never pending at once.
1411        sink.post(wide(10));
1412        sink.post(wide(11));
1413        assert_eq!(reader.npend(), 1);
1414    }
1415
1416    /// A monitor that never carries a wide value is untouched by the rule —
1417    /// `latest_only` stays clear and the C append/replace ring is unchanged.
1418    #[epics_macros_rs::epics_test]
1419    async fn narrow_only_monitor_keeps_the_ring_discipline() {
1420        let user = EventUser::new();
1421        let (sink, reader) = attach(&user, 1);
1422        for v in 1..=3 {
1423            assert!(matches!(sink.post(ev(v)), PostOutcome::Appended { .. }));
1424        }
1425        assert!(!reader.queue().latest_only(1));
1426        assert_eq!(reader.queue().ncollapse(1), 0);
1427        assert_eq!(reader.npend(), 3);
1428    }
1429}