From ac5e9d84f0798853f88ffb3687fb5a3bce07cfe7 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 10 Nov 2026 10:37:52 -0500 Subject: [PATCH] fix: propagate SDK request cancellation to historian calls --- .../OtOpcUaNodeManager.cs | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs index 57b1929a..9c7f539c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs @@ -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>(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>(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>(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>(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>(planned.Count); foreach (var (handle, tagname, operation) in planned) @@ -6753,16 +6753,19 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 } } - /// - /// Create the per-request deadline source for a HistoryRead arm (arch-review 03/S3): a - /// that fires after , so a - /// single request can never hold an SDK request thread longer than the deadline regardless of node - /// count. Returns null when the deadline is non-positive (unbounded — the arm then passes - /// ), matching the ServerHistorianOptions warning. - /// - /// The deadline source, or null for an unbounded (non-positive) deadline. - private CancellationTokenSource? CreateDeadline() => - HistoryReadDeadline > TimeSpan.Zero ? new CancellationTokenSource(HistoryReadDeadline) : null; + /// Link the SDK request lifetime to the application's per-batch history deadline. + /// The request's server context. + /// The linked deadline source, or null when neither request nor application can cancel. + 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; + } /// Awaits one work item under a semaphore permit, releasing it when the item completes. /// The concurrency-limiting semaphore.