refactor(runtime): delete dead per-value host ingress; batch fan-out review fixes
This commit is contained in:
@@ -48,8 +48,11 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||
/// Children (DriverInstance/VirtualTag/etc.) are fully wired: <c>DispatchDeployment</c> drives
|
||||
/// <see cref="PushDesiredSubscriptions"/> which runs the spawn-plan via <see cref="IDriverFactory"/>,
|
||||
/// maintains the <c>FullName→NodeId</c> live-value routing map, and forwards results to the
|
||||
/// OPC UA publish actor via <c>ForwardToMux</c> (per value) and <c>ForwardBatchToMux</c> (one whole
|
||||
/// driver production burst per message — the primary path for a batch-capable driver).
|
||||
/// OPC UA publish actor via <c>ForwardBatchToMux</c> — one whole driver production burst per message,
|
||||
/// and the ONLY value ingress from driver children (<see cref="DriverInstanceActor"/> wraps even a
|
||||
/// single data change in an <c>AttributeValueBatchPublished</c>). The per-value
|
||||
/// <c>AttributeValuePublished</c> survives only as the message <see cref="FanCommsLossToDependencyMux"/>
|
||||
/// fabricates straight to the dependency mux; the host itself no longer receives it.
|
||||
/// </summary>
|
||||
public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
@@ -186,7 +189,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
/// <summary>
|
||||
/// Driver live-value routing map: <c>(DriverInstanceId, RawPath) → materialised NodeId(s) (realm-tagged)</c>.
|
||||
/// Rebuilt every apply by <see cref="PushDesiredSubscriptions"/> from the composition's <c>RawTags</c>
|
||||
/// ∪ <c>UnsReferenceVariables</c>, and resolved in <see cref="ForwardToMux"/> so a driver value
|
||||
/// ∪ <c>UnsReferenceVariables</c>, and resolved in <see cref="ForwardBatchToMux"/> so a driver value
|
||||
/// published by wire-ref RawPath fans to its raw node AND every referencing UNS node with identical
|
||||
/// value/quality/timestamp — the single-source-fan-out (no independent buffer, so no drift). A set
|
||||
/// because one driver ref can back the raw node plus N UNS references; the per-apply rebuild dedups.
|
||||
@@ -924,7 +927,6 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
Receive<DispatchDeployment>(HandleDispatchFromSteady);
|
||||
Receive<GetDiagnostics>(HandleGetDiagnostics);
|
||||
Receive<DriverInstanceActor.AttributeValuePublished>(ForwardToMux);
|
||||
Receive<DriverInstanceActor.AttributeValueBatchPublished>(ForwardBatchToMux);
|
||||
Receive<DriverInstanceActor.AttributeAlarmPublished>(ForwardNativeAlarm);
|
||||
Receive<DriverInstanceActor.ConnectivityChanged>(OnDriverConnectivityChanged);
|
||||
@@ -959,7 +961,6 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
// then Become(Applying)); completing the apply transitions back to Steady.
|
||||
Receive<FetchedForApply>(HandleFetchedForApply);
|
||||
Receive<GetDiagnostics>(HandleGetDiagnostics);
|
||||
Receive<DriverInstanceActor.AttributeValuePublished>(ForwardToMux);
|
||||
Receive<DriverInstanceActor.AttributeValueBatchPublished>(ForwardBatchToMux);
|
||||
Receive<DriverInstanceActor.AttributeAlarmPublished>(ForwardNativeAlarm);
|
||||
Receive<DriverInstanceActor.ConnectivityChanged>(OnDriverConnectivityChanged);
|
||||
@@ -977,16 +978,25 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routes one batched driver production burst — the batched analogue of
|
||||
/// <see cref="ForwardToMux"/>, and the primary value ingress for a batch-capable driver.
|
||||
/// Routes one batched driver production burst — the ONLY value ingress from a driver child, since
|
||||
/// <see cref="DriverInstanceActor"/> wraps even a single data change in an
|
||||
/// <see cref="DriverInstanceActor.AttributeValueBatchPublished"/>.
|
||||
/// <para>
|
||||
/// The routing is identical per item: each <c>FullReference</c> (the tag's RawPath) resolves
|
||||
/// through <see cref="_nodeIdByDriverRef"/> to the v3 Batch 4 single-source fan-out — the RAW
|
||||
/// NodeId AND every referencing UNS NodeId, each carrying IDENTICAL value/status/timestamp — and
|
||||
/// an unmapped ref is Debug-logged and dropped without disturbing the rest of the burst. What
|
||||
/// changes is the delivery: the whole burst costs ONE hop to the dependency mux and ONE
|
||||
/// Each item's <c>FullReference</c> (the tag's RawPath) resolves through
|
||||
/// <see cref="_nodeIdByDriverRef"/> — rebuilt every apply from the composition's <c>RawTags</c>
|
||||
/// ∪ <c>UnsReferenceVariables</c> — to the v3 Batch 4 <b>single-source fan-out</b>: the RAW
|
||||
/// NodeId (<see cref="AddressSpaceRealm.Raw"/>) AND every referencing UNS NodeId
|
||||
/// (<see cref="AddressSpaceRealm.Uns"/>), each carrying IDENTICAL value/status/timestamp, so
|
||||
/// there is no independent buffer to drift. An unmapped ref is Debug-logged and dropped without
|
||||
/// disturbing the rest of the burst.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The whole burst costs ONE hop to the dependency mux (which filters per item itself, feeding
|
||||
/// VirtualTag inputs keyed by RawPath) and ONE
|
||||
/// <see cref="ZB.MOM.WW.OtOpcUa.Runtime.OpcUa.OpcUaPublishActor.AttributeValueBatchUpdate"/> to
|
||||
/// the publish actor, instead of one message per value.
|
||||
/// the publish actor — never one message per value. Both sinks are optional: without a mux,
|
||||
/// VirtualTagActor evaluation can't fire (the dev/Mac path, no virtual tags registered);
|
||||
/// production binds both via the RuntimeActors extension.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void ForwardBatchToMux(DriverInstanceActor.AttributeValueBatchPublished msg)
|
||||
@@ -998,13 +1008,17 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
|
||||
// Indexed for-loop, not foreach: IReadOnlyList<AttributeValueItem> enumerates through an interface
|
||||
// enumerator that would allocate once per burst on the hottest routing seam in the server.
|
||||
var count = msg.Items.Count;
|
||||
List<ValueWrite>? writes = null;
|
||||
for (var idx = 0; idx < msg.Items.Count; idx++)
|
||||
for (var idx = 0; idx < count; idx++)
|
||||
{
|
||||
var item = msg.Items[idx];
|
||||
if (_nodeIdByDriverRef.TryGetValue((msg.DriverInstanceId, item.FullReference), out var nodeIds))
|
||||
{
|
||||
writes ??= new List<ValueWrite>(msg.Items.Count * 2);
|
||||
// Sized for the dominant case — every item resolving to its raw node alone, no UNS
|
||||
// reference — where this is exact and the list never grows. A burst that does fan out
|
||||
// pays at most a doubling or two, which beats over-allocating on every burst that doesn't.
|
||||
writes ??= new List<ValueWrite>(count);
|
||||
foreach (var n in nodeIds)
|
||||
writes.Add(new ValueWrite(n.NodeId, item.Value, item.StatusCode, item.TimestampUtc, n.Realm));
|
||||
}
|
||||
@@ -1020,42 +1034,10 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
_opcUaPublishActor.Tell(new ZB.MOM.WW.OtOpcUa.Runtime.OpcUa.OpcUaPublishActor.AttributeValueBatchUpdate(writes));
|
||||
}
|
||||
|
||||
private void ForwardToMux(DriverInstanceActor.AttributeValuePublished msg)
|
||||
{
|
||||
// Pass driver-published values to the dependency mux when one is wired (VirtualTag inputs,
|
||||
// keyed by FullReference). Without a mux, VirtualTagActor evaluation can't fire — that's the
|
||||
// dev/Mac path (no virtual tags registered); production binds the mux via the RuntimeActors
|
||||
// extension. KEEP this unchanged — VirtualTag inputs are still keyed by FullReference.
|
||||
_dependencyMux?.Tell(msg);
|
||||
|
||||
if (_opcUaPublishActor is null) return;
|
||||
|
||||
// Route the value to the OPC UA sink at the variable's ACTUAL NodeId. Equipment-tag variables
|
||||
// are materialised with folder-scoped NodeIds (EquipmentId/FolderPath/Name), while the driver
|
||||
// publishes keyed by its wire-ref FullName (FullReference). The _nodeIdByDriverRef map — built
|
||||
// each apply from the composition's EquipmentTags — resolves (DriverInstanceId, FullName) to
|
||||
// the folder-scoped NodeId(s) the materialiser placed the variable(s) at, so the value lands
|
||||
// instead of leaving the variable at BadWaitingForInitialData. One driver ref can back several
|
||||
// equipment variables (identical machines sharing a register), hence the fan-out.
|
||||
if (_nodeIdByDriverRef.TryGetValue((msg.DriverInstanceId, msg.FullReference), out var nodeIds))
|
||||
{
|
||||
// v3 Batch 4 single-source fan-out: one driver publish for a RawPath lands on the raw NodeId AND
|
||||
// every referencing UNS NodeId, each with its realm, carrying IDENTICAL value/status/timestamp.
|
||||
foreach (var n in nodeIds)
|
||||
_opcUaPublishActor.Tell(new ZB.MOM.WW.OtOpcUa.Runtime.OpcUa.OpcUaPublishActor.AttributeValueUpdate(
|
||||
n.NodeId, msg.Value, msg.StatusCode, msg.TimestampUtc, n.Realm));
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.Debug("DriverHost {Node}: no bound NodeId for ({Driver},{Ref}) — value dropped",
|
||||
_localNode, msg.DriverInstanceId, msg.FullReference);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Routes a native alarm transition (published by a driver child as
|
||||
/// <see cref="DriverInstanceActor.AttributeAlarmPublished"/>) to its materialised Part 9 condition
|
||||
/// node(s). The alarm path analogue of <see cref="ForwardToMux"/>: the transition's <c>ConditionId</c>
|
||||
/// node(s). The alarm path analogue of <see cref="ForwardBatchToMux"/>: the transition's <c>ConditionId</c>
|
||||
/// (the dotted alarm full-reference, which equals the authored equipment-tag FullName — NOT the bare
|
||||
/// <c>SourceNodeId</c> owning object) is resolved by the <see cref="_alarmNodeIdByDriverRef"/> map —
|
||||
/// built each apply from the alarm-bearing EquipmentTags — to the folder-scoped condition NodeId(s)
|
||||
|
||||
@@ -13,11 +13,12 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
|
||||
/// subscriber.
|
||||
///
|
||||
/// DriverHostActor forwards driver-child value publishes to this mux (one mux per driver-role
|
||||
/// node). The PRIMARY ingress is the batched
|
||||
/// node). The ONLY driver ingress is the batched
|
||||
/// <see cref="DriverInstanceActor.AttributeValueBatchPublished"/> — one envelope per driver
|
||||
/// production burst, which the mux filters per item — while the per-value
|
||||
/// production burst, which the mux filters per item. The per-value
|
||||
/// <see cref="DriverInstanceActor.AttributeValuePublished"/> receive stays for the comms-loss fan
|
||||
/// (<c>DriverHostActor.FanCommsLossToDependencyMux</c> fabricates it) and per-tag drivers. Both
|
||||
/// ALONE: <c>DriverHostActor.FanCommsLossToDependencyMux</c> fabricates that message and Tells this
|
||||
/// mux directly, never through the host's own receive. Both
|
||||
/// produce the same <see cref="VirtualTagActor.DependencyValueChanged"/> per interested subscriber.
|
||||
/// The mux is deliberately not a DPS subscriber — virtual-tag evaluation is local to each node and
|
||||
/// would over-emit if it spanned the cluster.
|
||||
@@ -116,12 +117,19 @@ public sealed class DependencyMuxActor : ReceiveActor
|
||||
/// <summary>Filters one batched driver production burst per item — the batched analogue of
|
||||
/// <see cref="OnAttributeValuePublished"/>. Items nobody registered for are dropped (the common case:
|
||||
/// only a fraction of an address space feeds virtual-tag expressions), so a burst of N values costs one
|
||||
/// message plus one DependencyValueChanged per genuinely interested (item, subscriber) pair.</summary>
|
||||
/// message plus one DependencyValueChanged per genuinely interested (item, subscriber) pair.
|
||||
/// <para>The registered set is NOT just virtual-tag inputs: <c>ContinuousHistorizationRecorder</c>
|
||||
/// registers interest in every historized ref, so on a historizing node <see cref="_byRef"/> can be
|
||||
/// large and a sizeable fraction of each burst resolves.</para></summary>
|
||||
private void OnAttributeValueBatchPublished(DriverInstanceActor.AttributeValueBatchPublished msg)
|
||||
{
|
||||
// Nobody has registered anything — skip the whole burst rather than probing an empty map per item.
|
||||
if (_byRef.Count == 0) return;
|
||||
|
||||
// Indexed for-loop, not foreach: this is the hot value ingress and an IReadOnlyList<T> enumerator
|
||||
// would allocate once per burst.
|
||||
for (var i = 0; i < msg.Items.Count; i++)
|
||||
var count = msg.Items.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var item = msg.Items[i];
|
||||
if (!_byRef.TryGetValue(item.FullReference, out var subscribers) || subscribers.Count == 0) continue;
|
||||
|
||||
Reference in New Issue
Block a user