feat(runtime): publish actor routes status-only batches to the sink

This commit is contained in:
Joseph Doherty
2026-08-20 10:03:25 -04:00
parent ec5b48bfcd
commit 04019e9414
4 changed files with 247 additions and 1 deletions
@@ -56,6 +56,15 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
/// their realm each (<see cref="ValueWrite"/>); status codes ride verbatim (#519).</summary>
public sealed record AttributeValueBatchUpdate(IReadOnlyList<ValueWrite> Items);
/// <summary>Status-only batch: every named node keeps its CURRENT value and only its status +
/// timestamp change (archreview 03/S15). The comms-loss fan-out sends one of these covering every
/// NodeId a disconnected driver owns — the raw node and each referencing UNS node — so a client sees
/// <see cref="OpcUaStatus.BadNoCommunication"/> over the last known value instead of a stale Good.
/// <para>Deliberately a separate message from <see cref="AttributeValueBatchUpdate"/> rather than a
/// flag on it: the host holds no value cache (v3's single-source design), so it has nothing to put in
/// a <see cref="ValueWrite"/>, and only the node knows what value to retain.</para></summary>
public sealed record AttributeStatusBatchUpdate(IReadOnlyList<StatusWrite> Items);
/// <summary>Carries the full Part 9 condition state for a scripted alarm to the sink. The
/// <paramref name="State"/> snapshot is the Commons projection the Runtime host maps from the engine's
/// Core <c>AlarmConditionState</c> + severity/message — the actor stays decoupled from
@@ -292,6 +301,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
Receive<AttributeValueUpdate>(HandleAttributeUpdate);
Receive<AttributeValueBatchUpdate>(HandleAttributeBatchUpdate);
Receive<AttributeStatusBatchUpdate>(HandleAttributeStatusBatchUpdate);
Receive<AlarmStateUpdate>(HandleAlarmUpdate);
Receive<AlarmQualityUpdate>(HandleAlarmQualityUpdate);
Receive<RebuildAddressSpace>(HandleRebuild);
@@ -368,6 +378,36 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
}
}
/// <summary>Routes a status-only batch to <see cref="IOpcUaAddressSpaceSink.WriteStatuses"/> — the
/// comms-loss annotation (03/S15). Same shape as <see cref="HandleAttributeBatchUpdate"/>: metered per
/// ITEM (batching is a transport change, so dashboards must not drop by the batch factor), a
/// <see cref="ValueWriteBatchException"/> is a partial failure that still counts what landed and names
/// the offender, and any other throw is swallowed so a sink fault cannot restart the publish actor and
/// drop live traffic. Metered under <c>kind:"status"</c> so a status annotation is separable from a
/// value write on the same counter.</summary>
private void HandleAttributeStatusBatchUpdate(AttributeStatusBatchUpdate msg)
{
try
{
_sink.WriteStatuses(msg.Items);
Interlocked.Add(ref _writes, msg.Items.Count);
OtOpcUaTelemetry.OpcUaSinkWrite.Add(msg.Items.Count, new KeyValuePair<string, object?>("kind", "status"));
}
catch (ValueWriteBatchException ex)
{
_log.Warning(ex,
"OpcUaPublish: sink.WriteStatuses partially failed for a {Count}-item batch — {Failed} failed " +
"(first: {Node}), {Succeeded} applied",
msg.Items.Count, ex.FailedCount, ex.FirstFailedNodeId, ex.SucceededCount);
Interlocked.Add(ref _writes, ex.SucceededCount);
OtOpcUaTelemetry.OpcUaSinkWrite.Add(ex.SucceededCount, new KeyValuePair<string, object?>("kind", "status"));
}
catch (Exception ex)
{
_log.Warning(ex, "OpcUaPublish: sink.WriteStatuses threw for a {Count}-item batch", msg.Items.Count);
}
}
private void HandleAlarmUpdate(AlarmStateUpdate msg)
{
try