pub struct PvDatabase { /* private fields */ }Expand description
Database of all process variables hosted by this server.
Implementations§
Source§impl PvDatabase
impl PvDatabase
Sourcepub fn get_pv_blocking(&self, name: &str) -> CaResult<EpicsValue>
pub fn get_pv_blocking(&self, name: &str) -> CaResult<EpicsValue>
Get a PV value synchronously, from a thread that cannot await.
Works from a plain thread with no runtime entered (an iocsh thread, a
driver’s own thread) and from a multi-threaded runtime worker; see
crate::runtime::task::block_on_sync for which mechanism is used
where.
Kept as a distinct entry point for source compatibility with the
callers that predate Self::get_pv becoming a fn; there is no
blocking left to do, so there is no current-thread-runtime failure mode
either. C dbGetField is likewise a plain call from any thread.
Sourcepub fn get_pv(&self, name: &str) -> CaResult<EpicsValue>
pub fn get_pv(&self, name: &str) -> CaResult<EpicsValue>
Get the current value of a PV or record field. Uses resolve_field for records (3-level priority).
Sourcepub async fn put_pv(&self, name: &str, value: EpicsValue) -> CaResult<()>
pub async fn put_pv(&self, name: &str, value: EpicsValue) -> CaResult<()>
Set a PV value or record field — the C dbPut analogue
(dbAccess.c:1316-1419), whole: value write + special/on_put, the
value-field UDF clear, and the field’s DBE_VALUE|DBE_LOG monitor
post ([dbput_post_put_field], suppressed only for a pp(TRUE) value
field exactly as C’s tail suppresses it). Tries record put_field
first, then put_common_field as fallback.
Does NOT process the record — dbPutField’s pp gate is
Self::put_record_field_from_ca — so, as with a bare C dbPut, a
pp(TRUE) value field (ai/ao/waveform VAL …) posts nothing here and a
caller that needs a monitor on such a field must either drive a
process or use Self::put_pv_and_post.
Acquires the record’s advisory write gate.
Sourcepub fn put_pv_already_locked(
&self,
name: &str,
value: EpicsValue,
) -> CaResult<()>
pub fn put_pv_already_locked( &self, name: &str, value: EpicsValue, ) -> CaResult<()>
put_pv variant for a caller already holding the
record’s advisory write gate (QSRV atomic group PUT). See
Self::put_record_field_from_ca_already_locked.
This is the whole dbPut body — the gate-held region, and it is a
fn. See Self::acquire_put_gate.
Sourcepub async fn check_external_put_preconditions(
&self,
record_name: &str,
field: &str,
) -> CaResult<()>
pub async fn check_external_put_preconditions( &self, record_name: &str, field: &str, ) -> CaResult<()>
C IOCSource::doPreProcessing gate (pvxs iocsource.cpp:363-375).
Reject an external put (a PVA/CA client put routed through QSRV)
that C refuses before any write: a put to a DISP=1 record’s
non-DISP field (S_db_putDisabled) or to a read-only / SPC_NOMOD
field (S_db_noMod). No value is written — this is a precondition
check only. It mirrors the two gates inside
Self::put_record_field_from_ca (the Passive route) so the QSRV
Force/Inhibit routes — which go through Self::put_pv — enforce
the same preconditions. put_pv itself is the internal dbPut
analogue and deliberately does not gate DISP (internal
link/processing puts must bypass it), so the gate lives at the
external put boundary, exactly as C places doPreProcessing in the
source layer rather than in dbPut.
Sourcepub fn put_drives_processing(&self, record_name: &str, field: &str) -> bool
pub fn put_drives_processing(&self, record_name: &str, field: &str) -> bool
pvxs IOCSource::doPostProcessing’s record-side terms
(iocsource.cpp:397-403): does a put to record_name.field drive a
processing cycle on its own?
The QSRV group PUT asks this for a member whose write bypassed
Self::put_record_field_from_ca (a +type:"proc" trigger, or a
member that is changing but has no writable leaf), so that route
applies the SAME gate as a plain field put instead of processing
unconditionally. false for an unknown record — there is nothing to
process. Force (record._options.process=true) is the caller’s term
and is not asked about here; see put_drives_processing_of.
Sourcepub async fn put_pv_and_post(
&self,
name: &str,
value: EpicsValue,
) -> CaResult<()>
pub async fn put_pv_and_post( &self, name: &str, value: EpicsValue, ) -> CaResult<()>
Write a value and post monitor events if changed.
Equivalent to C EPICS dbPut + db_post_events(DBE_VALUE|DBE_LOG).
Use for readback/status mirror PVs that are written by sequencer-style code and need to be visible to CA monitors without triggering record processing. Clears UDF/UDF_ALARM on primary field write.
origin: writer ID for self-write filtering. Subscribers with the
same ignore_origin will skip this event. Pass 0 to disable.
Sourcepub fn post_alarm(&self, name: &str, severity: u16, status: u16) -> CaResult<()>
pub fn post_alarm(&self, name: &str, severity: u16, status: u16) -> CaResult<()>
Push a monitor event holding the simple PV’s current value
but with explicit alarm severity/status. Used by the gateway
to surface upstream-disconnect to downstream monitor
subscribers without dropping the shadow PV (which would force
downstream clients into ECA_DISCONN reconnect storms on every
transient hiccup). Returns ChannelNotFound for record-backed
PVs — those carry their own common.sevr/stat in record
processing.
Sourcepub async fn put_pv_and_post_snapshot(
&self,
name: &str,
snapshot: Snapshot,
) -> CaResult<()>
pub async fn put_pv_and_post_snapshot( &self, name: &str, snapshot: Snapshot, ) -> CaResult<()>
Propagate a full upstream snapshot (value + alarm status/severity +
IOC timestamp) to a simple shadow PV and fan out to downstream
monitor subscribers. Used by the CA gateway forwarding task to avoid
discarding the upstream alarm and timestamp decoded from the incoming
DBR_TIME_* frame. Returns ChannelNotFound for record-backed PVs
(those carry their own alarm engine and are not shadow PVs).
Sourcepub async fn set_pv_metadata(
&self,
name: &str,
snapshot: &Snapshot,
) -> CaResult<()>
pub async fn set_pv_metadata( &self, name: &str, snapshot: &Snapshot, ) -> CaResult<()>
Install upstream DBR_CTRL_* metadata (display / control limits,
enum labels) on a shadow simple PV WITHOUT posting an event.
The CA gateway calls this once on upstream connect, after its initial
DBR_CTRL_* get, so a later downstream DBR_CTRL_* / DBR_GR_* read
returns the real limits instead of zeroed ones. No DBE_PROPERTY
monitor event fires — nothing has changed yet, this only seeds the
attribute cache. Mirrors C gatePvData::getCB → runDataCB →
vc->setPvData(dd) (gatePv.cc:1693-1695), which seeds the property
cache from the initial control get in both cache modes before any
monitor is enabled.
Returns ChannelNotFound for record-backed PVs — those own their own
metadata via record processing and are not gateway shadow PVs.
Sourcepub async fn post_pv_property(
&self,
name: &str,
snapshot: Snapshot,
) -> CaResult<()>
pub async fn post_pv_property( &self, name: &str, snapshot: Snapshot, ) -> CaResult<()>
Refresh a shadow simple PV’s upstream metadata AND post a
DBE_PROPERTY monitor event carrying snapshot to downstream
property subscribers.
snapshot is the decoded upstream DBR_CTRL_* property event: it
carries the control value and the upstream status / severity,
and (because control DBR structs carry no timestamp) an undefined
timestamp the caller must NOT replace with a fresh wall-clock. The
gateway’s property monitor calls this on every upstream
DBE_PROPERTY event, mirroring C gatePvData::propEventCB →
runDataCB + setPvData + runValueDataCB +
vcPostEvent(propertyEventMask()) (gatePv.cc:1571-1607): the
attribute cache is refreshed and a property event is posted with the
upstream alarm state preserved (setStatSevr) and the undefined
control-DBR timestamp left as-is (gatePv.cc:1594-1595).
Returns ChannelNotFound for record-backed PVs.
Sourcepub async fn put_pv_and_post_with_origin(
&self,
name: &str,
value: EpicsValue,
origin: u64,
) -> CaResult<()>
pub async fn put_pv_and_post_with_origin( &self, name: &str, value: EpicsValue, origin: u64, ) -> CaResult<()>
Like put_pv_and_post but with explicit origin tag.
Sourcepub async fn put_record_field_from_ca(
&self,
record_name: &str,
field: &str,
value: EpicsValue,
) -> CaResult<ProcessCompletion>
pub async fn put_record_field_from_ca( &self, record_name: &str, field: &str, value: EpicsValue, ) -> CaResult<ProcessCompletion>
CA client’s unified entry point for record field put. Handles DISP/PROC/PACT/LCNT checks, field put, device write, and Passive process.
Acquires the record’s advisory write gate
(dbScanLock analogue) for the duration of the write.
Sourcepub fn put_record_field_from_ca_already_locked(
&self,
record_name: &str,
field: &str,
value: EpicsValue,
) -> CaResult<ProcessCompletion>
pub fn put_record_field_from_ca_already_locked( &self, record_name: &str, field: &str, value: EpicsValue, ) -> CaResult<ProcessCompletion>
Variant for a caller that already owns the target
record’s advisory write gate — the QSRV atomic group PUT,
which acquired every member-record gate up-front via
Self::lock_records. The per-record gate is NOT reentrant, so the
atomic group path MUST use this _already_locked entry to avoid
dead-locking on its own ManyRecordWriteGuard.
Sourcepub async fn put_record_field_from_ca_no_notify(
&self,
record_name: &str,
field: &str,
value: EpicsValue,
) -> CaResult<()>
pub async fn put_record_field_from_ca_no_notify( &self, record_name: &str, field: &str, value: EpicsValue, ) -> CaResult<()>
Fire-and-forget variant — C dbPutField semantics: the put
processes the record but creates NO put-notify wait-set (C
builds a putNotify only in dbPutNotify, i.e. for
WRITE_NOTIFY). A caller that does not await the returned
receiver MUST use this entry: parking a wait-set whose receiver
is dropped occupies RecordInstance::notify until the record’s
async work ends (a motor’s whole motion), failing every
legitimate WRITE_NOTIFY on the record with ECA_PUTCBINPROG in
the meantime.
Sourcepub async fn put_record_field_from_ca_no_notify_with_origin(
&self,
record_name: &str,
field: &str,
value: EpicsValue,
origin: u64,
) -> CaResult<()>
pub async fn put_record_field_from_ca_no_notify_with_origin( &self, record_name: &str, field: &str, value: EpicsValue, origin: u64, ) -> CaResult<()>
Self::put_record_field_from_ca_no_notify for an in-process writer
with a self-write-filtering origin (a ported SNL state machine’s
DbChannel): every event the put’s synchronous process cascade posts
is tagged with origin, so the writer’s own filtered subscriptions
skip them. The ambient scope is sound here because the body below is
fully synchronous — there is no await between entering the scope and
the cascade’s last post.
Sourcepub async fn put_alarm_ack_from_ca(
&self,
record_name: &str,
field: &str,
ack: AlarmAck,
value: u16,
) -> CaResult<()>
pub async fn put_alarm_ack_from_ca( &self, record_name: &str, field: &str, ack: AlarmAck, value: u16, ) -> CaResult<()>
C dbPut’s alarm-acknowledge interception (dbAccess.c:1331-1335) —
the ONLY route that may change ACKS/ACKT at runtime.
if (dbrType == DBR_PUT_ACKT && field_type <= DBF_DEVICE)
return putAckt(paddr, pbuffer, 1, 1, 0);
else if (dbrType == DBR_PUT_ACKS && field_type <= DBF_DEVICE)
return putAcks(paddr, pbuffer, 1, 1, 0);The dispatch is on the DBR request type, not on the field: a CA client
acknowledges by sending DBR_PUT_ACKS down its ordinary REC (VAL)
channel. It sits ABOVE the SPC_NOMOD gate, which is why
caput REC.ACKS 2 is refused by C (“Write access denied”, verified on
softIoc 7.0.10) while ca_put(DBR_PUT_ACKS, REC) clears the alarm.
The put-disable gate is still crossed: C tests precord->disp in
dbPutField, above dbPut (dbAccess.c:1255-1257), so an ack to a
DISP=1 record is refused. field is the channel’s field — only the
DISP gate looks at it, exactly as in C.
No process cycle: dbPut returns straight from putAckt/putAcks, and
dbPutField’s reprocess condition requires dbrType < DBR_PUT_ACKT.
Sourcepub fn put_record_field_from_ca_no_notify_already_locked(
&self,
record_name: &str,
field: &str,
value: EpicsValue,
) -> CaResult<()>
pub fn put_record_field_from_ca_no_notify_already_locked( &self, record_name: &str, field: &str, value: EpicsValue, ) -> CaResult<()>
Fire-and-forget + caller-held gate: see
Self::put_record_field_from_ca_no_notify and
Self::put_record_field_from_ca_already_locked.
Sourcepub async fn process_record_with_notify(
&self,
record_name: &str,
) -> CaResult<ProcessCompletion>
pub async fn process_record_with_notify( &self, record_name: &str, ) -> CaResult<ProcessCompletion>
Process a record UNCONDITIONALLY with a put-notify wait-set, returning
the completion receiver — the QSRV record[process=true,block=true]
(Force + block) barrier.
C dbProcessNotify: pvxs routes a blocking forced put through
dbProcessNotify (singlesource.cpp:360-369), whose completion fires
only after the record’s whole processing chain — including async device
work (a motor move, an asyn-backed AO) — settles. The value is written
by the caller’s preceding Self::put_pv (the dbPut analogue, no
process); this entry then mints the wait-set, registers it into the
record’s notify slot so PACT records join it, and runs the full
unconditional Self::process_record_with_links cycle (C dbProcess,
the Force analogue). A fully synchronous chain returns Ok(None) (the
wait-set already drained); an async record returns Ok(Some(rx)) for
the caller to await. A concurrent put-callback already in flight on the
record is rejected with PutCallbackInProgress, matching the PROC path.
Sourcepub async fn put_driven_process(&self, record_name: &str) -> CaResult<()>
pub async fn put_driven_process(&self, record_name: &str) -> CaResult<()>
C dbPutField’s put-driven process decision (dbAccess.c:1264-1277).
Reached once the put has selected the record for processing — the PROC
field, or a pp(TRUE) field on a Passive record. C then splits on PACT:
- async-active — C sets
rpro = TRUEand does NOT calldbProcess.recGblFwdLink(recGbl.c:296-300) consumes RPRO when the device round trip completes and queuesscanOnce, so the value this put just wrote still reaches the device, one cycle later. CallingdbProcesshere instead lands in dbProcess’s own PACT guard, which bumps LCNT and after MAX_LOCK raises SCAN_ALARM — an alarm C never raises for a client put — while dropping the deferred reprocess entirely: on two rapid puts to a Passive async output, C writes both values to the device and the port wrote only the first. - idle — C sets
putf = TRUE(the put-driven marker, cleared at the tail of the process cycle / incomplete_async_record_inner, both therecGblFwdLink:302analogue) and callsdbProcess.
Single owner of that decision for every external put: the PROC
intercept and the pp-field route in
Self::put_record_field_from_ca both go through it, so neither can
drift from C’s rule or from each other. The DB-link propagation path
applies the same PACT→RPRO rule at its own targets (processing.rs:3225,
:4220, links.rs:829).
Two entries, one rule. put_driven_process acquires record_name’s
advisory write gate (the dbScanLock analogue) itself;
Self::put_driven_process_already_locked is for a caller that
already owns it — _record_gate on the CA put path, or the QSRV
atomic group’s lock_records epoch. The gate is not
reentrant, so a caller holding it MUST take the _already_locked
entry.
QSRV’s group PUT is the second external caller (pvxs
IOCSource::doPostProcessing, iocsource.cpp:397-420, whose PACT
branch is this same RPRO deferral): it reaches the decision by its own
route — record._options.process, a +type:"proc" member, a pp field
— but once the answer is “process”, the transition is this owner’s, in
both gate modes.
The PACT (RPRO) branch is success, as it is in C: dbPutField returns
the dbProcess status only on the branch that ran it.
Sourcepub fn put_driven_process_already_locked(
&self,
record_name: &str,
) -> CaResult<()>
pub fn put_driven_process_already_locked( &self, record_name: &str, ) -> CaResult<()>
Self::put_driven_process for a caller that already owns the
record’s advisory write gate. The gate-held body — no .await.
Sourcepub async fn put_pv_no_process(
&self,
name: &str,
value: EpicsValue,
) -> CaResult<()>
pub async fn put_pv_no_process( &self, name: &str, value: EpicsValue, ) -> CaResult<()>
Put a PV value without triggering process (for restore).
Source§impl PvDatabase
impl PvDatabase
Sourcepub fn evaluate_calc_link(&self, calc: &CalcLink) -> Option<EpicsValue>
pub fn evaluate_calc_link(&self, calc: &CalcLink) -> Option<EpicsValue>
lnkCalc evaluation: fetch each input PV, bind to calc engine
vars A..L, run expr, return the result as EpicsValue::Double.
Returns None if any input fetch fails, expr compile fails, or
eval fails — the caller treats the link as unresolvable.
Sourcepub async fn evaluate_calc_link_with_time(
&self,
calc: &CalcLink,
) -> Option<(EpicsValue, Option<SystemTime>)>
pub async fn evaluate_calc_link_with_time( &self, calc: &CalcLink, ) -> Option<(EpicsValue, Option<SystemTime>)>
lnkCalc evaluation that also returns the timestamp pulled from
the input named by time_source (e.g. 'A' → first input).
Returns (value, Some(time)) when time_source is set and
the referenced input record has a timestamp, (value, None)
otherwise. The caller (link read path) uses None to mean
“consumer keeps its own apply_timestamp time”.
Sourcepub fn external_link_alarm_snapshot(&self, name: &str) -> Option<RemoteAlarm>
pub fn external_link_alarm_snapshot(&self, name: &str) -> Option<RemoteAlarm>
Ungated remote alarm snapshot for an external (pva:// /
ca://) link — the DB-link inspection counterpart of
Self::external_link_alarm.
Where external_link_alarm returns the gated maximize-severity
contribution folded into the owning record’s LINK_ALARM (pvxs
pvaGetValue applying the MS/NMS/MSI gate,
pvalink_lset.cpp:424-431), this returns the ungated remote
(severity, status, message) snapshot pvxs exposes through
dbGetAlarm / dbGetAlarmMsg (pvaGetAlarmMsg,
pvalink_lset.cpp:542-575). A default NMS link reports its
remote severity here even though it leaves the owning record
unraised.
None when no lset is registered for the scheme, the link is not
connected, or the lset does not track remote alarms. Scheme
dispatch mirrors Self::external_link_alarm.
Sourcepub fn external_link_metadata(&self, name: &str) -> Option<LinkMetadata>
pub fn external_link_metadata(&self, name: &str) -> Option<LinkMetadata>
Remote display / control / valueAlarm metadata for an external
(pva:// / ca://) link, resolved through the registered
lset’s crate::server::database::LinkSet::link_metadata hook.
This is the DB-link-API entry point that exposes the linked PV
metadata pvxs’s pvalink lset surfaces through its
pvaGetDBFtype / pvaGetElements / pvaGetControlLimits /
pvaGetGraphicLimits / pvaGetAlarmLimits / pvaGetPrecision
/ pvaGetUnits getters
(pvxs/ioc/pvalink_lset.cpp:700). Scheme dispatch mirrors
Self::external_link_alarm: an explicit pva:// / ca://
prefix selects the lset directly, a bare name tries every
registered lset until one reports metadata.
None when no lset is registered for the scheme or the lset
has no cached value for the link (not yet connected).
Sourcepub fn link_metadata(
&self,
link: &ParsedLink,
visited: &mut HashSet<String>,
) -> Option<LinkMetadata>
pub fn link_metadata( &self, link: &ParsedLink, visited: &mut HashSet<String>, ) -> Option<LinkMetadata>
C dbGetControlLimits / dbGetGraphicLimits / dbGetAlarmLimits /
dbGetPrecision / dbGetUnits (dbLink.c:344-393) — the five
metadata slots a record fetches THROUGH one of its links.
The single owner of “what does this link say about display/control/
alarm limits, units and precision”. Record support (the routing
layer) calls this from its get_graphic_double / get_control_double
/ get_alarm_double / get_units / get_precision implementations
for the link-backed fields — calc/calcout/sub/aSub INPA..INPU,
aSub OUTA.., seq DOn (calcRecord.c:223-230, aSubRecord.c:356-369,
subRecord.c:260-266, seqRecord.c:332-336).
§Return contract — this is C’s status, not a convenience Option
Each of C’s five dbGet* entry points writes the caller’s buffer
ONLY on a zero return; every caller listed above ignores the status
and keeps whatever the buffer already held. The two levels of
Option here encode exactly that:
None— C returned non-zero (S_db_noLSET/S_dbLib_badLink). The caller MUST leave its buffer untouched.Some(meta)— C returned 0. EachSomefield is a value C wrote; aNonefield is a slot this link cannot report.
§Per-link-class behaviour, from the C lset tables
- Constant link —
None, always.dbConst_lset(dbConstLink.c:234-248) leaves all five slotsNULL, so the!plset->getGraphicLimitstest indbGetGraphicLimits(dbLink.c:358-359) short-circuits toS_db_noLSETand NOTHING is written. This is the case for the oracle’sfield(INPA,"5")records: C’s answer is not a propagated limit and not a DBF-type-range default — the record’s buffer keeps the seed it held before the fetch (see the routing contract below). Measured: acalcfield(INPA,"5")serves display limits0/0. - DB link —
Some(meta)from the target field, viaSelf::db_link_metadata. - CA/PVA link — delegated to the registered lset
(
Self::external_link_metadata); C installsdbCa/pvalink’s own lset for these, notdbDb_lset. - Everything else (unset link, hardware link) —
None. C leavesplink->lsetNULL for these, sodbLink.c:358returnsS_db_noLSET.
visited is the caller’s chain state and carries C’s
DBLINK_FLAG_VISITED recursion guard — see
Self::db_link_metadata.
§Contract for the routing layer
This method reports what the LINK says. It does not know what the
calling record should do with a None, because C’s answer to that is
per-slot and not uniform. Record support must seed its buffer, call
this, and overwrite only on Some — the seed IS the answer whenever
the fetch reports nothing. C’s seeds, and the four slots that route
through a link at all:
| rset slot | seed before the fetch | C site |
|---|---|---|
get_graphic_double | (0.0, 0.0) | dbAccess.c:216 |
get_alarm_double | NaN×4 | dbAccess.c:290 |
get_units | "" (NOT the record’s EGU) | dbAccess.c:378 |
get_precision | the record’s own PREC | calcRecord.c:191 |
get_control_double | never fetched — see below |
Two of those seeds are counter-intuitive and are measured facts, not
deductions (softIoc + caget -d DBR_CTRL_DOUBLE, EPICS 7 base): on a
calc with field(INPA,"5") field(PREC,"7") field(EGU,"volts"), C
serves INPA with precision 7 (the record’s own PREC survives,
because get_precision seeds *pprecision = prec->prec before the
fetch and overwrites only on a zero status) but units "" (the
strncpy(units, prec->egu) fallback sits in the else arm that a
link-backed field never takes — calcRecord.c:172-181).
Control limits must NOT be routed through a link. dbDbLink.c:414
really does install dbDbGetControlLimits, and dbGetControlLimits is
public API (dbLink.h:436) — so this method reports it — but NOTHING
in EPICS base calls it. Every record instead sends its link-backed
fields to recGblGetControlDouble (calcRecord.c:251,
subRecord.c, calcoutRecord.c, aSubRecord.c:376), i.e. to
getMaxRangeValues — which is why C serves a calc INPA with
±1e300 control limits (measured) regardless of what the link points
at. Routing this method’s control_limits into a record’s
get_control_double would be a defect.
Sourcepub fn read_link_value_soft(
&self,
link: &ParsedLink,
is_soft: bool,
visited: &mut HashSet<String>,
depth: usize,
) -> Option<EpicsValue>
pub fn read_link_value_soft( &self, link: &ParsedLink, is_soft: bool, visited: &mut HashSet<String>, depth: usize, ) -> Option<EpicsValue>
Read a value from a parsed link for INP (only reads DB links when soft channel).
visited / depth are the caller’s processing-chain state — a PP
input link’s source is processed within that same chain so the
visited cycle guard spans the PP hop (see
Self::process_passive_db_source).
Sourcepub async fn sync_external_link_puts(&self)
pub async fn sync_external_link_puts(&self)
Drain the external-link put queue — C dbCaSync
(dbCa.c:1191-1194, the CA_SYNC action the dbCaTask answers only
once everything queued ahead of it has been serviced).
Returns when every staged write has been handed to its lset and that
lset’s put_value has returned. The barrier a caller needs when it
must observe the effect of a LinkPutOp::Plain write, which by
construction is no longer complete when write_external_pv returns.
Sourcepub fn external_link_puts_coalesced(&self) -> u64
pub fn external_link_puts_coalesced(&self) -> u64
Number of staged external-link writes a later write on the same link
overwrote — C’s pca->nNoWrite (dbCa.c:611-612).
Sourcepub fn external_link_puts_completed(&self) -> u64
pub fn external_link_puts_completed(&self) -> u64
Number of external-link writes the queue owner has completed.
Sourcepub async fn register_cp_link(
&self,
source_record: &str,
target_record: &str,
passive_only: bool,
)
pub async fn register_cp_link( &self, source_record: &str, target_record: &str, passive_only: bool, )
Register a CP link: when source_record changes, process target_record.
Both names are normalised to canonical form so the cp_links
map’s key/value always match the canonical record name that
dispatch_cp_targets uses for lookup. Without this, a user
who wrote INP="ALIAS_NAME CP" in their .db file would
register the CP edge under the alias key and then never see
the target processed (the source record’s canonical-name
dispatch would miss).
passive_only is true for a CPP edge (process the target only when
its SCAN is Passive) and false for CP (always process). When the
same source→target edge is registered from both a CP and a CPP link,
CP dominates: the merged edge keeps passive_only == false, matching
C, where an unconditional CP CA_DBPROCESS overrides any CPP gate on
the same record.
Sourcepub fn get_cp_targets(&self, source_record: &str) -> Vec<CpTarget>
pub fn get_cp_targets(&self, source_record: &str) -> Vec<CpTarget>
Get target edges to process when source_record changes (CP/CPP links).
Sourcepub async fn register_external_cp_link(
&self,
external_pv: &str,
target_record: &str,
passive_only: bool,
)
pub async fn register_external_cp_link( &self, external_pv: &str, target_record: &str, passive_only: bool, )
Register an EXTERNAL CP/CPP link: when the remote PV external_pv
(a cross-IOC CA/PVA source, e.g. OTHER:PV from
INP="OTHER:PV CP") changes, process target_record.
Twin of Self::register_cp_link for cross-IOC sources. The key is
the scheme-stripped external PV name — it is not a local record,
so it is NOT alias-resolved. It MUST equal the name the calink/pvalink
monitor dispatches under: that monitor is opened through the lset,
which strips the ca:// / pva:// scheme first, so its pv_name
(passed to Self::dispatch_external_cp_targets) is the bare PV.
Stripping the same schemes here — the set Self::resolve_external_pv
already knows — guarantees the registry key and the dispatch key can
never diverge. The target_record (the local holder) IS alias-resolved
so the dispatch processes the canonical record. CP dominates CPP on a
merged edge, identical to the local path.
Sourcepub fn get_external_cp_targets(&self, external_pv: &str) -> Vec<CpTarget>
pub fn get_external_cp_targets(&self, external_pv: &str) -> Vec<CpTarget>
Get holder edges to process when the remote PV external_pv changes
(external CA/PVA CP/CPP links).
Sourcepub async fn external_cp_pv_names(&self) -> Vec<String>
pub async fn external_cp_pv_names(&self) -> Vec<String>
Every external PV name that has at least one CP/CPP holder edge.
The calink/pvalink integration warms (opens a monitor for) each of
these at iocInit so the remote source has a live subscription whose
callback can drive Self::dispatch_external_cp_targets — a Passive
CP holder is otherwise never read and its monitor never opens.
Sourcepub async fn external_link_pv_names(&self) -> Vec<String>
pub async fn external_link_pv_names(&self) -> Vec<String>
Every DISTINCT external PV name this database’s link fields name, in any direction and under any policy — the set the link registry converges to once iocInit’s staged opens have all landed.
Distinct from Self::external_cp_pv_names, which is the CP/CPP
subset, and from the counts Self::setup_cp_links and
Self::setup_external_link_opens print: those count link fields
(two records pointing at one upstream PV are two links), whereas a
link set holds one entry per PV. Reporting a registry count against a
field count would never agree.
The enumeration goes through super::PvDatabase::record_link_fields
and external_pv_name, the same two owners both iocInit link phases
use, so “what is an external link” cannot diverge between staging a
link and counting it.
Sorted, so a caller can print it as a stable operator-facing list.
Sourcepub async fn initialize_link_locality(&self) -> usize
pub async fn initialize_link_locality(&self) -> usize
Commit C dbInitLink’s locality decision into every record’s parsed
link cache — the iocInit pass that makes the rule STATE rather than a
test each read path repeats.
Runs after all records have loaded and before setup_cp_links, which
is C’s ordering: initPVLinks walks the loaded database once. Only the
COMMON_LINK_FIELDS
have a parse cache to commit into; INPA../DOL.. and the lnkCalc
arguments are re-parsed from their raw text each process cycle and keep
the read path’s runtime locality fallback
(Self::read_target_value) instead.
The decision is taken from the field’s RAW text, not from the cache:
the cache is only guaranteed fresh when the field was written through
put_common_field, and a link whose raw string was installed some
other way must still be initialised. Only a genuine Db → Ca
conversion is written back, so a cache that legitimately differs from
its raw text is left alone.
Returns the number of links converted.
Sourcepub async fn setup_cp_links(&self)
pub async fn setup_cp_links(&self)
Scan all records for CP/CPP input links and register them. Local
Db links land in the local CP registry; external Ca links land
in the external CP registry, whose holders are processed by the
calink monitor callback.
Sourcepub async fn setup_external_link_opens(&self) -> usize
pub async fn setup_external_link_opens(&self) -> usize
Open every external link in the database at iocInit — C
dbInitLink (dbLink.c:95-143) hands EVERY non-local PV_LINK to
dbCaAddLinkCallbackOpt, which stages a CA_CONNECT action
(dbCa.c:389-417) for the dbCaTask to service. A C IOC therefore
reaches its first scan with every external channel already connecting.
Two properties come straight from the C:
- Direction-agnostic.
dbInitLinkreachesdbCaAddLinkforDBF_INLINK,DBF_OUTLINKandDBF_FWDLINKalike (dbLink.c:118onwards; thedbfTypecheck below it only setspvlOptInpNative/pvlOptFWD). An OUT-only external link is opened at init too. - Every link field, not just the CP/CPP subset.
Self::setup_cp_linkswarms only CP/CPP links, because those have a chicken-and-egg problem a lazy open cannot solve (a Passive holder is never scanned, so its monitor is never created). Every other external link would otherwise wait for its first cache miss to stage the open — one cold scan cycle per link that C does not spend.
The field enumeration is super::PvDatabase::record_link_fields, the
same single owner setup_cp_links uses, so “what is a link field”
cannot diverge between the two passes. Staging goes through
super::PvDatabase::stage_external_link_open_by_name →
stage_external_link_open, so the connect still runs on the link work
owner and never on a record-processing thread; nothing here calls
LinkSet::connect_link directly.
Idempotent against setup_cp_links: the queue’s open state is
per-[super::link_put_queue::LinkKey] and terminal, and both passes
derive the key from the same boundary name, so a CP link already warmed
above is not staged a second time.
Returns the number of links this pass staged.
Source§impl PvDatabase
impl PvDatabase
Sourcepub async fn process_record(&self, name: &str) -> CaResult<()>
pub async fn process_record(&self, name: &str) -> CaResult<()>
Process a record by name (process_local + notify). Alias-aware (epics-base PR #336).
Sourcepub async fn process_record_already_locked(&self, name: &str) -> CaResult<()>
pub async fn process_record_already_locked(&self, name: &str) -> CaResult<()>
process_record variant for a caller that already
owns the record’s advisory write gate — the QSRV atomic group
PUT applying a +proc member. The gate is not
reentrant; the atomic group path MUST use this entry. See
crate::server::database::PvDatabase::lock_records.
Sourcepub fn process_record_with_links<'a>(
&'a self,
name: &'a str,
visited: &'a mut HashSet<String>,
depth: usize,
) -> Pin<Box<dyn Future<Output = CaResult<()>> + Send + 'a>>
pub fn process_record_with_links<'a>( &'a self, name: &'a str, visited: &'a mut HashSet<String>, depth: usize, ) -> Pin<Box<dyn Future<Output = CaResult<()>> + Send + 'a>>
Process a record with full link handling (INP -> process -> alarms -> OUT -> FLNK). Uses visited set for cycle detection and depth limit.
Foreign-caller entry: FLNK dispatch, scan loop, scan_event, CA put,
process(PROC=1) etc. Hits the PACT entry guard (mirrors C dbProcess
at dbAccess.c:537-559) when the record is mid-async.
this is a foreign full-processing entry, so it acquires
the record’s advisory write gate (dbScanLock analogue) for the
entry record before processing. A QSRV atomic group or pvalink
atomic scan-on-update epoch that holds lock_records over the
same record blocks a foreign scan/event/FLNK-dispatch caller
here, and vice versa — restoring the DBManyLock exclusion. The
recursive FLNK / OUT / CP fan-out within one chain does NOT
re-acquire the gate (process_record_with_links_recursive),
mirroring C processTarget (dbDbLink.c:436) which asserts the
target’s lock set is already owned by the calling thread; the
visited cycle guard prevents re-processing the entry record.
Sourcepub fn process_record_readback<'a>(
&'a self,
name: &'a str,
visited: &'a mut HashSet<String>,
depth: usize,
) -> Pin<Box<dyn Future<Output = CaResult<()>> + Send + 'a>>
pub fn process_record_readback<'a>( &'a self, name: &'a str, visited: &'a mut HashSet<String>, depth: usize, ) -> Pin<Box<dyn Future<Output = CaResult<()>> + Send + 'a>>
Driver-callback (asyn:READBACK) full-processing entry.
The single owner of this entry is the I/O Intr wiring
(crate::server::ioc_app::setup_io_intr and its ioc_builder
twin): the spawned task processes a record because the driver
fired an interrupt callback, not because of a client put / FLNK /
scan. device_callback = true tells
Self::process_record_with_links_inner that, for an output
record, this cycle must READ the callback value back into VAL and
MUST NOT write it to the driver — C devAsynInt32.c::processBo
(and processAo/processLongout/…) take the readback branch when
newOutputCallbackValue is set, never processCallbackOutput’s
write(). Without this, the readback re-asserts the setpoint and
re-triggers the driver (e.g. AD Acquire looping). Input records
(!can_device_write) are unaffected: their read stage already
runs, and the no-write gate is keyed on the record being an output.
Acquires the entry record’s advisory write gate exactly like
Self::process_record_with_links — the callback task is a
foreign caller w.r.t. any QSRV atomic group / pvalink epoch.
Sourcepub fn process_record_with_links_already_locked(
&self,
name: &str,
visited: &mut HashSet<String>,
depth: usize,
) -> CaResult<()>
pub fn process_record_with_links_already_locked( &self, name: &str, visited: &mut HashSet<String>, depth: usize, ) -> CaResult<()>
full-processing entry for a caller that already owns the
record’s advisory write gate via PvDatabase::lock_records —
the QSRV atomic group GET/PUT and the pvalink atomic
scan-on-update epoch. The advisory gate is not
reentrant; a transaction owner holding lock_records over the
member set MUST use this entry to scan a member record, or it
would deadlock against its own epoch guard. Foreign (non-owner)
callers must use Self::process_record_with_links so the gate
is taken.
Synchronous: the gate is already held by the caller, so this entry has
nothing to wait for. It goes straight to
Self::process_record_with_links_body, which is where the H6
no-suspension contract lives.
Sourcepub fn process_record_continuation<'a>(
&'a self,
name: &'a str,
visited: &'a mut HashSet<String>,
depth: usize,
) -> Pin<Box<dyn Future<Output = CaResult<()>> + Send + 'a>>
pub fn process_record_continuation<'a>( &'a self, name: &'a str, visited: &'a mut HashSet<String>, depth: usize, ) -> Pin<Box<dyn Future<Output = CaResult<()>> + Send + 'a>>
Owner-driven continuation re-entry — bypasses the PACT entry guard.
Used by ProcessAction::ReprocessAfter timer fires: the spawned
re-entry task IS the owner of the async cycle, equivalent to C
callbackRequestDelayed’s direct call to the record’s process()
(which bypasses dbProcess). Foreign callers must still go through
process_record_with_links so FLNK / scan / CA put cannot race
during the wait window.
the timer fire is a fresh task — the original cycle’s
advisory gate was released when process_record_with_links
returned async-pending. In C, callbackRequestDelayed dispatches
through a callback that re-takes dbScanLock(precord) for the
completion process(). This entry therefore re-acquires the
advisory write gate, so the continuation cannot interleave with a
QSRV atomic group or another foreign scan of the same record.
Sourcepub fn async_handle(&self) -> AsyncDbHandle
pub fn async_handle(&self) -> AsyncDbHandle
A cycle-free AsyncDbHandle for this database, handed to each
record via crate::server::record::Record::set_async_context at
registration. Holds only a Weak reference, so a record stashing
it never keeps the database alive.
Sourcepub fn mint_async_token(&self, name: &str) -> Option<AsyncToken>
pub fn mint_async_token(&self, name: &str) -> Option<AsyncToken>
Mint a fresh async re-entry AsyncToken for name.
Minting advances the record’s generation counter, so any
previously-minted token for the same record is superseded — its
AsyncToken::fire becomes a structural no-op. This mirrors C
callbackRequestDelayed replacing an outstanding delayed callback
for a record. name must be the canonical record name (the value
of RecordInstance::name). Returns None if the record is absent.
Sourcepub fn cancel_async_reentry(&self, name: &str)
pub fn cancel_async_reentry(&self, name: &str)
Cancel any outstanding async re-entry token for name (C
callbackCancelDelayed): advance the record’s generation counter so
every previously-minted AsyncToken for it becomes stale and its
fire is a no-op. A subsequent Self::mint_async_token produces a
fresh, current token. No-op if the record is absent.
Sourcepub fn post_fields(
&self,
name: &str,
fields: Vec<(String, EpicsValue)>,
) -> CaResult<Vec<String>>
pub fn post_fields( &self, name: &str, fields: Vec<(String, EpicsValue)>, ) -> CaResult<Vec<String>>
Post an async-side field update for name — the C db_post_events
analogue called from device-support / async-callback context.
Each (field, value) is written through the internal put (bypassing
the read-only field gate, like a record’s own process() writes)
and a monitor event is posted with DBE_VALUE | DBE_LOG — the mask C
device support uses for an out-of-process value post
(db_post_events(precord, &prec->field, DBE_VALUE | DBE_LOG)).
Metadata-class writes invalidate the metadata cache via
notify_field_written, honouring the snapshot-cache contract.
Unlike Self::complete_async_record, this runs no alarm /
timestamp / FLNK tail: it is the immediate “push these fields to
monitors now” primitive (e.g. asyn TRACE info, motor intermediate
readback) that is independent of any process cycle. Returns the
field names actually posted, or CaError::ChannelNotFound if the
record is absent.
Sourcepub fn post_property_fields(
&self,
name: &str,
fields: Vec<(String, EpicsValue)>,
) -> CaResult<Vec<String>>
pub fn post_property_fields( &self, name: &str, fields: Vec<(String, EpicsValue)>, ) -> CaResult<Vec<String>>
Out-of-band PROPERTY-class field post — the C
db_post_events(precord, &precord->val, DBE_PROPERTY) analogue used
for enum-string table re-propagation (asyn callbackEnum,
devAsynInt32.c:711-762). Writes each (field, value) through the
internal put, invalidates the metadata cache, and posts a
DBE_PROPERTY event so subscribers re-read enum choices / control
metadata.
Unlike Self::post_fields (which posts DBE_VALUE | DBE_LOG) this
signals a property change, not a value change: a driver that re-keys
its enum strings has not produced a new reading, only new choice
labels. Returns the field names actually posted.
Sourcepub fn new_put_notify() -> (Arc<NotifyWaitSet>, Receiver<()>)
pub fn new_put_notify() -> (Arc<NotifyWaitSet>, Receiver<()>)
Create a put-notify wait-set for a downstream operation a record is
about to drive, returning the wait-set (to attach to the downstream
target instance’s notify) and the completion receiver.
C dbNotify.c processNotify: the set arms pending = 1 for the
downstream operation and fires the oneshot when that slot (plus any
FLNK/OUT chain members that enter it) drains to zero — i.e. on
dbNotifyCompletion. Pair with Self::reprocess_on_notify to
re-enter a waiting record when the downstream completes (SSEQ
WAITn).
Sourcepub fn reprocess_on_notify(
&self,
token: AsyncToken,
completion: Receiver<()>,
) -> TaskHandle<()>
pub fn reprocess_on_notify( &self, token: AsyncToken, completion: Receiver<()>, ) -> TaskHandle<()>
Wire a downstream put-notify completion to an async re-entry: spawn a
task that awaits completion (the oneshot from
Self::new_put_notify, fired on dbNotifyCompletion) and then
token.fires, re-entering the waiting record’s process(). A
superseded / cancelled token re-enters nothing. Returns the spawned
task handle; fire-and-forget callers may drop it.
Sourcepub async fn put_link_notify(
&self,
record_name: &str,
link_field: &str,
link_str: &str,
value: EpicsValue,
) -> Option<Receiver<()>>
pub async fn put_link_notify( &self, record_name: &str, link_field: &str, link_str: &str, value: EpicsValue, ) -> Option<Receiver<()>>
Issue a put-WITH-completion to an OUT link and hand the caller only
the completion receiver — the non-blocking sibling of
Self::reprocess_on_notify.
Each call mints its own put-notify wait-set (C dbProcessNotify),
writes the link through it with the source record’s committed PUTF /
alarm propagated (C recGblInheritSevrMsg), releases the initiator
count, and returns the oneshot that fires on dbNotifyCompletion.
The caller owns when (and whether) to await each receiver, so several
puts can be outstanding at once — unlike
crate::server::record::ProcessAction::WriteDbLinkNotify, which wires the completion
straight to a single superseding async re-entry token and so allows
only one outstanding put per record. This is the seam C
calcApp/src/sseqRecord.c needs to run multiple WAITn put-callbacks
concurrently in flight (processNextLink).
record_name is the source whose PUTF/alarm propagate into the
target, link_str the already-resolved OUT link spelling, value
the value to write. None if the source record is gone; an empty
link_str returns a receiver that fires immediately (nothing joined
the set).
Sourcepub fn complete_async_record<'a>(
&'a self,
name: &'a str,
) -> Pin<Box<dyn Future<Output = CaResult<()>> + Send + 'a>>
pub fn complete_async_record<'a>( &'a self, name: &'a str, ) -> Pin<Box<dyn Future<Output = CaResult<()>> + Send + 'a>>
Complete an asynchronous record’s post-process steps. Call after device support signals completion (clears PACT, runs alarms, snapshot, OUT, FLNK).
§The completion RE-TAKES the gate
This is the other half of C’s async-device shape. dbProcess released
dbScanLock when it set pact and returned; the completion runs on the
callback task, which takes the record’s lock again for the epilogue —
C callback.c:379-388 ProcessCallback:
dbScanLock(pRec);
(*pRec->rset->process)(pRec);
dbScanUnlock(pRec);So the epilogue below — alarm commit, snapshot, OUT writes, FLNK — runs under the SAME exclusion as the cycle that started it, and a put that arrived during the async window has either already been serialised ahead of it or waits behind it. Every caller reaches this from a completion task holding no gate (the device-write completion spawn above, the seq DLYn chain, the tests); nothing calls it with the gate held, which would dead-lock on the non-reentrant gate.
Sourcepub fn dispatch_external_cp_targets(&self, external_pv: &str)
pub fn dispatch_external_cp_targets(&self, external_pv: &str)
Process every holder of an EXTERNAL CP/CPP link to external_pv —
the cross-IOC twin of Self::dispatch_cp_targets. Called by the
calink/pvalink CA monitor callback on every remote change, this is
the Rust equivalent of C dbCa.c eventCallback adding
CA_DBPROCESS for a CP (or Passive CPP) link (dbCa.c:993-994)
and the worker thread running db_process(prec) (dbCa.c:1295).
A cross-IOC source never processes locally, so this callback is the
only trigger; without it a CP/CPP link’s holder never processes
on a remote change.
A fresh visited set and depth = 0 start a new process chain —
the monitor event is an independent external trigger, like a scan,
not a continuation of an in-flight local chain.
Source§impl PvDatabase
impl PvDatabase
Sourcepub fn lock_record(&self, record: &str) -> RecordWriteGuard
pub fn lock_record(&self, record: &str) -> RecordWriteGuard
Acquire the advisory write gate for a single record.
This is the dbScanLock(precord) analogue. The plain CA/PVA
write path holds this for the duration of one record write so
it cannot interleave with a multi-record transaction that owns
the same record’s gate via Self::lock_records.
record is alias-resolved internally, so an alias and its
target always map to the same gate as Self::lock_records
keys them.
Blocks the calling thread if the gate is held. The gate is not
reentrant — a caller that already owns it (a transaction owner inside
its own Self::lock_records epoch) MUST use the _already_locked
entry points instead, or it deadlocks against itself.
Sourcepub fn lock_records<I, S>(&self, records: I) -> ManyRecordWriteGuard
pub fn lock_records<I, S>(&self, records: I) -> ManyRecordWriteGuard
Acquire the advisory write gates for a set of records — the
DBManyLock / DBManyLocker equivalent.
Every name is alias-resolved to its canonical record name, the
set is sorted and de-duplicated, then the per-record gates are
acquired in that sorted order. Sorting guarantees two
overlapping multi-record transactions acquire their shared
records in the same global order and cannot deadlock (mirrors
pvxs DBManyLock sorting the lock set in dbLock.c); the
dedup means a record bound by more than one member link is
locked exactly once. That canonical order is load-bearing in a way
it was not while the gate was async: an out-of-order acquisition now
wedges two threads rather than two tasks.
The returned ManyRecordWriteGuard must be held for the
whole transaction. While it is alive, a concurrent plain write
to any of those records blocks on Self::lock_record, and a
concurrent overlapping transaction blocks on this method.
Names that do not resolve to a record still get a gate (keyed
by the post-alias name) — matching dbLockerAlloc, which
accepts the record pointers it is given without a liveness
re-check.
Source§impl PvDatabase
impl PvDatabase
Sourcepub fn update_scan_index(
&self,
name: &str,
old_scan: ScanType,
_new_scan: ScanType,
old_phas: i16,
_new_phas: i16,
)
pub fn update_scan_index( &self, name: &str, old_scan: ScanType, _new_scan: ScanType, old_phas: i16, _new_phas: i16, )
Update scan index when a record’s SCAN or PHAS field changes.
Takes registration_mutex so the read of
the records map (to verify the record still exists) and the
scan_index mutation are atomic vs. concurrent remove_record.
The new_scan / new_phas parameters
the caller passes are advisory only. After acquiring the
mutex we read the LIVE record’s current scan/phas and insert
based on those. Pre-fix a put-then-update sequence could
race a remove+re-add of the same name: the caller’s
new_scan reflected the old (now-removed) record’s value;
inserting that under the fresh record’s name produced a
stale scan-index entry pointing at a wrong scan rate. The
live-read makes the index strictly reflect the record’s
current state at insert time.
Synchronous. It had exactly two suspension points and neither was a
real one: the registration_mutex acquisition (L46) and two
scan_index write acquisitions (L8b) — both awaited before step 4.
Both are blocking PI mutexes now, so the whole scan-index update is a
bounded critical
section — which is what lets it run inside the L1 record-gate window
without putting an .await there. C reaches scanAdd/scanDelete
(dbScan.c:241-330) the same way, from inside dbPut under
dbScanLock. doc/rtems-priority-locks-design.md §5 step 4.
Sourcepub async fn records_for_scan(&self, scan_type: ScanType) -> Vec<String>
pub async fn records_for_scan(&self, scan_type: ScanType) -> Vec<String>
Get record names for a given scan type, sorted by PHAS then
database load order (C dbScan.c stable same-PHAS FIFO).
A SCAN value that names no list (Passive, or an index outside
menuScan) holds no records — there is no such bucket to look up.
Sourcepub async fn pini_records(&self, mode: PiniMode) -> Vec<String>
pub async fn pini_records(&self, mode: PiniMode) -> Vec<String>
Get all record names whose PINI is exactly mode.
C matches the menu index with != (iocInit.c:598
if (precord->pini != pphase->pini) return;), so each menuPini
choice selects a disjoint set of records driven by a different pass:
YES at initialProcess() (iocInit.c:656), RUN/RUNNING/PAUSE/
PAUSED from piniProcessHook (iocInit.c:629-646). A PINI=RUN
record must NOT be processed by the YES pass.
Snapshot the records map under the outer
read lock, then drop it before fanning out per-record reads.
Pre-fix the outer records.read() lock was held across every
rec.read().await — under contention with a pending
add_record (which now takes the registration_mutex →
records.write()), startup could stall while every PINI
record was inspected serially.
Sourcepub async fn pini_process(&self, mode: PiniMode)
pub async fn pini_process(&self, mode: PiniMode)
C piniProcess (iocInit.c:608-627) — process every record whose
PINI is exactly mode, in ascending PHAS order, each with its
full link chain.
The single owner of “run a PINI pass”: initialProcess() calls it with
PiniMode::Yes, and the initHook lifecycle calls it with
PiniMode::Run / PiniMode::Running. Every driver goes through
here, so neither the pass selection nor the phase ordering can diverge
between them.
The sweep is C’s, not a pre-sorted list: each pass over the database
processes the records at the current phase and, while doing so, finds
the next lowest PHAS still ahead of it. C spells this out
(iocInit.c:614-619) — “PHAS fields can be changed at runtime, so we
have to look for the lowest value of PHAS each time” — so a record whose
phase is raised by an earlier PINI record’s processing is still picked
up in the correct later pass, which a snapshot-and-sort would miss.
Sourcepub async fn post_event(&self)
pub async fn post_event(&self)
Process all records with SCAN=Event, regardless of EVNT.
Back-compat entry point for the iocsh postEvent command,
whose handler currently drops the numeric event argument.
Prefer Self::post_event_named for C-correct per-event
routing — see dbScan.c:548-552 post_event →
postEvent(pevent_list[event]).
Sourcepub async fn post_event_named(&self, event_name: &str)
pub async fn post_event_named(&self, event_name: &str)
Process only the SCAN=Event records whose EVNT resolves to
event_name. Mirrors C dbScan.c event routing: each
event_list (eventNameToHandle) holds exactly the records
whose EVNT matches, and postEvent walks only that list.
Event-name matching follows eventNameToHandle (dbScan.c:469):
surrounding whitespace is trimmed, and a numeric string with an
integer part in [1,255] is normalised to its integer form so
"5", " 5 " and "5.0" all name the same event.
Source§impl PvDatabase
impl PvDatabase
pub fn new() -> Self
Sourcepub async fn add_breaktables(
&self,
tables: Vec<BrkTable>,
) -> Arc<BreakTableRegistry> ⓘ
pub async fn add_breaktables( &self, tables: Vec<BrkTable>, ) -> Arc<BreakTableRegistry> ⓘ
Merge tables into the shared breakpoint-table registry (C bptList
accumulation across dbLoadDatabase/dbLoadRecords) and return the new
snapshot. Copy-on-write: a new merged registry replaces the old one.
add_breaktables is the single registry-mutation owner, so it also
restores the invariant every record can resolve against the current
registry on mutation: the new snapshot is re-installed into every
existing record. That covers a record created before its table was
loaded (an inline record added before dbLoadRecords, or a merge-reload
that repoints LINR to a table loaded in the same command) — neither
of which goes back through add_record’s install. install_* is a
no-op for non-ai/ao records and resets the cached table so the new
registry wins. Returns the current snapshot unchanged when tables is
empty (no mutation, so no re-install).
Sourcepub async fn install_subroutine_registry(
&self,
registry: HashMap<String, Arc<SubroutineFn>>,
)
pub async fn install_subroutine_registry( &self, registry: HashMap<String, Arc<SubroutineFn>>, )
Install the by-name subroutine registry, retained for runtime
re-resolution (aSub LFLG=READ / SUBL). Called once at iocInit with the
IocApp/IocBuilder registry. See Self::find_subroutine_named.
Sourcepub fn try_claim_scan_start(&self) -> bool
pub fn try_claim_scan_start(&self) -> bool
Atomically claim the right to start the scan scheduler for this DB.
Returns true on the first call, false on subsequent calls.
Used by ScanScheduler::run to prevent duplicate scan tasks
when multiple protocol servers (CA + PVA) both try to start scanning.
Sourcepub fn mark_pini_done(&self)
pub fn mark_pini_done(&self)
Mark PINI processing complete. Wakes any non-owner scan schedulers that were waiting before running their hooks.
Sourcepub fn pini_done(&self) -> bool
pub fn pini_done(&self) -> bool
True once the PINI=YES pass has completed for this database —
published by Self::mark_pini_done. The scan owner reads this
to keep the pass exactly-once (C initialProcess, iocInit.c:653
runs once, inside iocBuild): when the IOC init path already ran
PINI, the owner skips its own pass instead of re-processing every
PINI record.
Sourcepub async fn wait_for_pini(&self)
pub async fn wait_for_pini(&self)
Wait until the scan owner has completed PINI processing. Returns immediately if PINI has already completed.
Sourcepub async fn set_search_resolver(&self, resolver: SearchResolver)
pub async fn set_search_resolver(&self, resolver: SearchResolver)
Install an async resolver invoked when PvDatabase::has_name
fails to find a name. Used by proxy/gateway implementations to
lazily populate PVs on first search.
Sourcepub async fn clear_search_resolver(&self)
pub async fn clear_search_resolver(&self)
Remove the previously installed search resolver, if any.
Sourcepub async fn set_existence_gate(&self, gate: ExistenceGate)
pub async fn set_existence_gate(&self, gate: ExistenceGate)
Install the per-request existence gate (see ExistenceGate).
Replaces any previously installed gate. Used by the CA gateway so
a cached shadow PV re-runs host/state admission per request.
Sourcepub async fn clear_existence_gate(&self)
pub async fn clear_existence_gate(&self)
Remove the previously installed existence gate, if any.
Sourcepub async fn set_external_resolver(&self, resolver: ExternalPvResolver)
pub async fn set_external_resolver(&self, resolver: ExternalPvResolver)
Set an external PV resolver for CA/PVA link resolution. The resolver is called synchronously from link reads.
Sourcepub async fn register_link_set(&self, scheme: &str, lset: DynLinkSet)
pub async fn register_link_set(&self, scheme: &str, lset: DynLinkSet)
Register a LinkSet under scheme (e.g. "pva" /
"ca"). The lset is consulted for ParsedLink::Pva /
ParsedLink::Ca link reads/writes before falling back to
the legacy ExternalPvResolver. Subsequent calls for the
same scheme replace the previous binding.
Sourcepub async fn link_set(&self, scheme: &str) -> Option<DynLinkSet>
pub async fn link_set(&self, scheme: &str) -> Option<DynLinkSet>
Look up the lset for scheme, if any.
Sourcepub async fn registered_link_schemes(&self) -> Vec<String>
pub async fn registered_link_schemes(&self) -> Vec<String>
Snapshot of every registered scheme name. Stable order for
dbpvxr dumps.
Sourcepub async fn wait_for_external_links(&self, timeout: Duration) -> (usize, usize)
pub async fn wait_for_external_links(&self, timeout: Duration) -> (usize, usize)
Wait for the CA links to local records to report
is_connected() == true. Mirrors dbCa: iocInit wait for local CA links to connect (epics-base PR #768/#856). The working set is
exactly Self::external_link_targets: only the CA facility’s
local-target links — pva:// links and non-local CA links connect
in the background and are never waited on (pvxs parity).
Polls every 100 ms. Returns:
Ok(connected_count)— the number of links that ended up connected. May be smaller than the total when the timeout expired before everyone was ready.- The total link count — i.e. the size of the working set
that was checked.
(connected, total)lets the caller log “M/N CA links connected”.
Pure no-op when no CA link set is registered, or when its
link_names() has no local-target link yet (e.g. lazy-open lsets
that haven’t observed any record link — record processing creates
the entries on first read, after iocInit returns).
Sourcepub async fn unconnected_external_links(&self) -> Vec<String>
pub async fn unconnected_external_links(&self) -> Vec<String>
Names of the waited-on CA links (local-target, per
Self::external_link_targets) that are opened but not yet
connected. iocInit calls this after
Self::wait_for_external_links times out so the
“M/N connected” diagnostic can name the N-M it proceeded
without, instead of leaving the operator to run dbcar.
pva:// links are not in this set — they never block iocInit.
Sourcepub fn record_link_fields(
&self,
record_name: &str,
) -> Vec<(String, String, ParsedLink)>
pub fn record_link_fields( &self, record_name: &str, ) -> Vec<(String, String, ParsedLink)>
Enumerate every link-shaped field on record_name. Returns
(field_name, link_string, parsed) tuples for fields whose
raw value parses as a non-trivial link via
crate::server::record::parse_link_v2. Used by dbpvxr to
dump per-record link state without hardcoding the field-name
list — works across record types as long as they expose link
strings via Record::get_field.
parsed is the post-dbInitLink view, not the bare parse: each
link is mapped through PvDatabase::db_init_link_locality, so a
Db link naming a record this IOC does not have is reported as the
Ca link C’s dbDbInitLink → dbCaAddLink fallthrough makes it
(dbLink.c:117-129, dbDbLink.c:94-96). Every consumer — the CP
setup, the init open pass, dbcaxr, the pvalink install scan — then
sees one consistent answer to “is this link local or external”
instead of each re-deriving it. link_string is still the verbatim
field text.
Returns an empty Vec when the record doesn’t exist.
Sourcepub fn external_link_opens_completed(&self) -> u64
pub fn external_link_opens_completed(&self) -> u64
Number of external-link opens the work owner has completed —
diagnostic twin of Self::external_link_puts_completed.
Sourcepub async fn add_pv(&self, name: &str, initial: EpicsValue) -> CaResult<()>
pub async fn add_pv(&self, name: &str, initial: EpicsValue) -> CaResult<()>
Add a simple PV with an initial value.
Returns Err when name is already registered as a simple PV,
a record, or an alias — mirroring epics-base C IOC which treats
duplicate dbLoadRecords names as a fatal error. Callers that
want replace-on-overwrite semantics must first call
remove_simple_pv / remove_record / remove_alias.
Serialized through registration_mutex so the
cross-namespace check is atomic with the insert and the lock
order across all add_/remove_ methods is identical (no
cross-namespace deadlock).
Sourcepub async fn add_pv_with_hook(
&self,
name: &str,
initial: EpicsValue,
hook: WriteHook,
) -> CaResult<()>
pub async fn add_pv_with_hook( &self, name: &str, initial: EpicsValue, hook: WriteHook, ) -> CaResult<()>
Add a simple PV that already has a crate::server::pv::WriteHook installed.
Equivalent to add_pv followed by find_pv + set_write_hook,
but the PV is constructed with the hook in place so it is
inserted into the simple_pvs map ATOMICALLY with the hook
already attached. Closes a small race in proxy/gateway code
where a downstream client could (in principle) CREATE_CHAN +
WRITE_NOTIFY between the two awaits and hit the local
pv.set() fallback path before the hook landed.
Returns Err on duplicate name (see Self::add_pv).
Sourcepub async fn add_pv_with_hooks(
&self,
name: &str,
initial: EpicsValue,
write_hook: WriteHook,
access_hook: Option<AccessHook>,
) -> CaResult<()>
pub async fn add_pv_with_hooks( &self, name: &str, initial: EpicsValue, write_hook: WriteHook, access_hook: Option<AccessHook>, ) -> CaResult<()>
like Self::add_pv_with_hook but also installs an
optional AccessHook so the CA
gateway can route this shadow PV’s read/write access-rights
decision through its own ACF. Both hooks are attached before the
PV is inserted into simple_pvs, so a downstream CREATE_CHAN
cannot observe the PV without its access hook bound.
Sourcepub async fn add_pv_with_hooks_full(
&self,
name: &str,
initial: EpicsValue,
write_hook: WriteHook,
access_hook: Option<AccessHook>,
read_hook: Option<ReadHook>,
) -> CaResult<()>
pub async fn add_pv_with_hooks_full( &self, name: &str, initial: EpicsValue, write_hook: WriteHook, access_hook: Option<AccessHook>, read_hook: Option<ReadHook>, ) -> CaResult<()>
like Self::add_pv_with_hooks but also installs an optional
ReadHook so a proxy (the CA
gateway in no-cache mode) can serve each downstream GET from a
fresh upstream fetch instead of the stored value. All three hooks
are attached before the PV is inserted into simple_pvs, so a
downstream CREATE_CHAN cannot observe the PV without its hooks
bound — the read hook lands atomically with registration, closing
the same race the write/access hooks already close. read_hook: None is identical to Self::add_pv_with_hooks.
Sourcepub async fn remove_simple_pv(&self, name: &str) -> Option<Arc<ProcessVariable>>
pub async fn remove_simple_pv(&self, name: &str) -> Option<Arc<ProcessVariable>>
Remove a simple PV by name. Returns Some(pv) if a PV was
removed. Used by the gateway sweep so an evicted upstream
subscription doesn’t leave a stale shadow PV (with a now-dead
WriteHook capturing an aborted upstream channel).
Also purges any aliases that pointed AT this name (otherwise a re-add of the same alias name would fail with “already registered as an alias” even though its target is gone).
Sourcepub fn begin_load(&self) -> Result<(), IocAlreadyInitialized>
pub fn begin_load(&self) -> Result<(), IocAlreadyInitialized>
Enter the LOAD phase: records are being created and the database is not
yet the one C would classify links against. Called by every path that
begins creating records for an IOC — an IocBuilder build, an iocsh
dbLoadRecords / dbCreateRecord, IocApp::run — and idempotent within
the phase, because an st.cmd issues several loads and they are all one
iocInit (R18-92).
§Refused once the IOC is running (R19-63)
C admits no record creation after iocInit: dbReadCOM
(dbLexRoutines.c:236) fails every .db/.dbd read with -2 once
getIocState() != iocVoid, and dbCreateRecordCallFunc
(dbStaticIocRegister.c:288) fails with S_dbLib_postInitRecRegister.
Asking to create records IS asking to enter the load phase, so the answer
lives here and is a Result the caller cannot ignore — a creator that
never asked cannot be written by accident, and one that asked cannot
proceed on a refusal.
The phase is left ONLY by Self::ioc_init, and once left it is
TERMINAL (R19-62): the queue is drained by exactly one ioc_init, so
nothing can be pushed into it afterwards and stranded. A load that fails
halfway leaves the phase open, which strands nothing: a queued
classification blocks no caller, and it is dropped with the database.
Sourcepub async fn ioc_init(&self)
pub async fn ioc_init(&self)
The iocInit barrier: end the LOAD phase and run every classification
owed, to completion.
After this returns the database is complete and every link status is
FINAL — C’s guarantee, where init_record runs inside iocInit and a
dbgf REC.INAV right after it reads the classified value (before it, C
refuses dbgf outright). Idempotent: an st.cmd that spells iocInit
out and the IocApp that runs one anyway are the same single boundary.
Sourcepub async fn add_record(
&self,
name: &str,
record: Box<dyn Record>,
) -> CaResult<()>
pub async fn add_record( &self, name: &str, record: Box<dyn Record>, ) -> CaResult<()>
Add a record (accepts a boxed Record to avoid double-boxing).
Returns Err when name collides with an existing record,
simple PV, or alias. The C IOC’s dbLoadRecords treats this as
fatal; do not silently replace.
The records-map insert AND scan-index insert run
under the same registration_mutex hold, eliminating the
TOCTOU window where remove_record could land between them
and leave a phantom scan entry.
Sourcepub async fn add_loaded_record(
&self,
name: &str,
record: Box<dyn Record>,
load: RecordLoad,
) -> CaResult<()>
pub async fn add_loaded_record( &self, name: &str, record: Box<dyn Record>, load: RecordLoad, ) -> CaResult<()>
Add a record together with the field set its .db definition loaded
into it — the creation sink for every dbLoadRecords path.
C’s dbLoadRecords writes a record’s ENTIRE field set through
dbStaticLib (including the UDF = 0 that dbPutString
(dbStaticLib.c:2653-2661) implies for any put to a field named
VAL), and only afterwards does iocInit::doInitRecord0
(iocInit.c:508-536) evaluate if (udf && stat == UDF_ALARM) sevr = udfs. The port used to add the record first and apply its loaded common
fields afterwards, so the init passes ran against a PRE-LOAD field set:
every record with a field(VAL,…) latched SEVR = INVALID at creation
and the UDF = 0 that arrived a moment later could not lower it again.
A whole .db of setpoint defaults and sim constants came up red.
Taking the loaded fields here is what makes C’s ordering hold by
construction: there is no window in which the init passes can observe a
record whose .db fields have not landed, because the record is not
reachable until they have. RecordInstance::run_init_passes is
crate-private for the same reason — the sink is the only caller.
Sourcepub async fn remove_record(&self, name: &str) -> bool
pub async fn remove_record(&self, name: &str) -> bool
Remove a record by name. Returns true if a record was removed,
false if no such name was registered. Mirrors epics-base PR
#505 — deletion at database creation, exposed here as a public
API so iocsh dbDeleteRecord and tests can drive it.
The cleanup covers the three indices that add_record populates:
the records map, the scan index, and CP-link source/target lists.
Live subscribers on the removed record drop their Sender clone
when the RecordInstance is dropped — they observe Closed on
next recv, matching the existing dbEvent cancel flow.
Sourcepub async fn add_alias(&self, alias: &str, target: &str) -> CaResult<()>
pub async fn add_alias(&self, alias: &str, target: &str) -> CaResult<()>
Register an alias alias for an existing record target.
Mirrors epics-base PR #336. Returns Err(...) when the target
does not exist or the alias name is already in use anywhere
in the database (records, simple PVs, or other aliases).
Pre-fix the alias path checked only
records and aliases — a simple-PV with the same name as
the proposed alias was missed, leaving the database in a
state where find_pv(alias) could resolve to either the
simple PV or the alias-mapped record depending on lookup
order. Now we run the same cross-namespace check_name_free
guard the other add_* paths use.
Sourcepub fn resolve_alias(&self, name: &str) -> Option<String>
pub fn resolve_alias(&self, name: &str) -> Option<String>
Resolve an alias to its target record name, or None when the
name is not an alias.
Sourcepub fn queue_after_ioc_running(&self, line: impl Into<String>)
pub fn queue_after_ioc_running(&self, line: impl Into<String>)
Queue an iocsh command line for post-PINI execution.
Mirrors epics-base PR #558 — afterIocRunning <command> lets
the startup script schedule actions that run after iocInit
completes (when the record set is fully wired up).
Sourcepub fn take_after_ioc_running(&self) -> Vec<String>
pub fn take_after_ioc_running(&self) -> Vec<String>
Drain the post-PINI iocsh command queue. Called by
IocApplication::run after PINI processing.
Sourcepub async fn find_entry(&self, name: &str) -> Option<PvEntry>
pub async fn find_entry(&self, name: &str) -> Option<PvEntry>
Look up an entry by name. Supports “record.FIELD” syntax.
If the name is not found and a search resolver is installed, the resolver is invoked once. If the resolver returns true, the database is re-checked.
Sourcepub async fn find_entry_from(
&self,
name: &str,
peer: Option<SocketAddr>,
) -> Option<PvEntry>
pub async fn find_entry_from( &self, name: &str, peer: Option<SocketAddr>, ) -> Option<PvEntry>
Like Self::find_entry, but threads the downstream client’s
socket address into the search resolver. The CA TCP CREATE_CHANNEL
handler passes the connection peer so the gateway can apply
host-scoped .pvlist admission.
Sourcepub async fn has_name(&self, name: &str) -> bool
pub async fn has_name(&self, name: &str) -> bool
Check if a base name exists (for UDP search).
If the name is not in the database and a search resolver is installed, the resolver is invoked. The resolver may populate the database (e.g., subscribe to an upstream IOC and add a placeholder PV) and return true; this method then re-checks.
Sourcepub async fn has_name_from(&self, name: &str, peer: Option<SocketAddr>) -> bool
pub async fn has_name_from(&self, name: &str, peer: Option<SocketAddr>) -> bool
Like Self::has_name, but threads the downstream client’s
socket address into the search resolver. The CA UDP search
responder passes the datagram source address so the gateway can
apply host-scoped .pvlist admission.
Sourcepub async fn find_pv(&self, name: &str) -> Option<Arc<ProcessVariable>>
pub async fn find_pv(&self, name: &str) -> Option<Arc<ProcessVariable>>
Look up a simple PV by name (backward-compatible).
Sourcepub fn get_record(&self, name: &str) -> Option<Arc<RwLock<RecordInstance>>>
pub fn get_record(&self, name: &str) -> Option<Arc<RwLock<RecordInstance>>>
Get a record Arc by name. Alias-aware (epics-base PR #336):
when name is not a canonical record but matches a registered
alias, the alias’ target record is returned. Mirrors base
dbNameToAddr behaviour, so dbpf/dbpr/dbgf, CA channel lookup,
and DB-link target resolution all work transparently for
aliases.
Use Self::get_record_no_resolve when the caller already
holds a canonical name and wants to suppress the alias path
(e.g. to detect alias collisions during builder wiring).
Sourcepub fn get_record_no_resolve(
&self,
name: &str,
) -> Option<Arc<RwLock<RecordInstance>>>
pub fn get_record_no_resolve( &self, name: &str, ) -> Option<Arc<RwLock<RecordInstance>>>
Strict variant of Self::get_record — does NOT consult the
alias table. Returns Some only when a canonical record with
that exact name exists.
Sourcepub async fn all_record_names(&self) -> Vec<String>
pub async fn all_record_names(&self) -> Vec<String>
Every record name, in database load order — the single owner of whole-database iteration order.
C parity: dbFirstRecord/dbNextRecord walk the record list of each
record type in the order dbReadDatabase appended them, so every
whole-database pass a C IOC makes — initDevSup, initDatabase,
initialProcess (PINI), dbl/dbgrep dumps — visits records in load
order. Device support is written against that contract: a dynamic device
support whose record references another record (epics-modules/opcua’s
element records require their opcuaItem record to have bound first,
linkParser.cpp:226-234) only boots if the referenced record was wired
first, which the .db guarantees by declaring it first.
The names live in a HashMap, so returning keys() made that order the
hash order: neither load order nor even stable across runs of the same
binary (RandomState reseeds per process). Booting the same database
twice could wire records in two different orders — one boot succeeding
and the next failing. Ordering here, at the one accessor every
whole-database walk already goes through, makes every such pass
deterministic and load-ordered at once.
The order key is the existing per-record load_order sequence (the
scan-index’s secondary sort key), so this ordering and the scan lists’
ordering are the same fact, not two. A record with no sequence — none
exists; add_record is the only insertion path — would sort last by
name rather than nondeterministically.
Sourcepub fn all_alias_names(&self) -> Vec<String>
pub fn all_alias_names(&self) -> Vec<String>
Get all alias names registered against existing records.
Mirrors the alias-half of base’s dbFirstRecord iteration —
dbgrep / dbglob / dbsr walk both record names and
aliases when matching a glob.
Sourcepub fn aliases_for_record(&self, canonical: &str) -> Vec<String>
pub fn aliases_for_record(&self, canonical: &str) -> Vec<String>
Return every alias that points at canonical. Sorted for
stable output; empty when the record has no aliases. Used by
dbpr to surface alias-form names so admins can see how
clients may reach the record.
Sourcepub async fn all_simple_pv_names(&self) -> Vec<String>
pub async fn all_simple_pv_names(&self) -> Vec<String>
Get all simple PV names.
Trait Implementations§
Source§impl Clone for PvDatabase
impl Clone for PvDatabase
Source§fn clone(&self) -> PvDatabase
fn clone(&self) -> PvDatabase
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more