Files
lmxopcua/docs/IncrementalSync.md
Joseph Doherty 21e0fdd4cd Docs audit — fill gaps so the top-level docs/ reference matches shipped code
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>
2026-04-23 09:42:42 -04:00

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_deploy in 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/NamespaceArray change 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 DraftRevisionToken so Confirm returns 409 Conflict / refresh-required if 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:

  1. Diff — full-name comparison of the new DriverAttributeInfo set against the existing _variablesByFullRef map. Added / removed / modified references are partitioned.
  2. Snapshot subscriptions — before teardown, Core captures the current monitored-item ref-counts for every affected reference so subscriptions can be replayed after rebuild.
  3. Teardown — removed / modified variable nodes are deleted via CustomNodeManager2.DeleteNode. Driver-side subscriptions for those references are unwound via ISubscribable.UnsubscribeAsync.
  4. Rebuild — added / modified references get fresh BaseDataVariableState nodes via the standard IAddressSpaceBuilder.Variable(...) path. Alarm-flagged references re-register their IAlarmConditionSink through CapturingBuilder.
  5. 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 capability
  • src/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs — discovery orchestration
  • src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.csReinitializeAsync contract
  • src/ZB.MOM.WW.OtOpcUa.Admin/Services/GenerationService.cs — publish-flow driver
  • docs/v2/config-db-schema.mdsp_PublishGeneration + sp_ComputeGenerationDiff
  • docs/v2/admin-ui.md — DiffViewer + draft-revision-token flow