Files
lmxopcua/docs/IncrementalSync.md
T
Joseph Doherty e08855fb9d
v2-ci / build (push) Successful in 4m52s
v2-ci / unit-tests (push) Failing after 15m58s
docs: source-verified deferment register + correct 17 drifted docs
Adds deferment.md — a source-verified inventory of new-driver status, all 17
open issues, in-code deferrals, open live gates and plan bookkeeping. Every
claim was checked against src/ and git, not against documentation.

Headline finding: three subsystems are authored, persisted and shipped but
never executed —
  * Node ACLs: IPermissionEvaluator/TriePermissionEvaluator have zero
    production consumers and OtOpcUaNodeManager never references them, yet
    ClusterAcls.razor authors NodeAcl rows and ConfigComposer.cs:51 ships them
    in every artifact. An authored deny rule has no effect.
  * IRediscoverable / IHostConnectivityProbe raise into the void (#518/#507);
    nothing ever writes a DriverHostStatus row.
  * DriverTypeRegistry is vestigial, so no factory passes a tier and every
    driver runs Tier A with the Tier-C protections dormant.

Also records the Calculation driver as picker-visible but unauthorable
(DriverConfigModal has no case) — the "registered but unauthorable" class
recurring after the Sql picker defect — and notes that no parity test guards
DriverConfigModal/DeviceModal, which is why it survived review.

Documentation corrections (source-verified):
  * ReadWriteOperations.md claimed "a denied read never hits the driver" via
    four types that do not exist in src/. Bannered + struck through; a security
    review reading that page would have concluded a per-node ACL gate exists.
  * CLAUDE.md: the Change Detection sentence was false (DriverHost consumes
    nothing); the mesh Phase 4 and Phase 5 live gates and the auto-down 1-vs-1
    gate had all PASSED; the ScriptedAlarmState table was already dropped.
  * driver-expansion tracking: Modbus RTU and SQL poll are merged, not
    pending — its command table would have made someone rebuild two shipped
    drivers in a fresh worktree.
  * drivers/README.md: dead DriverTypeRegistry paragraph, retired
    SystemPlatform namespace kind, missing Sql + Calculation rows, missing
    Modbus RTU-over-TCP transport.
  * TwinCAT.md/Galaxy.md promised an address-space rebuild that never happens.
  * Historian.md gained the #491 unproven-value-capture pointer.
  * IncrementalSync.md and AddressSpace.md bannered as wholesale v2-era (the
    rewrite is recorded as still owed, not done here); four v2 status docs
    bannered as historical; three wrong-name-for-a-live-type fixes.

Left deliberately untouched: the secrets NoOpSecretReplicator line, which
makes a security claim and needs the real behaviour identified rather than a
rename.
2026-07-27 17:26:20 -04:00

8.3 KiB

Incremental Sync

⚠️ Accuracy warning (audited 2026-07-27) — most of this page describes machinery that is retired or was never wired. Read it as v2-era design history, not as current behaviour.

  • "Rebuild flow" (below) is not a production path. It is built on GenericDriverNodeManager, which is Core test scaffolding with zero production references (GenericDriverNodeManager.cs:71). Its _variablesByFullRef / _sourceByFullRef fields do not exist in src/.
  • Driver-backend rediscovery has no consumer. Nothing in src/Server/ or src/Core/ subscribes to IRediscoverable.OnRediscoveryNeeded; DriverHostActor.HandleDiscoveredNodes (DriverHostActor.cs:986-1002) short-circuits unconditionally. The ScopeHint is read by nothing. Drivers that raise it today: Galaxy, TwinCAT, MQTT/Sparkplug, MTConnect (#518, #507).
  • The generation-publish path is retired. sp_PublishGeneration and sp_ComputeGenerationDiff are stubs that RAISERROR (20260715230637_V3Initial.StoredProcedures.cs:194); DraftRevisionToken does not exist.

The one live rebuild path in v3 is the deploy artifact: ConfigComposerDeployment.ArtifactBlobDriverHostActor apply → AddressSpaceComposer / AddressSpaceApplier diff-apply.

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