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