fix(runtime): every spawn hands the child its desired subscriptions (archreview 03/S16)

This commit is contained in:
Joseph Doherty
2026-08-20 09:36:52 -04:00
parent bdc81a1372
commit daaeb854d6
3 changed files with 33 additions and 10 deletions
@@ -504,3 +504,20 @@ Deviation from the plan's literal step text (cosmetic, recorded for the reviewer
step 1 says `_desiredByDriver` *replaces* `_lastComposition` at `:347-349` while T3 step 2 says T3
deletes `_lastComposition`. Keeping T2 compilable means the two fields coexist for one commit;
`_lastComposition` and its assignment are deleted in T3 as T3's own step text specifies.
### T3 — `SpawnChild` always sends the desired set; `_lastComposition` deleted
The falsification for this production slice is **T5**, written and observed red afterwards (below) —
T3 lands the fix first because the plan sequences it that way and the ordering guard needs the whole
25-suite regression before any new test is trusted on top of it.
**HIGH-RISK regression gate (every child's spawn-time state changes, deploy path included):** the
FULL `Runtime.Tests` project, not just the `DriverHostActor` filter —
**639 passed, 3 skipped, 0 failed, 55 s**. The 3 skips are pre-existing (`DeploymentArtifact*` parity
tests, skipped on the base commit too).
`_lastComposition` is gone: `grep -c _lastComposition DriverHostActor.cs` → **0**. Its xmldoc had also
drifted (it claimed the field was set at the end of `PushDesiredSubscriptions`; the assignment was in
`ApplyDesiredSubscriptions`) — deleted with the field rather than corrected. The stale
"dropped-driver fallback" comment on the old `SendAuthoredOnly` local function was rewritten as the
`SendDesiredSubscriptions` remarks naming the three real consumers.
@@ -17,7 +17,7 @@
{
"id": "T3",
"subject": "SpawnChild always sends the desired set; delete the write-only _lastComposition (03/S16)",
"status": "pending",
"status": "done",
"blockedBy": [
"T2"
]
@@ -344,10 +344,6 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
/// the config (#516). Bounded by the number of driver children with a delta in flight.</summary>
private readonly Dictionary<CorrelationId, DriverInstanceSpec> _pendingDelta = new();
/// <summary>The composition from the most-recent apply (set at the END of
/// <see cref="PushDesiredSubscriptions"/>). Null until the first apply.</summary>
private AddressSpaceComposition? _lastComposition;
/// <summary>One driver child's desired subscription payload — exactly the
/// <see cref="DriverInstanceActor.SetDesiredSubscriptions"/> the apply's bulk push sent it.</summary>
/// <param name="ValueRefs">The non-alarm raw tags' RawPaths this driver should keep subscribed.</param>
@@ -2721,11 +2717,6 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
_log.Info("DriverHost {Node}: applied {Count} Equipment ScriptedAlarm(s) to the ScriptedAlarm host",
_localNode, composition.EquipmentScriptedAlarms.Count);
}
// Cache the applied composition LAST. Set here (not in HandleArtifactForApply) so both the fresh-apply and
// bootstrap-restore paths — which both route through this method — leave a current composition.
_lastComposition = composition;
}
/// <summary>
@@ -2827,6 +2818,21 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
_children[spec.DriverInstanceId] = new ChildEntry(child, spec, stub);
// EVERY spawn hands the child its desired set — deploy reconcile, operator Restart (03/S16), and the
// supervision reprovision. Sending it HERE rather than at each call site is the point: a child that
// exists without its subscription set is a dead driver, so the two facts are established together.
// Before this, the only sender was the apply's bulk push, so an operator's Restart produced a child
// that connected and then read nothing until the next deployment — the #488 periodic reconcile cannot
// help, because it early-returns on an empty desired set.
//
// ORDERING, deliberately: ApplyDriverSpecs (which spawns) runs BEFORE ApplyDesiredSubscriptions (which
// rebuilds _desiredByDriver), so a newly-spawned child is handed the PREVIOUS apply's set — or nothing,
// on the first deploy. That transient is superseded milliseconds later by the bulk push on the same
// sender→receiver pair (Akka guarantees per-pair ordering), and the child is still Connecting, where
// SetDesiredSubscriptions only STORES; the subscribe happens at Connected entry. So the stale set is
// never acted on. Sent after InitializeRequested above, which keeps init-before-subscribe ordering.
SendDesiredSubscriptions(child, spec.DriverInstanceId);
// v3 (WP7): if the (real, non-stub) driver consumes other tags' live values, spawn a mux-adapter
// child that registers its dependency refs on the per-node dependency mux and forwards changes
// into the driver. Requires a wired mux (dev/None deployments have none — then calc inputs simply