fix(runtime): harden batch forwarding — detach pins, malformed-raise guard, struct items

This commit is contained in:
Joseph Doherty
2026-08-15 12:31:29 -04:00
parent 5c61d0b133
commit 8ef02506f7
3 changed files with 121 additions and 4 deletions
@@ -109,8 +109,11 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
/// (#519) — not a severity bucket. Classify it with <see cref="OpcUaStatus.IsGood"/> and friends rather
/// than comparing it to <see cref="OpcUaStatus.Good"/>.</para></summary>
public sealed record AttributeValuePublished(string DriverInstanceId, string FullReference, object? Value, uint StatusCode, DateTime TimestampUtc);
/// <summary>One value within an <see cref="AttributeValueBatchPublished"/>.</summary>
public sealed record AttributeValueItem(string FullReference, object? Value, uint StatusCode, DateTime TimestampUtc);
/// <summary>One value within an <see cref="AttributeValueBatchPublished"/>.
/// <para>A struct deliberately: an <c>AttributeValueItem[]</c> then holds its items inline, so a burst of
/// N values costs one array allocation instead of one array plus N boxes. Indexed access through
/// <see cref="IReadOnlyList{T}"/> does not box.</para></summary>
public readonly record struct AttributeValueItem(string FullReference, object? Value, uint StatusCode, DateTime TimestampUtc);
/// <summary>Batched form of <see cref="AttributeValuePublished"/> — every changed attribute of ONE
/// driver production burst in one message. The parent fans the whole batch to the mux and the OPC UA
/// publish path in one hop each. Status codes ride verbatim (#519).</summary>
@@ -900,7 +903,13 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
// Tell, one parent hop, one mux pass per burst instead of per tag.
if (subscribable is IBatchSubscribable batchable)
{
_batchChangeHandler = (_, args) => self.Tell(new DataChangeBatchForward(args.Items));
_batchChangeHandler = (_, args) =>
{
// A null/empty raise from a buggy driver must never enter the mailbox: an unguarded null
// would throw in the handler, restart the actor, and tear down the driver transport.
if (args?.Items is not { Count: > 0 }) return;
self.Tell(new DataChangeBatchForward(args.Items));
};
batchable.OnDataChangeBatch += _batchChangeHandler;
}
else