Skip to main content

PvDatabase

Struct PvDatabase 

Source
pub struct PvDatabase { /* private fields */ }
Expand description

Database of all process variables hosted by this server.

Implementations§

Source§

impl PvDatabase

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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::getCBrunDataCBvc->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.

Source

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::propEventCBrunDataCB + 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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn put_record_field_from_ca_no_notify_already_locked( &self, record_name: &str, field: &str, value: EpicsValue, ) -> CaResult<()>

Source

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.

Source

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 = TRUE and does NOT call dbProcess. recGblFwdLink (recGbl.c:296-300) consumes RPRO when the device round trip completes and queues scanOnce, so the value this put just wrote still reaches the device, one cycle later. Calling dbProcess here 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 / in complete_async_record_inner, both the recGblFwdLink:302 analogue) and calls dbProcess.

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.

Source

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.

Source

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

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.

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”.

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.

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).

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. Each Some field is a value C wrote; a None field is a slot this link cannot report.
  • Constant linkNone, always. dbConst_lset (dbConstLink.c:234-248) leaves all five slots NULL, so the !plset->getGraphicLimits test in dbGetGraphicLimits (dbLink.c:358-359) short-circuits to S_db_noLSET and NOTHING is written. This is the case for the oracle’s field(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: a calc field(INPA,"5") serves display limits 0/0.
  • DB linkSome(meta) from the target field, via Self::db_link_metadata.
  • CA/PVA link — delegated to the registered lset (Self::external_link_metadata); C installs dbCa/pvalink’s own lset for these, not dbDb_lset.
  • Everything else (unset link, hardware link) — None. C leaves plink->lset NULL for these, so dbLink.c:358 returns S_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 slotseed before the fetchC site
get_graphic_double(0.0, 0.0)dbAccess.c:216
get_alarm_doubleNaN×4dbAccess.c:290
get_units"" (NOT the record’s EGU)dbAccess.c:378
get_precisionthe record’s own PRECcalcRecord.c:191
get_control_doublenever 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.

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).

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.

Number of staged external-link writes a later write on the same link overwrote — C’s pca->nNoWrite (dbCa.c:611-612).

Number of external-link writes the queue owner has completed.

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.

Source

pub fn get_cp_targets(&self, source_record: &str) -> Vec<CpTarget>

Get target edges to process when source_record changes (CP/CPP links).

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.

Source

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).

Source

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.

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.

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 DbCa conversion is written back, so a cache that legitimately differs from its raw text is left alone.

Returns the number of links converted.

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.

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. dbInitLink reaches dbCaAddLink for DBF_INLINK, DBF_OUTLINK and DBF_FWDLINK alike (dbLink.c:118 onwards; the dbfType check below it only sets pvlOptInpNative / pvlOptFWD). An OUT-only external link is opened at init too.
  • Every link field, not just the CP/CPP subset. Self::setup_cp_links warms 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_namestage_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

Source

pub async fn process_record(&self, name: &str) -> CaResult<()>

Process a record by name (process_local + notify). Alias-aware (epics-base PR #336).

Source

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.

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.

Source

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.

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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.

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).

Source

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.

Source

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

Source

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.

Source

pub fn lock_records<I, S>(&self, records: I) -> ManyRecordWriteGuard
where I: IntoIterator<Item = S>, S: AsRef<str>,

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

Source

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.

Source

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.

Source

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.

Source

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.

Source

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_eventpostEvent(pevent_list[event]).

Source

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

Source

pub fn new() -> Self

Source

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).

Source

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.

Source

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.

Source

pub fn mark_pini_done(&self)

Mark PINI processing complete. Wakes any non-owner scan schedulers that were waiting before running their hooks.

Source

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.

Source

pub async fn wait_for_pini(&self)

Wait until the scan owner has completed PINI processing. Returns immediately if PINI has already completed.

Source

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.

Source

pub async fn clear_search_resolver(&self)

Remove the previously installed search resolver, if any.

Source

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.

Source

pub async fn clear_existence_gate(&self)

Remove the previously installed existence gate, if any.

Source

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.

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.

Look up the lset for scheme, if any.

Snapshot of every registered scheme name. Stable order for dbpvxr dumps.

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).

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.

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 dbDbInitLinkdbCaAddLink 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.

Number of external-link opens the work owner has completed — diagnostic twin of Self::external_link_puts_completed.

Source

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).

Source

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).

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

pub fn take_after_ioc_running(&self) -> Vec<String>

Drain the post-PINI iocsh command queue. Called by IocApplication::run after PINI processing.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub async fn find_pv(&self, name: &str) -> Option<Arc<ProcessVariable>>

Look up a simple PV by name (backward-compatible).

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub async fn all_simple_pv_names(&self) -> Vec<String>

Get all simple PV names.

Trait Implementations§

Source§

impl Clone for PvDatabase

Source§

fn clone(&self) -> PvDatabase

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more