test(runtime): operator restart re-subscribes the respawned child (archreview 03/S16, U6)

Seen RED against a T3-reverted build: all 4 failed, Created[1].SubscribeCount == 0
on the headline case. 20x isolation run green after the fix was restored.
This commit is contained in:
Joseph Doherty
2026-08-20 09:53:38 -04:00
parent b3a71f6655
commit d97e441979
4 changed files with 360 additions and 7 deletions
@@ -2825,12 +2825,20 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
// 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.
// ORDERING, and its one accepted cost: ApplyDriverSpecs (which spawns) runs BEFORE
// ApplyDesiredSubscriptions (which rebuilds _desiredByDriver), so on a deploy that RESPAWNS a child
// (#516 — the merged DriverConfig folds in the raw tags, so a tag-set edit changes the spec) the new
// child is handed the PREVIOUS apply's set, and nothing at all on the first deploy. That transient is
// superseded a moment later by the bulk push, on the same sender→receiver pair, so Akka's per-pair
// ordering guarantees the child CONVERGES on the new set — but it is NOT guaranteed to be inert on
// the way there: the child runs concurrently with the rest of this handler, so a driver whose
// InitializeAsync completes quickly can reach Connected and subscribe the stale set before the bulk
// push arrives, then re-subscribe. The cost is one extra subscribe/unsubscribe round-trip on a
// respawning deploy (HandleSubscribeAsync drops the prior subscription before establishing the new
// one, so it converges); a real device driver, whose initialise opens a socket, rarely wins that race.
// Deliberately not "fixed" by withholding the send: a child that never gets a set is the 03/S16
// defect, and the operator-Restart and supervision paths have no bulk push coming at all.
// 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