fix(driver-calc): rebuild calc tag state on ReinitializeAsync + re-register deps post-apply (review HIGH)
A calc-tag add/remove/edit or a script edit changes the merged DriverConfig, which DriverSpawnPlanner routes to an in-place ApplyDelta (not a respawn), i.e. CalculationDriver.ReinitializeAsync. The old ReinitializeAsync ignored the new config: the tag table + dependency-ref set were built once at construction and never rebuilt, so after a redeploy added tags never computed, removed tags kept publishing, and edited scripts served stale results (deploy reported success). Part 1 (HIGH): extract RebuildTagTable(config) — shared by the ctor and reinit so they cannot drift — and have ReinitializeAsync re-bind the new config (ParseOptions) and rebuild _tagsByRawPath/_changeDependents/_dependencyRefs/ _stateByRawPath under _lock, preserving last-known state for surviving tags, dropping removed tags, and starting new tags fresh. _dependencyRefs now reflects the new authored tags. Corrected the now-false ReinitializeAsync comment. Part 2 (MEDIUM ordering hazard): the host's ReRegister was Tell-ed synchronously alongside the ApplyDelta Tell, so the adapter re-read DependencyRefs before the async ReinitializeAsync rebuilt them (stale set, no self-correction). Now DriverInstanceActor sends DeltaApplied(driverInstanceId) to DriverHostActor AFTER ReinitializeAsync completes, and HandleDeltaApplied re-registers only that driver's adapter — sequenced after the rebuild. Removed the inline loop. Test: added a redeploy test to CalculationDependencyFlowTests exercising the real DependencyMuxActor + adapter + driver — a new tag reading a NEW dep flows + computes, a removed tag stops publishing, an edited script serves the new result, and DependencyRefs re-registers the new set. Fails on pre-fix code (DependencyRefs stays stale ["src/a"]), passes after the fix. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
@@ -559,6 +559,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
Receive<DriverInstanceActor.AttributeValuePublished>(ForwardToMux);
|
||||
Receive<DriverInstanceActor.AttributeAlarmPublished>(ForwardNativeAlarm);
|
||||
Receive<DriverInstanceActor.DiscoveredNodesReady>(HandleDiscoveredNodes);
|
||||
Receive<DriverInstanceActor.DeltaApplied>(HandleDeltaApplied);
|
||||
Receive<RestartDriver>(HandleRestartDriver);
|
||||
Receive<ReconnectDriver>(HandleReconnectDriver);
|
||||
Receive<RouteNodeWrite>(HandleRouteNodeWrite);
|
||||
@@ -588,6 +589,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
Receive<DriverInstanceActor.AttributeValuePublished>(ForwardToMux);
|
||||
Receive<DriverInstanceActor.AttributeAlarmPublished>(ForwardNativeAlarm);
|
||||
Receive<DriverInstanceActor.DiscoveredNodesReady>(HandleDiscoveredNodes);
|
||||
Receive<DriverInstanceActor.DeltaApplied>(HandleDeltaApplied);
|
||||
Receive<RestartDriver>(HandleRestartDriver);
|
||||
Receive<ReconnectDriver>(HandleReconnectDriver);
|
||||
Receive<RouteNodeWrite>(HandleRouteNodeWrite);
|
||||
@@ -1262,6 +1264,9 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
// applied yet, so the equipment can't be resolved). Drop it — the re-discovery loop re-sends it
|
||||
// and the post-recovery re-apply self-heals it once an apply runs (matches the no-op drops above).
|
||||
Receive<DriverInstanceActor.DiscoveredNodesReady>(_ => { });
|
||||
// A late DeltaApplied (an apply completed just before the DB went Stale) — re-register the driver's
|
||||
// mux adapter anyway; it simply re-reads the driver's current refs (harmless, no DB access).
|
||||
Receive<DriverInstanceActor.DeltaApplied>(HandleDeltaApplied);
|
||||
Receive<SubscribeAck>(_ => { /* PubSub ack */ });
|
||||
Timers.StartPeriodicTimer("retry-db", RetryConfigDbConnection.Instance, ReconnectInterval);
|
||||
}
|
||||
@@ -1381,11 +1386,13 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
foreach (var spec in plan.ToApplyDelta) ApplyChildDelta(spec);
|
||||
foreach (var spec in plan.ToSpawn) SpawnChild(spec);
|
||||
|
||||
// v3 (WP7): re-register every dependency-consumer adapter's interest on each apply — a driver's
|
||||
// declared dependency refs change when its tags/scripts change. Newly-spawned adapters already
|
||||
// registered in PreStart; this refreshes the ones an ApplyDelta kept in place. A Tell (no blocking).
|
||||
foreach (var adapter in _depConsumerAdapters.Values)
|
||||
adapter.Tell(new ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags.DependencyConsumerMuxAdapter.ReRegister());
|
||||
// v3 (WP7): a dependency-consumer driver's declared dependency refs change when its tags/scripts are
|
||||
// edited — the adapter must re-register the NEW set on the mux. Newly-spawned adapters register in
|
||||
// PreStart; an ApplyDelta'd driver's adapter is re-registered when the child reports it finished
|
||||
// reinitialising (DriverInstanceActor.DeltaApplied → HandleDeltaApplied). That notification is
|
||||
// sequenced AFTER ReinitializeAsync rebuilds DependencyRefs — a synchronous re-register HERE would
|
||||
// re-read the STALE ref set (ApplyDelta runs asynchronously on the child), so it is deliberately NOT
|
||||
// done inline.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1786,6 +1793,21 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
_log.Debug("DriverHost {Node}: ApplyDelta queued for {Id}", _localNode, spec.DriverInstanceId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A driver child finished applying an in-place <see cref="DriverInstanceActor.ApplyDelta"/> (its
|
||||
/// <see cref="IDriver.ReinitializeAsync"/> completed). Re-register THAT driver's dependency-consumer
|
||||
/// mux adapter so a changed dependency-ref set (calc tags/scripts edited during the redeploy) is
|
||||
/// adopted on the mux. This runs AFTER reinit — closing the ordering hazard where a synchronous
|
||||
/// re-register at ApplyDelta dispatch would re-read the driver's stale
|
||||
/// <see cref="IDependencyConsumer.DependencyRefs"/>. A Tell (no blocking); a no-op for a driver with no
|
||||
/// adapter (not an <see cref="IDependencyConsumer"/>, or no mux wired on this node).
|
||||
/// </summary>
|
||||
private void HandleDeltaApplied(DriverInstanceActor.DeltaApplied msg)
|
||||
{
|
||||
if (_depConsumerAdapters.TryGetValue(msg.DriverInstanceId, out var adapter))
|
||||
adapter.Tell(new ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags.DependencyConsumerMuxAdapter.ReRegister());
|
||||
}
|
||||
|
||||
private void StopChild(string driverInstanceId)
|
||||
{
|
||||
if (!_children.TryGetValue(driverInstanceId, out var entry)) return;
|
||||
|
||||
@@ -48,6 +48,16 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
public sealed record DisconnectObserved(string Reason);
|
||||
public sealed record ApplyDelta(string DriverConfigJson, CorrelationId Correlation);
|
||||
public sealed record ApplyResult(bool Success, string? Reason, CorrelationId Correlation);
|
||||
/// <summary>
|
||||
/// Sent to the parent (<see cref="DriverHostActor"/>) AFTER an <see cref="ApplyDelta"/> has fully
|
||||
/// re-initialised the driver (i.e. <see cref="IDriver.ReinitializeAsync"/> completed). The host uses
|
||||
/// it to re-register this driver's dependency-consumer mux adapter: a driver may rebuild its declared
|
||||
/// <see cref="IDependencyConsumer.DependencyRefs"/> during reinit (the Calculation driver re-binds its
|
||||
/// calc tags), so the re-register MUST happen after reinit — NOT synchronously alongside the ApplyDelta
|
||||
/// dispatch, which would re-read the STALE ref set. Emitted only on a successful apply.
|
||||
/// </summary>
|
||||
/// <param name="DriverInstanceId">The instance whose adapter should be re-registered.</param>
|
||||
public sealed record DeltaApplied(string DriverInstanceId);
|
||||
public sealed record WriteAttribute(string TagId, object Value);
|
||||
public sealed record WriteAttributeResult(bool Success, string? Reason);
|
||||
/// <summary>
|
||||
@@ -566,6 +576,12 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
await _driver.ReinitializeAsync(msg.DriverConfigJson, CancellationToken.None);
|
||||
_currentConfigJson = msg.DriverConfigJson;
|
||||
// The driver may have rebuilt its dependency-consumer interest during reinit (the Calculation
|
||||
// driver re-binds its calc tags + dependency refs). Notify the parent so it re-registers this
|
||||
// driver's mux adapter AFTER reinit completed — a synchronous re-register at ApplyDelta-dispatch
|
||||
// time would re-read the driver's STALE DependencyRefs. Parent Tell (fire-and-forget); the
|
||||
// continuation resumes on the actor context (no ConfigureAwait) so Context.Parent is live.
|
||||
Context.Parent.Tell(new DeltaApplied(_driverInstanceId));
|
||||
replyTo.Tell(new ApplyResult(true, null, msg.Correlation));
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Reference in New Issue
Block a user