refactor(opcuaserver): async-ify ServeNode + Processed/AtTime HistoryRead internals, thread cancellation (03/S3)

This commit is contained in:
Joseph Doherty
2026-07-13 12:04:54 -04:00
parent 304442dba4
commit 1e83fe7085
2 changed files with 25 additions and 15 deletions
@@ -9,7 +9,7 @@
{ "id": "R2-08-T6", "subject": "Outage-sim integration test (real library, unroutable host, via internal HandleImpersonation) + SDK m_eventLock contract doc + docs/security.md", "status": "completed", "blockedBy": ["R2-08-T4"] },
{ "id": "R2-08-T7", "subject": "S3 sequential-serving repro test (RED): MaxObservedConcurrency >= 2 on an 8-node Raw batch — fails on current code", "status": "completed", "blockedBy": [] },
{ "id": "R2-08-T8", "subject": "ServerHistorianOptions: HistoryReadBatchParallelism/MaxConcurrentHistoryReadBatches/HistoryReadDeadline + Validate warnings + node-manager properties + hosted-service wiring", "status": "completed", "blockedBy": [] },
{ "id": "R2-08-T9", "subject": "Async-ify ServeNode + Processed/AtTime arms (ct threading, OCE=>BadTimeout, behavior-neutral)", "status": "pending", "blockedBy": ["R2-08-T7", "R2-08-T8"] },
{ "id": "R2-08-T9", "subject": "Async-ify ServeNode + Processed/AtTime arms (ct threading, OCE=>BadTimeout, behavior-neutral)", "status": "completed", "blockedBy": ["R2-08-T7", "R2-08-T8"] },
{ "id": "R2-08-T10", "subject": "Async-ify ServeRawPaged (page read + tie-cluster over-fetch) + extract ServeEventsAsync", "status": "pending", "blockedBy": ["R2-08-T9"] },
{ "id": "R2-08-T11", "subject": "RunBounded fan-out in all four HistoryRead arms, single bridge per arm (turns T7 green) + upper-bound + isolation tests", "status": "pending", "blockedBy": ["R2-08-T9", "R2-08-T10"] },
{ "id": "R2-08-T12", "subject": "Per-request HistoryRead deadline CTS — hung backend surfaces BadTimeout per node", "status": "pending", "blockedBy": ["R2-08-T11"] },
@@ -1861,13 +1861,14 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
// and the single-shot backend returns every bucket in one read, so there is no "full page ⇒
// maybe more" signal to page on. Returning the complete aggregate result with a null CP is
// spec-conformant (OPC UA Part 11 lets a server return all available data in one response).
ServeNode(handle, results, errors, (source, tagname) => source.ReadProcessedAsync(
// Interim: still bridged sequentially per node (bounded fan-out lands in a later slice).
ServeNodeAsync(handle, results, errors, (source, tagname, token) => source.ReadProcessedAsync(
tagname,
details.StartTime,
details.EndTime,
interval,
aggregate.Value,
CancellationToken.None));
token), CancellationToken.None).GetAwaiter().GetResult();
}
}
@@ -1886,10 +1887,11 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
var timestamps = details.ReqTimes?.ToList() ?? new List<DateTime>();
foreach (var handle in nodesToProcess)
{
ServeNode(handle, results, errors, (source, tagname) => source.ReadAtTimeAsync(
// Interim: still bridged sequentially per node (bounded fan-out lands in a later slice).
ServeNodeAsync(handle, results, errors, (source, tagname, token) => source.ReadAtTimeAsync(
tagname,
timestamps,
CancellationToken.None));
token), CancellationToken.None).GetAwaiter().GetResult();
}
}
@@ -2061,11 +2063,12 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// <param name="errors">The service-level errors list to fill at <c>handle.Index</c>.</param>
/// <param name="read">Invokes the resolved data-source read with the resolved tagname; only called
/// once the tagname is confirmed present.</param>
private void ServeNode(
private async Task ServeNodeAsync(
NodeHandle handle,
IList<SdkHistoryReadResult> results,
IList<ServiceResult> errors,
Func<IHistorianDataSource, string, Task<HistorianRead>> read)
Func<IHistorianDataSource, string, CancellationToken, Task<HistorianRead>> read,
CancellationToken ct)
{
var idString = handle.NodeId.Identifier?.ToString();
if (idString is null || !TryGetHistorizedTagname(idString, out var tagname))
@@ -2078,9 +2081,9 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
try
{
// HistoryRead is NOT invoked under the node-manager Lock (unlike OnWriteValue), so blocking
// on the async source here is safe and won't freeze the address space.
var sourceResult = read(HistorianDataSource, tagname!).GetAwaiter().GetResult();
// HistoryRead is NOT invoked under the node-manager Lock (unlike OnWriteValue), so awaiting
// the async source here is safe and won't freeze the address space.
var sourceResult = await read(HistorianDataSource, tagname!, ct).ConfigureAwait(false);
var historyData = ToHistoryData(sourceResult);
results[handle.Index] = new SdkHistoryReadResult
@@ -2095,13 +2098,20 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
};
errors[handle.Index] = ServiceResult.Good;
}
catch (OperationCanceledException)
{
// The per-request deadline elapsed while this node was in flight — surface BadTimeout for
// THIS node only (a spec-conformant history-read status), ahead of the generic catch.
errors[handle.Index] = StatusCodes.BadTimeout;
results[handle.Index] = new SdkHistoryReadResult { StatusCode = StatusCodes.BadTimeout };
}
catch (Exception ex)
{
// One node's backend failure (throw / timeout / cancellation) must not throw out of the
// batch — surface a Bad status for THIS node only. This CustomNodeManager2 carries no
// ILogger (see ReportConditionEvent), so log through the SDK's static trace rather than
// swallowing silently. Utils.LogError is [Obsolete] in 1.5.378 (favours an ITelemetryContext
// this manager doesn't wire) — suppress the deprecation, matching the existing pattern.
// One node's backend failure (throw / timeout) must not throw out of the batch — surface a
// Bad status for THIS node only. This CustomNodeManager2 carries no ILogger (see
// ReportConditionEvent), so log through the SDK's static trace rather than swallowing silently.
// Utils.LogError is [Obsolete] in 1.5.378 (favours an ITelemetryContext this manager doesn't
// wire) — suppress the deprecation, matching the existing pattern.
#pragma warning disable CS0618 // Type or member is obsolete
Utils.LogError(ex, "OtOpcUaNodeManager: HistoryRead failed for node {0}", handle.NodeId);
#pragma warning restore CS0618