feat(runtime): dependency mux accepts batched publishes

DependencyMuxActor now receives DriverInstanceActor.AttributeValueBatchPublished — the
primary value ingress since DriverHostActor's batched fan-out — and filters it per item,
emitting one DependencyValueChanged per genuinely interested (item, subscriber) pair with
that item's own value/timestamp/status sub-code (#519).

The per-item AttributeValuePublished receive stays: DriverHostActor's comms-loss fan
fabricates it, and per-tag drivers still take that path. Indexed for-loop over Items to
avoid an interface-enumerator allocation per burst.
This commit is contained in:
Joseph Doherty
2026-08-15 12:44:11 -04:00
parent c51568f2a5
commit d7edc709cf
2 changed files with 55 additions and 3 deletions
@@ -149,6 +149,36 @@ public sealed class DependencyMuxActorTests : RuntimeActorTestBase
subscriber.ExpectMsg<VirtualTagActor.DependencyValueChanged>().TagId.ShouldBe("ref-1");
}
/// <summary>A batched driver burst (the primary ingress from DriverHostActor since the value-pipeline
/// batching work) is filtered per item: a subscriber hears exactly one DependencyValueChanged for the
/// ref it registered, carrying that item's own value/timestamp/quality, and nothing for the rest of the
/// burst.</summary>
[Fact]
public void AttributeValueBatchPublished_is_filtered_per_item_to_interested_subscribers()
{
var mux = Sys.ActorOf(DependencyMuxActor.Props());
var sub = CreateTestProbe();
mux.Tell(new DependencyMuxActor.RegisterInterest(new[] { "tag-x" }, sub.Ref));
var tsX = new DateTime(2026, 8, 15, 9, 0, 0, DateTimeKind.Utc);
var tsY = new DateTime(2026, 8, 15, 9, 0, 1, DateTimeKind.Utc);
mux.Tell(new DriverInstanceActor.AttributeValueBatchPublished("driver-1", new[]
{
new DriverInstanceActor.AttributeValueItem("tag-y", 99, OpcUaStatus.Good, tsY),
new DriverInstanceActor.AttributeValueItem("tag-x", 10, OpcUaStatus.BadNoCommunication, tsX),
}));
var dep = sub.ExpectMsg<VirtualTagActor.DependencyValueChanged>(TimeSpan.FromSeconds(3));
dep.TagId.ShouldBe("tag-x");
dep.Value.ShouldBe(10);
dep.TimestampUtc.ShouldBe(tsX);
// #519: the item's own status sub-code rides through, not a Good/Bad bucket.
dep.StatusCode.ShouldBe(OpcUaStatus.BadNoCommunication);
// Nothing for tag-y — no subscriber registered for it.
sub.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
}
/// <summary>#478 — the mux forwards the published source quality onto DependencyValueChanged so the
/// scripted-alarm host can derive a condition's worst-of-input quality.</summary>
[Fact]