Skip to main content

epics_base_rs/server/
pv.rs

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