/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Opc.Ua.Client.Subscriptions; using Opc.Ua.Client.Subscriptions.MonitoredItems; namespace Opc.Ua.Client { /// /// A subscription engine that wraps the V2 /// to provide the /// contract. This allows /// a classic session to use the channel-based V2 /// publish pipeline as a drop-in replacement for the /// classic fire-and-forget engine. /// public sealed class DefaultSubscriptionEngine : ISubscriptionEngine { /// /// Initializes a new instance of the /// class. /// /// The session context that provides /// access to session state and services. public DefaultSubscriptionEngine( ISubscriptionEngineContext context) : this(context, null) { } /// /// Initializes a new instance of the /// class. /// /// The session context that provides /// access to session state and services. /// An optional /// used by the publish/subscribe pipeline for timer scheduling and /// duration calculations. When , /// is used. public DefaultSubscriptionEngine( ISubscriptionEngineContext context, TimeProvider? timeProvider = null) { if (context == null) { throw new ArgumentNullException(nameof(context)); } m_timeProvider = timeProvider ?? TimeProvider.System; m_manager = new SubscriptionManager( new EngineContextAdapter(context, m_timeProvider), context.Telemetry.LoggerFactory, context.ReturnDiagnostics, m_timeProvider); // The V2 SubscriptionManager starts paused. Resume it so // publish workers begin processing as soon as a // subscription is added. Reconnect logic toggles // pause/resume around connection transitions. m_manager.Resume(); } /// public int GoodPublishRequestCount => m_manager.GoodPublishRequestCount; /// public int BadPublishRequestCount => m_manager.BadPublishRequestCount; /// public int PublishWorkerCount => m_manager.PublishWorkerCount; /// /// The underlying that drives the /// new options-based subscription API. Exposed so callers can access /// the V2 manager via the engine. /// public ISubscriptionManager SubscriptionManager => m_manager; /// public int MinPublishRequestCount { get => m_manager.MinPublishWorkerCount; set => m_manager.MinPublishWorkerCount = value; } /// public int MaxPublishRequestCount { get => m_manager.MaxPublishWorkerCount; set => m_manager.MaxPublishWorkerCount = value; } /// public void StartPublishing(int timeout, bool fullQueue) { ThrowIfDisposed(); m_manager.Resume(); m_manager.Update(); } /// public async ValueTask StopPublishingAsync( CancellationToken ct) { if (!m_disposed) { await m_manager.DisposeAsync() .ConfigureAwait(false); m_disposed = true; } } /// public void PausePublishing() { ThrowIfDisposed(); m_manager.Pause(); } /// public void ResumePublishing() { ThrowIfDisposed(); m_manager.Resume(); } /// public void NotifySubscriptionsChanged() { if (!m_disposed) { m_manager.Update(); } } /// public ValueTask RecreateSubscriptionsAsync( NodeId? previousSessionId, CancellationToken ct = default) { ThrowIfDisposed(); return new ValueTask(m_manager.RecreateSubscriptionsAsync( previousSessionId, ct)); } /// public void Dispose() { if (!m_disposed) { m_manager.DisposeAsync() .AsTask() .GetAwaiter() .GetResult(); m_disposed = true; } } private void ThrowIfDisposed() { #if NET8_0_OR_GREATER ObjectDisposedException.ThrowIf(m_disposed, this); #else if (m_disposed) { throw new ObjectDisposedException( typeof(DefaultSubscriptionEngine).FullName); } #endif } /// /// Adapts to /// so the V2 /// can operate against /// a classic session. /// private sealed class EngineContextAdapter : ISubscriptionManagerContext { public EngineContextAdapter( ISubscriptionEngineContext context, TimeProvider timeProvider) { m_context = context; m_timeProvider = timeProvider; } /// public IManagedSubscription CreateSubscription( ISubscriptionNotificationHandler handler, IOptionsMonitor options, IMessageAckQueue queue, SubscriptionLoadState? loadState = null) { var subscriptionContext = new SubscriptionContextAdapter(m_context); return new DefaultSubscription( subscriptionContext, handler, queue, options, m_context.Telemetry, loadState, m_timeProvider); } /// public ValueTask PublishAsync( RequestHeader? requestHeader, ArrayOf subscriptionAcknowledgements, CancellationToken ct) { return m_context.PublishAsync( requestHeader ?? new RequestHeader(), subscriptionAcknowledgements, ct); } /// public ValueTask TransferSubscriptionsAsync( RequestHeader? requestHeader, ArrayOf subscriptionIds, bool sendInitialValues, CancellationToken ct) { return m_context.TransferSubscriptionsAsync( requestHeader, subscriptionIds, sendInitialValues, ct); } /// public ValueTask DeleteSubscriptionsAsync( RequestHeader? requestHeader, ArrayOf subscriptionIds, CancellationToken ct) { return m_context.DeleteSubscriptionsAsync( requestHeader, subscriptionIds, ct); } private readonly ISubscriptionEngineContext m_context; private readonly TimeProvider m_timeProvider; } /// /// Adapts the classic session services exposed by /// into the /// needed by the modern /// base class. /// private sealed class SubscriptionContextAdapter : ISubscriptionContext { public SubscriptionContextAdapter(ISubscriptionEngineContext context) { m_context = context; m_services = new ServiceSetAdapter(context); } /// public TimeSpan SessionTimeout => TimeSpan.FromMilliseconds(m_context.OperationTimeout); /// public ISubscriptionServiceSetClientMethods SubscriptionServiceSet => m_services; /// public IMonitoredItemServiceSetClientMethods MonitoredItemServiceSet => m_services; /// public IMethodServiceSetClientMethods MethodServiceSet => m_services; /// public override string ToString() { return m_context.SessionId.ToString(); } private readonly ISubscriptionEngineContext m_context; private readonly ServiceSetAdapter m_services; } /// /// Routes OPC UA service calls from subscriptions /// back through the classic /// . Operations /// that the subscriptions invoke for lifecycle /// management are delegated to the underlying session. /// private sealed class ServiceSetAdapter : ISubscriptionServiceSetClientMethods, IMonitoredItemServiceSetClientMethods, IMethodServiceSetClientMethods { public ServiceSetAdapter(ISubscriptionEngineContext context) { m_context = context; } /// public ValueTask CreateSubscriptionAsync( RequestHeader? requestHeader, double requestedPublishingInterval, uint requestedLifetimeCount, uint requestedMaxKeepAliveCount, uint maxNotificationsPerPublish, bool publishingEnabled, byte priority, CancellationToken ct) { return m_context.SubscriptionServiceSet.CreateSubscriptionAsync( requestHeader, requestedPublishingInterval, requestedLifetimeCount, requestedMaxKeepAliveCount, maxNotificationsPerPublish, publishingEnabled, priority, ct); } /// public ValueTask ModifySubscriptionAsync( RequestHeader? requestHeader, uint subscriptionId, double requestedPublishingInterval, uint requestedLifetimeCount, uint requestedMaxKeepAliveCount, uint maxNotificationsPerPublish, byte priority, CancellationToken ct) { return m_context.SubscriptionServiceSet.ModifySubscriptionAsync( requestHeader, subscriptionId, requestedPublishingInterval, requestedLifetimeCount, requestedMaxKeepAliveCount, maxNotificationsPerPublish, priority, ct); } /// public ValueTask SetPublishingModeAsync( RequestHeader? requestHeader, bool publishingEnabled, ArrayOf subscriptionIds, CancellationToken ct) { return m_context.SubscriptionServiceSet.SetPublishingModeAsync( requestHeader, publishingEnabled, subscriptionIds, ct); } /// public ValueTask RepublishAsync( RequestHeader? requestHeader, uint subscriptionId, uint retransmitSequenceNumber, CancellationToken ct) { return m_context.SubscriptionServiceSet.RepublishAsync( requestHeader, subscriptionId, retransmitSequenceNumber, ct); } /// public ValueTask DeleteSubscriptionsAsync( RequestHeader? requestHeader, ArrayOf subscriptionIds, CancellationToken ct) { return m_context.DeleteSubscriptionsAsync( requestHeader, subscriptionIds, ct); } /// public ValueTask CreateMonitoredItemsAsync( RequestHeader? requestHeader, uint subscriptionId, TimestampsToReturn timestampsToReturn, ArrayOf itemsToCreate, CancellationToken ct) { return m_context.MonitoredItemServiceSet.CreateMonitoredItemsAsync( requestHeader, subscriptionId, timestampsToReturn, itemsToCreate, ct); } /// public ValueTask ModifyMonitoredItemsAsync( RequestHeader? requestHeader, uint subscriptionId, TimestampsToReturn timestampsToReturn, ArrayOf itemsToModify, CancellationToken ct) { return m_context.MonitoredItemServiceSet.ModifyMonitoredItemsAsync( requestHeader, subscriptionId, timestampsToReturn, itemsToModify, ct); } /// public ValueTask DeleteMonitoredItemsAsync( RequestHeader? requestHeader, uint subscriptionId, ArrayOf monitoredItemIds, CancellationToken ct) { return m_context.MonitoredItemServiceSet.DeleteMonitoredItemsAsync( requestHeader, subscriptionId, monitoredItemIds, ct); } /// public ValueTask SetMonitoringModeAsync( RequestHeader? requestHeader, uint subscriptionId, MonitoringMode monitoringMode, ArrayOf monitoredItemIds, CancellationToken ct) { return m_context.MonitoredItemServiceSet.SetMonitoringModeAsync( requestHeader, subscriptionId, monitoringMode, monitoredItemIds, ct); } /// public ValueTask CallAsync( RequestHeader? requestHeader, ArrayOf methodsToCall, CancellationToken ct) { return m_context.MethodServiceSet.CallAsync( requestHeader, methodsToCall, ct); } /// /// public ValueTask PublishAsync( RequestHeader? requestHeader, ArrayOf subscriptionAcknowledgements, CancellationToken ct) { return m_context.SubscriptionServiceSet.PublishAsync( requestHeader, subscriptionAcknowledgements, ct); } /// public ValueTask TransferSubscriptionsAsync( RequestHeader? requestHeader, ArrayOf subscriptionIds, bool sendInitialValues, CancellationToken ct) { return m_context.SubscriptionServiceSet.TransferSubscriptionsAsync( requestHeader, subscriptionIds, sendInitialValues, ct); } /// public ValueTask SetTriggeringAsync( RequestHeader? requestHeader, uint subscriptionId, uint triggeringItemId, ArrayOf linksToAdd, ArrayOf linksToRemove, CancellationToken ct) { return m_context.MonitoredItemServiceSet.SetTriggeringAsync( requestHeader, subscriptionId, triggeringItemId, linksToAdd, linksToRemove, ct); } private readonly ISubscriptionEngineContext m_context; } /// /// A concrete that can /// be instantiated by the engine context adapter. The /// base class is abstract so this thin subclass exposes /// the constructor and provides a default monitored item /// factory. /// private sealed class DefaultSubscription : Subscriptions.Subscription { public DefaultSubscription( ISubscriptionContext context, ISubscriptionNotificationHandler handler, IMessageAckQueue completion, IOptionsMonitor options, ITelemetryContext telemetry, SubscriptionLoadState? loadState = null, TimeProvider? timeProvider = null) : base(context, handler, completion, options, telemetry, loadState, timeProvider) { } /// protected override Subscriptions.MonitoredItems.MonitoredItem CreateMonitoredItem( string name, IOptionsMonitor options, IMonitoredItemContext context, ITelemetryContext telemetry) { return new DefaultMonitoredItem( context, name, options, telemetry.LoggerFactory.CreateLogger()); } } /// /// A concrete that can /// be instantiated by the default subscription. The /// base class is abstract so this thin subclass simply /// exposes the constructor. /// private sealed class DefaultMonitoredItem : Subscriptions.MonitoredItems.MonitoredItem { public DefaultMonitoredItem( IMonitoredItemContext context, string name, IOptionsMonitor options, ILogger logger) : base(context, name, options, logger) { } } private readonly SubscriptionManager m_manager; private readonly TimeProvider m_timeProvider; private bool m_disposed; } }