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