refactor(runtime): one desired-subscription payload authority (_desiredByDriver)
This commit is contained in:
@@ -487,3 +487,20 @@ the reciprocal falsification-partner xmldoc on the existing
|
||||
anticipated, each of its trip cycles now costs a full `FastTimeout` (250 ms) instead of one 50 ms
|
||||
tick, and its `AwaitCondition(InitializeCount >= 2, 5 s)` still has ample headroom. No budget change
|
||||
was needed.
|
||||
|
||||
### T2 — desired-subscription payload authority
|
||||
|
||||
Behaviour-preserving refactor, so there is no red observation to record by design — the gate is that
|
||||
**no test moves**. `--filter "FullyQualifiedName~DriverHostActor"` → **114/114 passed**, unchanged
|
||||
from the base commit.
|
||||
|
||||
⚠️ One run of that filter reported `DriverHostActorNativeAlarmAckRoutingTests.RouteNativeAlarmAck_on_non_primary_is_dropped`
|
||||
failing on an `AwaitAssert`. Re-run in isolation: **3/3 passed**; re-run of the whole 114-test filter:
|
||||
**114/114 passed**. Treated as the known #500 flake set for this assembly (~13 %/run), not a
|
||||
regression — it touches none of the code this task edits (the alarm-ack Primary gate is a different
|
||||
method entirely).
|
||||
|
||||
Deviation from the plan's literal step text (cosmetic, recorded for the reviewer): the plan's Finding-1
|
||||
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.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{
|
||||
"id": "T2",
|
||||
"subject": "Desired-subscription payload authority: _desiredByDriver + SendDesiredSubscriptions (behaviour-preserving refactor)",
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"blockedBy": []
|
||||
},
|
||||
{
|
||||
|
||||
@@ -348,6 +348,22 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
/// <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>
|
||||
/// <param name="AlarmRefs">The alarm-bearing raw tags' RawPaths that un-gate this driver's
|
||||
/// <see cref="IAlarmSource"/> feed.</param>
|
||||
private readonly record struct DesiredSubscriptionSet(
|
||||
IReadOnlyList<string> ValueRefs,
|
||||
IReadOnlyList<string> AlarmRefs);
|
||||
|
||||
/// <summary>Per-driver desired subscription sets from the most-recent apply. THE authority: the bulk
|
||||
/// push, <see cref="SpawnChild"/> and the supervision-reprovision handler all send from here, so a
|
||||
/// child obtained by ANY route carries the same set. Cleared and repopulated whole on every apply,
|
||||
/// alongside the routing maps, so renames/removals can never leave a stale entry. Empty until the
|
||||
/// first apply — a child spawned before any apply is handed nothing, which is what it should have.</summary>
|
||||
private readonly Dictionary<string, DesiredSubscriptionSet> _desiredByDriver = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Cached local <see cref="RedundancyRole"/> from the latest <see cref="RedundancyStateChanged"/>
|
||||
/// snapshot (null = unknown until the first snapshot arrives, or no local node match). The Primary
|
||||
@@ -2652,23 +2668,25 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
}
|
||||
}
|
||||
|
||||
// One authored-only push (value refs + alarm refs from the maps built above), shared by the bulk loop AND
|
||||
// the dropped-driver fallback so the two CANNOT drift: the fallback's correctness depends on sending the
|
||||
// SAME payload the bulk loop would have, so it's a shared helper (structural), not a comment-maintained
|
||||
// invariant. An EMPTY set is valid — the child's Connected handler routes it to Unsubscribe (dropping a
|
||||
// stale handle) rather than a spurious subscribe. Returns the value-ref count for the bulk-loop log total.
|
||||
int SendAuthoredOnly(IActorRef actor, string driverId)
|
||||
// Cache the per-driver payload as THE authority, in the same clear-and-repopulate pass as the routing
|
||||
// maps above so a rename/removal can never leave a stale entry behind. Keyed over the UNION of the two
|
||||
// ref maps AND the running children: a child with no authored tags must get an explicit EMPTY set (which
|
||||
// it routes to Unsubscribe), not be absent from the authority — absence and empty must not differ here.
|
||||
_desiredByDriver.Clear();
|
||||
foreach (var driverId in refsByDriver.Keys
|
||||
.Concat(alarmRefsByDriver.Keys)
|
||||
.Concat(_children.Keys)
|
||||
.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
var refs = refsByDriver.TryGetValue(driverId, out var r) ? r : Array.Empty<string>();
|
||||
var alarmRefs = alarmRefsByDriver.TryGetValue(driverId, out var ar) ? ar : Array.Empty<string>();
|
||||
actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(refs, SubscriptionPublishingInterval, alarmRefs));
|
||||
return refs.Count;
|
||||
_desiredByDriver[driverId] = new DesiredSubscriptionSet(
|
||||
refsByDriver.TryGetValue(driverId, out var r) ? r : Array.Empty<string>(),
|
||||
alarmRefsByDriver.TryGetValue(driverId, out var ar) ? ar : Array.Empty<string>());
|
||||
}
|
||||
|
||||
var total = 0;
|
||||
foreach (var (driverId, entry) in _children)
|
||||
{
|
||||
total += SendAuthoredOnly(entry.Actor, driverId);
|
||||
total += SendDesiredSubscriptions(entry.Actor, driverId);
|
||||
}
|
||||
|
||||
if (total > 0)
|
||||
@@ -2710,6 +2728,34 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send <paramref name="driverId"/>'s desired subscription payload to <paramref name="actor"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The ONE place a <see cref="DriverInstanceActor.SetDesiredSubscriptions"/> is built, so the apply's
|
||||
/// bulk push, a respawn (<see cref="SpawnChild"/>) and a supervision reprovision
|
||||
/// (<c>HandleReprovisionRequested</c>) cannot drift. That is structural rather than
|
||||
/// comment-maintained on purpose: a second sender that assembled its own payload is exactly how
|
||||
/// 03/S16 happened — the restart path got a child without the set that makes a child useful.
|
||||
/// <para>
|
||||
/// An EMPTY set is valid and MEANINGFUL — the child routes it to <c>Unsubscribe</c> (dropping a
|
||||
/// stale handle) rather than to a spurious subscribe — so a driver missing from
|
||||
/// <see cref="_desiredByDriver"/> is sent an empty set, never skipped.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="actor">The driver child to hand the set to.</param>
|
||||
/// <param name="driverId">The driver instance whose set to send.</param>
|
||||
/// <returns>The value-ref count, for the bulk loop's log total.</returns>
|
||||
private int SendDesiredSubscriptions(IActorRef actor, string driverId)
|
||||
{
|
||||
var set = _desiredByDriver.TryGetValue(driverId, out var s)
|
||||
? s
|
||||
: new DesiredSubscriptionSet(Array.Empty<string>(), Array.Empty<string>());
|
||||
actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(
|
||||
set.ValueRefs, SubscriptionPublishingInterval, set.AlarmRefs));
|
||||
return set.ValueRefs.Count;
|
||||
}
|
||||
|
||||
private void SpawnChild(DriverInstanceSpec spec)
|
||||
{
|
||||
var stub = DriverInstanceActor.ShouldStub(spec.DriverType, _localRoles);
|
||||
|
||||
Reference in New Issue
Block a user