epics_base_rs/server/pv.rs
1use std::sync::Arc;
2use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
3
4use crate::runtime::sync::PriorityInheritanceMutex;
5
6use crate::error::CaError;
7use crate::server::event_queue::{EventReader, EventSink, EventUser, PostOutcome, TryRecvError};
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 /// The posted value, **shared** with every other subscriber this post
190 /// reached.
191 ///
192 /// C never copies a wide value into an event at all. `db_create_field_log`
193 /// stores anything wider than `union native_value` by reference
194 /// (`dbfl_type_ref`, `dtor == NULL`, so `dbfl_has_copy` is false), and the
195 /// value is read at DELIVERY: `read_reply` reserves the payload inside the
196 /// client's existing send buffer (`cas_copy_in_header`,
197 /// `camessage.c:516`) and `dbGet` converts straight from the record's live
198 /// field into it (`dbAccess.c:1020`, the `!dbfl_has_copy(pfl)` arm). One
199 /// array, N events pointing at it, zero retained copies.
200 ///
201 /// The port cannot read the record at delivery — the event outlives the
202 /// record lock — so it snapshots once at post time. `Arc` is what makes
203 /// that snapshot C's single array instead of one owned copy per
204 /// subscriber: `record_instance` built `make_monitor_snapshot` once and
205 /// then deep-cloned it per subscriber, so a 1 MiB waveform with four
206 /// monitors cost 4 MiB retained where C costs four ~100-byte field logs.
207 /// Measured consequence on `x86_64-wrs-vxworks`: `memory allocation of
208 /// 1048576 bytes failed` and `signal 6` at fan-out 4, while the same four
209 /// clients without array monitors survived at a HIGHER MEM_USED
210 /// (213,311,488 B vs the 211,804,160 B the aborting run died at) — the
211 /// fan-out was the discriminator, not the memory level.
212 pub snapshot: Arc<Snapshot>,
213 /// Origin writer ID. When non-zero, subscribers with the same
214 /// `ignore_origin` can filter out self-triggered events.
215 /// Used to prevent sequencer write-back loops.
216 ///
217 /// **Scope**: tagged explicitly by the `put_*_post` tier
218 /// (`put_pv_and_post_with_origin`, records and simple PVs alike), and
219 /// inherited by every post inside the `put_*_process` tier's
220 /// synchronous put+process cascade through the thread-local ambient
221 /// write origin (`AmbientWriteOriginScope`) — both record funnels
222 /// (`notify_field_with_origin`, `notify_from_snapshot`) and the
223 /// simple-PV funnel (`ProcessVariable::deliver`) apply the same
224 /// inheritance rule. Posts from work a cascade merely spawned (async
225 /// record completions, driver pollers) run outside any scope and stay
226 /// origin 0.
227 pub origin: u64,
228 /// The `DBE_*` event class(es) this post carries FOR THIS SUBSCRIBER
229 /// — the poster's mask intersected with the subscriber's `select`,
230 /// never the poster's mask alone. C stamps exactly that on the field
231 /// log (`pLog->mask = caEventMask & pevent->select`,
232 /// `dbEvent.c:896-900`) and pvxs narrows per event from
233 /// `pDbFieldLog->mask` (`groupsource.cpp:331-337`). The intersection
234 /// is produced by `Subscriber::delivered_mask`, which is also the
235 /// delivery gate, so the two cannot drift apart. Carrying it on the
236 /// event lets subscribers narrow what they decode (e.g. a QSRV group
237 /// monitor updating only alarm leaves on a `DBE_ALARM`-only event)
238 /// and lets the `.{dbnd}`/`.{sync}`/`.{dec}` pre-chain filters see the
239 /// classes the client actually asked for.
240 /// When events coalesce under a slow consumer, masks accumulate by
241 /// OR — the surviving snapshot is the newest, the mask reports every
242 /// class that changed since the last delivered event.
243 pub mask: crate::server::recgbl::EventMask,
244}
245
246/// A subscriber waiting for PV value updates — C `evSubscrip`'s producer-side
247/// view. Its pending events live in the shared event queue
248/// ([`crate::server::event_queue`]), reached only through `sink`.
249pub struct Subscriber {
250 pub sid: u32,
251 pub data_type: DbFieldType,
252 pub mask: u16,
253 /// Producer half of this monitor's slot in the circuit's event queue.
254 /// `pub(crate)` so no code outside this crate can enqueue past the
255 /// append-vs-replace rule the queue owns.
256 pub(crate) sink: EventSink,
257 /// Server-side channel filter chain (epics-base 3.15.7).
258 /// Defaults to empty — every event passes unchanged. Populated
259 /// by the subscription path when the channel name carries a
260 /// `.{filter:opts}` JSON suffix (`dbnd`, `arr`, `ts`, ...).
261 pub filters: crate::server::database::filters::FilterChain,
262 /// Delivery gate. `true` (the default) delivers events normally;
263 /// `false` suppresses every post to this subscriber at the source —
264 /// nothing reaches the event queue, no filter is evaluated — so a
265 /// paused monitor stops the record-event work entirely, not just the
266 /// downstream frame. Mirrors EPICS `db_event_disable` / pvxs
267 /// `onStart(false)` (singlesource.cpp:151-173, groupsource.cpp:151-281):
268 /// the subscription object survives, only its event flow is gated, so
269 /// the same subscriber resumes on re-enable. Flipped only under the
270 /// owner's write lock via [`super::record::record_instance::RecordInstance::set_subscriber_active`]
271 /// (records) — the post paths read it under the matching read lock.
272 pub active: bool,
273}
274
275impl Subscriber {
276 /// The mask this subscriber's field log carries for `post`, or `None`
277 /// when the post is not for it.
278 ///
279 /// C `db_post_events` evaluates `caEventMask & pevent->select` TWICE
280 /// (`dbEvent.c:896-900`): once as the delivery gate, and again as the
281 /// mask stamped on the log the pre-chain filters then see
282 /// (`pLog->mask = caEventMask & pevent->select`). Returning the
283 /// narrowed mask rather than a bool is what keeps the two inseparable
284 /// — a caller cannot learn that it may deliver without also learning
285 /// what mask to deliver under, so the wide poster mask can no longer
286 /// leak past the gate into `dbnd`/`sync`/`dec`.
287 ///
288 /// An all-zero intersection means no delivery, which is also C: an
289 /// empty `caEventMask` ands to zero and the poster loop skips the
290 /// subscriber entirely.
291 pub(crate) fn delivered_mask(
292 &self,
293 post: crate::server::recgbl::EventMask,
294 ) -> Option<crate::server::recgbl::EventMask> {
295 let narrowed = post & crate::server::recgbl::EventMask::from_bits(self.mask);
296 (!narrowed.is_empty()).then_some(narrowed)
297 }
298
299 /// The single post path for both event sources (`ProcessVariable` value /
300 /// alarm / property posts and `RecordInstance` field monitors): hand the
301 /// event to this monitor's event queue, which owns C's append-vs-replace
302 /// decision (`db_queue_event_log`), and apply the one piece of accounting
303 /// that lives outside the queue — the counter for a value that a later post
304 /// displaced before the consumer ever saw it (C `nreplace`, plus the
305 /// latest-only collapse that C leaves uncounted; both mean one value the
306 /// consumer will never see).
307 pub(crate) fn post(&self, event: MonitorEvent) {
308 if matches!(
309 self.sink.post(event),
310 PostOutcome::Replaced | PostOutcome::Collapsed
311 ) {
312 DROPPED_MONITOR_EVENTS.fetch_add(1, Ordering::Relaxed);
313 }
314 }
315
316 /// The consumer for this monitor is gone; the producer row can be reaped.
317 pub(crate) fn is_closed(&self) -> bool {
318 self.sink.is_closed()
319 }
320}
321
322/// Shadow `DBR_GR_*` / `DBR_CTRL_*` / enum metadata for a
323/// non-record-backed PV.
324///
325/// A bare [`ProcessVariable`] has no record engine to derive units /
326/// precision / display+alarm+control limits / enum labels from, so a
327/// proxy that fronts an upstream IOC (the CA / PVA gateway) fetches the
328/// upstream's control metadata and installs it here via
329/// [`ProcessVariable::set_metadata`]. Every snapshot the PV emits —
330/// the GET path ([`ProcessVariable::snapshot`]) and every monitor
331/// event ([`ProcessVariable::post_property`], value, alarm, and
332/// gateway snapshot posts) — then carries it, so a downstream client
333/// that requested a `DBR_GR_*` / `DBR_CTRL_*` type receives the
334/// upstream metadata instead of zeroed limits.
335///
336/// Mirrors the C ca-gateway, where `gatePvData` subscribes to
337/// `DBE_PROPERTY` and issues a control-type `ca_array_get_callback`
338/// (`gatePv.cc:850-934`) then copies units / precision / graphic +
339/// control limits into the gateway's gdd attributes
340/// (`gatePv.cc:1916-2007`).
341#[derive(Debug, Clone, Default)]
342pub struct PvMetadata {
343 pub display: Option<DisplayInfo>,
344 pub control: Option<ControlInfo>,
345 pub enums: Option<EnumInfo>,
346}
347
348/// Metadata of the most recent full-snapshot write to a bare PV:
349/// alarm + acquisition timestamp + userTag. A bare `ProcessVariable`
350/// has no alarm engine, so without this it would forget everything a
351/// full-value write carried beyond the raw value. pvxs mailbox
352/// `SharedPV::post()` assigns the *whole* posted value to the current
353/// value (`sharedpv.cpp:417-432`); to match that, a PV that received a
354/// full posted Value must reflect its alarm/time on every later GET,
355/// not just to the monitor subscribers that saw the post live.
356#[derive(Clone)]
357struct PostedMeta {
358 alarm: crate::server::snapshot::AlarmInfo,
359 timestamp: WallTime,
360 user_tag: i32,
361}
362
363/// A process variable hosted by the server.
364pub struct ProcessVariable {
365 pub name: String,
366 /// The stored value. A synchronous `parking_lot::RwLock` (matching the
367 /// sibling `posted_meta` / `metadata` / hook locks): every access is a
368 /// single-expression read-or-write with no `.await` held across the
369 /// guard, so the value-read path (`get`, `snapshot`) is pure lock work
370 /// with no reactor dependency — the sans-io READ path. The write side
371 /// (`set` / `set_snapshot`) still `.await`s the monitor fan-out, but
372 /// drops this guard first.
373 pub value: parking_lot::RwLock<EpicsValue>,
374 /// Monitor fan-out list — **L7**.
375 ///
376 /// A BLOCKING mutex, not the async one: every emission path runs from a
377 /// record-processing thread with the record's advisory gate (L1) held, and
378 /// C's `db_post_events` likewise takes `LOCKREC(prec)` — the record's own
379 /// `mlok`, which is what guards the `mlis` monitor list this field is the
380 /// counterpart of — from inside `dbScanLock` (`dbEvent.c:887`, macro at
381 /// `:123`), and its callee `db_queue_event_log` takes `LOCKEVQUE(ev_que)`,
382 /// the queue's `writelock` (`:788`, macro at `:121`). `evUser->lock` is NOT
383 /// on the post path at all. Holding an async mutex here would put a
384 /// suspension point inside that window. Every critical section below is
385 /// bounded list work (`retain` / `push` / `sub.post`), with no I/O and no
386 /// `.await` inside it.
387 ///
388 /// Specifically a [`PriorityInheritanceMutex`] rather than a plain
389 /// `parking_lot::Mutex`, because both of those are `epicsMutex`es and on
390 /// the RTEMS arm every `epicsMutex` is a `PTHREAD_PRIO_INHERIT` pthread
391 /// mutex (`os/posix/osdMutex.c:71-88`, compiled for RTEMS via
392 /// `os/RTEMS-posix/osdMutex.c:8`). It is taken from banded IOC threads on
393 /// both sides — the emitting record-processing thread and a `CAS-client`
394 /// thread running `remove_subscriber` — so a plain mutex here reintroduces
395 /// the inversion L1 was converted to remove. Off the PI targets this is
396 /// `parking_lot::Mutex`, i.e. exactly what it was.
397 ///
398 /// A leaf of the acquisition order (`record_lock.rs` module doc): no other
399 /// lock is taken while it is held.
400 pub subscribers: PriorityInheritanceMutex<Vec<Subscriber>>,
401 /// Sticky metadata of the last full-snapshot write. `None` until a
402 /// [`Self::set_snapshot`] lands; a value-only [`Self::set`] clears it
403 /// back to `None` (a plain value write carries no explicit
404 /// alarm/time, so it reverts to NO_ALARM + wall-clock-now). When
405 /// `Some`, [`Self::snapshot`] serves these instead of the defaults.
406 /// Single meaning: the served snapshot reflects the most recent
407 /// write — value always current, metadata from that write.
408 posted_meta: parking_lot::RwLock<Option<PostedMeta>>,
409 /// Shadow DBR_GR_*/DBR_CTRL_*/enum metadata, installed by a proxy
410 /// (CA / PVA gateway) via [`Self::set_metadata`]. Empty for a plain
411 /// local PV. Stored under the same sync `parking_lot::RwLock` slot
412 /// rationale as the hooks: every snapshot builder reads it without
413 /// an `.await`. See [`PvMetadata`].
414 metadata: parking_lot::RwLock<PvMetadata>,
415 /// Optional hook consulted on client-originated writes. When set,
416 /// the CA TCP write path delegates to the hook instead of doing a
417 /// local `pv.set()`. See [`WriteHook`].
418 ///
419 /// Stored under `parking_lot::RwLock` (sync) rather than the
420 /// async `tokio::sync::RwLock` so the hot put-path can read it
421 /// without an `.await` round-trip — `write_hook()` is now a
422 /// constant-time clone of the optional `Arc`. The hook itself
423 /// is async (returns a `Future`); only the slot is sync.
424 write_hook: parking_lot::RwLock<Option<WriteHook>>,
425 /// optional access hook consulted by the CA server's
426 /// `compute_access` to decide a downstream client's read/write
427 /// rights for this PV. When set, it overrides the server's own ACF
428 /// for this PV — the gateway uses it to enforce `.pvlist` ASG-based
429 /// `can_read` / `can_write`, symmetric to [`Self::write_hook`].
430 /// Same sync `parking_lot::RwLock` slot rationale as `write_hook`.
431 access_hook: parking_lot::RwLock<Option<AccessHook>>,
432 /// optional read hook consulted by the CA server's one-shot GET path
433 /// ([`Self::read_snapshot`]) to fetch a fresh value instead of the
434 /// stored cell. Used by the CA gateway's no-cache mode to forward
435 /// each downstream GET to upstream. `None` (the default) keeps the
436 /// read path serving the stored value, identical to before. Same
437 /// sync slot rationale as [`Self::write_hook`]: the GET path clones
438 /// the optional `Arc` without an `.await`, then awaits the hook
439 /// outside any lock. See [`ReadHook`].
440 read_hook: parking_lot::RwLock<Option<ReadHook>>,
441 /// Terminal destruction marker — the CAS `casPV` delete signal.
442 ///
443 /// Set once by [`Self::destroy`] and never cleared: a destroyed PV is
444 /// gone, not paused, so there is no state a later write could restore
445 /// and no second meaning the flag can carry. The database's removal
446 /// funnels are its only writers, which is what makes *removed from the
447 /// database* and *destroyed* the same event rather than two that a
448 /// caller has to remember to pair.
449 destroyed: std::sync::atomic::AtomicBool,
450}
451
452impl ProcessVariable {
453 pub fn new(name: String, initial: EpicsValue) -> Self {
454 Self {
455 name,
456 value: parking_lot::RwLock::new(initial),
457 subscribers: PriorityInheritanceMutex::new(Vec::new()),
458 metadata: parking_lot::RwLock::new(PvMetadata::default()),
459 posted_meta: parking_lot::RwLock::new(None),
460 write_hook: parking_lot::RwLock::new(None),
461 access_hook: parking_lot::RwLock::new(None),
462 read_hook: parking_lot::RwLock::new(None),
463 destroyed: std::sync::atomic::AtomicBool::new(false),
464 }
465 }
466
467 /// Install (or replace) the shadow DBR_GR_*/DBR_CTRL_*/enum
468 /// metadata served on this PV's snapshots. Used by the CA / PVA
469 /// gateway after fetching the upstream IOC's control metadata. See
470 /// [`PvMetadata`]. To publish the change to downstream property
471 /// monitors, follow with [`Self::post_property`].
472 pub fn set_metadata(&self, metadata: PvMetadata) {
473 *self.metadata.write() = metadata;
474 }
475
476 /// Snapshot (clone) of the installed shadow metadata; empty
477 /// (`Default`) for a plain local PV.
478 pub fn metadata(&self) -> PvMetadata {
479 self.metadata.read().clone()
480 }
481
482 /// Fill any metadata field the snapshot leaves `None` from the
483 /// installed shadow metadata. A field the caller already populated
484 /// (e.g. a gateway snapshot that carried its own metadata) wins —
485 /// this only supplies what is otherwise absent, so every emission
486 /// path serves the upstream metadata uniformly without clobbering a
487 /// richer source.
488 fn apply_metadata(&self, snap: &mut Snapshot) {
489 let meta = self.metadata.read();
490 if snap.display.is_none() {
491 snap.display = meta.display.clone();
492 }
493 if snap.control.is_none() {
494 snap.control = meta.control.clone();
495 }
496 if snap.enums.is_none() {
497 snap.enums = meta.enums.clone();
498 }
499 // A bare PV has no `rset`, so "which properties does this channel
500 // supply" is answered by what metadata it actually HAS: a proxy that
501 // shadowed an upstream IOC's display/control/enum info supplies those
502 // properties, a mailbox PV that nobody gave metadata to supplies none.
503 // Assigned here, in the one owner of a bare PV's metadata, so the mask
504 // and the values it describes cannot disagree.
505 // See [`crate::server::snapshot::PropertySupport`].
506 snap.properties = PropertySupport {
507 units: snap.display.is_some(),
508 precision: snap.display.is_some(),
509 graphic_double: snap.display.is_some(),
510 alarm_double: snap.display.is_some(),
511 control_double: snap.control.is_some(),
512 enum_strs: snap.enums.is_some(),
513 }
514 .narrowed_to_field(snap.value.db_field_type(), false);
515 }
516
517 /// Install an access hook. Replaces any previously
518 /// installed hook.
519 pub fn set_access_hook(&self, hook: AccessHook) {
520 *self.access_hook.write() = Some(hook);
521 }
522
523 /// Snapshot of the installed access hook (clone of the `Arc`), or
524 /// `None`. Consulted by the CA server's `compute_access`; cheap and
525 /// non-async, like [`Self::write_hook`].
526 pub fn access_hook(&self) -> Option<AccessHook> {
527 self.access_hook.read().clone()
528 }
529
530 /// Install a read hook. Replaces any previously-installed hook.
531 /// Used by the CA gateway's no-cache mode so each downstream GET is
532 /// served by a fresh upstream fetch. See [`ReadHook`].
533 pub fn set_read_hook(&self, hook: ReadHook) {
534 *self.read_hook.write() = Some(hook);
535 }
536
537 /// Snapshot of the installed read hook (clone of the `Arc`), or
538 /// `None`. Cheap and non-async, like [`Self::write_hook`]: the read
539 /// lock is released before the cloned `Arc` returns, so the caller's
540 /// subsequent `await` on the hook holds no lock.
541 pub fn read_hook(&self) -> Option<ReadHook> {
542 self.read_hook.read().clone()
543 }
544
545 /// Install a write hook. Replaces any previously-installed hook.
546 pub fn set_write_hook(&self, hook: WriteHook) {
547 *self.write_hook.write() = Some(hook);
548 }
549
550 /// Remove any installed write hook.
551 pub fn clear_write_hook(&self) {
552 *self.write_hook.write() = None;
553 }
554
555 /// Snapshot of the installed write hook (clone of the `Arc`), or
556 /// `None` if none. Used by the CA TCP write path; cheap and
557 /// non-async — the read lock is released before the cloned `Arc`
558 /// returns, so the caller's subsequent `await` on the hook does
559 /// not hold any lock.
560 pub fn write_hook(&self) -> Option<WriteHook> {
561 self.write_hook.read().clone()
562 }
563
564 /// Get the current value.
565 ///
566 /// Synchronous: a single-expression read-lock clone with no `.await`,
567 /// so the value-read path carries no reactor dependency (sans-io).
568 pub fn get(&self) -> EpicsValue {
569 self.value.read().clone()
570 }
571
572 /// Build a Snapshot for this bare PV.
573 ///
574 /// A `ProcessVariable` is a non-record-backed channel: it has no
575 /// alarm engine, no DESC/EGU/PREC metadata and no timestamp user
576 /// tag of its own. The snapshot is therefore value + `NO_ALARM` +
577 /// wall-clock now, with `user_tag` = 0. Display / control / enum
578 /// metadata is `None` *unless* a proxy installed it via
579 /// [`Self::set_metadata`] (the CA / PVA gateway shadowing an
580 /// upstream IOC) — see `Self::apply_metadata`. Record-backed
581 /// channels build their snapshot via
582 /// `RecordInstance::snapshot_for_field`, which carries the record's
583 /// own alarm/metadata. The only path that injects a non-zero alarm
584 /// onto a bare PV is [`Self::post_alarm`] (used by the gateway
585 /// adapter to surface upstream disconnect).
586 pub fn snapshot(&self) -> Snapshot {
587 let value = self.value.read().clone();
588 // Serve the sticky metadata of the last full-snapshot write if
589 // one landed (pvxs mailbox parity: a posted full Value stays the
590 // current value, alarm/time included); otherwise the bare-PV
591 // default of NO_ALARM + wall-clock-now.
592 let mut snap = match self.posted_meta.read().clone() {
593 Some(m) => {
594 let mut s = Snapshot::new(value, m.alarm.status, m.alarm.severity, m.timestamp);
595 s.alarm.ackt = m.alarm.ackt;
596 s.alarm.acks = m.alarm.acks;
597 s.user_tag = m.user_tag;
598 s
599 }
600 None => Snapshot::new(value, 0, 0, crate::runtime::time::now_wall()),
601 };
602 self.apply_metadata(&mut snap);
603 snap
604 }
605
606 /// Build the snapshot served on a one-shot client GET
607 /// (`CA_PROTO_READ` / `CA_PROTO_READ_NOTIFY`).
608 ///
609 /// When a [`ReadHook`] is installed (the CA gateway's no-cache mode),
610 /// the snapshot is fetched fresh through the hook — value *and* its
611 /// upstream alarm status/severity and IOC timestamp together — and the
612 /// shadow's last-known property metadata (display/control/enum) is
613 /// overlaid for the fields a `DBR_TIME_*` event does not carry; on hook
614 /// error the failure propagates so the server can answer `ECA_GETFAIL`,
615 /// matching C ca-gateway forwarding each read to the IOC under
616 /// `-no_cache` (`gateVc.cc:1361-1369`, `gatePv.cc:976`/`:1789-1794`).
617 /// Without a hook this is exactly [`Self::snapshot`] wrapped in `Ok`,
618 /// so the GET path is unchanged for every record-backed and cached PV.
619 ///
620 /// Only the GET path calls this; monitor fan-out, the initial monitor
621 /// event, and access-rights re-posts keep using [`Self::snapshot`]
622 /// (the stored value), so a no-cache PV still backs a downstream
623 /// monitor with its upstream subscription's events rather than a
624 /// per-event upstream get.
625 pub async fn read_snapshot(&self) -> Result<Snapshot, CaError> {
626 match self.read_hook() {
627 Some(hook) => {
628 // The hook issues a metadata-bearing upstream GET
629 // (`DbrClass::Time`), so the returned snapshot already
630 // carries the fresh value WITH its upstream alarm
631 // status/severity and IOC timestamp — mirroring C
632 // `getTimeCB` decoding the `DBR_TIME_*` event before
633 // `setEventData`. A `DBR_TIME_*` event does not carry
634 // display/control/enum metadata, so overlay the shadow's
635 // last-known property metadata for those absent fields only
636 // (a separate upstream path feeds it, exactly as C splits
637 // the value/time path from the property monitor). Never
638 // graft the fresh value onto the stored snapshot's
639 // alarm/time, which may be stale or the bare-PV default.
640 let mut snap = hook().await?;
641 self.apply_metadata(&mut snap);
642 Ok(snap)
643 }
644 None => Ok(self.snapshot()),
645 }
646 }
647
648 /// Synchronous companion to [`Self::read_snapshot`] for the one-shot GET
649 /// path (`CA_PROTO_READ` / `CA_PROTO_READ_NOTIFY`).
650 ///
651 /// `Some(snapshot)` when NO read hook is installed — the sans-io GET that
652 /// every record-backed and cached PV takes: [`Self::snapshot`] of the
653 /// stored value, produced with no `.await` and no reactor dependency.
654 /// `None` when a gateway no-cache [`ReadHook`] IS installed, whose `hook()`
655 /// is a genuine upstream network GET; the caller must then take the async
656 /// [`Self::read_snapshot`] instead. This keeps the hook / no-hook decision
657 /// in one owner, in lockstep with `read_snapshot` — the only difference is
658 /// that the async fallible upstream fetch is surfaced to the caller as
659 /// `None` rather than performed here.
660 pub fn read_snapshot_local(&self) -> Option<Snapshot> {
661 match self.read_hook() {
662 Some(_) => None,
663 None => Some(self.snapshot()),
664 }
665 }
666
667 /// Set a new value and notify all subscribers.
668 pub fn set(&self, new_value: EpicsValue) {
669 self.set_with_origin(new_value, 0);
670 }
671
672 /// [`Self::set`] tagged with the writer's origin: the value post carries
673 /// `origin` so an origin-aware consumer can recognise (and skip) the
674 /// writer's own event — the simple-PV side of the
675 /// `put_pv_and_post_with_origin` self-write contract. Origin 0 is the
676 /// untagged default (never filtered).
677 pub fn set_with_origin(&self, new_value: EpicsValue, origin: u64) {
678 {
679 let mut val = self.value.write();
680 *val = new_value.clone();
681 }
682 // A plain value write carries no explicit alarm/time — revert to
683 // the bare-PV default so a stale full-snapshot's metadata does
684 // not linger on a value the client never stamped.
685 *self.posted_meta.write() = None;
686 self.notify_subscribers(new_value, origin);
687 }
688
689 /// Set value from a full snapshot (value + alarm + timestamp) and notify
690 /// all subscribers. Used by the CA gateway forwarding task to propagate
691 /// the upstream alarm status/severity and IOC timestamp to downstream
692 /// monitors. Mirrors `gateVcData::setEventData` + `vcPostEvent` in the
693 /// C ca-gateway: the incoming `dbr_time_xxx` GDD carries all three fields.
694 pub fn set_snapshot(&self, snapshot: Snapshot) {
695 {
696 let mut val = self.value.write();
697 *val = snapshot.value.clone();
698 }
699 // Persist the posted alarm/time/userTag so a later GET reflects
700 // the full posted value, not just the live monitor fan-out.
701 *self.posted_meta.write() = Some(PostedMeta {
702 alarm: snapshot.alarm.clone(),
703 timestamp: snapshot.timestamp,
704 user_tag: snapshot.user_tag,
705 });
706 self.notify_subscribers_from_snapshot(snapshot);
707 }
708
709 /// Single delivery owner: emit `snapshot` to every live subscriber
710 /// whose `DBE_*` mask intersects `post`.
711 ///
712 /// Every emission path ([`Self::notify_subscribers`] value posts,
713 /// [`Self::post_alarm`], [`Self::notify_subscribers_from_snapshot`]
714 /// gateway posts, [`Self::post_property`]) routes through here so the
715 /// mask gate (`caEventMask & pevent->select`, `dbEvent.c:896-900`),
716 /// the per-subscriber channel-filter chain, and the slow-consumer
717 /// coalesce-overflow accounting are applied identically — one event
718 /// class differs per caller, nothing else. The snapshot is built once
719 /// by the caller (one timestamp per logical event) and SHARED with every
720 /// subscriber — C's one array behind N field logs. A per-subscription
721 /// filter that rewrites the value pays for its own copy, and only then
722 /// (`Arc::make_mut`), which is also C: the filter chain runs
723 /// per-subscription and a filter that changes the value makes its own
724 /// field log.
725 fn deliver(&self, post: crate::server::recgbl::EventMask, snapshot: Snapshot, origin: u64) {
726 use crate::server::database::filters::FilteredMonitorEvent;
727 let snapshot = Arc::new(snapshot);
728 // Same ambient-origin inheritance as the record funnels
729 // (`notify_field_with_origin` / `notify_from_snapshot`): a post
730 // carrying no origin of its own inherits the current thread's
731 // ambient write origin, so a simple PV written from inside an
732 // in-process writer's synchronous put cascade tags its event
733 // with the writer's origin too. 0 outside any scope.
734 let origin = if origin != 0 {
735 origin
736 } else {
737 crate::server::record::ambient_write_origin()
738 };
739 let mut subs = self.subscribers.lock();
740 // Remove subscribers whose consumer has been dropped.
741 subs.retain(|sub| !sub.is_closed());
742 for sub in subs.iter() {
743 // Paused subscribers (`db_event_disable`) receive nothing —
744 // skip before any work so a disabled monitor stops the event
745 // flow at the source.
746 if !sub.active {
747 continue;
748 }
749 // Gate and narrow in one step: `Subscriber::delivered_mask`
750 // owns C's twice-used `caEventMask & pevent->select`.
751 let Some(mask) = sub.delivered_mask(post) else {
752 continue;
753 };
754 let event = MonitorEvent {
755 snapshot: Arc::clone(&snapshot),
756 origin,
757 mask,
758 };
759 // The channel-filter chain may suppress this event (e.g.
760 // `dbnd` deadband not crossed); the event's mask tells value
761 // filters whether to pass through (446e0d4a).
762 let filtered = if sub.filters.is_empty() {
763 Some(event)
764 } else {
765 sub.filters
766 .apply(FilteredMonitorEvent::new(event))
767 .map(|fe| fe.event)
768 };
769 let Some(event) = filtered else {
770 continue;
771 };
772 // C `db_queue_event_log`: the queue appends, or replaces this
773 // monitor's last entry in place when it is in flow control or
774 // nearly full. Earlier distinct entries are never discarded.
775 sub.post(event);
776 }
777 }
778
779 /// Push a fresh monitor event holding the current value but with
780 /// the supplied alarm severity/status. Used by the PVA / CA
781 /// gateway adapter to surface upstream-disconnect to downstream
782 /// monitor subscribers without dropping the simple PV (which
783 /// would force every downstream client into ECA_DISCONN +
784 /// reconnect storms when the upstream is just briefly
785 /// unreachable). Mirrors gatePvData::death's "alarm-post"
786 /// alternative discussed in the C++ ca-gateway audit.
787 pub fn post_alarm(&self, severity: u16, status: u16) {
788 use crate::server::recgbl::EventMask;
789 let value = self.value.read().clone();
790 let mut snapshot = Snapshot::new(value, status, severity, crate::runtime::time::now_wall());
791 self.apply_metadata(&mut snapshot);
792 // ALARM|LOG so DBE_LOG (archiver) subscribers receive alarm events.
793 self.deliver(EventMask::ALARM | EventMask::LOG, snapshot, 0);
794 }
795
796 /// Post a `DBE_PROPERTY` monitor event carrying the decoded upstream
797 /// CTRL event `snapshot` — its value plus the upstream status /
798 /// severity and timestamp — overlaid with the installed shadow
799 /// metadata, so downstream property-change monitors re-read the units /
800 /// precision / limits / enum labels with the *upstream* alarm state.
801 ///
802 /// Used by the CA / PVA gateway when an upstream `DBE_PROPERTY` event
803 /// fires (metadata changed) after it has refreshed the shadow PV via
804 /// [`Self::set_metadata`]. The caller supplies the snapshot rather than
805 /// this method synthesising one: C ca-gateway decodes the upstream
806 /// `DBR_CTRL_*` callback and re-posts the value with `setStatSevr()`
807 /// status/severity preserved (`gatePv.cc:2413-2438`,
808 /// `runValueDataCB`), leaving the timestamp as the control DBR carries
809 /// none — it must NOT be replaced with a fresh `NO_ALARM` /
810 /// wall-clock-now snapshot just because metadata changed. Pass the
811 /// timestamp the upstream value carried (the control event has none of
812 /// its own); pass `status`/`severity` from the upstream CTRL payload.
813 /// Property events are a distinct class from value/alarm: only
814 /// `DBE_PROPERTY` subscribers receive them.
815 pub async fn post_property(&self, mut snapshot: Snapshot) {
816 use crate::server::recgbl::EventMask;
817 self.apply_metadata(&mut snapshot);
818 self.deliver(EventMask::PROPERTY, snapshot, 0);
819 }
820
821 /// Notify all subscribers of a new value, tagged with the writer's
822 /// `origin` (0 = untagged).
823 fn notify_subscribers(&self, value: EpicsValue, origin: u64) {
824 use crate::server::recgbl::EventMask;
825 let mut snapshot = Snapshot::new(value, 0, 0, crate::runtime::time::now_wall());
826 self.apply_metadata(&mut snapshot);
827 // VALUE|LOG so DBE_LOG (archiver) subscribers receive value events.
828 self.deliver(EventMask::VALUE | EventMask::LOG, snapshot, origin);
829 }
830
831 /// Notify all subscribers using a pre-built Snapshot (value + alarm +
832 /// timestamp). Used by `set_snapshot` to propagate the upstream alarm
833 /// and IOC timestamp without synthesising a new zero-alarm local-time
834 /// snapshot. Installed shadow metadata fills any metadata field the
835 /// gateway snapshot left absent (see [`Self::apply_metadata`]).
836 fn notify_subscribers_from_snapshot(&self, mut snapshot: Snapshot) {
837 use crate::server::recgbl::EventMask;
838 self.apply_metadata(&mut snapshot);
839 // C gateway fires postEvent(VALUE|ALARM|LOG) for every
840 // upstream event (gateVc.cc:374-376); match it so DBE_LOG
841 // archivers and DBE_ALARM-only monitors receive gateway snapshot posts.
842 self.deliver(
843 EventMask::VALUE | EventMask::LOG | EventMask::ALARM,
844 snapshot,
845 0,
846 );
847 }
848
849 /// Add an in-process subscriber, attached to an event queue of its own.
850 ///
851 /// C `db_add_event(ctx, ...)` puts a monitor on the queue chain of the
852 /// `event_user` (client) that owns it. An in-process consumer is its own
853 /// client, so it gets its own [`EventUser`] — nothing else shares its
854 /// queue, and flow control (a CA circuit concept) never engages on it.
855 /// The CA server, whose subscriptions DO share one circuit-wide queue, uses
856 /// [`Self::add_subscriber_on`].
857 pub fn add_subscriber(
858 &self,
859 sid: u32,
860 data_type: DbFieldType,
861 mask: u16,
862 ) -> Option<EventReader> {
863 self.add_subscriber_on(&EventUser::new(), sid, data_type, mask)
864 }
865
866 /// Add a subscriber whose events are queued on `user`'s event queue —
867 /// C `db_add_event` with the circuit's `event_user` as context. Every
868 /// subscription on one CA circuit shares that queue, and therefore its
869 /// `nDuplicates`: a duplicate queued for one of them releases the
870 /// EVENTS_OFF drain for all of them (`dbEvent.c:947`).
871 ///
872 /// Returns `None` when the per-PV subscriber cap is reached (defends
873 /// against a misbehaving client opening many MONITOR ops against one shared
874 /// PV; the per-channel cap limits channels but not subscriber rows on a
875 /// single PV). Operators override it via `EPICS_CAS_MAX_SUBSCRIBERS_PER_PV`.
876 pub fn add_subscriber_on(
877 &self,
878 user: &EventUser,
879 sid: u32,
880 data_type: DbFieldType,
881 mask: u16,
882 ) -> Option<EventReader> {
883 let cap = max_subscribers_per_pv();
884 let mut subs = self.subscribers.lock();
885 // A destroyed PV takes no new monitor. The flag is set under this
886 // same lock by `destroy`, so `destroyed => no subscribers` holds by
887 // construction and a CREATE_CHAN + EVENT_ADD racing the destruction
888 // cannot re-attach to a corpse.
889 if self.is_destroyed() {
890 return None;
891 }
892 // Reap rows whose consumer is gone BEFORE counting
893 // against the cap. `notify_subscribers` / `post_alarm`
894 // already retain-filter on every emission, but a PV with
895 // no value changes (e.g. a static catalog entry that
896 // dashboards latch onto and drop) never triggered the
897 // reaper — a long-lived subscribe / disconnect storm could
898 // pin the Vec at `cap` worth of dead rows and lock
899 // out genuine new subscribers with a false-positive cap-
900 // reached warning. Same defect class as the
901 // NDPluginPva subscribe reaper (qsrv/pva_adapter.rs:129).
902 subs.retain(|s| !s.is_closed());
903 if subs.len() >= cap {
904 tracing::warn!(
905 pv = %self.name,
906 live = subs.len(),
907 cap,
908 "PV subscriber cap reached, refusing add_subscriber"
909 );
910 return None;
911 }
912 let (sink, reader) = crate::server::event_queue::attach(user, sid);
913 subs.push(Subscriber {
914 sid,
915 data_type,
916 mask,
917 sink,
918 filters: crate::server::database::filters::FilterChain::new(),
919 active: true,
920 });
921 Some(reader)
922 }
923
924 /// attach a channel-filter chain to an already-added
925 /// subscriber (looked up by `sid`). The CA server first
926 /// `add_subscriber`s, then attaches the chain parsed from the
927 /// channel's `.{...}` suffix — symmetric with the record-field
928 /// `RecordInstance::attach_filter_to_last_subscriber` path, so a
929 /// `SimplePv` monitor runs the SAME filter chain as a record-field
930 /// monitor instead of the empty default `FilterChain` that
931 /// `add_subscriber` installs. Update delivery
932 /// (`Self::notify_subscribers` / [`Self::post_alarm`]) already
933 /// applies `sub.filters`; this is the missing wiring that populates
934 /// it.
935 ///
936 /// The caller passes a FRESH chain per subscriber so stateful
937 /// filters (`dbnd` last-value, `dec` counter, `sync` state) stay
938 /// isolated across subscribers. An empty chain is a no-op (keeps the
939 /// default). No-op when no subscriber matches `sid` (e.g. it was
940 /// reaped between add and attach).
941 pub fn attach_filters_to_subscriber(
942 &self,
943 sid: u32,
944 filters: crate::server::database::filters::FilterChain,
945 ) {
946 if filters.is_empty() {
947 return;
948 }
949 let mut subs = self.subscribers.lock();
950 if let Some(sub) = subs.iter_mut().find(|s| s.sid == sid) {
951 sub.filters = filters;
952 }
953 }
954
955 /// Remove a subscriber by subscription ID.
956 pub fn remove_subscriber(&self, sid: u32) {
957 let mut subs = self.subscribers.lock();
958 subs.retain(|s| s.sid != sid);
959 }
960
961 /// Destroy this PV: drop every monitor and refuse every future one.
962 ///
963 /// The `casPV` destruction ca-gateway performs with `delete vc` when an
964 /// upstream channel dies (`gatePv.cc:601`) — the downstream monitors
965 /// stop rather than receive one more frame. Dropping the [`Subscriber`]
966 /// rows drops their producer halves, so each consumer observes
967 /// end-of-stream instead of silence.
968 ///
969 /// Reserved to the database's removal funnels ([`crate::server::database::PvDatabase::remove_simple_pv`]),
970 /// so *removed from the database* and *destroyed* cannot come apart.
971 /// Returns `true` for the call that performed the transition.
972 pub(crate) fn destroy(&self) -> bool {
973 // Marked and drained under ONE hold of the subscriber lock, which is
974 // the lock `add_subscriber_on` reads the flag under: an add either
975 // sees the mark and refuses, or completes wholly before the drain.
976 // No interleaving leaves a live row on a destroyed PV.
977 let mut subs = self.subscribers.lock();
978 let first = !self
979 .destroyed
980 .swap(true, std::sync::atomic::Ordering::AcqRel);
981 subs.clear();
982 first
983 }
984
985 /// Whether `Self::destroy` has run. A server holding an `Arc` to this
986 /// PV reads it to learn that the channel it serves has to be torn down.
987 pub fn is_destroyed(&self) -> bool {
988 self.destroyed.load(std::sync::atomic::Ordering::Acquire)
989 }
990
991 /// A fresh, LIVE PV carrying this one's identity: same name, same
992 /// write / access / read hooks, same shadow metadata, seeded with
993 /// `initial`.
994 ///
995 /// The replacement a proxy installs when what it fronts comes back —
996 /// ca-gateway builds a new `gateVcData` on the next exist-test after
997 /// `gatePvData::death` deleted the old one (`gatePv.cc:601`). The
998 /// destruction mark is deliberately NOT carried: the corpse stays a
999 /// corpse, and the replacement is a different object that the retired
1000 /// channels never held, so no client can be handed the new PV without
1001 /// a fresh `CREATE_CHANNEL`.
1002 pub fn respawn(&self, initial: EpicsValue) -> Self {
1003 let fresh = Self::new(self.name.clone(), initial);
1004 if let Some(h) = self.write_hook() {
1005 fresh.set_write_hook(h);
1006 }
1007 if let Some(h) = self.access_hook() {
1008 fresh.set_access_hook(h);
1009 }
1010 if let Some(h) = self.read_hook() {
1011 fresh.set_read_hook(h);
1012 }
1013 fresh.set_metadata(self.metadata());
1014 fresh
1015 }
1016}
1017
1018/// Subscriber-id source for in-process [`PvSubscription`] monitors on a
1019/// [`ProcessVariable`]. A `ProcessVariable`'s subscriber `Vec` is disjoint
1020/// from any `RecordInstance`'s, so this is independent of the record-side
1021/// allocator; it only has to stay unique among the simple-PV subscribers
1022/// competing for one PV. Seeded at 1_000_000 for the same reason the
1023/// record allocator is — keep in-process sids clear of the low,
1024/// client-assigned wire subscription ids the CA server also registers on
1025/// the same PV.
1026static NEXT_PV_SUB_SID: AtomicU32 = AtomicU32::new(1_000_000);
1027
1028fn next_pv_sub_sid() -> u32 {
1029 NEXT_PV_SUB_SID.fetch_add(1, Ordering::Relaxed)
1030}
1031
1032/// In-process value-change monitor on a simple [`ProcessVariable`], the
1033/// counterpart of the record-side `DbSubscription`.
1034///
1035/// The PUT path (`ProcessVariable::set` / `set_snapshot`) calls
1036/// `notify_subscribers`, which fans the new value out to every registered
1037/// subscriber, so a consumer holding a `PvSubscription` observes every
1038/// later PUT — not just the connect-time snapshot. This mirrors pvxs
1039/// `SharedPV::post()` delivering a cloned update to each stored subscriber
1040/// (`sharedpv.cpp:417-440`).
1041///
1042/// The handle owns its `Subscriber` slot: `Drop` removes it, so a dropped
1043/// consumer cannot leave a dead subscriber row in
1044/// `ProcessVariable.subscribers` — the same leak `DbSubscription`'s `Drop`
1045/// closes for records.
1046pub struct PvSubscription {
1047 reader: EventReader,
1048 pv: Arc<ProcessVariable>,
1049 sid: u32,
1050}
1051
1052impl PvSubscription {
1053 /// Register a value-change monitor on `pv`. Returns `None` when the
1054 /// per-PV subscriber cap is reached. The caller emits the initial
1055 /// snapshot itself (pvxs `SharedPV::attach` posts the current value
1056 /// before storing the subscriber); registering the subscriber *before*
1057 /// reading that snapshot is the miss-free ordering — a PUT racing the
1058 /// two is then delivered through the stream rather than lost.
1059 pub async fn subscribe(pv: Arc<ProcessVariable>) -> Option<Self> {
1060 use crate::server::recgbl::EventMask;
1061 // VALUE|LOG matches the record-side `DbSubscription` default so
1062 // simple-PV and record-backed monitors gate identically; a
1063 // pure-alarm `post_alarm` (ALARM|LOG) still intersects via LOG.
1064 let mask = (EventMask::VALUE | EventMask::LOG).bits();
1065 let sid = next_pv_sub_sid();
1066 // `data_type` is nominal for snapshot consumers: `deliver` ships
1067 // the full `Snapshot` and gates only on mask/filters, never on the
1068 // stored type — `DbSubscription` likewise registers as `Double`.
1069 let reader = pv.add_subscriber(sid, DbFieldType::Double, mask)?;
1070 Some(Self { reader, pv, sid })
1071 }
1072
1073 /// Await the next value change as a full `Snapshot`. A consumer that falls
1074 /// behind sees the same thing a C monitor does: its earlier distinct queued
1075 /// updates, and then — once the queue ran short of room — a tail entry
1076 /// carrying the latest value, because further posts replaced that entry in
1077 /// place rather than appending (`db_queue_event_log`, `dbEvent.c:812-827`).
1078 pub async fn recv_snapshot(&mut self) -> Option<Snapshot> {
1079 // Free when this reader holds the last reference to the shared
1080 // snapshot, which is the single-subscriber case; a copy only when
1081 // another subscriber still holds it.
1082 Some(Arc::unwrap_or_clone(self.reader.recv().await?.snapshot))
1083 }
1084
1085 /// Non-blocking [`Self::recv_snapshot`]. Delegates to
1086 /// [`EventReader::try_recv`] (`event_queue.rs:807`) — same queue, same
1087 /// EVENTS_OFF gate, no suspension.
1088 ///
1089 /// Lets a PVA monitor source that adapts this stream be polled from a
1090 /// blocking drain loop with no reactor present.
1091 pub fn try_recv_snapshot(&mut self) -> Result<Snapshot, TryRecvError> {
1092 self.reader
1093 .try_recv()
1094 .map(|e| Arc::unwrap_or_clone(e.snapshot))
1095 }
1096
1097 /// Await the next change as the full [`MonitorEvent`] — snapshot plus the
1098 /// per-event `DBE_*` mask. The mask-carrying counterpart of
1099 /// [`recv_snapshot`](Self::recv_snapshot), matching
1100 /// `DbSubscription::recv_event` so a consumer can treat a simple-PV and a
1101 /// record subscription through one shape.
1102 pub async fn recv_event(&mut self) -> Option<MonitorEvent> {
1103 self.reader.recv().await
1104 }
1105
1106 /// Non-blocking [`Self::recv_event`].
1107 pub fn try_recv_event(&mut self) -> Result<MonitorEvent, TryRecvError> {
1108 self.reader.try_recv()
1109 }
1110}
1111
1112impl Drop for PvSubscription {
1113 /// Remove this monitor's row from the PV, on the dropping thread.
1114 ///
1115 /// C cancels a monitor synchronously on the caller's thread
1116 /// (`db_cancel_event`, dbEvent.c), and here that is reachable:
1117 /// [`ProcessVariable::remove_subscriber`] takes
1118 /// `ProcessVariable::subscribers`, an ordinary mutex, so the removal
1119 /// needs no executor and is complete when `drop` returns.
1120 ///
1121 /// `DbSubscription::drop` defers the same work to the background
1122 /// executor because *its* `remove_subscriber` is behind the record's
1123 /// async `RwLock`, which sync `drop` cannot take. Copying the deferral
1124 /// here also copied a `Handle::try_current()` test that decided whether
1125 /// to do the removal at all, and that predicate answers a question
1126 /// nobody asked: it is false on every callback-band worker and on every
1127 /// blocking CA connection thread, so a monitor dropped there kept its
1128 /// row for the life of the IOC and every later `notify_subscribers`
1129 /// paid to build an event for a reader that was gone.
1130 fn drop(&mut self) {
1131 self.pv.remove_subscriber(self.sid);
1132 }
1133}
1134
1135#[cfg(test)]
1136mod mask_gate_tests {
1137 use super::*;
1138
1139 // CA DBE_* monitor mask bits (db_access.h).
1140 const DBE_VALUE: u16 = 1;
1141 const DBE_LOG: u16 = 2;
1142 const DBE_ALARM: u16 = 4;
1143
1144 fn pv() -> ProcessVariable {
1145 ProcessVariable::new("test:pv".into(), EpicsValue::Double(0.0))
1146 }
1147
1148 /// A full-snapshot write must persist alarm + timestamp + userTag so
1149 /// a later `snapshot()` (the GET path) reflects them — not just the
1150 /// live monitor fan-out. A subsequent value-only `set()` carries no
1151 /// explicit metadata and must revert the snapshot to NO_ALARM.
1152 #[epics_macros_rs::epics_test]
1153 async fn set_snapshot_metadata_persists_then_value_set_clears() {
1154 let pv = pv();
1155
1156 // 42 ns exact: a `SystemTime` rounds this to 0 on Windows, so the
1157 // round-trip is built from `WallTime` integers to actually exercise
1158 // sub-100 ns persistence through `PostedMeta`.
1159 let posted_time = WallTime::from_unix(1_600_000_000, 42);
1160 let mut snap = Snapshot::new(EpicsValue::Double(7.0), 3, 2, posted_time);
1161 snap.user_tag = 9;
1162 pv.set_snapshot(snap);
1163
1164 let got = pv.snapshot();
1165 assert_eq!(got.value, EpicsValue::Double(7.0), "value persisted");
1166 assert_eq!(got.alarm.status, 3, "alarm.status persisted to GET");
1167 assert_eq!(got.alarm.severity, 2, "alarm.severity persisted to GET");
1168 assert_eq!(got.user_tag, 9, "userTag persisted to GET");
1169 assert_eq!(got.timestamp, posted_time, "timestamp persisted to GET");
1170
1171 // A plain value write reverts to the bare-PV default.
1172 pv.set(EpicsValue::Double(8.0));
1173 let after = pv.snapshot();
1174 assert_eq!(after.value, EpicsValue::Double(8.0));
1175 assert_eq!(after.alarm.status, 0, "value set clears posted alarm");
1176 assert_eq!(after.alarm.severity, 0, "value set clears posted severity");
1177 assert_eq!(after.user_tag, 0, "value set clears posted userTag");
1178 assert_ne!(
1179 after.timestamp, posted_time,
1180 "value set must restamp the timestamp, not keep the posted one"
1181 );
1182 }
1183
1184 /// a `DBE_ALARM`-only subscriber must not receive a plain
1185 /// value set, but must receive an alarm post.
1186 #[epics_macros_rs::epics_test]
1187 async fn alarm_only_subscriber_skips_value_post() {
1188 let pv = pv();
1189 let mut rx = pv
1190 .add_subscriber(1, DbFieldType::Double, DBE_ALARM)
1191 .expect("subscriber added");
1192 pv.set(EpicsValue::Double(1.0));
1193 assert!(
1194 rx.try_recv().is_err(),
1195 "DBE_ALARM-only subscriber must not receive a value post"
1196 );
1197 pv.post_alarm(2, 3);
1198 assert!(
1199 rx.try_recv().is_ok(),
1200 "DBE_ALARM subscriber must receive an alarm post"
1201 );
1202 }
1203
1204 /// a `DBE_VALUE`-only subscriber must not receive a
1205 /// `post_alarm`, but must receive value sets.
1206 #[epics_macros_rs::epics_test]
1207 async fn value_only_subscriber_skips_alarm_post() {
1208 let pv = pv();
1209 let mut rx = pv
1210 .add_subscriber(1, DbFieldType::Double, DBE_VALUE)
1211 .expect("subscriber added");
1212 pv.post_alarm(2, 3);
1213 assert!(
1214 rx.try_recv().is_err(),
1215 "DBE_VALUE-only subscriber must not receive an alarm post"
1216 );
1217 pv.set(EpicsValue::Double(1.0));
1218 assert!(
1219 rx.try_recv().is_ok(),
1220 "DBE_VALUE subscriber must receive a value post"
1221 );
1222 }
1223
1224 /// C `db_post_events` stamps the field log with `caEventMask &
1225 /// pevent->select` (`dbEvent.c:896-900`), not with the poster's mask.
1226 /// `set_snapshot` posts `VALUE|LOG|ALARM` (the gateway-parity class set
1227 /// at `notify_subscribers_from_snapshot`), so a `DBE_VALUE`-only
1228 /// subscriber must see `DBE_VALUE` alone on the delivered event.
1229 #[epics_macros_rs::epics_test]
1230 async fn delivered_mask_is_narrowed_to_the_subscriber_select() {
1231 use crate::server::recgbl::EventMask;
1232 let pv = pv();
1233 let mut rx = pv
1234 .add_subscriber(1, DbFieldType::Double, DBE_VALUE)
1235 .expect("subscriber added");
1236 pv.set_snapshot(snapshot());
1237 let ev = rx.try_recv().expect("value-class post delivered");
1238 assert_eq!(
1239 ev.mask,
1240 EventMask::VALUE,
1241 "delivered mask must be post & select, not the poster's full class set"
1242 );
1243
1244 // The same narrowing on the other side: an ALARM-only subscriber
1245 // hears the same post as DBE_ALARM alone.
1246 let mut rx_alarm = pv
1247 .add_subscriber(2, DbFieldType::Double, DBE_ALARM)
1248 .expect("subscriber added");
1249 pv.set_snapshot(snapshot());
1250 let ev = rx_alarm.try_recv().expect("alarm-class post delivered");
1251 assert_eq!(ev.mask, EventMask::ALARM, "narrowed for an ALARM-only sub");
1252 }
1253
1254 /// The consequence the narrowing exists for: a `.{dbnd}` pre-chain
1255 /// filter passes an event unconditionally when the log mask carries a
1256 /// class other than `DBE_VALUE`/`DBE_LOG` (`dbnd.c:84`, `send =
1257 /// pfl->mask & ~(DBE_VALUE|DBE_LOG)`). Handing it the poster's
1258 /// `VALUE|LOG|ALARM` therefore let every sub-deadband update through on
1259 /// the `DBE_ALARM` bit the client never subscribed to, silently
1260 /// defeating the deadband.
1261 #[epics_macros_rs::epics_test]
1262 async fn dbnd_on_a_value_only_subscriber_is_not_bypassed_by_the_alarm_bit() {
1263 use crate::server::database::filters::parser::parse_filter_chain;
1264 let pv = pv();
1265 let mut rx = pv
1266 .add_subscriber(1, DbFieldType::Double, DBE_VALUE)
1267 .expect("subscriber added");
1268 pv.attach_filters_to_subscriber(1, parse_filter_chain(r#"{"dbnd":{"d":10}}"#));
1269
1270 // First event: `dbnd`'s baseline is NaN, so C's `delta > deadband`
1271 // is INF > 10 and it always passes.
1272 pv.set_snapshot(Snapshot::new(
1273 EpicsValue::Double(0.0),
1274 0,
1275 0,
1276 std::time::SystemTime::UNIX_EPOCH,
1277 ));
1278 assert!(
1279 rx.try_recv().is_ok(),
1280 "first event establishes the baseline"
1281 );
1282
1283 // Second event moves 0 -> 6 with a MINOR alarm: inside the band, so
1284 // C drops it. The alarm class is not in this subscriber's select and
1285 // must not reach the filter.
1286 pv.set_snapshot(Snapshot::new(
1287 EpicsValue::Double(6.0),
1288 7,
1289 1,
1290 std::time::SystemTime::UNIX_EPOCH,
1291 ));
1292 assert!(
1293 rx.try_recv().is_err(),
1294 "sub-deadband update must stay dropped; the poster's DBE_ALARM \
1295 bit is not part of a DBE_VALUE-only subscription"
1296 );
1297 }
1298
1299 // --- Regression: set_snapshot must reach DBE_LOG and DBE_ALARM-only subs ---
1300
1301 fn snapshot() -> Snapshot {
1302 Snapshot::new(
1303 EpicsValue::Double(2.0),
1304 0,
1305 0,
1306 std::time::SystemTime::UNIX_EPOCH,
1307 )
1308 }
1309
1310 /// A DBE_LOG (archiver) subscriber must receive a set_snapshot post.
1311 #[epics_macros_rs::epics_test]
1312 async fn log_subscriber_receives_snapshot_post() {
1313 let pv = pv();
1314 let mut rx = pv
1315 .add_subscriber(1, DbFieldType::Double, DBE_LOG)
1316 .expect("subscriber added");
1317 pv.set_snapshot(snapshot());
1318 assert!(
1319 rx.try_recv().is_ok(),
1320 "DBE_LOG subscriber must receive a set_snapshot post"
1321 );
1322 }
1323
1324 /// A DBE_ALARM-only subscriber must receive a set_snapshot post.
1325 #[epics_macros_rs::epics_test]
1326 async fn alarm_only_subscriber_receives_snapshot_post() {
1327 let pv = pv();
1328 let mut rx = pv
1329 .add_subscriber(1, DbFieldType::Double, DBE_ALARM)
1330 .expect("subscriber added");
1331 pv.set_snapshot(snapshot());
1332 assert!(
1333 rx.try_recv().is_ok(),
1334 "DBE_ALARM-only subscriber must receive a set_snapshot post"
1335 );
1336 }
1337
1338 /// A DBE_VALUE subscriber must still receive a set_snapshot post.
1339 #[epics_macros_rs::epics_test]
1340 async fn value_subscriber_receives_snapshot_post() {
1341 let pv = pv();
1342 let mut rx = pv
1343 .add_subscriber(1, DbFieldType::Double, DBE_VALUE)
1344 .expect("subscriber added");
1345 pv.set_snapshot(snapshot());
1346 assert!(
1347 rx.try_recv().is_ok(),
1348 "DBE_VALUE subscriber must receive a set_snapshot post"
1349 );
1350 }
1351
1352 /// A `DBE_VALUE | DBE_ALARM` subscriber receives both event classes.
1353 #[epics_macros_rs::epics_test]
1354 async fn both_classes_receive_both_posts() {
1355 let pv = pv();
1356 let mut rx = pv
1357 .add_subscriber(1, DbFieldType::Double, DBE_VALUE | DBE_ALARM)
1358 .expect("subscriber added");
1359 pv.set(EpicsValue::Double(1.0));
1360 assert!(rx.try_recv().is_ok(), "value post delivered to VALUE|ALARM");
1361 pv.post_alarm(2, 3);
1362 assert!(rx.try_recv().is_ok(), "alarm post delivered to VALUE|ALARM");
1363 }
1364
1365 /// A DBE_LOG-only subscriber (archiver) must receive both value
1366 /// events and alarm events. Pre-fix: VALUE-only / ALARM-only post masks
1367 /// never intersected DBE_LOG(2), so archivers received silence.
1368 #[epics_macros_rs::epics_test]
1369 async fn br_r52_log_subscriber_receives_value_and_alarm_events() {
1370 const DBE_LOG: u16 = 2;
1371 let pv = pv();
1372 let mut rx = pv
1373 .add_subscriber(1, DbFieldType::Double, DBE_LOG)
1374 .expect("subscriber added");
1375 pv.set(EpicsValue::Double(1.0));
1376 assert!(
1377 rx.try_recv().is_ok(),
1378 "DBE_LOG subscriber must receive a value post"
1379 );
1380 pv.post_alarm(2, 3);
1381 assert!(
1382 rx.try_recv().is_ok(),
1383 "DBE_LOG subscriber must receive an alarm post"
1384 );
1385 }
1386
1387 /// Every delivered event carries its post's `DBE_*` class — the
1388 /// per-event mask C attaches to the field log (`db_field_log.mask`)
1389 /// and pvxs narrows monitor decoding with (`groupsource.cpp:331-337`).
1390 #[epics_macros_rs::epics_test]
1391 async fn monitor_event_carries_post_class_mask() {
1392 use crate::server::recgbl::EventMask;
1393 let pv = pv();
1394 let mut rx = pv
1395 .add_subscriber(1, DbFieldType::Double, DBE_VALUE | DBE_LOG | DBE_ALARM)
1396 .expect("subscriber added");
1397 pv.set(EpicsValue::Double(1.0));
1398 assert_eq!(
1399 rx.try_recv().expect("value event").mask,
1400 EventMask::VALUE | EventMask::LOG,
1401 "value post carries VALUE|LOG"
1402 );
1403 pv.post_alarm(2, 3);
1404 assert_eq!(
1405 rx.try_recv().expect("alarm event").mask,
1406 EventMask::ALARM | EventMask::LOG,
1407 "alarm post carries ALARM|LOG"
1408 );
1409 }
1410
1411 /// When the queue runs short of room and a post replaces this monitor's
1412 /// last entry in place, the surviving entry's mask is the OR of the
1413 /// displaced event's class and its own: the displaced *value* is gone (C
1414 /// frees the field log), but a narrow consumer must still learn that an
1415 /// ALARM-class change happened inside the coalesced tail.
1416 #[epics_macros_rs::epics_test]
1417 async fn in_place_replacement_accumulates_event_class_masks() {
1418 use crate::server::event_queue::{event_que_size, events_per_que};
1419 use crate::server::recgbl::EventMask;
1420 let pv = Arc::new(ProcessVariable::new(
1421 "coalesce:mask".into(),
1422 EpicsValue::Double(0.0),
1423 ));
1424 let mut reader = pv
1425 .add_subscriber(7, DbFieldType::Double, DBE_VALUE | DBE_LOG | DBE_ALARM)
1426 .expect("subscriber added");
1427 // Append VALUE|LOG posts until the ring space reaches the replace
1428 // threshold; from here every post overwrites the tail entry.
1429 let appended = event_que_size() - events_per_que();
1430 for i in 1..=appended {
1431 pv.set(EpicsValue::Double(i as f64));
1432 }
1433 // Replaces the tail: its class (ALARM|LOG) must not be lost.
1434 pv.post_alarm(2, 3);
1435 // Replaces it again with a value post — both classes fold into the
1436 // survivor.
1437 pv.set(EpicsValue::Double(99.0));
1438
1439 let mut last = None;
1440 while let Ok(event) = reader.try_recv() {
1441 last = Some(event);
1442 }
1443 let delivered = last.expect("the tail entry is delivered");
1444 assert_eq!(
1445 delivered.snapshot.value.to_f64(),
1446 Some(99.0),
1447 "the tail entry carries the newest value"
1448 );
1449 assert!(
1450 delivered
1451 .mask
1452 .contains(EventMask::VALUE | EventMask::ALARM | EventMask::LOG),
1453 "the displaced alarm class survives in the delivered mask (got {:?})",
1454 delivered.mask
1455 );
1456 }
1457
1458 /// R8-22 (simple-PV path): a monitor whose queue runs out of room during a
1459 /// burst must receive its EARLIER DISTINCT queued updates and then a tail
1460 /// entry carrying the latest value — C `db_queue_event_log` replaces only
1461 /// `*pLastLog` (`dbEvent.c:812-827`) and leaves the earlier entries queued.
1462 ///
1463 /// The old primitive parked the newest value in a side coalesce slot, and
1464 /// the consumer, finding it set, discarded the ENTIRE queued backlog and
1465 /// delivered only that newest value — so a 200-post burst came out as a
1466 /// single event instead of {1..107, 200}.
1467 #[epics_macros_rs::epics_test]
1468 async fn r8_22_pv_burst_keeps_earlier_distinct_updates() {
1469 use crate::server::event_queue::{event_que_size, events_per_que};
1470 use std::time::Duration;
1471 let pv = Arc::new(ProcessVariable::new(
1472 "coalesce:pv".into(),
1473 EpicsValue::Double(0.0),
1474 ));
1475 let mut sub = PvSubscription::subscribe(pv.clone())
1476 .await
1477 .expect("subscribe");
1478 // With nothing draining, the first `appended` posts take ring entries
1479 // and every later post replaces the tail entry in place.
1480 let appended = event_que_size() - events_per_que();
1481 let burst = appended + 92;
1482 for i in 1..=burst {
1483 pv.set(EpicsValue::Double(i as f64));
1484 }
1485 let mut seq = Vec::new();
1486 while let Ok(Some(snap)) =
1487 crate::runtime::task::timeout(Duration::from_millis(200), sub.recv_snapshot()).await
1488 {
1489 seq.push(snap.value.to_f64().expect("double value"));
1490 }
1491 let want: Vec<f64> = (1..appended)
1492 .map(|i| i as f64)
1493 .chain(std::iter::once(burst as f64))
1494 .collect();
1495 assert_eq!(
1496 seq, want,
1497 "burst delivery must be {{earlier distinct backlog…, coalesced tail}}"
1498 );
1499 }
1500}
1501
1502#[cfg(test)]
1503mod metadata_tests {
1504 use super::*;
1505
1506 fn meta() -> PvMetadata {
1507 PvMetadata {
1508 display: Some(DisplayInfo {
1509 units: "degC".into(),
1510 precision: 2,
1511 upper_disp_limit: 100.0,
1512 lower_disp_limit: -50.0,
1513 upper_alarm_limit: 90.0,
1514 upper_warning_limit: 80.0,
1515 lower_warning_limit: -20.0,
1516 lower_alarm_limit: -40.0,
1517 ..Default::default()
1518 }),
1519 control: Some(ControlInfo {
1520 upper_ctrl_limit: 95.0,
1521 lower_ctrl_limit: -45.0,
1522 }),
1523 enums: None,
1524 }
1525 }
1526
1527 fn pv() -> ProcessVariable {
1528 ProcessVariable::new("m:pv".into(), EpicsValue::Double(1.0))
1529 }
1530
1531 /// `set_with_origin` tags the value event with the writer's origin,
1532 /// plain `set` stays untagged, and a plain `set` inside an
1533 /// `AmbientWriteOriginScope` inherits the scope's origin — the
1534 /// simple-PV side of the record funnels' inheritance rule.
1535 #[epics_macros_rs::epics_test]
1536 async fn set_with_origin_tags_the_value_event() {
1537 const DBE_VALUE: u16 = 1;
1538 let pv = pv();
1539 let mut rx = pv
1540 .add_subscriber(1, DbFieldType::Double, DBE_VALUE)
1541 .expect("subscriber added");
1542
1543 pv.set(EpicsValue::Double(2.0));
1544 assert_eq!(rx.try_recv().expect("plain set posts").origin, 0);
1545
1546 pv.set_with_origin(EpicsValue::Double(3.0), 77);
1547 assert_eq!(rx.try_recv().expect("tagged set posts").origin, 77);
1548
1549 {
1550 let _scope = crate::server::record::ambient_write_origin_scope(88);
1551 pv.set(EpicsValue::Double(4.0));
1552 }
1553 assert_eq!(
1554 rx.try_recv().expect("ambient-scoped set posts").origin,
1555 88,
1556 "an originless simple-PV post inside an ambient scope must inherit it"
1557 );
1558 }
1559
1560 /// A bare PV serves no metadata until a proxy installs it; after
1561 /// `set_metadata`, the GET snapshot carries the shadow DBR_GR/DBR_CTRL.
1562 #[epics_macros_rs::epics_test]
1563 async fn set_metadata_serves_on_get_snapshot() {
1564 let pv = pv();
1565 assert!(
1566 pv.snapshot().display.is_none(),
1567 "bare PV must carry no metadata before install"
1568 );
1569 pv.set_metadata(meta());
1570 let snap = pv.snapshot();
1571 let d = snap.display.expect("display installed");
1572 assert_eq!(d.units, "degC");
1573 assert_eq!(d.precision, 2);
1574 assert_eq!(
1575 snap.control.expect("control installed").upper_ctrl_limit,
1576 95.0
1577 );
1578 }
1579
1580 /// A CTRL-type monitor must see the installed limits on every value
1581 /// event, not only the initial GET — value posts carry the metadata.
1582 #[epics_macros_rs::epics_test]
1583 async fn installed_metadata_rides_value_posts() {
1584 const DBE_VALUE: u16 = 1;
1585 let pv = pv();
1586 pv.set_metadata(meta());
1587 let mut rx = pv
1588 .add_subscriber(1, DbFieldType::Double, DBE_VALUE)
1589 .expect("subscriber added");
1590 pv.set(EpicsValue::Double(2.0));
1591 let ev = rx.try_recv().expect("value event delivered");
1592 assert_eq!(
1593 ev.snapshot
1594 .display
1595 .clone()
1596 .expect("metadata on value post")
1597 .units,
1598 "degC"
1599 );
1600 }
1601
1602 /// `apply_metadata` only supplies fields the caller left absent: a
1603 /// gateway snapshot that already carries its own display wins.
1604 #[epics_macros_rs::epics_test]
1605 async fn apply_metadata_does_not_clobber_caller_metadata() {
1606 const DBE_VALUE: u16 = 1;
1607 let pv = pv();
1608 pv.set_metadata(meta()); // installed units = degC
1609 let mut rx = pv
1610 .add_subscriber(1, DbFieldType::Double, DBE_VALUE)
1611 .expect("subscriber added");
1612 let mut snap = Snapshot::new(
1613 EpicsValue::Double(3.0),
1614 0,
1615 0,
1616 std::time::SystemTime::UNIX_EPOCH,
1617 );
1618 snap.display = Some(DisplayInfo {
1619 units: "volts".into(),
1620 ..Default::default()
1621 });
1622 pv.set_snapshot(snap);
1623 let ev = rx.try_recv().expect("snapshot delivered");
1624 assert_eq!(
1625 ev.snapshot
1626 .display
1627 .clone()
1628 .expect("caller display kept")
1629 .units,
1630 "volts"
1631 );
1632 }
1633
1634 /// `post_property` reaches DBE_PROPERTY subscribers (carrying the
1635 /// metadata) and not DBE_VALUE-only subscribers.
1636 #[epics_macros_rs::epics_test]
1637 async fn post_property_reaches_only_property_subscribers() {
1638 const DBE_VALUE: u16 = 1;
1639 const DBE_PROPERTY: u16 = 8;
1640 let pv = pv();
1641 pv.set_metadata(meta());
1642 let mut prop_rx = pv
1643 .add_subscriber(1, DbFieldType::Double, DBE_PROPERTY)
1644 .expect("subscriber added");
1645 let mut val_rx = pv
1646 .add_subscriber(2, DbFieldType::Double, DBE_VALUE)
1647 .expect("subscriber added");
1648 pv.post_property(Snapshot::new(
1649 EpicsValue::Double(1.0),
1650 0,
1651 0,
1652 std::time::SystemTime::UNIX_EPOCH,
1653 ))
1654 .await;
1655 let ev = prop_rx
1656 .try_recv()
1657 .expect("DBE_PROPERTY subscriber receives property post");
1658 assert_eq!(
1659 ev.snapshot
1660 .display
1661 .clone()
1662 .expect("property post carries metadata")
1663 .units,
1664 "degC"
1665 );
1666 assert!(
1667 val_rx.try_recv().is_err(),
1668 "DBE_VALUE-only subscriber must not receive a property post"
1669 );
1670 }
1671
1672 /// A property post
1673 /// must carry the upstream CTRL event's status/severity and timestamp,
1674 /// not a fabricated `NO_ALARM` / wall-clock-now snapshot. C ca-gateway
1675 /// preserves `setStatSevr()` on the property callback
1676 /// (`gatePv.cc:2413-2438`); a downstream `DBE_PROPERTY` monitor must
1677 /// see `severity=MAJOR` and the upstream timestamp, even though only
1678 /// metadata changed.
1679 #[epics_macros_rs::epics_test]
1680 async fn post_property_preserves_upstream_alarm_and_timestamp() {
1681 const DBE_PROPERTY: u16 = 8;
1682 const MAJOR: u16 = 2; // epicsSevMajor
1683 const HIGH: u16 = 3; // epicsAlarmHigh
1684 let pv = pv();
1685 pv.set_metadata(meta());
1686 let mut prop_rx = pv
1687 .add_subscriber(1, DbFieldType::Double, DBE_PROPERTY)
1688 .expect("subscriber added");
1689 // The upstream CTRL event timestamp: a fixed point in the past, so
1690 // it is unmistakably NOT a fresh wall clock minted by the post.
1691 let upstream_ts = WallTime::from_unix(1_000_000, 0);
1692 pv.post_property(Snapshot::new(
1693 EpicsValue::Double(2.0),
1694 HIGH,
1695 MAJOR,
1696 upstream_ts,
1697 ))
1698 .await;
1699 let ev = prop_rx.try_recv().expect("property post delivered");
1700 assert_eq!(
1701 ev.snapshot.alarm.severity, MAJOR,
1702 "property post must carry the upstream MAJOR severity, not NO_ALARM"
1703 );
1704 assert_eq!(ev.snapshot.alarm.status, HIGH, "upstream status preserved");
1705 assert_eq!(
1706 ev.snapshot.timestamp, upstream_ts,
1707 "property post must keep the upstream timestamp, not a fresh wall clock"
1708 );
1709 // Shadow metadata is still overlaid onto the upstream snapshot.
1710 assert_eq!(
1711 ev.snapshot
1712 .display
1713 .clone()
1714 .expect("property post carries shadow metadata")
1715 .units,
1716 "degC"
1717 );
1718 }
1719}
1720
1721#[cfg(test)]
1722mod read_hook_tests {
1723 use super::*;
1724
1725 fn pv() -> ProcessVariable {
1726 ProcessVariable::new("g:pv".into(), EpicsValue::Double(1.0))
1727 }
1728
1729 /// No hook installed (the default for every record-backed and cached
1730 /// PV): `read_snapshot` is exactly `snapshot` wrapped in `Ok` — the
1731 /// stored value, byte-for-byte unchanged.
1732 #[epics_macros_rs::epics_test]
1733 async fn read_snapshot_without_hook_equals_snapshot() {
1734 let pv = pv();
1735 let read = pv.read_snapshot().await.expect("no-hook read never errors");
1736 let stored = pv.snapshot();
1737 assert_eq!(read.value, stored.value);
1738 assert_eq!(read.value, EpicsValue::Double(1.0));
1739 }
1740
1741 /// With a hook installed (no-cache mode), the GET value comes fresh
1742 /// from the hook, NOT from the stored shadow value — the stored value
1743 /// stays a stale sentinel that the hook overrides.
1744 #[epics_macros_rs::epics_test]
1745 async fn read_snapshot_fires_hook_for_fresh_value() {
1746 let pv = pv();
1747 // Stored shadow value is a sentinel the hook must override.
1748 pv.set(EpicsValue::Double(999.0));
1749 pv.set_read_hook(Arc::new(|| {
1750 Box::pin(async {
1751 Ok(Snapshot::new(
1752 EpicsValue::Double(42.0),
1753 0,
1754 0,
1755 std::time::UNIX_EPOCH,
1756 ))
1757 })
1758 }));
1759 let read = pv.read_snapshot().await.expect("hook returns Ok");
1760 assert_eq!(
1761 read.value,
1762 EpicsValue::Double(42.0),
1763 "GET must serve the hook's fresh value, not the stored sentinel"
1764 );
1765 }
1766
1767 /// A hook failure propagates so the server can answer `ECA_GETFAIL`,
1768 /// matching C ca-gateway forwarding each read to the IOC.
1769 #[epics_macros_rs::epics_test]
1770 async fn read_snapshot_propagates_hook_error() {
1771 let pv = pv();
1772 pv.set_read_hook(Arc::new(|| Box::pin(async { Err(CaError::Disconnected) })));
1773 let err = pv.read_snapshot().await.expect_err("hook error propagates");
1774 assert!(matches!(err, CaError::Disconnected));
1775 }
1776
1777 /// No hook (every record-backed and cached PV): the sync companion
1778 /// `read_snapshot_local` yields `Some(snapshot)`, byte-for-byte the same
1779 /// value as `snapshot` / the async `read_snapshot` — the fully sans-io
1780 /// GET path.
1781 #[test]
1782 fn read_snapshot_local_without_hook_is_some_and_matches_snapshot() {
1783 let pv = pv();
1784 let local = pv
1785 .read_snapshot_local()
1786 .expect("no hook ⇒ sync snapshot is Some");
1787 assert_eq!(local.value, pv.snapshot().value);
1788 assert_eq!(local.value, EpicsValue::Double(1.0));
1789 }
1790
1791 /// A read hook installed (gateway no-cache): the sync companion returns
1792 /// `None`, the signal that the caller must take the async upstream-GET
1793 /// path — `read_snapshot_local` never fires the hook itself.
1794 #[test]
1795 fn read_snapshot_local_with_hook_is_none() {
1796 let pv = pv();
1797 pv.set_read_hook(Arc::new(|| {
1798 Box::pin(async {
1799 Ok(Snapshot::new(
1800 EpicsValue::Double(42.0),
1801 0,
1802 0,
1803 std::time::UNIX_EPOCH,
1804 ))
1805 })
1806 }));
1807 assert!(
1808 pv.read_snapshot_local().is_none(),
1809 "a read hook ⇒ the sync path defers to the async upstream GET"
1810 );
1811 }
1812
1813 /// The read hook is GET-path only: `snapshot` (monitor fan-out, the
1814 /// initial monitor event, access-rights re-posts) keeps serving the
1815 /// stored value even when a hook is installed.
1816 #[epics_macros_rs::epics_test]
1817 async fn snapshot_ignores_read_hook() {
1818 let pv = pv();
1819 pv.set(EpicsValue::Double(7.0));
1820 pv.set_read_hook(Arc::new(|| {
1821 Box::pin(async {
1822 Ok(Snapshot::new(
1823 EpicsValue::Double(42.0),
1824 0,
1825 0,
1826 std::time::UNIX_EPOCH,
1827 ))
1828 })
1829 }));
1830 let snap = pv.snapshot();
1831 assert_eq!(
1832 snap.value,
1833 EpicsValue::Double(7.0),
1834 "snapshot must serve the stored value, never the read hook"
1835 );
1836 }
1837
1838 /// Fresh value + upstream alarm/time ride from the hook; the shadow's
1839 /// installed *property* metadata (display/control/enum) — which a
1840 /// `DBR_TIME_*` event does not carry — is overlaid for those fields.
1841 #[epics_macros_rs::epics_test]
1842 async fn read_snapshot_carries_shadow_metadata() {
1843 let pv = pv();
1844 pv.set_metadata(PvMetadata {
1845 display: Some(DisplayInfo {
1846 units: "mm".into(),
1847 precision: 3,
1848 ..Default::default()
1849 }),
1850 control: None,
1851 enums: None,
1852 });
1853 // The hook returns a Time-class snapshot (value + alarm + time,
1854 // no display/control/enum), exactly as `get_with_metadata(Time)`.
1855 pv.set_read_hook(Arc::new(|| {
1856 Box::pin(async {
1857 Ok(Snapshot::new(
1858 EpicsValue::Double(5.0),
1859 0,
1860 0,
1861 std::time::UNIX_EPOCH,
1862 ))
1863 })
1864 }));
1865 let read = pv.read_snapshot().await.expect("hook returns Ok");
1866 assert_eq!(read.value, EpicsValue::Double(5.0));
1867 assert_eq!(
1868 read.display
1869 .expect("shadow property metadata rides fresh value")
1870 .units,
1871 "mm"
1872 );
1873 }
1874
1875 /// A no-cache GET must report the FRESH upstream alarm and timestamp
1876 /// that travel with the value (C `getTimeCB` decodes the `DBR_TIME_*`
1877 /// event's status/severity/time before `setEventData`,
1878 /// `gatePv.cc:1789-1794`), NOT the shadow's last monitor-posted (or
1879 /// bare-PV default) alarm/time. Before the fix the read hook returned
1880 /// a bare value and `read_snapshot` grafted it onto the stored
1881 /// snapshot, so the GET reported the new value with a stale or default
1882 /// status/severity/timestamp.
1883 #[epics_macros_rs::epics_test]
1884 async fn read_snapshot_carries_upstream_alarm_not_shadow() {
1885 use std::time::{Duration, UNIX_EPOCH};
1886 let pv = pv();
1887 // The shadow's stored snapshot carries one alarm/time (a prior
1888 // monitor post). Make it concrete and DIFFERENT from the upstream
1889 // GET so a graft-onto-shadow regression is observable.
1890 let shadow_time = UNIX_EPOCH + Duration::from_secs(1_000);
1891 pv.set_snapshot(Snapshot::new(EpicsValue::Double(1.0), 7, 1, shadow_time));
1892 // The fresh upstream GET reports a different value, alarm, and time.
1893 let upstream_time = WallTime::from_unix(2_000, 0);
1894 pv.set_read_hook(Arc::new(move || {
1895 Box::pin(
1896 async move { Ok(Snapshot::new(EpicsValue::Double(5.0), 17, 2, upstream_time)) },
1897 )
1898 }));
1899 let read = pv.read_snapshot().await.expect("hook returns Ok");
1900 assert_eq!(read.value, EpicsValue::Double(5.0), "fresh upstream value");
1901 assert_eq!(
1902 read.alarm.status, 17,
1903 "upstream alarm status, not shadow's 7"
1904 );
1905 assert_eq!(read.alarm.severity, 2, "upstream severity, not shadow's 1");
1906 assert_eq!(
1907 read.timestamp, upstream_time,
1908 "upstream timestamp, not shadow's"
1909 );
1910 }
1911}
1912
1913/// BR-3 — removal from the database IS destruction, and destruction stops
1914/// the monitors instead of handing them one more event.
1915///
1916/// C ca-gateway `gatePvData::death` deletes the downstream virtual channel
1917/// when its upstream dies (`delete vc; vc = NULL;`, gatePv.cc:600-601). The
1918/// tests below pin the two halves that make that expressible here: a
1919/// destroyed PV has no subscribers and can gain none, and `respawn` is the
1920/// only way back — onto a different object.
1921#[cfg(test)]
1922mod destruction_tests {
1923 use super::*;
1924 use crate::server::database::PvDatabase;
1925 use crate::server::snapshot::DisplayInfo;
1926 use crate::types::PvString;
1927
1928 #[epics_macros_rs::epics_test]
1929 async fn removing_a_simple_pv_destroys_it_and_ends_its_monitors() {
1930 let db = PvDatabase::new();
1931 db.add_pv("D:pv", EpicsValue::Double(1.0))
1932 .await
1933 .expect("fresh name registers");
1934 let pv = db.find_pv("D:pv").await.expect("just registered");
1935 let mut reader = pv
1936 .add_subscriber(7, DbFieldType::Double, u16::MAX)
1937 .expect("first subscriber");
1938
1939 let removed = db.remove_simple_pv("D:pv").await.expect("was registered");
1940 assert!(
1941 removed.is_destroyed(),
1942 "the removal funnel is `destroy`'s only caller, so removed => destroyed"
1943 );
1944 assert!(
1945 removed.subscribers.lock().is_empty(),
1946 "destruction drops the subscriber rows"
1947 );
1948 assert!(
1949 matches!(reader.try_recv(), Err(TryRecvError::Disconnected)),
1950 "the consumer must observe end-of-stream, not silence"
1951 );
1952 }
1953
1954 #[epics_macros_rs::epics_test]
1955 async fn a_destroyed_pv_refuses_a_new_monitor() {
1956 let db = PvDatabase::new();
1957 db.add_pv("D:refuse", EpicsValue::Double(1.0))
1958 .await
1959 .expect("fresh name registers");
1960 let removed = db
1961 .remove_simple_pv("D:refuse")
1962 .await
1963 .expect("was registered");
1964 assert!(
1965 removed
1966 .add_subscriber(1, DbFieldType::Double, u16::MAX)
1967 .is_none(),
1968 "a CREATE_CHAN + EVENT_ADD racing the removal must not re-attach"
1969 );
1970 }
1971
1972 #[epics_macros_rs::epics_test]
1973 async fn respawn_carries_the_hooks_and_metadata_onto_a_live_pv() {
1974 let pv = ProcessVariable::new("D:respawn".into(), EpicsValue::Double(1.0));
1975 pv.set_write_hook(Arc::new(|_v, _ctx| Box::pin(async { Ok(()) })));
1976 pv.set_access_hook(Arc::new(|_user, _host| AccessDecision {
1977 read: true,
1978 write: false,
1979 }));
1980 pv.set_read_hook(Arc::new(|| {
1981 Box::pin(async {
1982 Ok(Snapshot::new(
1983 EpicsValue::Double(9.0),
1984 0,
1985 0,
1986 std::time::SystemTime::UNIX_EPOCH,
1987 ))
1988 })
1989 }));
1990 let display = DisplayInfo {
1991 units: "mA".into(),
1992 ..Default::default()
1993 };
1994 pv.set_metadata(PvMetadata {
1995 display: Some(display),
1996 control: None,
1997 enums: None,
1998 });
1999 pv.destroy();
2000
2001 let fresh = pv.respawn(EpicsValue::Double(5.0));
2002 assert!(!fresh.is_destroyed(), "the replacement is live");
2003 assert!(pv.is_destroyed(), "the corpse stays a corpse");
2004 assert!(fresh.write_hook().is_some(), "write hook carried");
2005 assert!(fresh.access_hook().is_some(), "access hook carried");
2006 assert!(fresh.read_hook().is_some(), "read hook carried");
2007 assert_eq!(
2008 fresh.metadata().display.expect("display carried").units,
2009 PvString::from("mA"),
2010 "shadow DBR_CTRL metadata carried, not zeroed"
2011 );
2012 assert_eq!(*fresh.value.read(), EpicsValue::Double(5.0));
2013 }
2014
2015 #[epics_macros_rs::epics_test]
2016 async fn removing_a_record_destroys_it_too() {
2017 let db = PvDatabase::new();
2018 db.add_record(
2019 "D:rec",
2020 Box::new(crate::server::records::ai::AiRecord::default()),
2021 )
2022 .await
2023 .expect("ai record");
2024 let rec = db.get_record("D:rec").expect("just added");
2025 assert!(!rec.read().is_destroyed());
2026 assert!(db.remove_record("D:rec").await, "record was registered");
2027 assert!(
2028 rec.read().is_destroyed(),
2029 "the record funnel marks the instance a CA channel still holds"
2030 );
2031 }
2032}
2033
2034#[cfg(test)]
2035mod subscription_drop_tests {
2036 use super::*;
2037
2038 /// The thread a monitor is actually dropped on is not the thread it was
2039 /// created on. A blocking CA connection thread and a callback-band worker
2040 /// are both outside every tokio runtime, and that is where a circuit
2041 /// teardown releases its `PvSubscription`.
2042 #[epics_macros_rs::epics_test]
2043 async fn a_subscription_dropped_on_a_bare_thread_removes_its_row() {
2044 let pv = Arc::new(ProcessVariable::new(
2045 "D:baredrop".into(),
2046 EpicsValue::Double(0.0),
2047 ));
2048 let sub = PvSubscription::subscribe(pv.clone())
2049 .await
2050 .expect("first subscriber");
2051 assert_eq!(pv.subscribers.lock().len(), 1, "the monitor registered");
2052
2053 let dropper = {
2054 let pv = pv.clone();
2055 std::thread::spawn(move || {
2056 assert!(
2057 tokio::runtime::Handle::try_current().is_err(),
2058 "the case is only meaningful off a runtime"
2059 );
2060 drop(sub);
2061 // Synchronous, so the row is gone before this thread joins —
2062 // no polling, no executor to wait for.
2063 assert!(
2064 pv.subscribers.lock().is_empty(),
2065 "removal completes inside `drop`"
2066 );
2067 })
2068 };
2069 dropper.join().expect("the dropping thread must not panic");
2070
2071 assert!(
2072 pv.subscribers.lock().is_empty(),
2073 "a monitor dropped off a runtime must not leave its row behind"
2074 );
2075 }
2076
2077 /// The other boundary: a drop removes one row, not the PV's whole
2078 /// subscriber list.
2079 #[epics_macros_rs::epics_test]
2080 async fn dropping_one_subscription_leaves_its_sibling_registered() {
2081 let pv = Arc::new(ProcessVariable::new(
2082 "D:sibling".into(),
2083 EpicsValue::Double(0.0),
2084 ));
2085 let first = PvSubscription::subscribe(pv.clone())
2086 .await
2087 .expect("first subscriber");
2088 let second = PvSubscription::subscribe(pv.clone())
2089 .await
2090 .expect("second subscriber");
2091 assert_eq!(pv.subscribers.lock().len(), 2, "both monitors registered");
2092
2093 drop(first);
2094 assert_eq!(
2095 pv.subscribers.lock().len(),
2096 1,
2097 "only the dropped monitor's row goes"
2098 );
2099 drop(second);
2100 assert!(pv.subscribers.lock().is_empty(), "and then the other one");
2101 }
2102}