Skip to main content

epics_base_rs/server/
pv.rs

1use std::sync::Arc;
2use std::sync::Mutex as StdMutex;
3use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
4
5use crate::runtime::sync::{Mutex, RwLock, mpsc};
6
7use crate::error::CaError;
8use crate::server::snapshot::{ControlInfo, DisplayInfo, EnumInfo, Snapshot};
9use crate::types::{DbFieldType, EpicsValue, WallTime};
10
11/// Per-subscriber bounded mpsc depth. Lifted from
12/// `EPICS_CAS_MAX_EVENTS_PER_CHAN`, default 64. Floor 4 so even
13/// hostile env values (`0`/`1`) leave room for last-value
14/// coalescing to make progress.
15fn per_channel_event_depth() -> usize {
16    crate::runtime::env::get("EPICS_CAS_MAX_EVENTS_PER_CHAN")
17        .and_then(|s| s.parse::<usize>().ok())
18        .unwrap_or(64)
19        .max(4)
20}
21
22/// Per-PV subscriber cap. Default 1024 — comfortably above
23/// any realistic dashboard fan-out, small enough to bound the
24/// per-PV `Vec<Subscriber>` under abuse. Override via
25/// `EPICS_CAS_MAX_SUBSCRIBERS_PER_PV`.
26pub(crate) fn max_subscribers_per_pv() -> usize {
27    crate::runtime::env::get("EPICS_CAS_MAX_SUBSCRIBERS_PER_PV")
28        .and_then(|s| s.parse::<usize>().ok())
29        .unwrap_or(1024)
30        .max(8)
31}
32
33/// Process-global counter of monitor events dropped because the
34/// per-channel mpsc was full AND the coalesce slot was already
35/// occupied by an even-newer overflow value. Covers both
36/// `ProcessVariable` and `RecordInstance` overflow via the single
37/// [`Subscriber::coalesce_overflow`] owner. Mirrors the pattern of
38/// `dropped_monitors` on the client side (subscribe_with_deadband).
39///
40/// read via [`dropped_monitor_events`]. That reader is not yet
41/// wired to a live scrape surface — the `/queues` admin endpoint
42/// currently renders configured limits only, not this counter — so do
43/// not assume the value is observable through an endpoint until that
44/// wiring lands.
45static DROPPED_MONITOR_EVENTS: AtomicU64 = AtomicU64::new(0);
46
47/// Read the cumulative count of dropped monitor events. Intended for
48/// introspection / metrics; see [`DROPPED_MONITOR_EVENTS`] for the
49/// current wiring status.
50pub fn dropped_monitor_events() -> u64 {
51    DROPPED_MONITOR_EVENTS.load(Ordering::Relaxed)
52}
53
54/// Internal: record a dropped event. Called from
55/// `notify_subscribers` when both the bounded mpsc and the
56/// coalesce slot are full.
57fn record_dropped_monitor() {
58    DROPPED_MONITOR_EVENTS.fetch_add(1, Ordering::Relaxed);
59}
60
61/// Identity of the client driving a `WriteHook` invocation. Carries
62/// the user/host/peer fields the CA TCP handler already tracks for
63/// audit + access security, so a proxy hook (gateway, ACL filter,
64/// putlog) can make decisions without re-deriving them.
65#[derive(Debug, Clone, Default)]
66pub struct WriteContext {
67    /// CA `CLIENT_NAME` username, or empty if unknown.
68    pub user: String,
69    /// CA `HOST_NAME` hostname (or peer IP fallback), used for ACF
70    /// matching against `HAG(...)` groups.
71    pub host: String,
72    /// Raw `peer.ip():peer.port()` string, retained for audit/log use.
73    pub peer: String,
74}
75
76/// Async hook invoked by client-originated writes (CA `caput`, CA
77/// `WRITE_NOTIFY`) before the PV's local value is set. Used by the CA
78/// gateway and similar proxies to forward writes upstream instead of
79/// landing them in the local `ProcessVariable`.
80///
81/// The hook receives the proposed new value plus a [`WriteContext`]
82/// identifying the client, and must return either:
83/// * `Ok(())` — the write was accepted (e.g. forwarded to upstream).
84///   The caller does NOT update the local `value` field — the
85///   subsequent upstream-monitor event is expected to do that. This
86///   matches CA-gateway semantics where the cached value reflects
87///   reality after the round-trip.
88/// * `Err(CaError)` — the write was rejected. The caller surfaces
89///   the error to the CA client (`WRITE_NOTIFY` carries the ECA
90///   status). The hook itself decides whether to update local state
91///   on rejection.
92///
93/// The hook is consulted only on the client → server path. Internal
94/// callers (`ProcessVariable::set`, `put_pv_and_post`) bypass it so
95/// the upstream-monitor forwarder can update local state without
96/// recursing into itself.
97///
98/// ## Stale-local hazard
99///
100/// "Hook returns `Ok` → caller does NOT update local value" assumes
101/// the upstream will emit a monitor event reflecting the new value.
102/// EPICS records can violate that assumption: PP=NO fields,
103/// PUT-only fields (e.g. `.PROC`), and records configured to suppress
104/// monitor events on identical values. In those cases the shadow
105/// PV remains at its pre-put value indefinitely — caput appears to
106/// succeed but `caget` afterwards returns the old value.
107///
108/// Hook implementors who target such records SHOULD update the local
109/// `ProcessVariable` themselves on `Ok` — typically by invoking
110/// `pv.set(new_value).await` AFTER the upstream put-ack, accepting
111/// the cost of one local mutation per put. The base hook contract
112/// stays "do nothing on Ok" because most monitor-driven shadows
113/// (the CA gateway's primary use case) WILL receive a monitor event
114/// and updating locally would race with it.
115///
116/// ## Reentrancy
117///
118/// The TCP write path clones the hook `Arc` and releases the read
119/// guard BEFORE invoking it, so a hook that calls
120/// `pv.set_write_hook(...)` to swap itself does not deadlock. A hook
121/// that calls `pv.set(...)` reentrantly is allowed but defeats the
122/// "let the upstream-monitor update local state" contract — the
123/// reentrant `set` will be silently overwritten by the next
124/// upstream event.
125pub type WriteHook = Arc<
126    dyn Fn(
127            EpicsValue,
128            WriteContext,
129        )
130            -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), CaError>> + Send>>
131        + Send
132        + Sync,
133>;
134
135/// read/write access decision for a gateway shadow PV,
136/// evaluated for a specific downstream `(user, host)`. Mirrors the CA
137/// access-rights model the server reports to the client and gates
138/// reads on.
139#[derive(Debug, Clone, Copy)]
140pub struct AccessDecision {
141    /// Client may GET / MONITOR (`EVENT_ADD`) the PV.
142    pub read: bool,
143    /// Client may PUT (`WRITE` / `WRITE_NOTIFY`) the PV.
144    pub write: bool,
145}
146
147/// per-PV access hook installed by a proxy (the CA
148/// gateway) so the CA server routes a shadow PV's access-rights
149/// decision through the proxy's own ACF instead of the server's.
150/// Given the downstream client's `(user, host)`, it returns the
151/// [`AccessDecision`].
152///
153/// Symmetric to [`WriteHook`]: the gateway captures its single
154/// `ArcSwap<AccessConfig>` and the PV's `.pvlist` ASG/ASL in the
155/// closure, so `compute_access` reports access rights and gates reads
156/// with the same `can_read` / `can_write` the write hook uses — one
157/// ACF authority, no second copy to keep in sync. The hook is
158/// synchronous (it only reads an in-memory `ArcSwap`, no `.await`); the
159/// server consults it at `CREATE_CHAN` and on access-rights
160/// re-evaluation.
161pub type AccessHook = Arc<dyn Fn(&str, &str) -> AccessDecision + Send + Sync>;
162
163/// per-PV read hook consulted by the CA server's one-shot GET path
164/// (`CA_PROTO_READ` / `CA_PROTO_READ_NOTIFY`) when set. A bare PV serves
165/// reads straight from its stored value cell; a proxy (the CA gateway in
166/// its no-cache mode) installs this hook so each downstream GET is
167/// satisfied by a *fresh* upstream fetch instead of the last cached
168/// value. Mirrors C ca-gateway `-no_cache`, where a connected channel
169/// with caching disabled forwards every read as a fresh
170/// `ca_array_get_callback()` to the IOC (`gateVc.cc:1361-1369`) rather
171/// than returning `vc->eventData()`.
172///
173/// The hook returns a full [`Snapshot`], not a bare value: C `-no_cache`
174/// reads issue `ca_array_get_callback(eventType(), ...)` with `eventType()`
175/// a `DBR_TIME_*` class, and `getTimeCB` decodes the event's status,
176/// severity, and timestamp into `setEventData` before the GET completes
177/// (`gatePv.cc:976`, `:1789-1794`). The hook therefore owns producing the
178/// fresh value *together with* its upstream alarm/timestamp so the read
179/// path never synthesizes metadata by grafting a fresh value onto an
180/// unrelated cached snapshot. Property metadata (display/control/enum) is
181/// not carried by a `DBR_TIME_*` event in either C or here; the consumer
182/// overlays the shadow's last-known property metadata for those fields
183/// (a separate upstream path feeds them, as C splits value/time from the
184/// property monitor).
185///
186/// The hook is async (it performs an upstream get) and fallible: on
187/// `Err` the server surfaces the failure to the client (`ECA_GETFAIL`)
188/// exactly as the IOC's own get-callback error would propagate. Only the
189/// GET path consults it ([`ProcessVariable::read_snapshot`]); monitor
190/// fan-out, the initial monitor event, and access-rights re-posts keep
191/// serving the stored snapshot, so a no-cache PV still backs a downstream
192/// monitor with its upstream subscription's events.
193///
194/// `None` (the default) leaves the read path byte-for-byte unchanged for
195/// every record-backed and cached PV — the hook is purely additive.
196pub type ReadHook = Arc<
197    dyn Fn()
198            -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Snapshot, CaError>> + Send>>
199        + Send
200        + Sync,
201>;
202
203/// A monitor event sent to subscribers when a PV value changes.
204/// Carries a full Snapshot so GR/CTRL metadata (PREC, EGU, limits) is available.
205#[derive(Debug, Clone)]
206pub struct MonitorEvent {
207    pub snapshot: Snapshot,
208    /// Origin writer ID. When non-zero, subscribers with the same
209    /// `ignore_origin` can filter out self-triggered events.
210    /// Used to prevent sequencer write-back loops.
211    ///
212    /// **Scope**: Currently tagged on `put_pv_and_post_with_origin` events only.
213    /// Events from `process_record_with_links` (process path) always have
214    /// origin=0. If a future sequencer needs to filter process-path events
215    /// too, origin tagging can be extended to the process path by passing
216    /// origin through `ProcessOutcome` or `process_record_with_links`.
217    pub origin: u64,
218    /// The `DBE_*` event class(es) this post carries — C attaches the
219    /// posting mask to each event's field log (`db_field_log.mask`,
220    /// dbEvent.c) and pvxs narrows per event from `pDbFieldLog->mask`
221    /// (`groupsource.cpp:331-337`). Producer-side it was already used to
222    /// gate delivery (`Subscriber::accepts`); carrying it on the event
223    /// lets subscribers narrow what they decode (e.g. a QSRV group
224    /// monitor updating only alarm leaves on a `DBE_ALARM`-only event).
225    /// When events coalesce under a slow consumer, masks accumulate by
226    /// OR — the surviving snapshot is the newest, the mask reports every
227    /// class that changed since the last delivered event.
228    pub mask: crate::server::recgbl::EventMask,
229}
230
231/// A subscriber waiting for PV value updates.
232pub struct Subscriber {
233    pub sid: u32,
234    pub data_type: DbFieldType,
235    pub mask: u16,
236    pub tx: mpsc::Sender<MonitorEvent>,
237    /// Last-value coalescing slot. When the bounded mpsc above is full,
238    /// the producer stores the newest event here, overwriting any prior
239    /// pending overflow value. After each normal recv() the consumer pops
240    /// this slot through [`coalesce_consume`]: a set slot is newer than the
241    /// whole queue, so it is delivered and the now-stale queue tail is
242    /// drained — matching libca rsrv "drop oldest, keep newest" semantics
243    /// while never delivering the newest value and then an older queued one.
244    pub coalesced: Arc<StdMutex<Option<MonitorEvent>>>,
245    /// Server-side channel filter chain (epics-base 3.15.7).
246    /// Defaults to empty — every event passes unchanged. Populated
247    /// by the subscription path when the channel name carries a
248    /// `.{filter:opts}` JSON suffix (`dbnd`, `arr`, `ts`, ...).
249    pub filters: crate::server::database::filters::FilterChain,
250    /// Delivery gate. `true` (the default) delivers events normally;
251    /// `false` suppresses every post to this subscriber at the source —
252    /// no `try_send`, no coalesce-overflow, no filter evaluation — so a
253    /// paused monitor stops the record-event work entirely, not just the
254    /// downstream frame. Mirrors EPICS `db_event_disable` / pvxs
255    /// `onStart(false)` (singlesource.cpp:151-173, groupsource.cpp:151-281):
256    /// the subscription object survives, only its event flow is gated, so
257    /// the same subscriber resumes on re-enable. Flipped only under the
258    /// owner's write lock via [`super::record::record_instance::RecordInstance::set_subscriber_active`]
259    /// (records) — the post paths read it under the matching read lock.
260    pub active: bool,
261}
262
263impl Subscriber {
264    /// a monitor delivery is gated on the requested `DBE_*`
265    /// mask. Returns true only when the post's event class intersects
266    /// this subscriber's mask — the single rule C rsrv enforces with
267    /// `caEventMask & pevent->select` (`dbEvent.c:892-900`) and the
268    /// same intersection the record-field monitor path applies. An
269    /// empty post class (no specific class) delivers unconditionally.
270    fn accepts(&self, post: crate::server::recgbl::EventMask) -> bool {
271        post.is_empty() || crate::server::recgbl::EventMask::from_bits(self.mask).intersects(post)
272    }
273
274    /// single owner of the slow-consumer coalesce overflow.
275    /// Call after the bounded `tx` rejected a send: store the newest
276    /// `event` in the coalesce slot, and — when it displaces a value the
277    /// consumer never observed — record one dropped monitor event.
278    ///
279    /// Every coalesced-slot overwrite on the overflow path (both
280    /// `ProcessVariable` value/alarm posts and `RecordInstance`
281    /// field monitors) MUST go through here, so `dropped_monitor_events()`
282    /// cannot undercount one path: the record-field path previously
283    /// overwrote the slot directly and never bumped the counter, hiding
284    /// slow-consumer loss for the path most CA/PVA database monitors use.
285    pub(crate) fn coalesce_overflow(&self, mut event: MonitorEvent) {
286        if let Ok(mut slot) = self.coalesced.lock() {
287            if let Some(prior) = slot.as_ref() {
288                record_dropped_monitor();
289                // The displaced value is lost but its event class must
290                // not be: fold it into the surviving event so a narrow
291                // consumer still learns that class changed (C keeps the
292                // mask on the squashed field log).
293                event.mask |= prior.mask;
294            }
295            *slot = Some(event);
296        }
297    }
298}
299
300/// Fold the producer's coalesce overflow slot into the next monitor
301/// delivery, preserving arrival order.
302///
303/// A subscriber's pending events live in two places: the bounded
304/// per-subscriber `rx` and the side `coalesced` slot. The producer fills
305/// `rx` first via `try_send` and parks an event in the slot ONLY after
306/// `rx` is full ([`Subscriber::coalesce_overflow`]), so a set slot value
307/// is strictly newer than every event still queued in `rx`. Delivery
308/// must stay monotonic in arrival order: pvxs keeps overflow and delivery
309/// in one queue and never sends the newest value and then an older queued
310/// one — `Subscription::post()` squashes overflow into `queue.back()`
311/// while `doReply()` sends `queue.front()` (`sharedpv.cpp:417-439`,
312/// `servermon.cpp:172-285`).
313///
314/// Given the freshly recv'd `queued` head and the popped `coalesced` slot,
315/// this returns the single event to deliver next:
316/// * slot empty → deliver `queued` (the in-order front, unchanged);
317/// * slot set → deliver the slot's newest value and DRAIN the now-stale
318///   `rx` backlog, because every queued entry (including `queued`) predates
319///   it. Each discarded entry is counted as a dropped monitor event — the
320///   "drop all stale, keep newest" coalescing policy. This is what keeps
321///   the consumer from returning the coalesced newest and then replaying
322///   older queued values.
323///
324/// This is the single owner of the rx-vs-slot ordering rule; every
325/// in-process and server-side monitor consumer that drains the slot
326/// (`PvSubscription`, `DbSubscription`, and the CA server monitor tasks)
327/// routes through it, so none can re-introduce newest-then-old. The
328/// caller pops the slot itself — the slot lives behind a different lock
329/// owner per source (`ProcessVariable` vs `RecordInstance`) — and hands
330/// the result here together with the `rx` it owns.
331pub fn coalesce_consume(
332    rx: &mut mpsc::Receiver<MonitorEvent>,
333    queued: MonitorEvent,
334    coalesced: Option<MonitorEvent>,
335) -> MonitorEvent {
336    let Some(mut newest) = coalesced else {
337        return queued;
338    };
339    // `queued` and everything still queued in `rx` predate `newest`;
340    // discard them so delivery converges to the newest value without
341    // ever stepping backward to an older queued snapshot. Their event
342    // classes are folded into the delivered mask — the values are
343    // superseded, but a narrow consumer must still learn which classes
344    // changed since its last delivery.
345    record_dropped_monitor();
346    newest.mask |= queued.mask;
347    while let Ok(stale) = rx.try_recv() {
348        record_dropped_monitor();
349        newest.mask |= stale.mask;
350    }
351    newest
352}
353
354/// Shadow `DBR_GR_*` / `DBR_CTRL_*` / enum metadata for a
355/// non-record-backed PV.
356///
357/// A bare [`ProcessVariable`] has no record engine to derive units /
358/// precision / display+alarm+control limits / enum labels from, so a
359/// proxy that fronts an upstream IOC (the CA / PVA gateway) fetches the
360/// upstream's control metadata and installs it here via
361/// [`ProcessVariable::set_metadata`]. Every snapshot the PV emits —
362/// the GET path ([`ProcessVariable::snapshot`]) and every monitor
363/// event ([`ProcessVariable::post_property`], value, alarm, and
364/// gateway snapshot posts) — then carries it, so a downstream client
365/// that requested a `DBR_GR_*` / `DBR_CTRL_*` type receives the
366/// upstream metadata instead of zeroed limits.
367///
368/// Mirrors the C ca-gateway, where `gatePvData` subscribes to
369/// `DBE_PROPERTY` and issues a control-type `ca_array_get_callback`
370/// (`gatePv.cc:850-934`) then copies units / precision / graphic +
371/// control limits into the gateway's gdd attributes
372/// (`gatePv.cc:1916-2007`).
373#[derive(Debug, Clone, Default)]
374pub struct PvMetadata {
375    pub display: Option<DisplayInfo>,
376    pub control: Option<ControlInfo>,
377    pub enums: Option<EnumInfo>,
378}
379
380/// Metadata of the most recent full-snapshot write to a bare PV:
381/// alarm + acquisition timestamp + userTag. A bare `ProcessVariable`
382/// has no alarm engine, so without this it would forget everything a
383/// full-value write carried beyond the raw value. pvxs mailbox
384/// `SharedPV::post()` assigns the *whole* posted value to the current
385/// value (`sharedpv.cpp:417-432`); to match that, a PV that received a
386/// full posted Value must reflect its alarm/time on every later GET,
387/// not just to the monitor subscribers that saw the post live.
388#[derive(Clone)]
389struct PostedMeta {
390    alarm: crate::server::snapshot::AlarmInfo,
391    timestamp: WallTime,
392    user_tag: i32,
393}
394
395/// A process variable hosted by the server.
396pub struct ProcessVariable {
397    pub name: String,
398    pub value: RwLock<EpicsValue>,
399    pub subscribers: Mutex<Vec<Subscriber>>,
400    /// Sticky metadata of the last full-snapshot write. `None` until a
401    /// [`Self::set_snapshot`] lands; a value-only [`Self::set`] clears it
402    /// back to `None` (a plain value write carries no explicit
403    /// alarm/time, so it reverts to NO_ALARM + wall-clock-now). When
404    /// `Some`, [`Self::snapshot`] serves these instead of the defaults.
405    /// Single meaning: the served snapshot reflects the most recent
406    /// write — value always current, metadata from that write.
407    posted_meta: parking_lot::RwLock<Option<PostedMeta>>,
408    /// Shadow DBR_GR_*/DBR_CTRL_*/enum metadata, installed by a proxy
409    /// (CA / PVA gateway) via [`Self::set_metadata`]. Empty for a plain
410    /// local PV. Stored under the same sync `parking_lot::RwLock` slot
411    /// rationale as the hooks: every snapshot builder reads it without
412    /// an `.await`. See [`PvMetadata`].
413    metadata: parking_lot::RwLock<PvMetadata>,
414    /// Optional hook consulted on client-originated writes. When set,
415    /// the CA TCP write path delegates to the hook instead of doing a
416    /// local `pv.set()`. See [`WriteHook`].
417    ///
418    /// Stored under `parking_lot::RwLock` (sync) rather than the
419    /// async `tokio::sync::RwLock` so the hot put-path can read it
420    /// without an `.await` round-trip — `write_hook()` is now a
421    /// constant-time clone of the optional `Arc`. The hook itself
422    /// is async (returns a `Future`); only the slot is sync.
423    write_hook: parking_lot::RwLock<Option<WriteHook>>,
424    /// optional access hook consulted by the CA server's
425    /// `compute_access` to decide a downstream client's read/write
426    /// rights for this PV. When set, it overrides the server's own ACF
427    /// for this PV — the gateway uses it to enforce `.pvlist` ASG-based
428    /// `can_read` / `can_write`, symmetric to [`Self::write_hook`].
429    /// Same sync `parking_lot::RwLock` slot rationale as `write_hook`.
430    access_hook: parking_lot::RwLock<Option<AccessHook>>,
431    /// optional read hook consulted by the CA server's one-shot GET path
432    /// ([`Self::read_snapshot`]) to fetch a fresh value instead of the
433    /// stored cell. Used by the CA gateway's no-cache mode to forward
434    /// each downstream GET to upstream. `None` (the default) keeps the
435    /// read path serving the stored value, identical to before. Same
436    /// sync slot rationale as [`Self::write_hook`]: the GET path clones
437    /// the optional `Arc` without an `.await`, then awaits the hook
438    /// outside any lock. See [`ReadHook`].
439    read_hook: parking_lot::RwLock<Option<ReadHook>>,
440}
441
442impl ProcessVariable {
443    pub fn new(name: String, initial: EpicsValue) -> Self {
444        Self {
445            name,
446            value: RwLock::new(initial),
447            subscribers: Mutex::new(Vec::new()),
448            metadata: parking_lot::RwLock::new(PvMetadata::default()),
449            posted_meta: parking_lot::RwLock::new(None),
450            write_hook: parking_lot::RwLock::new(None),
451            access_hook: parking_lot::RwLock::new(None),
452            read_hook: parking_lot::RwLock::new(None),
453        }
454    }
455
456    /// Install (or replace) the shadow DBR_GR_*/DBR_CTRL_*/enum
457    /// metadata served on this PV's snapshots. Used by the CA / PVA
458    /// gateway after fetching the upstream IOC's control metadata. See
459    /// [`PvMetadata`]. To publish the change to downstream property
460    /// monitors, follow with [`Self::post_property`].
461    pub fn set_metadata(&self, metadata: PvMetadata) {
462        *self.metadata.write() = metadata;
463    }
464
465    /// Snapshot (clone) of the installed shadow metadata; empty
466    /// (`Default`) for a plain local PV.
467    pub fn metadata(&self) -> PvMetadata {
468        self.metadata.read().clone()
469    }
470
471    /// Fill any metadata field the snapshot leaves `None` from the
472    /// installed shadow metadata. A field the caller already populated
473    /// (e.g. a gateway snapshot that carried its own metadata) wins —
474    /// this only supplies what is otherwise absent, so every emission
475    /// path serves the upstream metadata uniformly without clobbering a
476    /// richer source.
477    fn apply_metadata(&self, snap: &mut Snapshot) {
478        let meta = self.metadata.read();
479        if snap.display.is_none() {
480            snap.display = meta.display.clone();
481        }
482        if snap.control.is_none() {
483            snap.control = meta.control.clone();
484        }
485        if snap.enums.is_none() {
486            snap.enums = meta.enums.clone();
487        }
488    }
489
490    /// Install an access hook. Replaces any previously
491    /// installed hook.
492    pub fn set_access_hook(&self, hook: AccessHook) {
493        *self.access_hook.write() = Some(hook);
494    }
495
496    /// Snapshot of the installed access hook (clone of the `Arc`), or
497    /// `None`. Consulted by the CA server's `compute_access`; cheap and
498    /// non-async, like [`Self::write_hook`].
499    pub fn access_hook(&self) -> Option<AccessHook> {
500        self.access_hook.read().clone()
501    }
502
503    /// Install a read hook. Replaces any previously-installed hook.
504    /// Used by the CA gateway's no-cache mode so each downstream GET is
505    /// served by a fresh upstream fetch. See [`ReadHook`].
506    pub fn set_read_hook(&self, hook: ReadHook) {
507        *self.read_hook.write() = Some(hook);
508    }
509
510    /// Snapshot of the installed read hook (clone of the `Arc`), or
511    /// `None`. Cheap and non-async, like [`Self::write_hook`]: the read
512    /// lock is released before the cloned `Arc` returns, so the caller's
513    /// subsequent `await` on the hook holds no lock.
514    pub fn read_hook(&self) -> Option<ReadHook> {
515        self.read_hook.read().clone()
516    }
517
518    /// Install a write hook. Replaces any previously-installed hook.
519    pub fn set_write_hook(&self, hook: WriteHook) {
520        *self.write_hook.write() = Some(hook);
521    }
522
523    /// Remove any installed write hook.
524    pub fn clear_write_hook(&self) {
525        *self.write_hook.write() = None;
526    }
527
528    /// Snapshot of the installed write hook (clone of the `Arc`), or
529    /// `None` if none. Used by the CA TCP write path; cheap and
530    /// non-async — the read lock is released before the cloned `Arc`
531    /// returns, so the caller's subsequent `await` on the hook does
532    /// not hold any lock.
533    pub fn write_hook(&self) -> Option<WriteHook> {
534        self.write_hook.read().clone()
535    }
536
537    /// Get the current value.
538    pub async fn get(&self) -> EpicsValue {
539        self.value.read().await.clone()
540    }
541
542    /// Build a Snapshot for this bare PV.
543    ///
544    /// A `ProcessVariable` is a non-record-backed channel: it has no
545    /// alarm engine, no DESC/EGU/PREC metadata and no timestamp user
546    /// tag of its own. The snapshot is therefore value + `NO_ALARM` +
547    /// wall-clock now, with `user_tag` = 0. Display / control / enum
548    /// metadata is `None` *unless* a proxy installed it via
549    /// [`Self::set_metadata`] (the CA / PVA gateway shadowing an
550    /// upstream IOC) — see [`Self::apply_metadata`]. Record-backed
551    /// channels build their snapshot via
552    /// `RecordInstance::snapshot_for_field`, which carries the record's
553    /// own alarm/metadata. The only path that injects a non-zero alarm
554    /// onto a bare PV is [`Self::post_alarm`] (used by the gateway
555    /// adapter to surface upstream disconnect).
556    pub async fn snapshot(&self) -> Snapshot {
557        let value = self.value.read().await.clone();
558        // Serve the sticky metadata of the last full-snapshot write if
559        // one landed (pvxs mailbox parity: a posted full Value stays the
560        // current value, alarm/time included); otherwise the bare-PV
561        // default of NO_ALARM + wall-clock-now.
562        let mut snap = match self.posted_meta.read().clone() {
563            Some(m) => {
564                let mut s = Snapshot::new(value, m.alarm.status, m.alarm.severity, m.timestamp);
565                s.alarm.ackt = m.alarm.ackt;
566                s.alarm.acks = m.alarm.acks;
567                s.user_tag = m.user_tag;
568                s
569            }
570            None => Snapshot::new(value, 0, 0, crate::runtime::time::now_wall()),
571        };
572        self.apply_metadata(&mut snap);
573        snap
574    }
575
576    /// Build the snapshot served on a one-shot client GET
577    /// (`CA_PROTO_READ` / `CA_PROTO_READ_NOTIFY`).
578    ///
579    /// When a [`ReadHook`] is installed (the CA gateway's no-cache mode),
580    /// the snapshot is fetched fresh through the hook — value *and* its
581    /// upstream alarm status/severity and IOC timestamp together — and the
582    /// shadow's last-known property metadata (display/control/enum) is
583    /// overlaid for the fields a `DBR_TIME_*` event does not carry; on hook
584    /// error the failure propagates so the server can answer `ECA_GETFAIL`,
585    /// matching C ca-gateway forwarding each read to the IOC under
586    /// `-no_cache` (`gateVc.cc:1361-1369`, `gatePv.cc:976`/`:1789-1794`).
587    /// Without a hook this is exactly [`Self::snapshot`] wrapped in `Ok`,
588    /// so the GET path is unchanged for every record-backed and cached PV.
589    ///
590    /// Only the GET path calls this; monitor fan-out, the initial monitor
591    /// event, and access-rights re-posts keep using [`Self::snapshot`]
592    /// (the stored value), so a no-cache PV still backs a downstream
593    /// monitor with its upstream subscription's events rather than a
594    /// per-event upstream get.
595    pub async fn read_snapshot(&self) -> Result<Snapshot, CaError> {
596        match self.read_hook() {
597            Some(hook) => {
598                // The hook issues a metadata-bearing upstream GET
599                // (`DbrClass::Time`), so the returned snapshot already
600                // carries the fresh value WITH its upstream alarm
601                // status/severity and IOC timestamp — mirroring C
602                // `getTimeCB` decoding the `DBR_TIME_*` event before
603                // `setEventData`. A `DBR_TIME_*` event does not carry
604                // display/control/enum metadata, so overlay the shadow's
605                // last-known property metadata for those absent fields only
606                // (a separate upstream path feeds it, exactly as C splits
607                // the value/time path from the property monitor). Never
608                // graft the fresh value onto the stored snapshot's
609                // alarm/time, which may be stale or the bare-PV default.
610                let mut snap = hook().await?;
611                self.apply_metadata(&mut snap);
612                Ok(snap)
613            }
614            None => Ok(self.snapshot().await),
615        }
616    }
617
618    /// Set a new value and notify all subscribers.
619    pub async fn set(&self, new_value: EpicsValue) {
620        {
621            let mut val = self.value.write().await;
622            *val = new_value.clone();
623        }
624        // A plain value write carries no explicit alarm/time — revert to
625        // the bare-PV default so a stale full-snapshot's metadata does
626        // not linger on a value the client never stamped.
627        *self.posted_meta.write() = None;
628        self.notify_subscribers(new_value).await;
629    }
630
631    /// Set value from a full snapshot (value + alarm + timestamp) and notify
632    /// all subscribers. Used by the CA gateway forwarding task to propagate
633    /// the upstream alarm status/severity and IOC timestamp to downstream
634    /// monitors. Mirrors `gateVcData::setEventData` + `vcPostEvent` in the
635    /// C ca-gateway: the incoming `dbr_time_xxx` GDD carries all three fields.
636    pub async fn set_snapshot(&self, snapshot: Snapshot) {
637        {
638            let mut val = self.value.write().await;
639            *val = snapshot.value.clone();
640        }
641        // Persist the posted alarm/time/userTag so a later GET reflects
642        // the full posted value, not just the live monitor fan-out.
643        *self.posted_meta.write() = Some(PostedMeta {
644            alarm: snapshot.alarm.clone(),
645            timestamp: snapshot.timestamp,
646            user_tag: snapshot.user_tag,
647        });
648        self.notify_subscribers_from_snapshot(snapshot).await;
649    }
650
651    /// Single delivery owner: emit `snapshot` to every live subscriber
652    /// whose `DBE_*` mask intersects `post`.
653    ///
654    /// Every emission path ([`Self::notify_subscribers`] value posts,
655    /// [`Self::post_alarm`], [`Self::notify_subscribers_from_snapshot`]
656    /// gateway posts, [`Self::post_property`]) routes through here so the
657    /// mask gate (`caEventMask & pevent->select`, `dbEvent.c:892-900`),
658    /// the per-subscriber channel-filter chain, and the slow-consumer
659    /// coalesce-overflow accounting are applied identically — one event
660    /// class differs per caller, nothing else. The snapshot is built once
661    /// by the caller (one timestamp per logical event) and cloned per
662    /// subscriber.
663    async fn deliver(&self, post: crate::server::recgbl::EventMask, snapshot: Snapshot) {
664        use crate::server::database::filters::FilteredMonitorEvent;
665        let mut subs = self.subscribers.lock().await;
666        // Remove subscribers whose channel has been dropped.
667        subs.retain(|sub| !sub.tx.is_closed());
668        for sub in subs.iter() {
669            // Paused subscribers (`db_event_disable`) receive nothing —
670            // skip before any work so a disabled monitor stops the event
671            // flow at the source.
672            if !sub.active {
673                continue;
674            }
675            // Skip subscribers whose requested class does not intersect
676            // this post's event class.
677            if !sub.accepts(post) {
678                continue;
679            }
680            let event = MonitorEvent {
681                snapshot: snapshot.clone(),
682                origin: 0,
683                mask: post,
684            };
685            // The channel-filter chain may suppress this event (e.g.
686            // `dbnd` deadband not crossed); the event's mask tells value
687            // filters whether to pass through (446e0d4a).
688            let filtered = if sub.filters.is_empty() {
689                Some(event)
690            } else {
691                sub.filters
692                    .apply(FilteredMonitorEvent::new(event))
693                    .map(|fe| fe.event)
694            };
695            let Some(event) = filtered else {
696                continue;
697            };
698            if sub.tx.try_send(event.clone()).is_err() {
699                // Queue full — overwrite any prior pending overflow with
700                // the newest event (consumer drains it via `pop_coalesced`
701                // after the next normal recv). The single coalesce-overflow
702                // owner counts a value the consumer never observed as a
703                // dropped monitor event, uniformly across event classes.
704                sub.coalesce_overflow(event);
705            }
706        }
707    }
708
709    /// Push a fresh monitor event holding the current value but with
710    /// the supplied alarm severity/status. Used by the PVA / CA
711    /// gateway adapter to surface upstream-disconnect to downstream
712    /// monitor subscribers without dropping the simple PV (which
713    /// would force every downstream client into ECA_DISCONN +
714    /// reconnect storms when the upstream is just briefly
715    /// unreachable). Mirrors gatePvData::death's "alarm-post"
716    /// alternative discussed in the C++ ca-gateway audit.
717    pub async fn post_alarm(&self, severity: u16, status: u16) {
718        use crate::server::recgbl::EventMask;
719        let value = self.value.read().await.clone();
720        let mut snapshot = Snapshot::new(value, status, severity, crate::runtime::time::now_wall());
721        self.apply_metadata(&mut snapshot);
722        // ALARM|LOG so DBE_LOG (archiver) subscribers receive alarm events.
723        self.deliver(EventMask::ALARM | EventMask::LOG, snapshot)
724            .await;
725    }
726
727    /// Post a `DBE_PROPERTY` monitor event carrying the decoded upstream
728    /// CTRL event `snapshot` — its value plus the upstream status /
729    /// severity and timestamp — overlaid with the installed shadow
730    /// metadata, so downstream property-change monitors re-read the units /
731    /// precision / limits / enum labels with the *upstream* alarm state.
732    ///
733    /// Used by the CA / PVA gateway when an upstream `DBE_PROPERTY` event
734    /// fires (metadata changed) after it has refreshed the shadow PV via
735    /// [`Self::set_metadata`]. The caller supplies the snapshot rather than
736    /// this method synthesising one: C ca-gateway decodes the upstream
737    /// `DBR_CTRL_*` callback and re-posts the value with `setStatSevr()`
738    /// status/severity preserved (`gatePv.cc:2413-2438`,
739    /// `runValueDataCB`), leaving the timestamp as the control DBR carries
740    /// none — it must NOT be replaced with a fresh `NO_ALARM` /
741    /// wall-clock-now snapshot just because metadata changed. Pass the
742    /// timestamp the upstream value carried (the control event has none of
743    /// its own); pass `status`/`severity` from the upstream CTRL payload.
744    /// Property events are a distinct class from value/alarm: only
745    /// `DBE_PROPERTY` subscribers receive them.
746    pub async fn post_property(&self, mut snapshot: Snapshot) {
747        use crate::server::recgbl::EventMask;
748        self.apply_metadata(&mut snapshot);
749        self.deliver(EventMask::PROPERTY, snapshot).await;
750    }
751
752    /// Notify all subscribers of a new value.
753    async fn notify_subscribers(&self, value: EpicsValue) {
754        use crate::server::recgbl::EventMask;
755        let mut snapshot = Snapshot::new(value, 0, 0, crate::runtime::time::now_wall());
756        self.apply_metadata(&mut snapshot);
757        // VALUE|LOG so DBE_LOG (archiver) subscribers receive value events.
758        self.deliver(EventMask::VALUE | EventMask::LOG, snapshot)
759            .await;
760    }
761
762    /// Notify all subscribers using a pre-built Snapshot (value + alarm +
763    /// timestamp). Used by `set_snapshot` to propagate the upstream alarm
764    /// and IOC timestamp without synthesising a new zero-alarm local-time
765    /// snapshot. Installed shadow metadata fills any metadata field the
766    /// gateway snapshot left absent (see [`Self::apply_metadata`]).
767    async fn notify_subscribers_from_snapshot(&self, mut snapshot: Snapshot) {
768        use crate::server::recgbl::EventMask;
769        self.apply_metadata(&mut snapshot);
770        // C gateway fires postEvent(VALUE|ALARM|LOG) for every
771        // upstream event (gateVc.cc:374-376); match it so DBE_LOG
772        // archivers and DBE_ALARM-only monitors receive gateway snapshot posts.
773        self.deliver(
774            EventMask::VALUE | EventMask::LOG | EventMask::ALARM,
775            snapshot,
776        )
777        .await;
778    }
779
780    /// Add a subscriber. Returns the receiver for monitor events,
781    /// or `None` when the per-PV subscriber cap has been reached
782    /// (defends against a misbehaving client opening many
783    /// MONITOR ops against one shared PV; per-channel cap limits
784    /// channels but not subscriber rows on a single PV). Operators
785    /// override the cap via `EPICS_CAS_MAX_SUBSCRIBERS_PER_PV`
786    /// (default 1024 — large enough for any realistic dashboard
787    /// fan-out, small enough to bound memory under abuse).
788    ///
789    /// Channel depth defaults to 64 events; the operator can lift the
790    /// cap via `EPICS_CAS_MAX_EVENTS_PER_CHAN` for sites that need
791    /// deeper coalescing buffers. C rsrv does not advertise this knob
792    /// (its queue is internally fixed) — exposing it lets us tune
793    /// memory vs latency for slow-viewer workloads.
794    pub async fn add_subscriber(
795        &self,
796        sid: u32,
797        data_type: DbFieldType,
798        mask: u16,
799    ) -> Option<mpsc::Receiver<MonitorEvent>> {
800        let cap = max_subscribers_per_pv();
801        let (tx, rx) = mpsc::channel(per_channel_event_depth());
802        let sub = Subscriber {
803            sid,
804            data_type,
805            mask,
806            tx,
807            coalesced: Arc::new(StdMutex::new(None)),
808            filters: crate::server::database::filters::FilterChain::new(),
809            active: true,
810        };
811        let mut subs = self.subscribers.lock().await;
812        // Reap dead Senders BEFORE counting
813        // against the cap. `notify_subscribers` / `post_alarm`
814        // already retain-filter on every emission, but a PV with
815        // no value changes (e.g. a static catalog entry that
816        // dashboards latch onto and drop) never triggered the
817        // reaper — a long-lived subscribe / disconnect storm could
818        // pin the Vec at `cap` worth of closed `Sender`s and lock
819        // out genuine new subscribers with a false-positive cap-
820        // reached warning. Same defect class as the
821        // NDPluginPva subscribe reaper (qsrv/pva_adapter.rs:247).
822        subs.retain(|s| !s.tx.is_closed());
823        if subs.len() >= cap {
824            tracing::warn!(
825                pv = %self.name,
826                live = subs.len(),
827                cap,
828                "PV subscriber cap reached, refusing add_subscriber"
829            );
830            return None;
831        }
832        subs.push(sub);
833        Some(rx)
834    }
835
836    /// attach a channel-filter chain to an already-added
837    /// subscriber (looked up by `sid`). The CA server first
838    /// `add_subscriber`s, then attaches the chain parsed from the
839    /// channel's `.{...}` suffix — symmetric with the record-field
840    /// `RecordInstance::attach_filter_to_last_subscriber` path, so a
841    /// `SimplePv` monitor runs the SAME filter chain as a record-field
842    /// monitor instead of the empty default `FilterChain` that
843    /// `add_subscriber` installs. Update delivery
844    /// ([`Self::notify_subscribers`] / [`Self::post_alarm`]) already
845    /// applies `sub.filters`; this is the missing wiring that populates
846    /// it.
847    ///
848    /// The caller passes a FRESH chain per subscriber so stateful
849    /// filters (`dbnd` last-value, `dec` counter, `sync` state) stay
850    /// isolated across subscribers. An empty chain is a no-op (keeps the
851    /// default). No-op when no subscriber matches `sid` (e.g. it was
852    /// reaped between add and attach).
853    pub async fn attach_filters_to_subscriber(
854        &self,
855        sid: u32,
856        filters: crate::server::database::filters::FilterChain,
857    ) {
858        if filters.is_empty() {
859            return;
860        }
861        let mut subs = self.subscribers.lock().await;
862        if let Some(sub) = subs.iter_mut().find(|s| s.sid == sid) {
863            sub.filters = filters;
864        }
865    }
866
867    /// Remove a subscriber by subscription ID.
868    pub async fn remove_subscriber(&self, sid: u32) {
869        let mut subs = self.subscribers.lock().await;
870        subs.retain(|s| s.sid != sid);
871    }
872
873    /// Take any pending coalesced overflow value for the given subscriber.
874    /// Called by the per-subscription forwarder task after each `rx.recv()`
875    /// and folded in via [`coalesce_consume`], which drains the now-stale
876    /// queue tail so a slow consumer converges on the latest known value
877    /// without ever delivering it before older queued values.
878    pub async fn pop_coalesced(&self, sid: u32) -> Option<MonitorEvent> {
879        let subs = self.subscribers.lock().await;
880        let sub = subs.iter().find(|s| s.sid == sid)?;
881        sub.coalesced.lock().ok()?.take()
882    }
883}
884
885/// Subscriber-id source for in-process [`PvSubscription`] monitors on a
886/// [`ProcessVariable`]. A `ProcessVariable`'s subscriber `Vec` is disjoint
887/// from any `RecordInstance`'s, so this is independent of the record-side
888/// allocator; it only has to stay unique among the simple-PV subscribers
889/// competing for one PV. Seeded at 1_000_000 for the same reason the
890/// record allocator is — keep in-process sids clear of the low,
891/// client-assigned wire subscription ids the CA server also registers on
892/// the same PV.
893static NEXT_PV_SUB_SID: AtomicU32 = AtomicU32::new(1_000_000);
894
895fn next_pv_sub_sid() -> u32 {
896    NEXT_PV_SUB_SID.fetch_add(1, Ordering::Relaxed)
897}
898
899/// In-process value-change monitor on a simple [`ProcessVariable`], the
900/// counterpart of the record-side `DbSubscription`.
901///
902/// The PUT path (`ProcessVariable::set` / `set_snapshot`) calls
903/// `notify_subscribers`, which fans the new value out to every registered
904/// subscriber, so a consumer holding a `PvSubscription` observes every
905/// later PUT — not just the connect-time snapshot. This mirrors pvxs
906/// `SharedPV::post()` delivering a cloned update to each stored subscriber
907/// (`sharedpv.cpp:417-440`).
908///
909/// The handle owns its `Subscriber` slot: `Drop` removes it, so a dropped
910/// consumer cannot leave a dead `(sid, tx, coalesced)` row in
911/// `ProcessVariable.subscribers` — the same leak `DbSubscription`'s `Drop`
912/// closes for records.
913pub struct PvSubscription {
914    rx: mpsc::Receiver<MonitorEvent>,
915    pv: Arc<ProcessVariable>,
916    sid: u32,
917}
918
919impl PvSubscription {
920    /// Register a value-change monitor on `pv`. Returns `None` when the
921    /// per-PV subscriber cap is reached. The caller emits the initial
922    /// snapshot itself (pvxs `SharedPV::attach` posts the current value
923    /// before storing the subscriber); registering the subscriber *before*
924    /// reading that snapshot is the miss-free ordering — a PUT racing the
925    /// two is then delivered through the stream rather than lost.
926    pub async fn subscribe(pv: Arc<ProcessVariable>) -> Option<Self> {
927        use crate::server::recgbl::EventMask;
928        // VALUE|LOG matches the record-side `DbSubscription` default so
929        // simple-PV and record-backed monitors gate identically; a
930        // pure-alarm `post_alarm` (ALARM|LOG) still intersects via LOG.
931        let mask = (EventMask::VALUE | EventMask::LOG).bits();
932        let sid = next_pv_sub_sid();
933        // `data_type` is nominal for snapshot consumers: `deliver` ships
934        // the full `Snapshot` and gates only on mask/filters, never on the
935        // stored type — `DbSubscription` likewise registers as `Double`.
936        let rx = pv.add_subscriber(sid, DbFieldType::Double, mask).await?;
937        Some(Self { rx, pv, sid })
938    }
939
940    /// Await the next value change as a full `Snapshot`. When the producer
941    /// parked a newer value in the coalesce slot because the bounded mpsc
942    /// filled mid-burst, [`coalesce_consume`] returns that newest value and
943    /// drains the now-stale queue tail, so a briefly-slow consumer converges
944    /// on the freshest value and never observes it stepping back to an older
945    /// queued snapshot — the same `coalesce_consume` discipline
946    /// `DbSubscription::next_event` applies.
947    pub async fn recv_snapshot(&mut self) -> Option<Snapshot> {
948        let queued = self.rx.recv().await?;
949        let coalesced = self.pv.pop_coalesced(self.sid).await;
950        let event = coalesce_consume(&mut self.rx, queued, coalesced);
951        Some(event.snapshot)
952    }
953}
954
955impl Drop for PvSubscription {
956    fn drop(&mut self) {
957        let pv = self.pv.clone();
958        let sid = self.sid;
959        // Mirror `DbSubscription::drop`: `remove_subscriber` needs an async
960        // lock, so remove the slot off-thread. No current runtime means no
961        // live subscription to clean up.
962        if tokio::runtime::Handle::try_current().is_ok() {
963            tokio::spawn(async move {
964                pv.remove_subscriber(sid).await;
965            });
966        }
967    }
968}
969
970#[cfg(test)]
971mod mask_gate_tests {
972    use super::*;
973
974    // CA DBE_* monitor mask bits (db_access.h).
975    const DBE_VALUE: u16 = 1;
976    const DBE_LOG: u16 = 2;
977    const DBE_ALARM: u16 = 4;
978
979    fn pv() -> ProcessVariable {
980        ProcessVariable::new("test:pv".into(), EpicsValue::Double(0.0))
981    }
982
983    /// A full-snapshot write must persist alarm + timestamp + userTag so
984    /// a later `snapshot()` (the GET path) reflects them — not just the
985    /// live monitor fan-out. A subsequent value-only `set()` carries no
986    /// explicit metadata and must revert the snapshot to NO_ALARM.
987    #[tokio::test]
988    async fn set_snapshot_metadata_persists_then_value_set_clears() {
989        let pv = pv();
990
991        // 42 ns exact: a `SystemTime` rounds this to 0 on Windows, so the
992        // round-trip is built from `WallTime` integers to actually exercise
993        // sub-100 ns persistence through `PostedMeta`.
994        let posted_time = WallTime::from_unix(1_600_000_000, 42);
995        let mut snap = Snapshot::new(EpicsValue::Double(7.0), 3, 2, posted_time);
996        snap.user_tag = 9;
997        pv.set_snapshot(snap).await;
998
999        let got = pv.snapshot().await;
1000        assert_eq!(got.value, EpicsValue::Double(7.0), "value persisted");
1001        assert_eq!(got.alarm.status, 3, "alarm.status persisted to GET");
1002        assert_eq!(got.alarm.severity, 2, "alarm.severity persisted to GET");
1003        assert_eq!(got.user_tag, 9, "userTag persisted to GET");
1004        assert_eq!(got.timestamp, posted_time, "timestamp persisted to GET");
1005
1006        // A plain value write reverts to the bare-PV default.
1007        pv.set(EpicsValue::Double(8.0)).await;
1008        let after = pv.snapshot().await;
1009        assert_eq!(after.value, EpicsValue::Double(8.0));
1010        assert_eq!(after.alarm.status, 0, "value set clears posted alarm");
1011        assert_eq!(after.alarm.severity, 0, "value set clears posted severity");
1012        assert_eq!(after.user_tag, 0, "value set clears posted userTag");
1013        assert_ne!(
1014            after.timestamp, posted_time,
1015            "value set must restamp the timestamp, not keep the posted one"
1016        );
1017    }
1018
1019    /// a `DBE_ALARM`-only subscriber must not receive a plain
1020    /// value set, but must receive an alarm post.
1021    #[tokio::test]
1022    async fn alarm_only_subscriber_skips_value_post() {
1023        let pv = pv();
1024        let mut rx = pv
1025            .add_subscriber(1, DbFieldType::Double, DBE_ALARM)
1026            .await
1027            .expect("subscriber added");
1028        pv.set(EpicsValue::Double(1.0)).await;
1029        assert!(
1030            rx.try_recv().is_err(),
1031            "DBE_ALARM-only subscriber must not receive a value post"
1032        );
1033        pv.post_alarm(2, 3).await;
1034        assert!(
1035            rx.try_recv().is_ok(),
1036            "DBE_ALARM subscriber must receive an alarm post"
1037        );
1038    }
1039
1040    /// a `DBE_VALUE`-only subscriber must not receive a
1041    /// `post_alarm`, but must receive value sets.
1042    #[tokio::test]
1043    async fn value_only_subscriber_skips_alarm_post() {
1044        let pv = pv();
1045        let mut rx = pv
1046            .add_subscriber(1, DbFieldType::Double, DBE_VALUE)
1047            .await
1048            .expect("subscriber added");
1049        pv.post_alarm(2, 3).await;
1050        assert!(
1051            rx.try_recv().is_err(),
1052            "DBE_VALUE-only subscriber must not receive an alarm post"
1053        );
1054        pv.set(EpicsValue::Double(1.0)).await;
1055        assert!(
1056            rx.try_recv().is_ok(),
1057            "DBE_VALUE subscriber must receive a value post"
1058        );
1059    }
1060
1061    // --- Regression: set_snapshot must reach DBE_LOG and DBE_ALARM-only subs ---
1062
1063    fn snapshot() -> Snapshot {
1064        Snapshot::new(
1065            EpicsValue::Double(2.0),
1066            0,
1067            0,
1068            std::time::SystemTime::UNIX_EPOCH,
1069        )
1070    }
1071
1072    /// A DBE_LOG (archiver) subscriber must receive a set_snapshot post.
1073    #[tokio::test]
1074    async fn log_subscriber_receives_snapshot_post() {
1075        let pv = pv();
1076        let mut rx = pv
1077            .add_subscriber(1, DbFieldType::Double, DBE_LOG)
1078            .await
1079            .expect("subscriber added");
1080        pv.set_snapshot(snapshot()).await;
1081        assert!(
1082            rx.try_recv().is_ok(),
1083            "DBE_LOG subscriber must receive a set_snapshot post"
1084        );
1085    }
1086
1087    /// A DBE_ALARM-only subscriber must receive a set_snapshot post.
1088    #[tokio::test]
1089    async fn alarm_only_subscriber_receives_snapshot_post() {
1090        let pv = pv();
1091        let mut rx = pv
1092            .add_subscriber(1, DbFieldType::Double, DBE_ALARM)
1093            .await
1094            .expect("subscriber added");
1095        pv.set_snapshot(snapshot()).await;
1096        assert!(
1097            rx.try_recv().is_ok(),
1098            "DBE_ALARM-only subscriber must receive a set_snapshot post"
1099        );
1100    }
1101
1102    /// A DBE_VALUE subscriber must still receive a set_snapshot post.
1103    #[tokio::test]
1104    async fn value_subscriber_receives_snapshot_post() {
1105        let pv = pv();
1106        let mut rx = pv
1107            .add_subscriber(1, DbFieldType::Double, DBE_VALUE)
1108            .await
1109            .expect("subscriber added");
1110        pv.set_snapshot(snapshot()).await;
1111        assert!(
1112            rx.try_recv().is_ok(),
1113            "DBE_VALUE subscriber must receive a set_snapshot post"
1114        );
1115    }
1116
1117    /// A `DBE_VALUE | DBE_ALARM` subscriber receives both event classes.
1118    #[tokio::test]
1119    async fn both_classes_receive_both_posts() {
1120        let pv = pv();
1121        let mut rx = pv
1122            .add_subscriber(1, DbFieldType::Double, DBE_VALUE | DBE_ALARM)
1123            .await
1124            .expect("subscriber added");
1125        pv.set(EpicsValue::Double(1.0)).await;
1126        assert!(rx.try_recv().is_ok(), "value post delivered to VALUE|ALARM");
1127        pv.post_alarm(2, 3).await;
1128        assert!(rx.try_recv().is_ok(), "alarm post delivered to VALUE|ALARM");
1129    }
1130
1131    /// A DBE_LOG-only subscriber (archiver) must receive both value
1132    /// events and alarm events.  Pre-fix: VALUE-only / ALARM-only post masks
1133    /// never intersected DBE_LOG(2), so archivers received silence.
1134    #[tokio::test]
1135    async fn br_r52_log_subscriber_receives_value_and_alarm_events() {
1136        const DBE_LOG: u16 = 2;
1137        let pv = pv();
1138        let mut rx = pv
1139            .add_subscriber(1, DbFieldType::Double, DBE_LOG)
1140            .await
1141            .expect("subscriber added");
1142        pv.set(EpicsValue::Double(1.0)).await;
1143        assert!(
1144            rx.try_recv().is_ok(),
1145            "DBE_LOG subscriber must receive a value post"
1146        );
1147        pv.post_alarm(2, 3).await;
1148        assert!(
1149            rx.try_recv().is_ok(),
1150            "DBE_LOG subscriber must receive an alarm post"
1151        );
1152    }
1153
1154    /// Every delivered event carries its post's `DBE_*` class — the
1155    /// per-event mask C attaches to the field log (`db_field_log.mask`)
1156    /// and pvxs narrows monitor decoding with (`groupsource.cpp:331-337`).
1157    #[tokio::test]
1158    async fn monitor_event_carries_post_class_mask() {
1159        use crate::server::recgbl::EventMask;
1160        let pv = pv();
1161        let mut rx = pv
1162            .add_subscriber(1, DbFieldType::Double, DBE_VALUE | DBE_LOG | DBE_ALARM)
1163            .await
1164            .expect("subscriber added");
1165        pv.set(EpicsValue::Double(1.0)).await;
1166        assert_eq!(
1167            rx.try_recv().expect("value event").mask,
1168            EventMask::VALUE | EventMask::LOG,
1169            "value post carries VALUE|LOG"
1170        );
1171        pv.post_alarm(2, 3).await;
1172        assert_eq!(
1173            rx.try_recv().expect("alarm event").mask,
1174            EventMask::ALARM | EventMask::LOG,
1175            "alarm post carries ALARM|LOG"
1176        );
1177    }
1178
1179    /// When a slow consumer forces coalescing, the surviving event's
1180    /// mask is the OR of every squashed event's class — the values are
1181    /// superseded by the newest snapshot, but a narrow consumer must
1182    /// still learn that an ALARM-class change happened inside the
1183    /// squashed burst. Covers both accumulation points: the producer's
1184    /// coalesce-slot displacement (`Subscriber::coalesce_overflow`) and
1185    /// the consumer's stale-tail drain (`coalesce_consume`).
1186    #[tokio::test]
1187    async fn coalescing_accumulates_event_class_masks() {
1188        use crate::server::recgbl::EventMask;
1189        let pv = Arc::new(ProcessVariable::new(
1190            "coalesce:mask".into(),
1191            EpicsValue::Double(0.0),
1192        ));
1193        let mut rx = pv
1194            .add_subscriber(7, DbFieldType::Double, DBE_VALUE | DBE_LOG | DBE_ALARM)
1195            .await
1196            .expect("subscriber added");
1197        // Fill the bounded queue (cap 64) with VALUE|LOG posts.
1198        for i in 1..=64u32 {
1199            pv.set(EpicsValue::Double(i as f64)).await;
1200        }
1201        // Overflow: the alarm post (ALARM|LOG) parks in the coalesce slot.
1202        pv.post_alarm(2, 3).await;
1203        // Displace the slot with a newer value post — the displaced
1204        // alarm's class must fold into the survivor.
1205        pv.set(EpicsValue::Double(99.0)).await;
1206
1207        let queued = rx.recv().await.expect("queued event");
1208        let coalesced = pv.pop_coalesced(7).await;
1209        let delivered = coalesce_consume(&mut rx, queued, coalesced);
1210        assert_eq!(
1211            delivered.snapshot.value.to_f64(),
1212            Some(99.0),
1213            "delivery converges on the newest value"
1214        );
1215        assert!(
1216            delivered
1217                .mask
1218                .contains(EventMask::VALUE | EventMask::ALARM | EventMask::LOG),
1219            "squashed alarm class survives in the delivered mask (got {:?})",
1220            delivered.mask
1221        );
1222    }
1223
1224    /// A simple-PV monitor whose bounded queue overflows mid-burst must
1225    /// never deliver the coalesced newest value and then step back to an
1226    /// older queued one. The producer fills the cap-64 queue with values
1227    /// `1..=64` and parks the newest (`80`) in the coalesce slot; the
1228    /// consumer must converge on `80` and never observe a value smaller
1229    /// than one it already delivered.
1230    ///
1231    /// Before the fix `recv_snapshot()` returned the coalesced `80` and
1232    /// then replayed the stale queue tail `1..=64`, so the delivered
1233    /// sequence ran `80, 1, 2, ...` — value time going backwards, which
1234    /// neither pvxs `SharedPV` nor the server monitor queue allow
1235    /// (`sharedpv.cpp:417-439`, `servermon.cpp:172-285`).
1236    #[tokio::test]
1237    async fn r0604_pv_overflow_never_delivers_newest_then_old() {
1238        use std::time::Duration;
1239        let pv = Arc::new(ProcessVariable::new(
1240            "coalesce:pv".into(),
1241            EpicsValue::Double(0.0),
1242        ));
1243        let mut sub = PvSubscription::subscribe(pv.clone())
1244            .await
1245            .expect("subscribe");
1246        // Fill the bounded queue (default cap 64) then overflow: with no
1247        // consumer draining, values 1..=64 queue and the newest (80) lands
1248        // in the coalesce slot.
1249        for i in 1..=80u32 {
1250            pv.set(EpicsValue::Double(i as f64)).await;
1251        }
1252        // Drain every immediately-available delivery; the recv past the
1253        // last event has nothing queued and times out, ending collection.
1254        let mut seq = Vec::new();
1255        while let Ok(Some(snap)) =
1256            tokio::time::timeout(Duration::from_millis(200), sub.recv_snapshot()).await
1257        {
1258            seq.push(snap.value.to_f64().expect("double value"));
1259        }
1260        assert!(!seq.is_empty(), "consumer must observe at least one value");
1261        for w in seq.windows(2) {
1262            assert!(
1263                w[0] <= w[1],
1264                "monitor delivery stepped backward {} -> {} (sequence {seq:?})",
1265                w[0],
1266                w[1],
1267            );
1268        }
1269        assert_eq!(
1270            *seq.last().unwrap(),
1271            80.0,
1272            "consumer must converge on the newest produced value (sequence {seq:?})"
1273        );
1274    }
1275}
1276
1277#[cfg(test)]
1278mod metadata_tests {
1279    use super::*;
1280
1281    fn meta() -> PvMetadata {
1282        PvMetadata {
1283            display: Some(DisplayInfo {
1284                units: "degC".into(),
1285                precision: 2,
1286                upper_disp_limit: 100.0,
1287                lower_disp_limit: -50.0,
1288                upper_alarm_limit: 90.0,
1289                upper_warning_limit: 80.0,
1290                lower_warning_limit: -20.0,
1291                lower_alarm_limit: -40.0,
1292                ..Default::default()
1293            }),
1294            control: Some(ControlInfo {
1295                upper_ctrl_limit: 95.0,
1296                lower_ctrl_limit: -45.0,
1297            }),
1298            enums: None,
1299        }
1300    }
1301
1302    fn pv() -> ProcessVariable {
1303        ProcessVariable::new("m:pv".into(), EpicsValue::Double(1.0))
1304    }
1305
1306    /// A bare PV serves no metadata until a proxy installs it; after
1307    /// `set_metadata`, the GET snapshot carries the shadow DBR_GR/DBR_CTRL.
1308    #[tokio::test]
1309    async fn set_metadata_serves_on_get_snapshot() {
1310        let pv = pv();
1311        assert!(
1312            pv.snapshot().await.display.is_none(),
1313            "bare PV must carry no metadata before install"
1314        );
1315        pv.set_metadata(meta());
1316        let snap = pv.snapshot().await;
1317        let d = snap.display.expect("display installed");
1318        assert_eq!(d.units, "degC");
1319        assert_eq!(d.precision, 2);
1320        assert_eq!(
1321            snap.control.expect("control installed").upper_ctrl_limit,
1322            95.0
1323        );
1324    }
1325
1326    /// A CTRL-type monitor must see the installed limits on every value
1327    /// event, not only the initial GET — value posts carry the metadata.
1328    #[tokio::test]
1329    async fn installed_metadata_rides_value_posts() {
1330        const DBE_VALUE: u16 = 1;
1331        let pv = pv();
1332        pv.set_metadata(meta());
1333        let mut rx = pv
1334            .add_subscriber(1, DbFieldType::Double, DBE_VALUE)
1335            .await
1336            .expect("subscriber added");
1337        pv.set(EpicsValue::Double(2.0)).await;
1338        let ev = rx.try_recv().expect("value event delivered");
1339        assert_eq!(
1340            ev.snapshot.display.expect("metadata on value post").units,
1341            "degC"
1342        );
1343    }
1344
1345    /// `apply_metadata` only supplies fields the caller left absent: a
1346    /// gateway snapshot that already carries its own display wins.
1347    #[tokio::test]
1348    async fn apply_metadata_does_not_clobber_caller_metadata() {
1349        const DBE_VALUE: u16 = 1;
1350        let pv = pv();
1351        pv.set_metadata(meta()); // installed units = degC
1352        let mut rx = pv
1353            .add_subscriber(1, DbFieldType::Double, DBE_VALUE)
1354            .await
1355            .expect("subscriber added");
1356        let mut snap = Snapshot::new(
1357            EpicsValue::Double(3.0),
1358            0,
1359            0,
1360            std::time::SystemTime::UNIX_EPOCH,
1361        );
1362        snap.display = Some(DisplayInfo {
1363            units: "volts".into(),
1364            ..Default::default()
1365        });
1366        pv.set_snapshot(snap).await;
1367        let ev = rx.try_recv().expect("snapshot delivered");
1368        assert_eq!(
1369            ev.snapshot.display.expect("caller display kept").units,
1370            "volts"
1371        );
1372    }
1373
1374    /// `post_property` reaches DBE_PROPERTY subscribers (carrying the
1375    /// metadata) and not DBE_VALUE-only subscribers.
1376    #[tokio::test]
1377    async fn post_property_reaches_only_property_subscribers() {
1378        const DBE_VALUE: u16 = 1;
1379        const DBE_PROPERTY: u16 = 8;
1380        let pv = pv();
1381        pv.set_metadata(meta());
1382        let mut prop_rx = pv
1383            .add_subscriber(1, DbFieldType::Double, DBE_PROPERTY)
1384            .await
1385            .expect("subscriber added");
1386        let mut val_rx = pv
1387            .add_subscriber(2, DbFieldType::Double, DBE_VALUE)
1388            .await
1389            .expect("subscriber added");
1390        pv.post_property(Snapshot::new(
1391            EpicsValue::Double(1.0),
1392            0,
1393            0,
1394            std::time::SystemTime::UNIX_EPOCH,
1395        ))
1396        .await;
1397        let ev = prop_rx
1398            .try_recv()
1399            .expect("DBE_PROPERTY subscriber receives property post");
1400        assert_eq!(
1401            ev.snapshot
1402                .display
1403                .expect("property post carries metadata")
1404                .units,
1405            "degC"
1406        );
1407        assert!(
1408            val_rx.try_recv().is_err(),
1409            "DBE_VALUE-only subscriber must not receive a property post"
1410        );
1411    }
1412
1413    /// A property post
1414    /// must carry the upstream CTRL event's status/severity and timestamp,
1415    /// not a fabricated `NO_ALARM` / wall-clock-now snapshot. C ca-gateway
1416    /// preserves `setStatSevr()` on the property callback
1417    /// (`gatePv.cc:2413-2438`); a downstream `DBE_PROPERTY` monitor must
1418    /// see `severity=MAJOR` and the upstream timestamp, even though only
1419    /// metadata changed.
1420    #[tokio::test]
1421    async fn post_property_preserves_upstream_alarm_and_timestamp() {
1422        const DBE_PROPERTY: u16 = 8;
1423        const MAJOR: u16 = 2; // epicsSevMajor
1424        const HIGH: u16 = 3; // epicsAlarmHigh
1425        let pv = pv();
1426        pv.set_metadata(meta());
1427        let mut prop_rx = pv
1428            .add_subscriber(1, DbFieldType::Double, DBE_PROPERTY)
1429            .await
1430            .expect("subscriber added");
1431        // The upstream CTRL event timestamp: a fixed point in the past, so
1432        // it is unmistakably NOT a fresh wall clock minted by the post.
1433        let upstream_ts = WallTime::from_unix(1_000_000, 0);
1434        pv.post_property(Snapshot::new(
1435            EpicsValue::Double(2.0),
1436            HIGH,
1437            MAJOR,
1438            upstream_ts,
1439        ))
1440        .await;
1441        let ev = prop_rx.try_recv().expect("property post delivered");
1442        assert_eq!(
1443            ev.snapshot.alarm.severity, MAJOR,
1444            "property post must carry the upstream MAJOR severity, not NO_ALARM"
1445        );
1446        assert_eq!(ev.snapshot.alarm.status, HIGH, "upstream status preserved");
1447        assert_eq!(
1448            ev.snapshot.timestamp, upstream_ts,
1449            "property post must keep the upstream timestamp, not a fresh wall clock"
1450        );
1451        // Shadow metadata is still overlaid onto the upstream snapshot.
1452        assert_eq!(
1453            ev.snapshot
1454                .display
1455                .expect("property post carries shadow metadata")
1456                .units,
1457            "degC"
1458        );
1459    }
1460}
1461
1462#[cfg(test)]
1463mod read_hook_tests {
1464    use super::*;
1465
1466    fn pv() -> ProcessVariable {
1467        ProcessVariable::new("g:pv".into(), EpicsValue::Double(1.0))
1468    }
1469
1470    /// No hook installed (the default for every record-backed and cached
1471    /// PV): `read_snapshot` is exactly `snapshot` wrapped in `Ok` — the
1472    /// stored value, byte-for-byte unchanged.
1473    #[tokio::test]
1474    async fn read_snapshot_without_hook_equals_snapshot() {
1475        let pv = pv();
1476        let read = pv.read_snapshot().await.expect("no-hook read never errors");
1477        let stored = pv.snapshot().await;
1478        assert_eq!(read.value, stored.value);
1479        assert_eq!(read.value, EpicsValue::Double(1.0));
1480    }
1481
1482    /// With a hook installed (no-cache mode), the GET value comes fresh
1483    /// from the hook, NOT from the stored shadow value — the stored value
1484    /// stays a stale sentinel that the hook overrides.
1485    #[tokio::test]
1486    async fn read_snapshot_fires_hook_for_fresh_value() {
1487        let pv = pv();
1488        // Stored shadow value is a sentinel the hook must override.
1489        pv.set(EpicsValue::Double(999.0)).await;
1490        pv.set_read_hook(Arc::new(|| {
1491            Box::pin(async {
1492                Ok(Snapshot::new(
1493                    EpicsValue::Double(42.0),
1494                    0,
1495                    0,
1496                    std::time::UNIX_EPOCH,
1497                ))
1498            })
1499        }));
1500        let read = pv.read_snapshot().await.expect("hook returns Ok");
1501        assert_eq!(
1502            read.value,
1503            EpicsValue::Double(42.0),
1504            "GET must serve the hook's fresh value, not the stored sentinel"
1505        );
1506    }
1507
1508    /// A hook failure propagates so the server can answer `ECA_GETFAIL`,
1509    /// matching C ca-gateway forwarding each read to the IOC.
1510    #[tokio::test]
1511    async fn read_snapshot_propagates_hook_error() {
1512        let pv = pv();
1513        pv.set_read_hook(Arc::new(|| Box::pin(async { Err(CaError::Disconnected) })));
1514        let err = pv.read_snapshot().await.expect_err("hook error propagates");
1515        assert!(matches!(err, CaError::Disconnected));
1516    }
1517
1518    /// The read hook is GET-path only: `snapshot` (monitor fan-out, the
1519    /// initial monitor event, access-rights re-posts) keeps serving the
1520    /// stored value even when a hook is installed.
1521    #[tokio::test]
1522    async fn snapshot_ignores_read_hook() {
1523        let pv = pv();
1524        pv.set(EpicsValue::Double(7.0)).await;
1525        pv.set_read_hook(Arc::new(|| {
1526            Box::pin(async {
1527                Ok(Snapshot::new(
1528                    EpicsValue::Double(42.0),
1529                    0,
1530                    0,
1531                    std::time::UNIX_EPOCH,
1532                ))
1533            })
1534        }));
1535        let snap = pv.snapshot().await;
1536        assert_eq!(
1537            snap.value,
1538            EpicsValue::Double(7.0),
1539            "snapshot must serve the stored value, never the read hook"
1540        );
1541    }
1542
1543    /// Fresh value + upstream alarm/time ride from the hook; the shadow's
1544    /// installed *property* metadata (display/control/enum) — which a
1545    /// `DBR_TIME_*` event does not carry — is overlaid for those fields.
1546    #[tokio::test]
1547    async fn read_snapshot_carries_shadow_metadata() {
1548        let pv = pv();
1549        pv.set_metadata(PvMetadata {
1550            display: Some(DisplayInfo {
1551                units: "mm".into(),
1552                precision: 3,
1553                ..Default::default()
1554            }),
1555            control: None,
1556            enums: None,
1557        });
1558        // The hook returns a Time-class snapshot (value + alarm + time,
1559        // no display/control/enum), exactly as `get_with_metadata(Time)`.
1560        pv.set_read_hook(Arc::new(|| {
1561            Box::pin(async {
1562                Ok(Snapshot::new(
1563                    EpicsValue::Double(5.0),
1564                    0,
1565                    0,
1566                    std::time::UNIX_EPOCH,
1567                ))
1568            })
1569        }));
1570        let read = pv.read_snapshot().await.expect("hook returns Ok");
1571        assert_eq!(read.value, EpicsValue::Double(5.0));
1572        assert_eq!(
1573            read.display
1574                .expect("shadow property metadata rides fresh value")
1575                .units,
1576            "mm"
1577        );
1578    }
1579
1580    /// A no-cache GET must report the FRESH upstream alarm and timestamp
1581    /// that travel with the value (C `getTimeCB` decodes the `DBR_TIME_*`
1582    /// event's status/severity/time before `setEventData`,
1583    /// `gatePv.cc:1789-1794`), NOT the shadow's last monitor-posted (or
1584    /// bare-PV default) alarm/time. Before the fix the read hook returned
1585    /// a bare value and `read_snapshot` grafted it onto the stored
1586    /// snapshot, so the GET reported the new value with a stale or default
1587    /// status/severity/timestamp.
1588    #[tokio::test]
1589    async fn read_snapshot_carries_upstream_alarm_not_shadow() {
1590        use std::time::{Duration, UNIX_EPOCH};
1591        let pv = pv();
1592        // The shadow's stored snapshot carries one alarm/time (a prior
1593        // monitor post). Make it concrete and DIFFERENT from the upstream
1594        // GET so a graft-onto-shadow regression is observable.
1595        let shadow_time = UNIX_EPOCH + Duration::from_secs(1_000);
1596        pv.set_snapshot(Snapshot::new(EpicsValue::Double(1.0), 7, 1, shadow_time))
1597            .await;
1598        // The fresh upstream GET reports a different value, alarm, and time.
1599        let upstream_time = WallTime::from_unix(2_000, 0);
1600        pv.set_read_hook(Arc::new(move || {
1601            Box::pin(
1602                async move { Ok(Snapshot::new(EpicsValue::Double(5.0), 17, 2, upstream_time)) },
1603            )
1604        }));
1605        let read = pv.read_snapshot().await.expect("hook returns Ok");
1606        assert_eq!(read.value, EpicsValue::Double(5.0), "fresh upstream value");
1607        assert_eq!(
1608            read.alarm.status, 17,
1609            "upstream alarm status, not shadow's 7"
1610        );
1611        assert_eq!(read.alarm.severity, 2, "upstream severity, not shadow's 1");
1612        assert_eq!(
1613            read.timestamp, upstream_time,
1614            "upstream timestamp, not shadow's"
1615        );
1616    }
1617}