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}