perf(dcl): batch subscribe/read/write seam, bounded reconnect, sharded subscriptions
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Per-tag outcome of a batch subscribe. A failed row is a genuine per-tag fault
|
||||
/// (bad node id, unresolvable path) and never aborts the batch — mirroring the
|
||||
/// MxAccess Gateway's <c>SubscribeResult</c> and OPC UA's per-monitored-item create
|
||||
/// status. A batch that fails at connection level throws instead, so the
|
||||
/// DataConnectionActor can drive the reconnect state machine.
|
||||
/// </summary>
|
||||
/// <param name="TagPath">The requested tag path.</param>
|
||||
/// <param name="Success">Whether the tag was subscribed.</param>
|
||||
/// <param name="SubscriptionId">Adapter subscription handle when <paramref name="Success"/>; otherwise <c>null</c>.</param>
|
||||
/// <param name="ErrorMessage">Per-tag failure reason when not successful.</param>
|
||||
public record TagSubscribeResult(string TagPath, bool Success, string? SubscriptionId, string? ErrorMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Optional capability for an <see cref="IDataConnection"/> implementation whose
|
||||
/// protocol can subscribe/unsubscribe MANY tags in one round trip, and whose
|
||||
/// <see cref="IDataConnection.ReadBatchAsync"/> / <see cref="IDataConnection.WriteBatchAsync"/>
|
||||
/// are TRUE bulk service calls rather than a loop over the single-tag methods.
|
||||
/// Mirrors the <see cref="IBrowsableDataConnection"/> / <see cref="IAlarmSubscribableConnection"/>
|
||||
/// capability-interface pattern; consumed by the DataConnectionActor only.
|
||||
///
|
||||
/// <para>
|
||||
/// Implementing this interface is the adapter's assertion that batching is genuinely
|
||||
/// cheaper than N single calls: the actor then subscribes, unsubscribes and seed-reads
|
||||
/// in bounded chunks instead of per tag. An adapter that does NOT implement it keeps
|
||||
/// the historical per-tag behaviour, so the capability is purely additive.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IBatchSubscribableConnection
|
||||
{
|
||||
/// <summary>
|
||||
/// Subscribes every tag in <paramref name="tagPaths"/> in as few protocol round
|
||||
/// trips as the adapter allows, returning one <see cref="TagSubscribeResult"/> per
|
||||
/// requested tag (in any order — callers key by <see cref="TagSubscribeResult.TagPath"/>).
|
||||
/// All tags share ONE <paramref name="callback"/>; the callback already carries the
|
||||
/// tag path, so per-tag delegates would carry nothing extra.
|
||||
/// </summary>
|
||||
/// <param name="tagPaths">The tag paths to subscribe.</param>
|
||||
/// <param name="callback">Callback invoked for every value change on any of the tags.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>One result row per requested tag path.</returns>
|
||||
/// <exception cref="Exception">
|
||||
/// A connection-level fault (adapter not connected / transport down) is thrown so the
|
||||
/// caller classifies it as a connection failure; per-tag faults are result rows.
|
||||
/// </exception>
|
||||
Task<IReadOnlyList<TagSubscribeResult>> SubscribeBatchAsync(
|
||||
IReadOnlyList<string> tagPaths, SubscriptionCallback callback, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Releases every supplied subscription id in as few protocol round trips as the
|
||||
/// adapter allows. Unknown/stale ids are ignored, mirroring
|
||||
/// <see cref="IDataConnection.UnsubscribeAsync"/>.
|
||||
/// </summary>
|
||||
/// <param name="subscriptionIds">Subscription ids previously returned by a subscribe call.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task UnsubscribeBatchAsync(IReadOnlyList<string> subscriptionIds, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -195,6 +195,7 @@ public static class OpcUaEndpointConfigSerializer
|
||||
["DiscardOldest"] = config.DiscardOldest.ToString(),
|
||||
["SubscriptionPriority"] = config.SubscriptionPriority.ToString(),
|
||||
["SubscriptionDisplayName"] = config.SubscriptionDisplayName,
|
||||
["MaxMonitoredItemsPerSubscription"] = config.MaxMonitoredItemsPerSubscription.ToString(),
|
||||
["TimestampsToReturn"] = config.TimestampsToReturn.ToString(),
|
||||
};
|
||||
if (config.Heartbeat is { } hb)
|
||||
@@ -248,6 +249,7 @@ public static class OpcUaEndpointConfigSerializer
|
||||
TryAssignInt(dict, "KeepAliveCount", v => c.KeepAliveCount = v);
|
||||
TryAssignInt(dict, "LifetimeCount", v => c.LifetimeCount = v);
|
||||
TryAssignInt(dict, "MaxNotificationsPerPublish", v => c.MaxNotificationsPerPublish = v);
|
||||
TryAssignInt(dict, "MaxMonitoredItemsPerSubscription", v => c.MaxMonitoredItemsPerSubscription = v);
|
||||
|
||||
if (dict.TryGetValue("DiscardOldest", out var doStr) && bool.TryParse(doStr, out var doVal))
|
||||
c.DiscardOldest = doVal;
|
||||
|
||||
@@ -66,6 +66,14 @@ public sealed class OpcUaEndpointConfig
|
||||
/// Display name for the subscription.
|
||||
/// </summary>
|
||||
public string SubscriptionDisplayName { get; set; } = "ScadaBridge";
|
||||
/// <summary>
|
||||
/// Maximum monitored items placed on a single OPC UA subscription before the
|
||||
/// adapter shards onto an additional subscription. Item-count ceilings are a
|
||||
/// property of the target server, so this is per endpoint rather than global.
|
||||
/// A site sized at 37,500 tags shards into 8 subscriptions at the default.
|
||||
/// Values <= 0 are treated as the default.
|
||||
/// </summary>
|
||||
public int MaxMonitoredItemsPerSubscription { get; set; } = 5000;
|
||||
|
||||
// Read / filter
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user