fix(opcuaserver): chunked, per-item-isolated WriteValues with typed partial-failure outcome
This commit is contained in:
@@ -25,8 +25,13 @@ public interface IOpcUaAddressSpaceSink
|
||||
/// <summary>Write a batch of Variable values. Semantically identical to calling
|
||||
/// <see cref="WriteValue"/> per item; an SDK-backed sink overrides this to take its internal
|
||||
/// lock ONCE per batch instead of once per value (the hot-path win). The default implementation
|
||||
/// loops <see cref="WriteValue"/> so existing sinks and fakes behave unchanged.</summary>
|
||||
/// loops <see cref="WriteValue"/> so existing sinks and fakes behave unchanged.
|
||||
/// <para>A batching implementation MUST isolate per item — one bad item may not drop the tail of the
|
||||
/// batch — and report the aggregate as a <see cref="ValueWriteBatchException"/>; see that type for why.
|
||||
/// The default loop above predates the contract and is only used by fakes / the no-op sink.</para></summary>
|
||||
/// <param name="values">The value writes to apply, in order.</param>
|
||||
/// <exception cref="ValueWriteBatchException">A batching implementation throws this when some — but not
|
||||
/// necessarily all — items failed; every other item has been applied.</exception>
|
||||
void WriteValues(IReadOnlyList<ValueWrite> values)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(values);
|
||||
@@ -185,6 +190,30 @@ public interface IOpcUaAddressSpaceSink
|
||||
public readonly record struct ValueWrite(
|
||||
string NodeId, object? Value, uint StatusCode, DateTime SourceTimestampUtc, AddressSpaceRealm Realm);
|
||||
|
||||
/// <summary>Aggregate outcome of a partially-failed <see cref="IOpcUaAddressSpaceSink.WriteValues"/> batch:
|
||||
/// every non-failing item HAS been applied; this carries what the caller needs to log and meter honestly.</summary>
|
||||
/// <remarks>A batched sink must NOT abandon the tail of a batch when one item throws: the poll engine
|
||||
/// updates its last-value cache BEFORE raising, so a dropped item is never re-raised until that tag's value
|
||||
/// changes again — the node would serve a stale value indefinitely. Hence the sink isolates per item and
|
||||
/// reports the aggregate here rather than letting the first failure escape.</remarks>
|
||||
/// <param name="firstFailedNodeId">The NodeId of the first item that failed (empty when that item's own
|
||||
/// NodeId was null/empty — the validation failure this type most often reports).</param>
|
||||
/// <param name="failedCount">How many items of the batch failed.</param>
|
||||
/// <param name="succeededCount">How many items of the batch were applied.</param>
|
||||
/// <param name="firstFailure">The exception the first failing item produced; the inner exception.</param>
|
||||
public sealed class ValueWriteBatchException(string firstFailedNodeId, int failedCount, int succeededCount, Exception firstFailure)
|
||||
: Exception($"WriteValues batch: {failedCount} of {failedCount + succeededCount} items failed (first: '{firstFailedNodeId}')", firstFailure)
|
||||
{
|
||||
/// <summary>Gets the NodeId of the first item that failed.</summary>
|
||||
public string FirstFailedNodeId { get; } = firstFailedNodeId;
|
||||
|
||||
/// <summary>Gets the number of items of the batch that failed.</summary>
|
||||
public int FailedCount { get; } = failedCount;
|
||||
|
||||
/// <summary>Gets the number of items of the batch that were successfully applied.</summary>
|
||||
public int SucceededCount { get; } = succeededCount;
|
||||
}
|
||||
|
||||
/// <summary>No-op sink. Bound by default so the actors are safe to run in dev / Mac /
|
||||
/// integration tests without a real SDK behind them.</summary>
|
||||
public sealed class NullOpcUaAddressSpaceSink : IOpcUaAddressSpaceSink
|
||||
|
||||
Reference in New Issue
Block a user