fix: propagate SDK request cancellation to historian calls

This commit is contained in:
Joseph Doherty
2026-11-10 10:37:52 -05:00
parent 9712e66ff4
commit ac5e9d84f0
@@ -5577,7 +5577,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
nodesToProcess = AuthorizeHistoryRead(context, nodesToProcess, results, errors);
WithHistoryReadBatchLimiter(nodesToProcess, results, errors, () =>
{
using var deadline = CreateDeadline();
using var deadline = CreateDeadline(context);
var ct = deadline?.Token ?? CancellationToken.None;
var work = new List<Func<Task>>(nodesToProcess.Count);
foreach (var handle in nodesToProcess)
@@ -5632,7 +5632,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
var served = AdvertisedAggregateFunctions.ServedBy(HistorianDataSource);
WithHistoryReadBatchLimiter(nodesToProcess, results, errors, () =>
{
using var deadline = CreateDeadline();
using var deadline = CreateDeadline(context);
var ct = deadline?.Token ?? CancellationToken.None;
var work = new List<Func<Task>>(nodesToProcess.Count);
foreach (var handle in nodesToProcess)
@@ -5691,7 +5691,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
nodesToProcess = AuthorizeHistoryRead(context, nodesToProcess, results, errors);
WithHistoryReadBatchLimiter(nodesToProcess, results, errors, () =>
{
using var deadline = CreateDeadline();
using var deadline = CreateDeadline(context);
var ct = deadline?.Token ?? CancellationToken.None;
var work = new List<Func<Task>>(nodesToProcess.Count);
foreach (var handle in nodesToProcess)
@@ -5781,7 +5781,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
nodesToProcess = AuthorizeHistoryRead(context, nodesToProcess, results, errors);
WithHistoryReadBatchLimiter(nodesToProcess, results, errors, () =>
{
using var deadline = CreateDeadline();
using var deadline = CreateDeadline(context);
var ct = deadline?.Token ?? CancellationToken.None;
var work = new List<Func<Task>>(nodesToProcess.Count);
foreach (var handle in nodesToProcess)
@@ -6381,7 +6381,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
WithHistoryReadBatchLimiter(planned.Select(p => p.Handle).ToList(), results, errors, () =>
{
using var deadline = CreateDeadline();
using var deadline = CreateDeadline(context);
var ct = deadline?.Token ?? CancellationToken.None;
var work = new List<Func<Task>>(planned.Count);
foreach (var (handle, tagname, operation) in planned)
@@ -6753,16 +6753,19 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
}
}
/// <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>Link the SDK request lifetime to the application's per-batch history deadline.</summary>
/// <param name="context">The request's server context.</param>
/// <returns>The linked deadline source, or null when neither request nor application can cancel.</returns>
private CancellationTokenSource? CreateDeadline(ServerSystemContext context)
{
var request = context.OperationContext?.CancellationToken ?? CancellationToken.None;
if (!request.CanBeCanceled && HistoryReadDeadline <= TimeSpan.Zero) return null;
var deadline = request.CanBeCanceled
? CancellationTokenSource.CreateLinkedTokenSource(request)
: new CancellationTokenSource();
if (HistoryReadDeadline > TimeSpan.Zero) deadline.CancelAfter(HistoryReadDeadline);
return deadline;
}
/// <summary>Awaits one work item under a semaphore permit, releasing it when the item completes.</summary>
/// <param name="gate">The concurrency-limiting semaphore.</param>