57ae69a05e
R08 1.4: raw and modified history reads share HistoryReadPaging. A read stopped by the caller's cap releases the server continuation point. The raw path also passed releaseContinuationPoints = (cp != null), which released page two instead of reading it, so every raw read was silently truncated to its first server page. R08 1.5: DefaultSubscriptionAdapter removes its Subscription from the SDK Session on delete/dispose (and a failed CreateAsync removes it too), so re-targeted alarm subscriptions no longer accumulate in Session.Subscriptions. Refs #747 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
67 lines
3.3 KiB
C#
67 lines
3.3 KiB
C#
using Opc.Ua;
|
|
using Serilog;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Helpers;
|
|
|
|
/// <summary>
|
|
/// The client-side HistoryRead paging loop shared by the raw and modified reads. Continuation pages are
|
|
/// READ (<c>releaseContinuationPoints = false</c>) while the caller still wants values; a read stopped by the
|
|
/// caller's own cap while the server still holds a cursor releases that cursor so it does not occupy one of the
|
|
/// session's continuation-point slots until eviction.
|
|
/// </summary>
|
|
internal static class HistoryReadPaging
|
|
{
|
|
private static readonly ILogger Logger = Log.ForContext(typeof(HistoryReadPaging));
|
|
|
|
/// <summary>Reads (or, with <paramref name="releaseContinuationPoints"/>, releases) one page.</summary>
|
|
/// <param name="continuationPoint">The cursor to resume, or <see langword="null"/> for the first page.</param>
|
|
/// <param name="releaseContinuationPoints"><see langword="true"/> to release the cursor instead of reading.</param>
|
|
/// <param name="ct">The caller's cancellation token.</param>
|
|
/// <returns>The per-node result, or <see langword="null"/> when the response carried none.</returns>
|
|
public delegate Task<HistoryReadResult?> PageReader(
|
|
byte[]? continuationPoint, bool releaseContinuationPoints, CancellationToken ct);
|
|
|
|
/// <summary>Reads pages until the server finishes or the caller's cap is reached.</summary>
|
|
/// <typeparam name="T">The element type extracted from each page.</typeparam>
|
|
/// <param name="readPage">Issues one HistoryRead for the single node.</param>
|
|
/// <param name="extract">Extracts the page's elements from a result.</param>
|
|
/// <param name="maxValues">The caller's cap; <c><= 0</c> means no cap (follow every continuation point).</param>
|
|
/// <param name="nodeId">The node being read, for refusal messages.</param>
|
|
/// <param name="ct">The caller's cancellation token.</param>
|
|
/// <returns>Every element read.</returns>
|
|
/// <exception cref="ServiceResultException">A page was refused (see <see cref="HistoryReadStatus"/>).</exception>
|
|
public static async Task<List<T>> ReadAllAsync<T>(
|
|
PageReader readPage, Func<HistoryReadResult, IEnumerable<T>> extract, int maxValues, NodeId nodeId,
|
|
CancellationToken ct)
|
|
{
|
|
var all = new List<T>();
|
|
byte[]? continuationPoint = null;
|
|
|
|
while (true)
|
|
{
|
|
var result = await readPage(continuationPoint, false, ct).ConfigureAwait(false);
|
|
if (result == null) return all;
|
|
|
|
HistoryReadStatus.ThrowIfRefused(result.StatusCode, nodeId);
|
|
all.AddRange(extract(result));
|
|
|
|
continuationPoint = result.ContinuationPoint;
|
|
if (continuationPoint is not { Length: > 0 }) return all;
|
|
if (maxValues > 0 && all.Count >= maxValues) break;
|
|
}
|
|
|
|
// Stopped by the client's own cap: the server still holds a cursor for this session. Release it; failing to
|
|
// is not worth failing a read whose values are already in hand.
|
|
try
|
|
{
|
|
await readPage(continuationPoint, true, ct).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
Logger.Warning(ex, "Releasing the HistoryRead continuation point for {NodeId} failed", nodeId);
|
|
}
|
|
|
|
return all;
|
|
}
|
|
}
|