fix(drivers): mid-connect ApplyDelta adopts config in Calculation, refuses it in Galaxy (#664)

AdoptConfigDuringInit re-runs InitializeAsync(newConfig) and acks success, but
Calculation and Galaxy ignored the JSON there: Calculation kept its old tag
set, and Galaxy connected on its old endpoint. Calculation now rebuilds its tag
table from a config with a body (sharing AdoptConfig with ReinitializeAsync and
stopping any running timer scheduler first). Galaxy, which cannot hot-swap its
options, refuses a non-equivalent one the way ReinitializeAsync does. The actor
now sends DeltaApplied once the adopted init succeeds, so a dependency
consumer's mux interest is re-registered. Unreachable from deploys since #516.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 11:05:58 -04:00
parent 1355049f50
commit 97fcf624ae
8 changed files with 234 additions and 52 deletions
@@ -221,6 +221,12 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
private readonly ILoggingAdapter _log = Context.GetLogger();
private string? _currentConfigJson;
/// <summary>#664: set when <see cref="AdoptConfigDuringInit"/> adopts a new config mid-connect; the next
/// (generation-matched) <see cref="InitializeSucceeded"/> sends <see cref="DeltaApplied"/> to the parent and
/// clears it, so a dependency consumer's mux interest is re-registered against the ADOPTED config — the
/// same post-reinit notification the Connected <see cref="HandleApplyDeltaAsync"/> path sends.</summary>
private bool _adoptedConfigPendingNotify;
/// <summary>Monotonic token tagging each <see cref="InitializeAsync"/> attempt. An init result is
/// honoured only when its generation matches the latest; an older result is from a superseded attempt
/// (e.g. an <see cref="ApplyDelta"/> adopted a new config mid-(re)connect) and is dropped. Touched only
@@ -468,6 +474,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
if (msg.Generation != _initGeneration) return;
_log.Info("DriverInstance {Id}: connected", _driverInstanceId);
Become(Connected);
NotifyAdoptedConfig();
PublishHealthSnapshot();
ResubscribeDesired();
AttachAlarmSource();
@@ -771,6 +778,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
Timers.Cancel("retry-connect");
_log.Info("DriverInstance {Id}: reconnected", _driverInstanceId);
Become(Connected);
NotifyAdoptedConfig();
PublishHealthSnapshot();
ResubscribeDesired();
AttachAlarmSource();
@@ -878,9 +886,19 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
_log.Info("DriverInstance {Id}: ApplyDelta during (re)connect — adopting new config, re-initialising now",
_driverInstanceId);
InitializeAsync(msg.DriverConfigJson);
_adoptedConfigPendingNotify = true;
Sender.Tell(new ApplyResult(true, "config adopted; reinitializing", msg.Correlation));
}
/// <summary>Send the deferred <see cref="DeltaApplied"/> for a mid-connect adoption (#664) once the driver
/// has actually initialised on the adopted config. A no-op otherwise.</summary>
private void NotifyAdoptedConfig()
{
if (!_adoptedConfigPendingNotify) return;
_adoptedConfigPendingNotify = false;
Context.Parent.Tell(new DeltaApplied(_driverInstanceId));
}
/// <summary>Adopt a new config while Connected, in place, via <see cref="IDriver.ReinitializeAsync"/>.
/// <para>#663: every driver's reinit releases its device/session state (#630), so the driver-side data and
/// native-alarm subscriptions do not survive it — but this handler used to keep both handles, and the