Audit of docs/ against src/ surfaced shipped features without current-reference coverage (FOCAS CLI, Core.Scripting+VirtualTags, Core.ScriptedAlarms, Core.AlarmHistorian), an out-of-date driver count + capability matrix, ADR-002's virtual-tag dispatch not reflected in data-path docs, broken cross-references, and OpcUaServerReqs declaring OPC-020..022 that were never scoped. This commit closes all of those so operators + integrators can stay inside docs/ without falling back to v2/implementation/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.4 KiB
Incremental Sync
Two distinct change-detection paths feed the running server: driver-backend rediscovery (Galaxy's time_of_last_deploy, TwinCAT's symbol-version-changed, OPC UA Client's upstream namespace change) and generation-level config publishes from the Admin UI. Both flow into re-runs of ITagDiscovery.DiscoverAsync, but they originate differently.
Driver-backend rediscovery — IRediscoverable
Drivers whose backend has a native change signal implement IRediscoverable (src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs):
public interface IRediscoverable
{
event EventHandler<RediscoveryEventArgs>? OnRediscoveryNeeded;
}
public sealed record RediscoveryEventArgs(string Reason, string? ScopeHint);
The driver fires the event with a reason string (for the diagnostic log) and an optional scope hint — a non-null hint lets Core scope the rebuild surgically to that subtree; null means "the whole address space may have changed".
Drivers that implement the capability today:
- Galaxy — polls
galaxy.time_of_last_deployin the Galaxy repository DB and fires on change. This is Galaxy-internal change detection, not the platform-wide mechanism. - TwinCAT — observes ADS symbol-version-changed notifications (
0x0702). - OPC UA Client — subscribes to the upstream server's
Server/NamespaceArraychange notifications.
Static drivers (Modbus, S7, AB CIP, AB Legacy, FOCAS) do not implement IRediscoverable — their tags only change when a new generation is published from the Config DB. Core sees absence of the interface and skips change-detection wiring for those drivers (decision #54).
Config-DB generation publishes
Tag-set changes authored in the Admin UI (UNS edits, CSV imports, driver-config edits) accumulate in a draft generation and commit via sp_PublishGeneration. The delta between the currently-published generation and the proposed next one is computed by sp_ComputeGenerationDiff, which drives:
- The DiffViewer in Admin (
src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffViewer.razor) so operators can preview what will change before clicking Publish. - The 409-on-stale-draft flow (decision #161) — a UNS drag-reorder preview carries a
DraftRevisionTokenso Confirm returns409 Conflict / refresh-requiredif the draft advanced between preview and commit.
After publish, the server's generation applier invokes IDriver.ReinitializeAsync(driverConfigJson, ct) on every driver whose DriverInstance.DriverConfig row changed in the new generation. Reinitialize is the in-process recovery path for Tier A/B drivers; if it fails the driver is marked DriverState.Faulted and its nodes go Bad quality — but the server process stays running. See docs/v2/driver-stability.md.
Drivers whose discovery depends on Config DB state (Modbus register maps, S7 DBs, AB CIP tag lists) re-run their discovery inside ReinitializeAsync; Core then diffs the new node set against the current address space.
Rebuild flow
When a rediscovery is triggered (by either source), GenericDriverNodeManager re-runs ITagDiscovery.DiscoverAsync into the same CapturingBuilder it used at first build. The new node set is diffed against the current:
- Diff — full-name comparison of the new
DriverAttributeInfoset against the existing_variablesByFullRefmap. Added / removed / modified references are partitioned. - Snapshot subscriptions — before teardown, Core captures the current monitored-item ref-counts for every affected reference so subscriptions can be replayed after rebuild.
- Teardown — removed / modified variable nodes are deleted via
CustomNodeManager2.DeleteNode. Driver-side subscriptions for those references are unwound viaISubscribable.UnsubscribeAsync. - Rebuild — added / modified references get fresh
BaseDataVariableStatenodes via the standardIAddressSpaceBuilder.Variable(...)path. Alarm-flagged references re-register theirIAlarmConditionSinkthroughCapturingBuilder. - Restore subscriptions — for every captured reference that still exists after rebuild, Core re-opens the driver subscription and restores the original ref-count.
Exceptions during teardown are swallowed per decision #12 — a driver throw must not leave the node tree half-deleted.
Scope hint
When RediscoveryEventArgs.ScopeHint is non-null (e.g. a folder path), Core restricts the diff to that subtree. This matters for Galaxy Platform-scoped deployments where a time_of_last_deploy advance may only affect one platform's subtree, and for OPC UA Client where an upstream change may be localized. Null scope falls back to a full-tree diff.
Virtual tags in the rebuild
Per ADR-002, virtual (scripted) tags live in the same address space as driver tags and flow through the same rebuild. EquipmentNodeWalker (src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs) emits virtual-tag children alongside driver-tag children with DriverAttributeInfo.Source = NodeSourceKind.Virtual, and DriverNodeManager registers each variable's source in _sourceByFullRef so the dispatch branches correctly after rebuild. Virtual-tag script changes published from the Admin UI land through the same generation-publish path — the VirtualTagEngine recompiles its script bundle when its config row changes and DriverNodeManager re-registers any added/removed virtual variables through the standard diff path. Subscription restoration after rebuild runs through each source's ISubscribable — either the driver's or VirtualTagSource — without special-casing.
Active subscriptions survive rebuild
Subscriptions for unchanged references stay live across rebuilds — their ref-count map is not disturbed. Clients monitoring a stable tag never see a data-change gap during a deploy, only clients monitoring a tag that was genuinely removed see the subscription drop.
Key source files
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs— backend-change capabilitysrc/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs— discovery orchestrationsrc/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs—ReinitializeAsynccontractsrc/ZB.MOM.WW.OtOpcUa.Admin/Services/GenerationService.cs— publish-flow driverdocs/v2/config-db-schema.md—sp_PublishGeneration+sp_ComputeGenerationDiffdocs/v2/admin-ui.md— DiffViewer + draft-revision-token flow