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//!
45//! # The two decisions, each in exactly one place
46//!
47//! * **Append or replace** — [`EventSink::post`], C `db_queue_event_log`
48//! (`dbEvent.c:806-852`): with an entry already queued for this monitor AND
49//! (`flowCtrlMode` OR the ring within `EVENTSPERQUE` of full), overwrite
50//! `*pLastLog` in place; otherwise append and grow the ring.
51//! * **Drain or suspend** — [`EventReader::recv`] / [`EventReader::try_recv`],
52//! C `event_read` (`dbEvent.c:940-952`): suspend only while `flowCtrlMode &&
53//! nDuplicates == 0`; otherwise drain the queue to empty. Once a drain pass
54//! starts it runs to `EVENTQEMPTY` without re-checking the gate (C's `while
55//! (evque[getix] != EVENTQEMPTY)` loop), which is what `draining` tracks.
56//!
57//! Those two are the ONLY ways in and out of the queue: [`EvQue`] hands out no
58//! mutating method of its own, so no caller can enqueue past the replace rule
59//! or dequeue past the EVENTS_OFF gate.
60//!
61//! # Documented deviations from C
62//!
63//! * C's ring is drained in strict ring order by one event task per
64//! `event_user`; here each subscription has its own reader task (the CA
65//! server frames every subscription separately), so entries are taken in
66//! per-subscription FIFO order and the *interleaving* of two subscriptions on
67//! one circuit is not fixed. Every quantity C's queue behaviour depends on —
68//! ring occupancy, `nDuplicates`, `quota`, per-monitor `npend`/`pLastLog` —
69//! is shared and accounted exactly as in C.
70//! * C's `db_queue_event_log` early-drops a post when both the queued and the
71//! incoming field log are by-reference with no copy (`dbEvent.c:794-800`).
72//! Every event here carries an owned `Snapshot` (C `dbfl_has_copy` is always
73//! true), so that branch cannot be reached.
74//! * When a post replaces `*pLastLog`, C frees the displaced field log and with
75//! it its `mask`. The port ORs the displaced mask into the survivor so a
76//! class-narrowing consumer still learns which `DBE_*` classes changed since
77//! its last delivery (pre-existing deliberate deviation, 446e0d4a); the
78//! surviving *value* is C's.
79
80use std::collections::HashMap;
81use std::collections::VecDeque;
82use std::sync::Mutex;
83use std::sync::atomic::{AtomicBool, Ordering};
84
85use crate::runtime::sync::{Arc, Notify};
86use crate::server::pv::MonitorEvent;
87
88/// C `EVENTENTRIES` (`dbEvent.c:62`) — ring entries reserved per attached
89/// subscription.
90pub const EVENT_ENTRIES: usize = 4;
91
92/// C `EVENTSPERQUE` (`dbEvent.c:61`) — the ring-space threshold at or below
93/// which a post replaces the monitor's last entry instead of appending, sized
94/// by C from the Ethernet MTU. Operators scale the queue with
95/// `EPICS_CAS_MAX_EVENTS_PER_CHAN`; the default is C's 36, giving C's 144-entry
96/// ring.
97pub fn events_per_que() -> usize {
98 crate::runtime::env::get("EPICS_CAS_MAX_EVENTS_PER_CHAN")
99 .and_then(|v| v.parse::<usize>().ok())
100 .filter(|n| *n >= EVENT_ENTRIES)
101 .unwrap_or(36)
102}
103
104/// C `EVENTQUESIZE` (`dbEvent.c:63`) — total ring entries in one queue.
105pub fn event_que_size() -> usize {
106 EVENT_ENTRIES * events_per_que()
107}
108
109/// C `event_user::flowCtrlMode` (`dbEvent.c:101`) — the circuit-wide EVENTS_OFF
110/// flag. Held behind its own `Arc` so an [`EvQue`] can read it without a
111/// back-pointer to the [`EventUser`] that owns the chain.
112#[derive(Default, Debug)]
113struct FlowCtrl {
114 on: AtomicBool,
115}
116
117impl FlowCtrl {
118 fn is_on(&self) -> bool {
119 self.on.load(Ordering::Acquire)
120 }
121}
122
123/// What [`EventSink::post`] did, for the caller to account for. The queue
124/// applies every change to its own state itself (it is the single owner); this
125/// reports the part that lives outside it — the dropped-monitor-event counter.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum PostOutcome {
128 /// C append branch (`dbEvent.c:832-852`): the event took a new ring entry.
129 /// `first_event` mirrors C's `firstEventFlag` — the ring was empty before
130 /// this post.
131 Appended { first_event: bool },
132 /// C replace branch (`dbEvent.c:812-827`): the monitor already had an entry
133 /// queued and the queue is in flow control or within `EVENTSPERQUE` of full,
134 /// so `*pLastLog` was overwritten. The displaced value is never delivered —
135 /// one lost monitor event (C `nreplace`).
136 Replaced,
137 /// The subscription is gone (reader dropped, or never attached). Nothing was
138 /// queued — the `mpsc::Sender::try_send`-on-closed-channel case.
139 Closed,
140}
141
142/// C `evSubscrip` — one subscription's state inside a queue.
143struct SubQ {
144 /// C `npend` is `events.len()`; C `*pLastLog` is `events.back_mut()`. Every
145 /// pending event of this monitor lives here and nowhere else.
146 events: VecDeque<MonitorEvent>,
147 /// C `nreplace` — posts that overwrote `*pLastLog`.
148 nreplace: u64,
149 /// The producer row (`EventSink`) is gone: no further posts can arrive. The
150 /// reader still drains what is queued and then sees end-of-stream, exactly
151 /// as an `mpsc::Receiver` does when the last `Sender` drops.
152 producer_gone: bool,
153 /// The reader (`EventReader`) is gone: posts are refused from here on.
154 reader_gone: bool,
155}
156
157impl SubQ {
158 fn new() -> Self {
159 Self {
160 events: VecDeque::new(),
161 nreplace: 0,
162 producer_gone: false,
163 reader_gone: false,
164 }
165 }
166}
167
168/// The queue's mutable state. Everything here moves under one lock — C's
169/// `LOCKEVQUE`.
170struct QueInner {
171 /// Occupied ring entries, `Σ npend`. C derives this from `putix`/`getix`
172 /// (`ringSpace`); the port counts it because entries are taken in
173 /// per-subscription order (see the module's documented deviations).
174 total_pending: usize,
175 /// C `event_que::nDuplicates` (`dbEvent.c:80`) — entries queued beyond the
176 /// first for their monitor, summed over every subscription attached HERE.
177 /// This is what makes a duplicate on subscription B unblock the drain of
178 /// subscription A's entry under EVENTS_OFF.
179 n_duplicates: usize,
180 /// C `event_read`'s drain loop is in flight: it empties the queue in one
181 /// pass and does not re-consult `flowCtrlMode` per entry.
182 draining: bool,
183 subs: HashMap<u32, SubQ>,
184 /// C `event_que::quota` (`dbEvent.c:79`) — ring entries reserved by the
185 /// subscriptions attached here, `EVENT_ENTRIES` each. A subscription may
186 /// attach while `quota < size - EVENT_ENTRIES` (`dbEvent.c:453`); this is
187 /// what caps a queue at `size / EVENT_ENTRIES - 1` monitors and guarantees
188 /// the ring cannot fill.
189 quota: usize,
190 size: usize,
191 replace_threshold: usize,
192}
193
194impl QueInner {
195 /// C `ringSpace` (`dbEvent.c:136-147`) — unused ring entries.
196 fn ring_space(&self) -> usize {
197 self.size - self.total_pending
198 }
199
200 /// C `event_remove` (`dbEvent.c:542-558`): take this monitor's oldest entry
201 /// and keep `nDuplicates` / occupancy symmetric. The ONLY removal path —
202 /// the reader, subscription teardown, and queue detach all go through it, so
203 /// no caller can move one counter without the other.
204 fn remove_front(&mut self, sid: u32) -> Option<MonitorEvent> {
205 let sub = self.subs.get_mut(&sid)?;
206 let event = sub.events.pop_front()?;
207 // C: `npend == 1` ⇒ the monitor has no last log any more; otherwise the
208 // entry just removed was one of its duplicates.
209 if !sub.events.is_empty() {
210 debug_assert!(self.n_duplicates >= 1, "nDuplicates underflow");
211 self.n_duplicates -= 1;
212 }
213 self.total_pending -= 1;
214 // C's drain loop ends at `EVENTQEMPTY`.
215 self.draining = self.total_pending > 0;
216 Some(event)
217 }
218
219 /// Drop every entry a departing subscription still holds, then release its
220 /// quota. Symmetric by construction: it drains through `remove_front`
221 /// rather than zeroing counters, so `n_duplicates` / `total_pending` cannot
222 /// drift. C does the same per entry in `event_remove`, and releases the
223 /// quota once the cancelled subscription has drained (`dbEvent.c:999-1002`).
224 fn detach(&mut self, sid: u32) {
225 while self.remove_front(sid).is_some() {}
226 if self.subs.remove(&sid).is_some() {
227 self.quota -= EVENT_ENTRIES;
228 }
229 }
230
231 /// C `db_add_event`'s attach test (`dbEvent.c:451-457`): take this queue if
232 /// it still has room for one more monitor's reservation.
233 fn try_attach(&mut self, sid: u32) -> bool {
234 if self.quota >= self.size - EVENT_ENTRIES {
235 return false;
236 }
237 self.quota += EVENT_ENTRIES;
238 self.subs.insert(sid, SubQ::new());
239 true
240 }
241
242 /// May a reader take an entry right now? C `event_read`'s gate
243 /// (`dbEvent.c:947`), plus the in-pass stickiness of its drain loop.
244 fn may_drain(&self, flow_ctrl_on: bool) -> bool {
245 self.draining || !flow_ctrl_on || self.n_duplicates > 0
246 }
247}
248
249/// C `event_que` (`dbEvent.c:69-82`) — one ring, shared by every subscription
250/// attached to it.
251///
252/// Exposes no mutating method: events enter through [`EventSink::post`] and
253/// leave through [`EventReader::recv`] / [`EventReader::try_recv`], which are
254/// the sole owners of C's two decisions. The accessors below are read-only
255/// views of C's counters for diagnostics and tests.
256pub struct EvQue {
257 flow: Arc<FlowCtrl>,
258 inner: Mutex<QueInner>,
259 /// Broadcast wakeup: a post, a flow-control change, or a producer/reader
260 /// teardown re-arms every reader on this queue. C signals the one
261 /// `ppendsem` per `event_user` for the same reason.
262 wake: Notify,
263}
264
265impl EvQue {
266 fn new(flow: Arc<FlowCtrl>) -> Self {
267 Self {
268 flow,
269 inner: Mutex::new(QueInner {
270 total_pending: 0,
271 n_duplicates: 0,
272 draining: false,
273 subs: HashMap::new(),
274 quota: 0,
275 size: event_que_size(),
276 replace_threshold: events_per_que(),
277 }),
278 wake: Notify::new(),
279 }
280 }
281
282 fn lock(&self) -> std::sync::MutexGuard<'_, QueInner> {
283 self.inner
284 .lock()
285 .unwrap_or_else(std::sync::PoisonError::into_inner)
286 }
287
288 /// C `db_queue_event_log` (`dbEvent.c:776-868`).
289 fn post(&self, sid: u32, event: MonitorEvent) -> PostOutcome {
290 let outcome = {
291 let mut q = self.lock();
292 let flow_on = self.flow.is_on();
293 let ring_space = q.ring_space();
294 let size = q.size;
295 let threshold = q.replace_threshold;
296 let Some(sub) = q.subs.get_mut(&sid) else {
297 return PostOutcome::Closed;
298 };
299 if sub.reader_gone {
300 return PostOutcome::Closed;
301 }
302 let npend = sub.events.len();
303 if npend > 0 && (flow_on || ring_space <= threshold) {
304 // C: `db_delete_field_log(*pLastLog); *pLastLog = pLog;` — the
305 // ring does not grow and the earlier distinct entries stay put.
306 let last = sub
307 .events
308 .back_mut()
309 .expect("npend > 0 ⇒ this monitor has a last log");
310 let displaced = last.mask;
311 *last = event;
312 // The displaced VALUE is gone (C frees it); its event class is
313 // kept — see the module's documented deviations.
314 last.mask |= displaced;
315 sub.nreplace += 1;
316 PostOutcome::Replaced
317 } else {
318 sub.events.push_back(event);
319 // C: an entry appended while the monitor already had one queued
320 // is a duplicate — the queue-level count the EVENTS_OFF gate
321 // reads (`dbEvent.c:837-839`).
322 if npend > 0 {
323 q.n_duplicates += 1;
324 }
325 q.total_pending += 1;
326 debug_assert!(
327 q.total_pending < size,
328 "the quota reservation must keep the ring from filling"
329 );
330 PostOutcome::Appended {
331 first_event: ring_space == size,
332 }
333 }
334 };
335 if outcome != PostOutcome::Closed {
336 self.wake.notify_waiters();
337 }
338 outcome
339 }
340
341 /// C `event_read` (`dbEvent.c:932-1014`), reader half.
342 async fn next(&self, sid: u32) -> Option<MonitorEvent> {
343 loop {
344 // Arm the wakeup BEFORE inspecting the queue: `notify_waiters()`
345 // stores no permit, so a post or an EVENTS_ON landing between the
346 // check and the await must find this waiter already registered.
347 let wake = self.wake.notified();
348 tokio::pin!(wake);
349 wake.as_mut().enable();
350 {
351 let mut q = self.lock();
352 let flow_on = self.flow.is_on();
353 let sub = q.subs.get(&sid)?;
354 let has_entry = !sub.events.is_empty();
355 let producer_gone = sub.producer_gone;
356 if has_entry {
357 if q.may_drain(flow_on) {
358 q.draining = true;
359 return q.remove_front(sid);
360 }
361 // Suspended: C's event_read returns without delivering.
362 } else if producer_gone {
363 return None;
364 }
365 }
366 wake.await;
367 }
368 }
369
370 /// Non-blocking [`Self::next`]: same gate, no suspension.
371 fn try_next(&self, sid: u32) -> Result<MonitorEvent, TryRecvError> {
372 let mut q = self.lock();
373 let flow_on = self.flow.is_on();
374 let Some(sub) = q.subs.get(&sid) else {
375 return Err(TryRecvError::Disconnected);
376 };
377 if sub.events.is_empty() {
378 return Err(if sub.producer_gone {
379 TryRecvError::Disconnected
380 } else {
381 TryRecvError::Empty
382 });
383 }
384 if !q.may_drain(flow_on) {
385 // Suspended by EVENTS_OFF — nothing is deliverable to this monitor.
386 return Err(TryRecvError::Empty);
387 }
388 q.draining = true;
389 q.remove_front(sid).ok_or(TryRecvError::Empty)
390 }
391
392 /// Is this subscription's reader gone (C: the monitor was cancelled)?
393 /// Producer rows are reaped on this, replacing `mpsc::Sender::is_closed`.
394 fn reader_gone(&self, sid: u32) -> bool {
395 self.lock().subs.get(&sid).is_none_or(|s| s.reader_gone)
396 }
397
398 /// The producer row for `sid` is gone: no more posts, but what is already
399 /// queued is still delivered.
400 fn close_producer(&self, sid: u32) {
401 {
402 let mut q = self.lock();
403 let Some(sub) = q.subs.get_mut(&sid) else {
404 return;
405 };
406 sub.producer_gone = true;
407 if sub.reader_gone {
408 q.detach(sid);
409 }
410 }
411 self.wake.notify_waiters();
412 }
413
414 /// The reader for `sid` is gone: its queued entries leave the ring through
415 /// the same accounting every other removal uses.
416 fn close_reader(&self, sid: u32) {
417 {
418 let mut q = self.lock();
419 let Some(sub) = q.subs.get_mut(&sid) else {
420 return;
421 };
422 sub.reader_gone = true;
423 q.detach(sid);
424 }
425 self.wake.notify_waiters();
426 }
427
428 /// C `nDuplicates` — entries queued beyond the first for their monitor,
429 /// across every subscription on this queue.
430 pub fn n_duplicates(&self) -> usize {
431 self.lock().n_duplicates
432 }
433
434 /// C `nreplace` for one monitor — posts that overwrote its last entry.
435 pub fn nreplace(&self, sid: u32) -> u64 {
436 self.lock().subs.get(&sid).map_or(0, |s| s.nreplace)
437 }
438
439 /// C `npend` for one monitor — entries queued and not yet delivered.
440 pub fn npend(&self, sid: u32) -> usize {
441 self.lock().subs.get(&sid).map_or(0, |s| s.events.len())
442 }
443
444 /// C `quota` — ring entries reserved by the monitors attached here.
445 pub fn quota(&self) -> usize {
446 self.lock().quota
447 }
448}
449
450/// C `event_user` (`dbEvent.c:84-105`) — one per CA circuit. Owns the EVENTS_OFF
451/// flag and the chain of queues subscriptions attach to.
452pub struct EventUser {
453 flow: Arc<FlowCtrl>,
454 /// C `firstque` + `nextque`: a subscription attaches to the first queue with
455 /// spare quota, and a new queue is chained when none has
456 /// (`dbEvent.c:446-469`). This — not the client — is the sharing granularity
457 /// of `nDuplicates`.
458 ques: Mutex<Vec<Arc<EvQue>>>,
459}
460
461impl Default for EventUser {
462 fn default() -> Self {
463 Self::new()
464 }
465}
466
467impl EventUser {
468 /// C `db_init_events`.
469 pub fn new() -> Self {
470 let flow = Arc::new(FlowCtrl::default());
471 Self {
472 ques: Mutex::new(vec![Arc::new(EvQue::new(flow.clone()))]),
473 flow,
474 }
475 }
476
477 /// C `db_add_event`'s queue-selection loop (`dbEvent.c:446-469`): walk the
478 /// chain for the first queue whose `quota` still admits a monitor, and chain
479 /// a fresh one only when none does.
480 ///
481 /// The sharing this produces is not an implementation detail — it is where
482 /// `nDuplicates` lives (R8-23). A duplicate queued for any monitor on the
483 /// queue releases the EVENTS_OFF drain of every monitor on it, which
484 /// per-subscription rings cannot express.
485 fn attach_que(&self, sid: u32) -> Arc<EvQue> {
486 let mut ques = self
487 .ques
488 .lock()
489 .unwrap_or_else(std::sync::PoisonError::into_inner);
490 for que in ques.iter() {
491 if que.lock().try_attach(sid) {
492 return que.clone();
493 }
494 }
495 let que = Arc::new(EvQue::new(self.flow.clone()));
496 let attached = que.lock().try_attach(sid);
497 debug_assert!(attached, "a fresh queue must admit its first monitor");
498 ques.push(que.clone());
499 que
500 }
501
502 /// C `db_event_flow_ctrl_mode_on` — EVENTS_OFF. Posts now replace each
503 /// monitor's last queued entry in place, and a reader suspends once its
504 /// queue holds no duplicates.
505 pub fn flow_ctrl_on(&self) {
506 self.flow.on.store(true, Ordering::Release);
507 }
508
509 /// C `db_event_flow_ctrl_mode_off` — EVENTS_ON. Releases every suspended
510 /// reader on this circuit; C posts `ppendsem` for the same reason.
511 pub fn flow_ctrl_off(&self) {
512 self.flow.on.store(false, Ordering::Release);
513 let ques = self
514 .ques
515 .lock()
516 .unwrap_or_else(std::sync::PoisonError::into_inner);
517 for que in ques.iter() {
518 que.wake.notify_waiters();
519 }
520 }
521
522 pub fn is_flow_ctrl_on(&self) -> bool {
523 self.flow.is_on()
524 }
525}
526
527/// Why a non-blocking take found nothing. Mirrors `mpsc::error::TryRecvError` so
528/// consumers of the channel this replaced read unchanged.
529#[derive(Debug, Clone, Copy, PartialEq, Eq)]
530pub enum TryRecvError {
531 /// Nothing deliverable to this monitor right now — either its queue is empty
532 /// or EVENTS_OFF is suspending the drain.
533 Empty,
534 /// The producer row is gone and everything queued has been delivered.
535 Disconnected,
536}
537
538impl std::fmt::Display for TryRecvError {
539 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540 match self {
541 Self::Empty => write!(f, "no monitor event available"),
542 Self::Disconnected => write!(f, "monitor producer gone"),
543 }
544 }
545}
546
547impl std::error::Error for TryRecvError {}
548
549/// The consumer half of one subscription — C's per-monitor view of `event_read`,
550/// and the single owner of the EVENTS_OFF drain-or-suspend decision. Both CA
551/// server monitor loops and every in-process consumer reach the gate through
552/// here, so they cannot disagree about what a pause does.
553///
554/// Dropping it cancels the subscription's queue slot (C `db_cancel_event`),
555/// releasing its ring entries and its quota.
556pub struct EventReader {
557 que: Arc<EvQue>,
558 sid: u32,
559}
560
561impl EventReader {
562 /// Await this subscription's next event, suspending exactly where C's
563 /// `event_read` does (`flowCtrlMode && nDuplicates == 0`, no drain pass in
564 /// flight). `None` = producer gone and queue drained.
565 pub async fn recv(&mut self) -> Option<MonitorEvent> {
566 self.que.next(self.sid).await
567 }
568
569 /// Non-blocking [`Self::recv`].
570 pub fn try_recv(&mut self) -> Result<MonitorEvent, TryRecvError> {
571 self.que.try_next(self.sid)
572 }
573
574 /// The queue this subscription is attached to — a read-only handle for
575 /// diagnostics and tests (`n_duplicates`, `npend`, `nreplace`).
576 pub fn queue(&self) -> Arc<EvQue> {
577 self.que.clone()
578 }
579
580 /// C `npend` for this subscription.
581 pub fn npend(&self) -> usize {
582 self.que.npend(self.sid)
583 }
584}
585
586impl Drop for EventReader {
587 fn drop(&mut self) {
588 self.que.close_reader(self.sid);
589 }
590}
591
592/// The producer half of one subscription, held by the record / PV that posts to
593/// it. Dropping it is the end-of-stream signal a monitor gets when its channel
594/// goes away.
595pub struct EventSink {
596 que: Arc<EvQue>,
597 sid: u32,
598}
599
600impl EventSink {
601 /// C `db_queue_event_log` for this subscription: append, or replace this
602 /// monitor's last queued entry in place. The single owner of that decision.
603 pub fn post(&self, event: MonitorEvent) -> PostOutcome {
604 self.que.post(self.sid, event)
605 }
606
607 /// The reader is gone — the producer row can be reaped. Replaces
608 /// `mpsc::Sender::is_closed`.
609 pub fn is_closed(&self) -> bool {
610 self.que.reader_gone(self.sid)
611 }
612}
613
614impl Drop for EventSink {
615 fn drop(&mut self) {
616 self.que.close_producer(self.sid);
617 }
618}
619
620/// C `db_add_event`: attach `sid` to `user`'s queue chain and hand back the
621/// producer and consumer halves.
622pub fn attach(user: &EventUser, sid: u32) -> (EventSink, EventReader) {
623 let que = user.attach_que(sid);
624 (
625 EventSink {
626 que: que.clone(),
627 sid,
628 },
629 EventReader { que, sid },
630 )
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636 use crate::server::recgbl::EventMask;
637 use crate::server::snapshot::Snapshot;
638 use crate::types::EpicsValue;
639
640 fn ev(v: i32) -> MonitorEvent {
641 MonitorEvent {
642 snapshot: Snapshot::new(EpicsValue::Long(v), 0, 0, std::time::SystemTime::UNIX_EPOCH),
643 origin: 0,
644 mask: EventMask::VALUE,
645 }
646 }
647
648 fn val(e: &MonitorEvent) -> i32 {
649 match e.snapshot.value {
650 EpicsValue::Long(v) => v,
651 ref other => panic!("expected Long, got {other:?}"),
652 }
653 }
654
655 /// Boundary `npend == 0`: C appends (`dbEvent.c:832-852`) — there is no last
656 /// log to replace — even under flow control, and flags the ring's
657 /// empty→non-empty transition (C `firstEventFlag`).
658 #[tokio::test]
659 async fn npend_zero_appends_even_under_flow_control() {
660 let user = EventUser::new();
661 user.flow_ctrl_on();
662 let (sink, reader) = attach(&user, 1);
663 assert_eq!(
664 sink.post(ev(1)),
665 PostOutcome::Appended { first_event: true }
666 );
667 assert_eq!(reader.npend(), 1);
668 assert_eq!(
669 reader.queue().n_duplicates(),
670 0,
671 "a monitor's first entry is not a duplicate"
672 );
673 }
674
675 /// Boundary `npend > 0` with flow control ON: C replaces `*pLastLog` in
676 /// place (`dbEvent.c:812-820`). The queue does not grow and no duplicate is
677 /// created — which is exactly why the reader stays suspended.
678 #[tokio::test]
679 async fn npend_positive_under_flow_control_replaces_in_place() {
680 let user = EventUser::new();
681 user.flow_ctrl_on();
682 let (sink, mut reader) = attach(&user, 1);
683 sink.post(ev(1));
684 assert_eq!(sink.post(ev(2)), PostOutcome::Replaced);
685 assert_eq!(sink.post(ev(3)), PostOutcome::Replaced);
686 assert_eq!(reader.npend(), 1, "the queue never grew past one entry");
687 assert_eq!(reader.queue().nreplace(1), 2);
688 assert_eq!(reader.queue().n_duplicates(), 0);
689 user.flow_ctrl_off();
690 assert_eq!(
691 val(&reader.recv().await.unwrap()),
692 3,
693 "the latest value survives in the held entry"
694 );
695 assert!(matches!(reader.try_recv(), Err(TryRecvError::Empty)));
696 }
697
698 /// Boundary `npend > 0`, flow control OFF, ring space ABOVE the threshold:
699 /// C appends (`dbEvent.c:832-852`), so distinct updates each get their own
700 /// entry and each is delivered.
701 #[tokio::test]
702 async fn ring_space_above_threshold_appends_distinct_entries() {
703 let user = EventUser::new();
704 let (sink, mut reader) = attach(&user, 1);
705 for v in 1..=3 {
706 assert!(matches!(sink.post(ev(v)), PostOutcome::Appended { .. }));
707 }
708 assert_eq!(reader.npend(), 3);
709 assert_eq!(reader.queue().n_duplicates(), 2, "npend 3 ⇒ 2 duplicates");
710 let got: Vec<i32> = (0..3).map(|_| val(&reader.try_recv().unwrap())).collect();
711 assert_eq!(got, vec![1, 2, 3]);
712 assert_eq!(reader.queue().n_duplicates(), 0, "symmetric on drain");
713 }
714
715 /// R8-22 boundary — `npend > 0`, flow control OFF, ring space AT/BELOW the
716 /// threshold: C replaces ONLY the monitor's last entry (`dbEvent.c:812-820`)
717 /// and keeps every earlier distinct entry, so burst delivery is
718 /// {earlier distinct backlog…, coalesced tail}. The old primitive parked the
719 /// newest event in a side slot and the consumer then discarded the whole
720 /// backlog, delivering only the newest.
721 #[tokio::test]
722 async fn ring_space_at_threshold_replaces_only_the_last_entry() {
723 let user = EventUser::new();
724 let (sink, mut reader) = attach(&user, 1);
725 let appended = event_que_size() - events_per_que();
726 for v in 0..appended as i32 {
727 assert!(
728 matches!(sink.post(ev(v)), PostOutcome::Appended { .. }),
729 "post {v} must append while ring space is above the threshold"
730 );
731 }
732 assert_eq!(reader.npend(), appended);
733 // Ring space is now AT the threshold: every further post replaces the
734 // tail in place.
735 for v in 100..110 {
736 assert_eq!(sink.post(ev(v)), PostOutcome::Replaced);
737 }
738 assert_eq!(reader.npend(), appended, "the backlog did not grow");
739 assert_eq!(reader.queue().nreplace(1), 10);
740 let got: Vec<i32> = (0..appended)
741 .map(|_| val(&reader.try_recv().unwrap()))
742 .collect();
743 let mut want: Vec<i32> = (0..appended as i32 - 1).collect();
744 want.push(109);
745 assert_eq!(
746 got, want,
747 "earlier distinct entries survive; only the tail coalesced"
748 );
749 assert!(matches!(reader.try_recv(), Err(TryRecvError::Empty)));
750 }
751
752 /// The `nDuplicates` invariant survives the removal path that bypasses the
753 /// reader: a subscription torn down with entries still queued (C
754 /// `event_remove` per entry, `dbEvent.c:542-558`). Teardown drains through
755 /// the same accounting the reader uses, so the counters cannot drift.
756 #[tokio::test]
757 async fn detach_with_queued_entries_keeps_the_duplicate_count_symmetric() {
758 let user = EventUser::new();
759 let (sink, reader) = attach(&user, 1);
760 let que = reader.queue();
761 for v in 1..=3 {
762 sink.post(ev(v)); // npend 3 ⇒ 2 duplicates
763 }
764 assert_eq!(que.n_duplicates(), 2);
765 assert_eq!(que.npend(1), 3);
766 drop(reader);
767 assert_eq!(que.n_duplicates(), 0, "teardown removed its duplicates");
768 assert_eq!(que.npend(1), 0, "its entries left the ring");
769 // The row is gone, so the producer reaps itself on the next post.
770 assert_eq!(sink.post(ev(4)), PostOutcome::Closed);
771 assert!(sink.is_closed());
772 }
773
774 /// Producer gone with entries still queued: the reader drains them and only
775 /// then sees end-of-stream — the `mpsc` contract every consumer was written
776 /// against.
777 #[tokio::test]
778 async fn producer_drop_drains_then_ends_the_stream() {
779 let user = EventUser::new();
780 let (sink, mut reader) = attach(&user, 1);
781 sink.post(ev(1));
782 sink.post(ev(2));
783 drop(sink);
784 assert_eq!(val(&reader.recv().await.unwrap()), 1);
785 assert_eq!(val(&reader.recv().await.unwrap()), 2);
786 assert!(reader.recv().await.is_none(), "drained ⇒ end of stream");
787 assert!(matches!(reader.try_recv(), Err(TryRecvError::Disconnected)));
788 }
789
790 /// A reader suspended by EVENTS_OFF wakes on EVENTS_ON without needing a
791 /// further post (C signals `ppendsem` from `db_event_flow_ctrl_mode_off`).
792 /// A lost wake here would strand the monitor for good.
793 #[tokio::test]
794 async fn flow_ctrl_off_wakes_a_suspended_reader() {
795 let user = Arc::new(EventUser::new());
796 user.flow_ctrl_on();
797 let (sink, mut reader) = attach(&user, 1);
798 sink.post(ev(1));
799 sink.post(ev(2)); // replaces in place: still one entry, no duplicate
800 let u2 = user.clone();
801 let waker = tokio::spawn(async move {
802 tokio::task::yield_now().await;
803 u2.flow_ctrl_off();
804 });
805 let got = tokio::time::timeout(std::time::Duration::from_secs(2), reader.recv())
806 .await
807 .expect("EVENTS_ON must wake the suspended reader")
808 .expect("the held entry is delivered");
809 waker.await.unwrap();
810 assert_eq!(val(&got), 2, "the held entry carries the latest value");
811 }
812
813 /// R8-23 boundary — a duplicate on a SIBLING subscription. C's `nDuplicates`
814 /// is a field of the queue (`dbEvent.c:80`), so `event_read`'s gate
815 /// (`flowCtrlMode && nDuplicates == 0`, `dbEvent.c:947`) is answered by the
816 /// queue as a whole: a duplicate queued for monitor B releases the drain of
817 /// monitor A's entry even though A has no duplicate of its own.
818 ///
819 /// The per-subscription queues this replaced evaluated the gate per monitor,
820 /// so A stayed suspended until EVENTS_ON.
821 #[tokio::test]
822 async fn duplicate_on_a_sibling_subscription_releases_the_events_off_drain() {
823 let user = EventUser::new();
824 let (sink_a, mut reader_a) = attach(&user, 1);
825 let (sink_b, _reader_b) = attach(&user, 2);
826 assert!(
827 Arc::ptr_eq(&reader_a.queue(), &_reader_b.queue()),
828 "the quota admits both monitors to one queue — C's sharing granularity"
829 );
830
831 sink_a.post(ev(1)); // A: npend 1, no duplicate of its own
832 sink_b.post(ev(10));
833 sink_b.post(ev(11)); // B: npend 2 ⇒ the queue holds one duplicate
834 assert_eq!(reader_a.queue().n_duplicates(), 1);
835
836 user.flow_ctrl_on();
837 let got = tokio::time::timeout(std::time::Duration::from_secs(2), reader_a.recv())
838 .await
839 .expect("a duplicate anywhere on the queue must release the drain")
840 .expect("A's entry is delivered");
841 assert_eq!(val(&got), 1);
842 }
843
844 /// The complement of the boundary above — EVENTS_OFF with the queue holding
845 /// NO duplicate on any subscription: every reader on it suspends, and
846 /// EVENTS_ON releases them all.
847 #[tokio::test]
848 async fn flow_control_without_duplicates_suspends_every_reader_on_the_queue() {
849 let user = EventUser::new();
850 user.flow_ctrl_on();
851 let (sink_a, mut reader_a) = attach(&user, 1);
852 let (sink_b, mut reader_b) = attach(&user, 2);
853 sink_a.post(ev(1));
854 sink_b.post(ev(2));
855 assert_eq!(reader_a.queue().n_duplicates(), 0);
856 assert!(matches!(reader_a.try_recv(), Err(TryRecvError::Empty)));
857 assert!(matches!(reader_b.try_recv(), Err(TryRecvError::Empty)));
858 user.flow_ctrl_off();
859 assert_eq!(val(&reader_a.try_recv().unwrap()), 1);
860 assert_eq!(val(&reader_b.try_recv().unwrap()), 2);
861 }
862
863 /// Boundary `quota == size - EVENT_ENTRIES`: C chains a new queue rather than
864 /// overbooking the ring (`dbEvent.c:446-469`), so a circuit's monitors past
865 /// the cap share a *different* `nDuplicates`.
866 #[tokio::test]
867 async fn quota_exhaustion_chains_a_second_queue() {
868 let user = EventUser::new();
869 let cap = event_que_size() / EVENT_ENTRIES - 1;
870 let mut held = Vec::new();
871 for sid in 0..cap as u32 {
872 held.push(attach(&user, sid));
873 }
874 let first = held[0].1.queue();
875 for (_, reader) in &held {
876 assert!(Arc::ptr_eq(&reader.queue(), &first), "all within quota");
877 }
878 assert_eq!(first.quota(), event_que_size() - EVENT_ENTRIES);
879
880 let (_sink, overflow) = attach(&user, cap as u32);
881 assert!(
882 !Arc::ptr_eq(&overflow.queue(), &first),
883 "the {cap}th monitor exhausts the quota; the next one chains a queue"
884 );
885 assert_eq!(overflow.queue().quota(), EVENT_ENTRIES);
886 }
887
888 /// C releases the cancelled monitor's quota (`dbEvent.c:999-1002`), so the
889 /// freed slot is reusable — the next attach lands back on the first queue
890 /// instead of chaining forever.
891 #[tokio::test]
892 async fn detaching_a_monitor_releases_its_quota() {
893 let user = EventUser::new();
894 let cap = event_que_size() / EVENT_ENTRIES - 1;
895 let mut held: Vec<_> = (0..cap as u32).map(|sid| attach(&user, sid)).collect();
896 let first = held[0].1.queue();
897 assert_eq!(first.quota(), event_que_size() - EVENT_ENTRIES);
898 held.pop(); // cancel one monitor: sink and reader both go
899 assert_eq!(first.quota(), event_que_size() - 2 * EVENT_ENTRIES);
900 let (_sink, reader) = attach(&user, 900);
901 assert!(
902 Arc::ptr_eq(&reader.queue(), &first),
903 "the released quota must be reusable"
904 );
905 }
906
907 /// Teardown symmetry on a SHARED queue: cancelling one monitor removes only
908 /// its own duplicates from the queue-level count; its sibling's stay.
909 #[tokio::test]
910 async fn detaching_one_monitor_leaves_a_siblings_duplicates_counted() {
911 let user = EventUser::new();
912 let (sink_a, reader_a) = attach(&user, 1);
913 let (sink_b, reader_b) = attach(&user, 2);
914 let que = reader_a.queue();
915 for v in 1..=3 {
916 sink_a.post(ev(v)); // A: npend 3 ⇒ 2 duplicates
917 }
918 for v in 10..=11 {
919 sink_b.post(ev(v)); // B: npend 2 ⇒ 1 duplicate
920 }
921 assert_eq!(que.n_duplicates(), 3);
922 drop(reader_a);
923 drop(sink_a);
924 assert_eq!(que.n_duplicates(), 1, "only A's duplicates left the ring");
925 assert_eq!(que.npend(2), 2, "B's entries are untouched");
926 drop(reader_b);
927 drop(sink_b);
928 assert_eq!(que.n_duplicates(), 0);
929 assert_eq!(que.quota(), 0, "both monitors released their reservation");
930 }
931}