Files
lmxopcua/docs/IncrementalSync.md
T
Joseph Doherty f5552c23d4 docs(audit): ScriptedAlarms.md — accuracy pass
CODE-REALITY (file:line evidence)
- Definition section: removed reference to non-existent
  Phase7EngineComposer.ProjectScriptedAlarms; Phase7Composer is a pure
  data composer (entities → Phase7CompositionResult)
  (src/Server/.../OpcUaServer/Phase7Composer.cs:82-183)
- AlarmSeverity: removed "Phase7EngineComposer.MapSeverity bands it" —
  no such class exists; clarified that AlarmSeverity is defined in
  Core.Abstractions/IAlarmSource.cs not in AlarmTypes.cs
  (src/Core/.../Core.Abstractions/IAlarmSource.cs:87)
- State persistence: replaced "Stream E wires..." planning language with
  actual production class EfAlarmActorStateStore
  (src/Server/.../Runtime/ScriptedAlarms/EfAlarmActorStateStore.cs)
- Composition section: replaced Phase7EngineComposer / Phase7ComposedSources
  references (non-existent) with the actual v2 actor-system composition
  path (ScriptedAlarmEngine + ScriptedAlarmActor + driver-role host startup)
- Key source files: AlarmTypes.cs annotation corrected (adds ShelvingKind,
  names all four state enums, notes AlarmSeverity lives in Core.Abstractions)
- Key source files: Phase7Composer.cs annotation corrected to "pure data
  composer"
- Key source files: ScriptedAlarmActor.cs annotation corrected to describe
  AlarmTransitionEvent + DPS alerts topic (not "OPC UA variable reads")
- Key source files: added EfAlarmActorStateStore as the production
  IAlarmActorStateStore implementation

STALE-STATUS
- "Stream E wires the production implementation" — removed; production
  implementation ships and is named EfAlarmActorStateStore
2026-06-03 15:44:11 -04:00

6.9 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) 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/Core/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:

  • GalaxyDeployWatcher (src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DeployWatcher.cs) subscribes to the mxaccessgw gRPC stream (IGalaxyDeployWatchSource.WatchAsync) and fires on a new time_of_last_deploy value. The gateway polls the Galaxy repository DB internally; the driver side is event-driven.
  • TwinCAT — observes ADS symbol-version-changed notifications (ADS error DeviceSymbolVersionInvalid, decimal 1809 / 0x0711). Note: legacy Beckhoff documentation sometimes cites 0x0702 (DeviceInvalidGroup) — that is a transcription error; the correct code is 0x0711 per TwinCATStatusMapper.AdsSymbolVersionChanged (src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATStatusMapper.cs:35).

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 publish-preview surface in the Admin UI (src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Deployments.razor, backed by AdminOperationsClient) 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. 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/Core/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/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs — backend-change capability
  • src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs — discovery orchestration
  • src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.csReinitializeAsync contract
  • src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs — publish-flow driver
  • src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs — cluster singleton invoked by the Admin UI's AdminOperationsClient
  • docs/v2/config-db-schema.mdsp_PublishGeneration + sp_ComputeGenerationDiff
  • docs/v2/admin-ui.md — DiffViewer + draft-revision-token flow