feat(opcuaserver): per-request HistoryRead deadline — hung backends surface BadTimeout (03/S3)
This commit is contained in:
@@ -1816,6 +1816,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
IDictionary<NodeId, NodeState> cache)
|
||||
{
|
||||
var session = context.OperationContext?.Session;
|
||||
using var deadline = CreateDeadline();
|
||||
var ct = deadline?.Token ?? CancellationToken.None;
|
||||
var work = new List<Func<Task>>(nodesToProcess.Count);
|
||||
foreach (var handle in nodesToProcess)
|
||||
{
|
||||
@@ -1829,7 +1831,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
work.Add(() => ServeRawPagedAsync(
|
||||
handle, session, nodesToRead, results, errors,
|
||||
details.StartTime, details.EndTime, details.NumValuesPerNode,
|
||||
CancellationToken.None));
|
||||
ct));
|
||||
}
|
||||
|
||||
RunBounded(work, HistoryReadBatchParallelism);
|
||||
@@ -1848,6 +1850,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
// OPC UA ProcessingInterval is a Duration in milliseconds — convert once per batch.
|
||||
var interval = TimeSpan.FromMilliseconds(details.ProcessingInterval);
|
||||
using var deadline = CreateDeadline();
|
||||
var ct = deadline?.Token ?? CancellationToken.None;
|
||||
var work = new List<Func<Task>>(nodesToProcess.Count);
|
||||
foreach (var handle in nodesToProcess)
|
||||
{
|
||||
@@ -1873,7 +1877,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
details.EndTime,
|
||||
interval,
|
||||
agg,
|
||||
token), CancellationToken.None));
|
||||
token), ct));
|
||||
}
|
||||
|
||||
RunBounded(work, HistoryReadBatchParallelism);
|
||||
@@ -1892,13 +1896,15 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
// Snapshot the requested timestamps once — the same list is read for every node.
|
||||
var timestamps = details.ReqTimes?.ToList() ?? new List<DateTime>();
|
||||
using var deadline = CreateDeadline();
|
||||
var ct = deadline?.Token ?? CancellationToken.None;
|
||||
var work = new List<Func<Task>>(nodesToProcess.Count);
|
||||
foreach (var handle in nodesToProcess)
|
||||
{
|
||||
work.Add(() => ServeNodeAsync(handle, results, errors, (source, tagname, token) => source.ReadAtTimeAsync(
|
||||
tagname,
|
||||
timestamps,
|
||||
token), CancellationToken.None));
|
||||
token), ct));
|
||||
}
|
||||
|
||||
RunBounded(work, HistoryReadBatchParallelism);
|
||||
@@ -1918,6 +1924,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// Snapshot the select clauses once — the same filter projects every node's events.
|
||||
var selectClauses = details.Filter?.SelectClauses ?? new SimpleAttributeOperandCollection();
|
||||
|
||||
using var deadline = CreateDeadline();
|
||||
var ct = deadline?.Token ?? CancellationToken.None;
|
||||
var work = new List<Func<Task>>(nodesToProcess.Count);
|
||||
foreach (var handle in nodesToProcess)
|
||||
{
|
||||
@@ -1933,7 +1941,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
|
||||
var sn = sourceName;
|
||||
work.Add(() => ServeEventsAsync(handle, sn, details, selectClauses, results, errors, CancellationToken.None));
|
||||
work.Add(() => ServeEventsAsync(handle, sn, details, selectClauses, results, errors, ct));
|
||||
}
|
||||
|
||||
RunBounded(work, HistoryReadBatchParallelism);
|
||||
@@ -2138,6 +2146,17 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
Task.WhenAll(tasks).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the per-request deadline source for a HistoryRead arm (arch-review 03/S3): a
|
||||
/// <see cref="CancellationTokenSource"/> that fires after <see cref="HistoryReadDeadline"/>, so a
|
||||
/// single request can never hold an SDK request thread longer than the deadline regardless of node
|
||||
/// count. Returns <c>null</c> when the deadline is non-positive (unbounded — the arm then passes
|
||||
/// <see cref="CancellationToken.None"/>), matching the <c>ServerHistorianOptions</c> warning.
|
||||
/// </summary>
|
||||
/// <returns>The deadline source, or <c>null</c> for an unbounded (non-positive) deadline.</returns>
|
||||
private CancellationTokenSource? CreateDeadline() =>
|
||||
HistoryReadDeadline > TimeSpan.Zero ? new CancellationTokenSource(HistoryReadDeadline) : null;
|
||||
|
||||
/// <summary>Awaits one work item under a semaphore permit, releasing it when the item completes.</summary>
|
||||
/// <param name="gate">The concurrency-limiting semaphore.</param>
|
||||
/// <param name="work">The per-node work item.</param>
|
||||
|
||||
Reference in New Issue
Block a user