Files
lmxopcua/docs/IncrementalSync.md
T
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

72 lines
5.1 KiB
Markdown

# 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