Mid-connect ApplyDelta: Calculation and Galaxy ignore the adopted config; no DeltaApplied re-register #664

Closed
opened 2026-10-09 11:01:24 -04:00 by dohertj2 · 1 comment
Owner

Found while fixing #663.

Defect (latent). An ApplyDelta that reaches a driver while it is Connecting/Reconnecting goes through DriverInstanceActor.AdoptConfigDuringInit, which re-runs IDriver.InitializeAsync(newConfigJson) and acks success. Since #516/#630 every driver's InitializeAsync re-parses the supplied JSON, except two:

  • CalculationDriver.InitializeAsync ignores the JSON. Calc-tag adds, removes and script edits in the delta are dropped silently, and the deploy still acks success.
  • GalaxyDriver.InitializeAsync ignores the JSON. Its ReinitializeAsync deliberately refuses a non-equivalent config (NotSupportedException, "restart the DriverInstance"), but the mid-connect path never reaches that refusal. The driver silently connects on its old gateway endpoint, client name and reconnect policy.

Separately, a mid-connect adoption never sends DeltaApplied to the host, so a dependency consumer such as Calculation keeps its OLD mux interest even once the driver adopts the new tag set.

Reachability. Not reachable from a deploy today: since #516 a DriverConfig change is stop + respawn, so ApplyChildDelta is never called.

Proposed fix.

  • Calculation: InitializeAsync adopts a config with a real body (HasConfigBody, the same convention as the other drivers): rebuild the tag table and drop compiled scripts before arming timers.
  • Galaxy: InitializeAsync runs the same equivalence check ReinitializeAsync uses, so a non-equivalent config is REFUSED loudly instead of ignored. Hot-swapping Galaxy options is out of scope by design.
  • Actor: after a mid-connect adoption's init succeeds, send DeltaApplied so the host re-registers the dependency-consumer adapter.
Found while fixing #663. **Defect (latent).** An `ApplyDelta` that reaches a driver while it is Connecting/Reconnecting goes through `DriverInstanceActor.AdoptConfigDuringInit`, which re-runs `IDriver.InitializeAsync(newConfigJson)` and acks success. Since #516/#630 every driver's `InitializeAsync` re-parses the supplied JSON, **except two**: - `CalculationDriver.InitializeAsync` ignores the JSON. Calc-tag adds, removes and script edits in the delta are dropped silently, and the deploy still acks success. - `GalaxyDriver.InitializeAsync` ignores the JSON. Its `ReinitializeAsync` deliberately refuses a non-equivalent config (`NotSupportedException`, "restart the DriverInstance"), but the mid-connect path never reaches that refusal. The driver silently connects on its old gateway endpoint, client name and reconnect policy. Separately, a mid-connect adoption never sends `DeltaApplied` to the host, so a dependency consumer such as Calculation keeps its OLD mux interest even once the driver adopts the new tag set. **Reachability.** Not reachable from a deploy today: since #516 a DriverConfig change is stop + respawn, so `ApplyChildDelta` is never called. **Proposed fix.** - **Calculation:** `InitializeAsync` adopts a config with a real body (`HasConfigBody`, the same convention as the other drivers): rebuild the tag table and drop compiled scripts before arming timers. - **Galaxy:** `InitializeAsync` runs the same equivalence check `ReinitializeAsync` uses, so a non-equivalent config is REFUSED loudly instead of ignored. Hot-swapping Galaxy options is out of scope by design. - **Actor:** after a mid-connect adoption's init succeeds, send `DeltaApplied` so the host re-registers the dependency-consumer adapter.
Author
Owner

Fixed on master in f622d0be (merging 97fcf624).

Calculation. InitializeAsync now adopts a config that has a real body. It rebuilds the tag table through the same AdoptConfig routine ReinitializeAsync uses, so the two paths cannot drift, and it disposes any running timer scheduler before arming a new one (#630 hygiene: re-initialising without a shutdown used to start a second scheduler). A "{}" or empty document keeps the constructor tags.

Galaxy. It cannot hot-swap its options, so InitializeAsync now runs the same equivalence check as ReinitializeAsync (extracted to RefuseNonEquivalentConfig). A non-equivalent config is refused with NotSupportedException instead of being ignored, so the driver stays Reconnecting and the failure is logged instead of connecting on the old endpoint. RawTags remain excluded from the equivalence check, as they already were for reinit. A normal spawn passes the factory's own JSON, which is always equivalent.

Actor. A mid-connect adoption now sends DeltaApplied once, after the init on the adopted config succeeds, so the host re-registers the dependency-consumer adapter.

Tests. Each new test fails when its production change is reverted:

  • InitializeAsync_on_a_live_driver_adopts_a_config_with_a_body (Calculation)
  • InitializeAsync_refuses_a_non_equivalent_config_and_accepts_an_equivalent_one (Galaxy)
  • A_mid_connect_adoption_notifies_DeltaApplied_once_after_the_adopted_init_succeeds (actor; the "once" half has a reconnect positive control)

Two existing Galaxy tests initialised a driver with a config that was not equivalent to its constructor options; they now use the "{}" lifecycle shape. I dropped a timer-leak test because it was vacuous: the timer callback is gated on _running, so a leaked scheduler is invisible after shutdown. That hygiene fix is untested.

Full suite on the merged master: 13219 passed, 0 failed.

The ack is still sent on adoption, so a refused Galaxy change acks success and then fails its connects. This is the existing AdoptConfigDuringInit contract, and none of this is reachable from a deploy since #516. Docs: docs/DriverLifecycle.md, CLAUDE.md. The scadaproj history entry is 0b056c6; ScadaBridge impact is none.

Fixed on master in `f622d0be` (merging `97fcf624`). **Calculation.** `InitializeAsync` now adopts a config that has a real body. It rebuilds the tag table through the same `AdoptConfig` routine `ReinitializeAsync` uses, so the two paths cannot drift, and it disposes any running timer scheduler before arming a new one (#630 hygiene: re-initialising without a shutdown used to start a second scheduler). A `"{}"` or empty document keeps the constructor tags. **Galaxy.** It cannot hot-swap its options, so `InitializeAsync` now runs the same equivalence check as `ReinitializeAsync` (extracted to `RefuseNonEquivalentConfig`). A non-equivalent config is refused with `NotSupportedException` instead of being ignored, so the driver stays Reconnecting and the failure is logged instead of connecting on the old endpoint. RawTags remain excluded from the equivalence check, as they already were for reinit. A normal spawn passes the factory's own JSON, which is always equivalent. **Actor.** A mid-connect adoption now sends `DeltaApplied` once, after the init on the adopted config succeeds, so the host re-registers the dependency-consumer adapter. **Tests.** Each new test fails when its production change is reverted: - `InitializeAsync_on_a_live_driver_adopts_a_config_with_a_body` (Calculation) - `InitializeAsync_refuses_a_non_equivalent_config_and_accepts_an_equivalent_one` (Galaxy) - `A_mid_connect_adoption_notifies_DeltaApplied_once_after_the_adopted_init_succeeds` (actor; the "once" half has a reconnect positive control) Two existing Galaxy tests initialised a driver with a config that was not equivalent to its constructor options; they now use the `"{}"` lifecycle shape. I dropped a timer-leak test because it was vacuous: the timer callback is gated on `_running`, so a leaked scheduler is invisible after shutdown. That hygiene fix is untested. Full suite on the merged master: 13219 passed, 0 failed. The ack is still sent on adoption, so a refused Galaxy change acks success and then fails its connects. This is the existing `AdoptConfigDuringInit` contract, and none of this is reachable from a deploy since #516. Docs: `docs/DriverLifecycle.md`, `CLAUDE.md`. The scadaproj history entry is `0b056c6`; ScadaBridge impact is none.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#664