Files
lmxopcua/docs/IncrementalSync.md
Joseph Doherty 1ea1da8236 refactor(runtime): delete the dead in-place ApplyDelta seam; config change = respawn only (#665)
Since #516 a DriverConfig change is stop + respawn, so DriverSpawnPlan.ToApplyDelta
was always empty and the in-place path had no producer. #663 and #664 were both
latent bugs in it. This deletes it:
- host: ApplyChildDelta, _pendingDelta, HandleApplyResult and HandleDeltaApplied
- plan: DriverSpawnPlan.ToApplyDelta
- actor: the ApplyDelta, ApplyResult and DeltaApplied messages, the Connected
  reinit handler and the mid-connect AdoptConfigDuringInit
- tests: the actor tests that drove the seam

IDriver.ReinitializeAsync and each driver's implementation and unit tests stay.
Comments and docs that cited AdoptConfigDuringInit as the reason a driver must
release prior state at init now cite the IDriver contract and overlapping
retries, which still apply.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-09 11:23:16 -04:00

5.1 KiB

Incremental Sync

How a change reaches the running server's address space in v3. There is exactly one path that changes the served address space — the deploy artifact — and one advisory signal (IRediscoverable) that never changes it.

The deploy-artifact path

  1. Compose. ConfigComposer (src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/ConfigComposer.cs) snapshots the whole Config DB into one gzip-compressed JSON blob (Deployment.ArtifactBlob) whose RevisionHash is the SHA-256 of those exact bytes. A deployment is fleet-wide; each node self-scopes the artifact (DeploymentArtifact.ResolveClusterScope).
  2. Dispatch. ConfigPublishCoordinator (.../ControlPlane/Coordinators/) sends each enabled ClusterNode a payload-free dispatch and waits for acks until the apply deadline. A node obtains the bytes either straight from central SQL (ConfigSource:Mode=Direct) or over gRPC with a SHA-256 check (FetchAndCache) — see docs/Configuration.md §ConfigSource.
  3. Apply. DriverHostActor (src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/) opens the artifact and reconciles in two halves:
    • Drivers — DriverSpawnPlanner.Compute yields ToStop / ToSpawn. A changed driver config (DriverConfig, DriverType or ResilienceConfig) is a stop + respawn (#516) — there is no in-place path (#665 deleted the unreachable ApplyDelta seam); a removed driver is stopped. An unreadable or empty blob is "no answer": the apply fails and the running children are left alone (never an empty configuration).
    • Address space — AddressSpaceComposer builds an AddressSpaceComposition from the artifact; AddressSpacePlan diffs it against the last applied composition; AddressSpaceChangeClassifier picks the delta class; AddressSpaceApplier (src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/) mutates OtOpcUaNodeManager through the sink.
  4. Classes (AddressSpaceChangeKind): Empty, AttributeOnly, PureAdd, PureRemove, AddRemoveMix — all applied surgically, so unrelated MonitoredItems survive — and Rebuild, the default-closed fallback for anything the classifier does not whitelist. Any surgical false/throw also ratchets to a safe rebuild, and a degraded non-rebuild apply does not advance the last-applied composition (the next deploy re-diffs and re-ensures). The whitelist, the UNS-mirror repair rule and the add/remove announcements are described in the "surgical deploys" paragraph of CLAUDE.md and docs/plans/2026-08-15-surgical-deploys.md.

Virtual tags and scripted alarms ride the same artifact and the same plan; a script edit is just another changed record in the composition (see docs/VirtualTags.md, docs/ScriptedAlarms.md).

Driver-backend rediscovery is advisory

Drivers whose backend signals a tag-set change implement IRediscoverable (src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs; OnRediscoveryNeeded carries a Reason and an optional ScopeHint). Raisers today: Galaxy (DeployWatcher, new time_of_last_deploy), TwinCAT (ADS symbol-version-changed), MQTT/Sparkplug and MTConnect. AbCip, AbLegacy, FOCAS, Modbus, S7 and the rest raise nothing.

DriverInstanceActor is the only production consumer: it subscribes in PreStart, detaches in PostStop, and folds the raise into the driver-health snapshot (DriverHealthChanged.RediscoveryNeededUtc / .RediscoveryReason), which the AdminUI /hosts page shows as a re-browse chip. It never rebuilds or edits the served address space — v3 raw tags are authored explicitly in /raw, so an operator re-browses the device and commits, and the result reaches the server as an ordinary deployment. ScopeHint is not read by the actor. (The older runtime injection path was removed, #507 closed as superseded.)

Not part of the v3 path

  • sp_PublishGeneration / sp_ComputeGenerationDiff are stubs that RAISERROR in the v3 baseline migration; there is no generation-publish flow and no DraftRevisionToken.
  • GenericDriverNodeManager (Core/OpcUa/) is retained scaffolding with no production caller; the diff / teardown / subscription-replay "rebuild flow" described by earlier versions of this page does not exist in src/.

Key source files

  • src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/ConfigComposer.cs — artifact composition + hash
  • src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs — dispatch + ack set
  • src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs — apply + driver reconcile
  • src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs — node-side artifact parsing
  • src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs, AddressSpacePlan.cs, AddressSpaceChangeClassifier.cs, AddressSpaceApplier.cs — compose, diff, classify, apply
  • src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs — IRediscoverable consumer
  • src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/ISurgicalAddressSpaceSink.cs — in-place update seam