/* ======================================================================== * 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.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Opc.Ua.Server { /// /// Manages a subscription created by a client. /// public class Subscription : ISubscription { /// /// Initializes the object. /// public Subscription( IServerInternal server, ISession session, uint subscriptionId, double publishingInterval, uint maxLifetimeCount, uint maxKeepAliveCount, uint maxNotificationsPerPublish, byte priority, bool publishingEnabled, uint maxMessageCount) : this( server, session, subscriptionId, publishingInterval, maxLifetimeCount, maxKeepAliveCount, maxNotificationsPerPublish, priority, publishingEnabled, maxMessageCount, null) { } /// /// Initializes the subscription with an explicit /// so the monotonic publish-timer expiry can /// be mocked in tests. /// public Subscription( IServerInternal server, ISession session, uint subscriptionId, double publishingInterval, uint maxLifetimeCount, uint maxKeepAliveCount, uint maxNotificationsPerPublish, byte priority, bool publishingEnabled, uint maxMessageCount, TimeProvider? timeProvider) { Id = subscriptionId; Session = session ?? throw new ArgumentNullException(nameof(session)); m_server = server ?? throw new ArgumentNullException(nameof(server)); m_logger = server.Telemetry.CreateLogger(); m_timeProvider = timeProvider ?? (server as ITimeProviderProvider)?.TimeProvider ?? TimeProvider.System; m_publishingInterval = publishingInterval; m_maxLifetimeCount = maxLifetimeCount; m_maxKeepAliveCount = maxKeepAliveCount; m_maxNotificationsPerPublish = maxNotificationsPerPublish; m_publishingEnabled = publishingEnabled; Priority = priority; m_publishTimerExpiry = m_timeProvider.GetTimestampMilliseconds() + (long)publishingInterval; //Per OPC UA spec Part 4 Section 5.13.1.2, the server must send a message (notification or keep-alive) //at the end of the first publishing cycle to inform the client the subscription is operational //So we initialize the keep-alive counter to maxKeepAliveCount to force a message at the end of the first publish cycle m_keepAliveCounter = maxKeepAliveCount; m_lifetimeCounter = 0; m_waitingForPublish = false; m_maxMessageCount = maxMessageCount; m_sentMessages = []; m_supportsDurable = m_server.MonitoredItemQueueFactory.SupportsDurableQueues; IsDurable = false; m_monitoredItems = []; m_itemsToCheck = new LinkedList(); m_itemsToPublish = new LinkedList(); m_itemsToTrigger = []; // m_itemsReadyToPublish = new Queue(); // m_itemsNotificationsAvailable = new LinkedList(); m_sequenceNumber = 1; // initialize diagnostics. Diagnostics = new SubscriptionDiagnosticsDataType { SessionId = Session.Id, SubscriptionId = Id, Priority = priority, PublishingInterval = publishingInterval, MaxKeepAliveCount = maxKeepAliveCount, MaxLifetimeCount = maxLifetimeCount, MaxNotificationsPerPublish = maxNotificationsPerPublish, PublishingEnabled = publishingEnabled, ModifyCount = 0, EnableCount = 0, DisableCount = 0, RepublishMessageRequestCount = 0, RepublishMessageCount = 0, TransferRequestCount = 0, TransferredToSameClientCount = 0, TransferredToAltClientCount = 0, PublishRequestCount = 0, DataChangeNotificationsCount = 0, EventNotificationsCount = 0, NotificationsCount = 0, LatePublishRequestCount = 0, CurrentKeepAliveCount = 0, CurrentLifetimeCount = 0, UnacknowledgedMessageCount = 0, DiscardedMessageCount = 0, MonitoredItemCount = 0, DisabledMonitoredItemCount = 0, MonitoringQueueOverflowCount = 0, NextSequenceNumber = m_sequenceNumber }; ServerSystemContext systemContext = m_server.DefaultSystemContext.Copy(session); m_diagnosticsId = server.DiagnosticsNodeManager.CreateSubscriptionDiagnosticsAsync( systemContext, Diagnostics, OnUpdateDiagnostics).AsTask().GetAwaiter().GetResult(); TraceState(LogLevel.Information, TraceStateId.Config, "CREATED"); } /// /// Restore a subscription and its monitored items after a restart from a template /// public static ValueTask RestoreAsync( IServerInternal server, IStoredSubscription storedSubscription, CancellationToken cancellationToken = default) { return RestoreAsync(server, storedSubscription, null, cancellationToken); } /// /// Restore a subscription and its monitored items after a restart from a template /// with an explicit . /// public static async ValueTask RestoreAsync( IServerInternal server, IStoredSubscription storedSubscription, TimeProvider? timeProvider, CancellationToken cancellationToken = default) { var subscription = new Subscription(server, storedSubscription, timeProvider); await subscription.RestoreMonitoredItemsAsync(storedSubscription.MonitoredItems, cancellationToken) .ConfigureAwait(false); return subscription; } /// /// Initialize subscription after a restart from a template /// protected Subscription(IServerInternal server, IStoredSubscription storedSubscription) : this(server, storedSubscription, null) { } /// /// Initialize subscription after a restart from a template with an /// explicit . /// protected Subscription( IServerInternal server, IStoredSubscription storedSubscription, TimeProvider? timeProvider) { if (server.IsRunning) { throw new InvalidOperationException( "Subscription restore can only occur on startup"); } m_server = server; m_logger = server.Telemetry.CreateLogger(); m_timeProvider = timeProvider ?? (server as ITimeProviderProvider)?.TimeProvider ?? TimeProvider.System; Session = null!; Id = storedSubscription.Id; m_publishingInterval = storedSubscription.PublishingInterval; m_maxLifetimeCount = storedSubscription.MaxLifetimeCount; m_lifetimeCounter = storedSubscription.LifetimeCounter; m_maxKeepAliveCount = storedSubscription.MaxKeepaliveCount; m_maxNotificationsPerPublish = storedSubscription.MaxNotificationsPerPublish; m_publishingEnabled = false; Priority = storedSubscription.Priority; m_publishTimerExpiry = m_timeProvider.GetTimestampMilliseconds() + (long)storedSubscription.PublishingInterval; m_keepAliveCounter = 0; m_waitingForPublish = false; m_maxMessageCount = storedSubscription.MaxMessageCount; m_sentMessages = storedSubscription.SentMessages; m_supportsDurable = m_server.MonitoredItemQueueFactory.SupportsDurableQueues; IsDurable = storedSubscription.IsDurable; // UserIdentityToken is null for anonymous sessions; preserve the saved-owner // identity in that case (the field already supports null). m_savedOwnerIdentity = storedSubscription.UserIdentityToken != null ? new UserIdentity(storedSubscription.UserIdentityToken) : null; m_sequenceNumber = storedSubscription.SequenceNumber; m_lastSentMessage = storedSubscription.LastSentMessage; m_monitoredItems = []; m_itemsToCheck = new LinkedList(); m_itemsToPublish = new LinkedList(); m_itemsToTrigger = []; // initialize diagnostics. Diagnostics = new SubscriptionDiagnosticsDataType { SubscriptionId = Id, Priority = Priority, PublishingInterval = m_publishingInterval, MaxKeepAliveCount = m_maxKeepAliveCount, MaxLifetimeCount = m_maxLifetimeCount, MaxNotificationsPerPublish = m_maxNotificationsPerPublish, PublishingEnabled = m_publishingEnabled, ModifyCount = 0, EnableCount = 0, DisableCount = 0, RepublishMessageRequestCount = 0, RepublishMessageCount = 0, TransferRequestCount = 0, TransferredToSameClientCount = 0, TransferredToAltClientCount = 0, PublishRequestCount = 0, DataChangeNotificationsCount = 0, EventNotificationsCount = 0, NotificationsCount = 0, LatePublishRequestCount = 0, CurrentKeepAliveCount = 0, CurrentLifetimeCount = 0, UnacknowledgedMessageCount = 0, DiscardedMessageCount = 0, MonitoredItemCount = 0, DisabledMonitoredItemCount = 0, MonitoringQueueOverflowCount = 0, NextSequenceNumber = m_sequenceNumber }; ServerSystemContext systemContext = m_server.DefaultSystemContext.Copy(); m_diagnosticsId = server.DiagnosticsNodeManager.CreateSubscriptionDiagnosticsAsync( systemContext, Diagnostics, OnUpdateDiagnostics).AsTask().GetAwaiter().GetResult(); TraceState(LogLevel.Information, TraceStateId.Config, "RESTORED"); } /// /// Frees any unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// An overrideable version of the Dispose. /// protected virtual void Dispose(bool disposing) { if (disposing) { lock (m_lock) { foreach (KeyValuePair> monitoredItemKVP in m_monitoredItems) { monitoredItemKVP.Value?.Value?.Dispose(); } m_monitoredItems.Clear(); for (int ii = 0; ii < m_sentMessages.Count; ii++) { ReuseNotificationPayloads(m_sentMessages[ii]); } m_sentMessages.Clear(); m_itemsToCheck.Clear(); m_itemsToPublish.Clear(); } } } /// /// The session that owns the monitored item. /// public ISession Session { get; private set; } = null!; /// /// The unique identifier assigned to the subscription. /// public uint Id { get; } /// /// The subscriptions owner identity. /// public IUserIdentity EffectiveIdentity => Session != null ? Session.EffectiveIdentity : m_savedOwnerIdentity!; /// /// Queues an item that is ready to publish. /// public void ItemReadyToPublish(IMonitoredItem monitoredItem) { /* lock (m_itemsReadyToPublish) { m_itemsReadyToPublish.Enqueue(monitoredItem); } */ } /// /// Tells the subscription that notifications are available but the item is not ready to publish. /// public void ItemNotificationsAvailable(IMonitoredItem monitoredItem) { /* lock (m_itemsReadyToPublish) { m_itemsNotificationsAvailable.AddLast(monitoredItem); } */ } /// /// The identifier for the session that owns the subscription. /// public NodeId SessionId { get { lock (m_lock) { if (Session == null) { return default; } return Session.Id; } } } /// /// True if the subscription is set to durable and supports long lifetime and queue size /// public bool IsDurable { get; private set; } /// /// Gets the lock that must be acquired before accessing the contents of the Diagnostics property. /// public object DiagnosticsLock => Diagnostics; /// /// Gets the lock that must be acquired before updating the contents of the Diagnostics property. /// public object DiagnosticsWriteLock { get { // mark diagnostic nodes dirty if (m_server != null && m_server.DiagnosticsNodeManager != null) { m_server.DiagnosticsNodeManager.ForceDiagnosticsScan(); } return DiagnosticsLock; } } /// /// Gets the current diagnostics for the subscription. /// public SubscriptionDiagnosticsDataType Diagnostics { get; } /// /// The publishing rate for the subscription. /// public double PublishingInterval { get { lock (m_lock) { return m_publishingInterval; } } } /// /// The number of monitored items. /// public int MonitoredItemCount { get { lock (m_lock) { return m_monitoredItems.Count; } } } /// /// The priority assigned to the subscription. /// public byte Priority { get; private set; } /// /// Deletes the subscription. /// public async ValueTask DeleteAsync(OperationContext context, CancellationToken cancellationToken = default) { // delete the diagnostics. if (!m_diagnosticsId.IsNull) { ServerSystemContext systemContext = m_server.DefaultSystemContext.Copy(Session); await m_server.DiagnosticsNodeManager .DeleteSubscriptionDiagnosticsAsync(systemContext, m_diagnosticsId, cancellationToken).ConfigureAwait(false); } try { TraceState(LogLevel.Information, TraceStateId.Deleted, "DELETED"); // the context may be null if the server is cleaning up expired subscriptions. // in this case we create a context with a dummy request and use the current session. if (context == null) { var requestHeader = new RequestHeader { ReturnDiagnostics = (int)DiagnosticsMasks.OperationSymbolicIdAndText }; context = new OperationContext(requestHeader, null, RequestType.Unknown, RequestLifetime.None); } await DeleteMonitoredItemsAsync( context, [.. m_monitoredItems.Keys], true, cancellationToken).ConfigureAwait(false); } catch (Exception e) { m_logger.LogError(e, "Delete items for subscription failed."); } } /// /// Checks if the subscription is ready to publish. /// public PublishingState PublishTimerExpired() { lock (m_lock) { long currentTime = m_timeProvider.GetTimestampMilliseconds(); // check if publish interval has elapsed. if (m_publishTimerExpiry >= currentTime) { // check if waiting for publish. if (m_waitingForPublish) { return PublishingState.WaitingForPublish; } return PublishingState.Idle; } // set next expiry time. while (m_publishTimerExpiry < currentTime) { m_publishTimerExpiry += (long)m_publishingInterval; } // check lifetime has elapsed. if (m_waitingForPublish) { m_lifetimeCounter++; lock (DiagnosticsWriteLock) { Diagnostics.LatePublishRequestCount++; Diagnostics.CurrentLifetimeCount = m_lifetimeCounter; } if (m_lifetimeCounter >= m_maxLifetimeCount) { TraceState(LogLevel.Information, TraceStateId.Deleted, "EXPIRED"); return PublishingState.Expired; } } // increment keep alive counter. m_keepAliveCounter++; lock (DiagnosticsWriteLock) { Diagnostics.CurrentKeepAliveCount = m_keepAliveCounter; } // check for monitored items. if (m_publishingEnabled && Session != null) { // check for monitored items that are ready to publish. LinkedListNode? current = m_itemsToCheck.First; bool itemsTriggered = false; while (current != null) { LinkedListNode? next = current.Next; IMonitoredItem monitoredItem = current.Value; // check if the item is ready to publish. if (monitoredItem.IsResendData || monitoredItem.IsReadyToPublish) { m_itemsToCheck.Remove(current); m_itemsToPublish.AddLast(current); } // Check for triggering only if there are triggered items configured if (m_itemsToTrigger.Count > 0) { bool isReadyToTrigger = monitoredItem.IsReadyToTrigger; // update any triggered items. if (isReadyToTrigger && m_itemsToTrigger.TryGetValue( current.Value.Id, out List? triggeredItems)) { for (int ii = 0; ii < triggeredItems.Count; ii++) { if (triggeredItems[ii].SetTriggered()) { itemsTriggered = true; } } // clear ReadyToTrigger flag after trigger monitoredItem.IsReadyToTrigger = false; } } current = next; } // need to go through the list again if items were triggered. if (itemsTriggered) { current = m_itemsToCheck.First; while (current != null) { LinkedListNode? next = current.Next; IMonitoredItem monitoredItem = current.Value; if (monitoredItem.IsReadyToPublish) { m_itemsToCheck.Remove(current); m_itemsToPublish.AddLast(current); } current = next; } } if (m_itemsToPublish.Count > 0) { if (!m_waitingForPublish) { // TraceState(LogLevel.Trace, TraceStateId.Deleted, "READY TO PUBLISH"); } m_waitingForPublish = true; return PublishingState.NotificationsAvailable; } } // check if keep alive expired. if (m_keepAliveCounter >= m_maxKeepAliveCount) { if (!m_waitingForPublish) { // TraceState(LogLevel.Trace, TraceStateId.Items, "READY TO KEEPALIVE"); } m_waitingForPublish = true; return PublishingState.NotificationsAvailable; } // do nothing. return PublishingState.Idle; } } /// /// Transfers the subscription to a new session. /// /// The session to which the subscription is transferred. /// Whether the first Publish response shall contain current values. /// The cancellation token. public async ValueTask TransferSessionAsync(OperationContext context, bool sendInitialValues, CancellationToken cancellationToken = default) { // locked by caller Session = context.Session; var monitoredItems = m_monitoredItems.Select(v => v.Value.Value).ToList(); var errors = new List(monitoredItems.Count); for (int ii = 0; ii < monitoredItems.Count; ii++) { errors.Add(null!); } await m_server.NodeManager .TransferMonitoredItemsAsync(context, sendInitialValues, monitoredItems, errors, cancellationToken) .ConfigureAwait(false); int badTransfers = 0; for (int ii = 0; ii < errors.Count; ii++) { if (ServiceResult.IsBad(errors[ii])) { badTransfers++; } } if (badTransfers > 0) { m_logger.LogTrace("Failed to transfer {Count} Monitored Items", badTransfers); } lock (DiagnosticsWriteLock) { Diagnostics.SessionId = Session.Id; } } /// /// Initiates resending of all data monitored items in a Subscription /// public void ResendData(OperationContext context) { // check session. VerifySession(context); lock (m_lock) { foreach (IMonitoredItem monitoredItem in m_monitoredItems.Select(v => v.Value.Value) .ToList()) { monitoredItem.SetupResendDataTrigger(); } } } /// /// Tells the subscription that the owning session is being closed. /// public void SessionClosed() { lock (m_lock) { if (Session != null) { m_savedOwnerIdentity = Session.EffectiveIdentity; Session = null!; } } lock (DiagnosticsWriteLock) { Diagnostics.SessionId = default; } } /// /// Resets the keepalive counter. /// private void ResetKeepaliveCount() { m_keepAliveCounter = 0; lock (DiagnosticsWriteLock) { Diagnostics.CurrentKeepAliveCount = 0; } } /// /// Resets the lifetime count. /// private void ResetLifetimeCount() { m_lifetimeCounter = 0; lock (DiagnosticsWriteLock) { Diagnostics.CurrentLifetimeCount = 0; } } /// /// Update the monitoring queue overflow count. /// public void QueueOverflowHandler() { lock (DiagnosticsWriteLock) { Diagnostics.MonitoringQueueOverflowCount++; } } /// /// Removes a message from the message queue. /// public ServiceResult? Acknowledge(OperationContext context, uint sequenceNumber) { lock (m_lock) { // check session. VerifySession(context); // clear lifetime counter. ResetLifetimeCount(); // find message in queue. for (int ii = 0; ii < m_sentMessages.Count; ii++) { if (m_sentMessages[ii].SequenceNumber == sequenceNumber) { if (m_lastSentMessage > ii) { m_lastSentMessage--; } NotificationMessage removed = m_sentMessages[ii]; m_sentMessages.RemoveAt(ii); ReuseNotificationPayloads(removed); return null; } } if (sequenceNumber == 0) { return StatusCodes.BadSequenceNumberInvalid; } // TraceState(LogLevel.Trace, TraceStateId.Items, "ACK " + sequenceNumber.ToString()); // message not found. return StatusCodes.BadSequenceNumberUnknown; } } /// /// Returns all available notifications. /// /// is null. public NotificationMessage? Publish( OperationContext context, out ArrayOf availableSequenceNumbers, out bool moreNotifications) { if (context == null) { throw new ArgumentNullException(nameof(context)); } NotificationMessage? message = null; lock (m_lock) { moreNotifications = false; availableSequenceNumbers = default; // check if expired. if (m_expired) { return null; } try { // update diagnostics. lock (DiagnosticsWriteLock) { Diagnostics.PublishRequestCount++; } message = InnerPublish( context, out availableSequenceNumbers, out moreNotifications); lock (DiagnosticsWriteLock) { Diagnostics.UnacknowledgedMessageCount = (uint)availableSequenceNumbers.Count; } } finally { // clear counters on success. if (message != null) { // TraceState(LogLevel.Trace, TraceStateId.Items, Utils.Format("PUBLISH #{0}", message.SequenceNumber)); ResetKeepaliveCount(); m_waitingForPublish = moreNotifications; ResetLifetimeCount(); } } } return message; } /// /// Publishes a timeout status message. /// public NotificationMessage PublishTimeout() { NotificationMessage? message = null; lock (m_lock) { m_expired = true; message = (NotificationMessage)NotificationMessageActivator.Instance.CreateInstance(); message.SequenceNumber = m_sequenceNumber; message.PublishTime = DateTimeUtc.Now; Utils.IncrementIdentifier(ref m_sequenceNumber); lock (DiagnosticsWriteLock) { Diagnostics.NextSequenceNumber = m_sequenceNumber; } var notification = (StatusChangeNotification)StatusChangeNotificationActivator.Instance.CreateInstance(); notification.Status = StatusCodes.BadTimeout; message.NotificationData = message.NotificationData.AddItem( new ExtensionObject(notification)); } return message; } /// /// Publishes a SubscriptionTransferred status message. /// public NotificationMessage SubscriptionTransferred() { NotificationMessage? message = null; lock (m_lock) { message = (NotificationMessage)NotificationMessageActivator.Instance.CreateInstance(); message.SequenceNumber = m_sequenceNumber; message.PublishTime = DateTimeUtc.Now; Utils.IncrementIdentifier(ref m_sequenceNumber); lock (DiagnosticsWriteLock) { Diagnostics.NextSequenceNumber = m_sequenceNumber; } var notification = (StatusChangeNotification)StatusChangeNotificationActivator.Instance.CreateInstance(); notification.Status = StatusCodes.GoodSubscriptionTransferred; message.NotificationData = message.NotificationData.AddItem(new ExtensionObject(notification)); } return message; } /// /// Returns all available notifications. /// private NotificationMessage? InnerPublish( OperationContext context, out ArrayOf availableSequenceNumbers, out bool moreNotifications) { // check session. VerifySession(context); // TraceState(LogLevel.Trace, TraceStateId.Items, "PUBLISH"); // check if a keep alive should be sent if there is no data. bool keepAliveIfNoData = m_keepAliveCounter >= m_maxKeepAliveCount; List availableSequenceNumberList = []; moreNotifications = false; if (m_lastSentMessage < m_sentMessages.Count) { // return the available sequence numbers. for (int ii = 0; ii <= m_lastSentMessage && ii < m_sentMessages.Count; ii++) { availableSequenceNumberList.Add(m_sentMessages[ii].SequenceNumber); } moreNotifications = m_waitingForPublish = (m_lastSentMessage < m_sentMessages.Count - 1) || m_itemsToPublish.Count > 0; // TraceState(LogLevel.Trace, TraceStateId.Items, "PUBLISH QUEUED MESSAGE"); availableSequenceNumbers = availableSequenceNumberList; return m_sentMessages[m_lastSentMessage++]; } var messages = new List(); if (m_publishingEnabled) { DateTime start1 = DateTime.UtcNow; // collect notifications to publish. var events = new Queue(); var datachanges = new Queue(); var datachangeDiagnostics = new Queue(); // check for monitored items that are ready to publish. LinkedListNode? current = m_itemsToPublish.First; //Limit the amount of values a monitored item publishes at once uint maxNotificationsPerMonitoredItem = m_maxNotificationsPerPublish == 0 ? uint.MaxValue : m_maxNotificationsPerPublish * 3; while (current != null) { LinkedListNode? next = current.Next; IMonitoredItem monitoredItem = current.Value; bool hasMoreValuesToPublish; if ((monitoredItem.MonitoredItemType & MonitoredItemTypeMask.DataChange) != 0) { hasMoreValuesToPublish = ((IDataChangeMonitoredItem)monitoredItem).Publish( context, datachanges, datachangeDiagnostics, maxNotificationsPerMonitoredItem, m_logger); } else { hasMoreValuesToPublish = ((IEventMonitoredItem)monitoredItem).Publish( context, events, maxNotificationsPerMonitoredItem); } // if item has more values to publish leave it at the front of the list // to execute publish in next cycle, no checking needed // if no more values to publish are left add it to m_itemsToCheck // to check status on next publish cylce if (!hasMoreValuesToPublish) { m_itemsToPublish.Remove(current); m_itemsToCheck.AddLast(current); } // check there are enough notifications for a message. if (m_maxNotificationsPerPublish > 0 && events.Count + datachanges.Count > m_maxNotificationsPerPublish) { // construct message. int eventCount = events.Count; int dataChangeCount = datachanges.Count; NotificationMessage message = ConstructMessage( events, datachanges, datachangeDiagnostics, out int notificationCount); // add to list of messages to send. messages.Add(message); lock (DiagnosticsWriteLock) { Diagnostics.DataChangeNotificationsCount += (uint)(dataChangeCount - datachanges.Count); Diagnostics.EventNotificationsCount += (uint)(eventCount - events.Count); Diagnostics.NotificationsCount += (uint)notificationCount; } //stop fetching messages from MIs when message queue is full to avoid discards // use m_maxMessageCount - 2 to put remaining values into the last allowed message (each MI is allowed to publish 3 up to messages at once) if (messages.Count >= m_maxMessageCount - 2) { break; } } current = next; } // publish the remaining notifications. while (events.Count + datachanges.Count > 0) { // construct message. int eventCount = events.Count; int dataChangeCount = datachanges.Count; NotificationMessage message = ConstructMessage( events, datachanges, datachangeDiagnostics, out int notificationCount); // add to list of messages to send. messages.Add(message); lock (DiagnosticsWriteLock) { Diagnostics.DataChangeNotificationsCount += (uint)(dataChangeCount - datachanges.Count); Diagnostics.EventNotificationsCount += (uint)(eventCount - events.Count); Diagnostics.NotificationsCount += (uint)notificationCount; } } // check for missing notifications. if (!keepAliveIfNoData && messages.Count == 0) { m_logger.LogError("Oops! MonitoredItems queued but no notifications available."); m_waitingForPublish = false; availableSequenceNumbers = availableSequenceNumberList; return null; } DateTime end1 = DateTime.UtcNow; double delta1 = ((double)(end1.Ticks - start1.Ticks)) / TimeSpan.TicksPerMillisecond; if (delta1 > 200) { TraceState( LogLevel.Trace, TraceStateId.Publish, Utils.Format("PUBLISHING DELAY ({0}ms)", delta1)); } } if (messages.Count == 0) { // create a keep alive message. var message = (NotificationMessage)NotificationMessageActivator.Instance.CreateInstance(); message.SequenceNumber = m_sequenceNumber; message.PublishTime = DateTimeUtc.Now; // return the available sequence numbers. for (int ii = 0; ii <= m_lastSentMessage && ii < m_sentMessages.Count; ii++) { availableSequenceNumberList.Add(m_sentMessages[ii].SequenceNumber); } // TraceState(LogLevel.Trace, TraceStateId.Items, "PUBLISH KEEPALIVE"); availableSequenceNumbers = availableSequenceNumberList; return message; } // have to drop unsent messages if out of queue space. int overflowCount = messages.Count - (int)m_maxMessageCount; if (overflowCount > 0) { m_logger.LogWarning( "WARNING: QUEUE OVERFLOW. Dropping {Count} Messages. Increase MaxMessageQueueSize. SubId={SubscriptionId}, MaxMessageQueueSize={MaxMessageCount}", overflowCount, Id, m_maxMessageCount); for (int ii = 0; ii < overflowCount; ii++) { ReuseNotificationPayloads(messages[ii]); } messages.RemoveRange(0, overflowCount); } // remove old messages if queue is full. if (m_sentMessages.Count > m_maxMessageCount - messages.Count) { lock (DiagnosticsWriteLock) { Diagnostics.UnacknowledgedMessageCount += (uint)messages.Count; } if (m_maxMessageCount <= messages.Count) { for (int ii = 0; ii < m_sentMessages.Count; ii++) { ReuseNotificationPayloads(m_sentMessages[ii]); } m_sentMessages.Clear(); } else { for (int ii = 0; ii < messages.Count; ii++) { ReuseNotificationPayloads(m_sentMessages[ii]); } m_sentMessages.RemoveRange(0, messages.Count); } } // save new message m_lastSentMessage = m_sentMessages.Count; m_sentMessages.AddRange(messages); // check if there are more notifications to send. moreNotifications = m_waitingForPublish = messages.Count > 1; // return the available sequence numbers. for (int ii = 0; ii <= m_lastSentMessage && ii < m_sentMessages.Count; ii++) { availableSequenceNumberList.Add(m_sentMessages[ii].SequenceNumber); } // TraceState(LogLevel.Trace, TraceStateId.Items, "PUBLISH NEW MESSAGE"); availableSequenceNumbers = availableSequenceNumberList; return m_sentMessages[m_lastSentMessage++]; } /// /// Returns the available sequence numbers for retransmission /// For example used in Transfer Subscription /// public ArrayOf AvailableSequenceNumbersForRetransmission() { var availableSequenceNumbers = new List(); // Assumption we do not check lastSentMessage < sentMessages.Count because // in case of subscription transfer original client might have crashed by handling message, // therefor new client should have to chance to process all available messages for (int ii = 0; ii < m_sentMessages.Count; ii++) { availableSequenceNumbers.Add(m_sentMessages[ii].SequenceNumber); } return availableSequenceNumbers; } /// /// Construct a message from the queues. /// private NotificationMessage ConstructMessage( Queue events, Queue datachanges, Queue datachangeDiagnostics, out int notificationCount) { notificationCount = 0; var message = (NotificationMessage)NotificationMessageActivator.Instance.CreateInstance(); message.SequenceNumber = m_sequenceNumber; message.PublishTime = DateTimeUtc.Now; Utils.IncrementIdentifier(ref m_sequenceNumber); lock (DiagnosticsWriteLock) { Diagnostics.NextSequenceNumber = m_sequenceNumber; } // add events. if (events.Count > 0 && notificationCount < m_maxNotificationsPerPublish) { var eventList = new List(); while (events.Count > 0 && notificationCount < m_maxNotificationsPerPublish) { eventList.Add(events.Dequeue()); notificationCount++; } var notification = (EventNotificationList)EventNotificationListActivator.Instance.CreateInstance(); notification.Events = eventList; message.NotificationData = message.NotificationData.AddItem(new ExtensionObject(notification)); } // add datachanges (space permitting). if (datachanges.Count > 0 && notificationCount < m_maxNotificationsPerPublish) { bool diagnosticsExist = false; var dataChangeList = new List(datachanges.Count); var diagnosticInfos = new List(datachanges.Count); while (datachanges.Count > 0 && notificationCount < m_maxNotificationsPerPublish) { MonitoredItemNotification datachange = datachanges.Dequeue(); dataChangeList.Add(datachange); DiagnosticInfo diagnosticInfo = datachangeDiagnostics.Dequeue(); if (diagnosticInfo != null) { diagnosticsExist = true; } diagnosticInfos.Add(diagnosticInfo!); notificationCount++; } var notification = (DataChangeNotification)DataChangeNotificationActivator.Instance.CreateInstance(); notification.MonitoredItems = dataChangeList; notification.DiagnosticInfos = diagnosticsExist ? diagnosticInfos : default!; message.NotificationData = message.NotificationData.AddItem(new ExtensionObject(notification)); } return message; } /// /// Returns a cached notification message. /// /// is null. /// public NotificationMessage Republish( OperationContext context, uint retransmitSequenceNumber) { if (context == null) { throw new ArgumentNullException(nameof(context)); } lock (DiagnosticsWriteLock) { Diagnostics.RepublishMessageRequestCount++; } lock (m_lock) { // check session. VerifySession(context); // clear lifetime counter. ResetLifetimeCount(); lock (DiagnosticsWriteLock) { Diagnostics.RepublishRequestCount++; Diagnostics.RepublishMessageRequestCount++; } // find message. foreach (NotificationMessage sentMessage in m_sentMessages) { if (sentMessage.SequenceNumber == retransmitSequenceNumber) { lock (DiagnosticsWriteLock) { Diagnostics.RepublishMessageCount++; } return sentMessage; } } // message not available. throw new ServiceResultException(StatusCodes.BadMessageNotAvailable); } } /// /// Updates the publishing parameters for the subscription. /// public void Modify( OperationContext context, double publishingInterval, uint maxLifetimeCount, uint maxKeepAliveCount, uint maxNotificationsPerPublish, byte priority) { lock (m_lock) { // check session. VerifySession(context); // clear lifetime counter. ResetLifetimeCount(); m_maxLifetimeCount = maxLifetimeCount; // update publishing interval. if (publishingInterval != m_publishingInterval) { m_publishingInterval = publishingInterval; m_publishTimerExpiry = m_timeProvider.GetTimestampMilliseconds() + (long)publishingInterval; ResetKeepaliveCount(); } // update keep alive count. if (maxKeepAliveCount != m_maxKeepAliveCount) { m_maxKeepAliveCount = maxKeepAliveCount; } m_maxNotificationsPerPublish = maxNotificationsPerPublish; // update priority. Priority = priority; // update diagnostics lock (DiagnosticsWriteLock) { Diagnostics.ModifyCount++; Diagnostics.PublishingInterval = m_publishingInterval; Diagnostics.MaxKeepAliveCount = m_maxKeepAliveCount; Diagnostics.MaxLifetimeCount = m_maxLifetimeCount; Diagnostics.Priority = Priority; Diagnostics.MaxNotificationsPerPublish = m_maxNotificationsPerPublish; } TraceState(LogLevel.Information, TraceStateId.Config, "MODIFIED"); } } /// /// Enables/disables publishing for the subscription. /// public void SetPublishingMode(OperationContext context, bool publishingEnabled) { lock (m_lock) { // check session. VerifySession(context); // clear lifetime counter. ResetLifetimeCount(); // update publishing interval. if (publishingEnabled != m_publishingEnabled) { m_publishingEnabled = publishingEnabled; // update diagnostics lock (DiagnosticsWriteLock) { Diagnostics.PublishingEnabled = m_publishingEnabled; if (m_publishingEnabled) { Diagnostics.EnableCount++; } else { Diagnostics.DisableCount++; } } } TraceState( LogLevel.Information, TraceStateId.Config, publishingEnabled ? "ENABLED" : "DISABLED"); } } /// /// Updates the triggers for the monitored item. /// /// is null. /// public void SetTriggering( OperationContext context, uint triggeringItemId, ArrayOf linksToAdd, ArrayOf linksToRemove, out ArrayOf addResults, out ArrayOf addDiagnosticInfos, out ArrayOf removeResults, out ArrayOf removeDiagnosticInfos) { if (context == null) { throw new ArgumentNullException(nameof(context)); } // allocate results. bool diagnosticsExist = false; List addResultList = []; List? addDiagnosticInfoList = null; List removeResultList = []; List? removeDiagnosticInfoList = null; if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { addDiagnosticInfoList = []; removeDiagnosticInfoList = []; } // build list of items to modify. lock (m_lock) { // check session. VerifySession(context); // clear lifetime counter. ResetLifetimeCount(); // look up triggering item. if (!m_monitoredItems.TryGetValue( triggeringItemId, out LinkedListNode? triggerNode)) { throw new ServiceResultException(StatusCodes.BadMonitoredItemIdInvalid); } // lookup existing list. if (!m_itemsToTrigger.TryGetValue( triggeringItemId, out List? triggeredItems)) { m_itemsToTrigger[triggeringItemId] = triggeredItems = []; } // remove old links. for (int ii = 0; ii < linksToRemove.Count; ii++) { removeResultList.Add(StatusCodes.Good); bool found = false; for (int jj = 0; jj < triggeredItems.Count; jj++) { if (triggeredItems[jj].Id == linksToRemove[ii]) { found = true; triggeredItems.RemoveAt(jj); break; } } if (!found) { removeResultList[ii] = StatusCodes.BadMonitoredItemIdInvalid; // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { DiagnosticInfo? diagnosticInfo = ServerUtils.CreateDiagnosticInfo( m_server, context, removeResultList[ii], m_logger); diagnosticsExist = true; removeDiagnosticInfoList!.Add(diagnosticInfo!); } continue; } // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { removeDiagnosticInfoList!.Add(null!); } } // add new links. for (int ii = 0; ii < linksToAdd.Count; ii++) { addResultList.Add(StatusCodes.Good); if (!m_monitoredItems.TryGetValue( linksToAdd[ii], out LinkedListNode? node)) { addResultList[ii] = StatusCodes.BadMonitoredItemIdInvalid; // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { DiagnosticInfo? diagnosticInfo = ServerUtils.CreateDiagnosticInfo( m_server, context, addResultList[ii], m_logger); diagnosticsExist = true; addDiagnosticInfoList!.Add(diagnosticInfo!); } continue; } // check if triggering interface is supported. if (node.Value is not ITriggeredMonitoredItem triggeredItem) { addResultList[ii] = StatusCodes.BadNotSupported; // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { DiagnosticInfo? diagnosticInfo = ServerUtils.CreateDiagnosticInfo( m_server, context, addResultList[ii], m_logger); diagnosticsExist = true; addDiagnosticInfoList!.Add(diagnosticInfo!); } continue; } // add value if not already in list. bool found = false; for (int jj = 0; jj < triggeredItems.Count; jj++) { if (triggeredItems[jj].Id == triggeredItem.Id) { found = true; break; } } if (!found) { triggeredItems.Add(triggeredItem); } // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { addDiagnosticInfoList!.Add(null!); } } // remove an empty list. if (triggeredItems.Count == 0) { m_itemsToTrigger.Remove(triggeringItemId); } // clear diagnostics if not required. if (!diagnosticsExist) { addDiagnosticInfoList?.Clear(); removeDiagnosticInfoList?.Clear(); } } addResults = addResultList; removeResults = removeResultList; addDiagnosticInfos = addDiagnosticInfoList!; removeDiagnosticInfos = removeDiagnosticInfoList!; } /// /// Adds monitored items to a subscription. /// /// is null. public async ValueTask CreateMonitoredItemsAsync( OperationContext context, TimestampsToReturn timestampsToReturn, ArrayOf itemsToCreate, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } int count = itemsToCreate.Count; lock (m_lock) { // check session. VerifySession(context); // clear lifetime counter. ResetLifetimeCount(); } // create the monitored items. var monitoredItems = new List(count); var errors = new List(count); var filterResults = new List(count); for (int ii = 0; ii < count; ii++) { monitoredItems.Add(null!); errors.Add(null!); filterResults.Add(null!); } await m_server.NodeManager.CreateMonitoredItemsAsync( context, Id, m_publishingInterval, timestampsToReturn, itemsToCreate, errors, filterResults, monitoredItems, IsDurable, cancellationToken).ConfigureAwait(false); // allocate results. bool diagnosticsExist = false; var results = new List(count); List? diagnosticInfos = null; if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos = new List(count); } lock (m_lock) { // check session again after CreateMonitoredItems. VerifySession(context); for (int ii = 0; ii < errors.Count; ii++) { // update results. MonitoredItemCreateResult? result = null; if (ServiceResult.IsBad(errors[ii])) { result = new MonitoredItemCreateResult { StatusCode = errors[ii].Code }; if (filterResults[ii] != null) { result.FilterResult = new ExtensionObject(filterResults[ii]); } } else { IMonitoredItem monitoredItem = monitoredItems[ii]; if (monitoredItem != null) { monitoredItem.SubscriptionCallback = this; LinkedListNode node = m_itemsToCheck.AddLast( monitoredItem); m_monitoredItems.Add(monitoredItem.Id, node); errors[ii] = monitoredItem.GetCreateResult(out result); // update sampling interval diagnostics. AddItemToSamplingInterval( result.RevisedSamplingInterval, itemsToCreate[ii].MonitoringMode); } } results.Add(result!); // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { DiagnosticInfo? diagnosticInfo = null; if (errors[ii] != null && errors[ii].Code != StatusCodes.Good) { diagnosticInfo = ServerUtils.CreateDiagnosticInfo( m_server, context, errors[ii], m_logger); diagnosticsExist = true; } diagnosticInfos!.Add(diagnosticInfo!); } } // clear diagnostics if not required. if (!diagnosticsExist && diagnosticInfos != null) { diagnosticInfos.Clear(); } TraceState(LogLevel.Information, TraceStateId.Items, "ITEMS CREATED"); } return new CreateMonitoredItemsResponse { Results = results, DiagnosticInfos = diagnosticInfos! }; } /// /// Adds an item to the sampling interval. /// private void AddItemToSamplingInterval( double samplingInterval, MonitoringMode monitoringMode) { // update diagnostics lock (DiagnosticsWriteLock) { if (monitoringMode == MonitoringMode.Disabled) { Diagnostics.DisabledMonitoredItemCount++; } Diagnostics.MonitoredItemCount++; } } /// /// Adds an item to the sampling interval. /// private static void ModifyItemSamplingInterval( double oldInterval, double newInterval, MonitoringMode monitoringMode) { // TBD } /// /// Removes an item from the sampling interval. /// private void RemoveItemToSamplingInterval( double samplingInterval, MonitoringMode monitoringMode) { // update diagnostics lock (DiagnosticsWriteLock) { if (monitoringMode == MonitoringMode.Disabled) { Diagnostics.DisabledMonitoredItemCount--; } Diagnostics.MonitoredItemCount--; } } /// /// Changes the monitoring mode for an item. /// private void ModifyItemMonitoringMode( double samplingInterval, MonitoringMode oldMode, MonitoringMode newMode) { if (newMode != oldMode) { // update diagnostics lock (DiagnosticsWriteLock) { if (newMode == MonitoringMode.Disabled) { Diagnostics.DisabledMonitoredItemCount++; } else { Diagnostics.DisabledMonitoredItemCount--; } } } } /// /// Modifies monitored items in a subscription. /// /// is null. public async ValueTask ModifyMonitoredItemsAsync( OperationContext context, TimestampsToReturn timestampsToReturn, ArrayOf itemsToModify, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } int count = itemsToModify.Count; // allocate results. bool diagnosticsExist = false; var results = new List(count); List? diagnosticInfos = null; if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos = new List(count); } // build list of items to modify. var monitoredItems = new List(count); var errors = new List(count); var filterResults = new List(count); double[] originalSamplingIntervals = new double[count]; bool validItems = false; lock (m_lock) { // check session. VerifySession(context); // clear lifetime counter. ResetLifetimeCount(); for (int ii = 0; ii < count; ii++) { filterResults.Add(null!); if (!m_monitoredItems.TryGetValue( itemsToModify[ii].MonitoredItemId, out LinkedListNode? node)) { monitoredItems.Add(null!); errors.Add(StatusCodes.BadMonitoredItemIdInvalid); // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { DiagnosticInfo? diagnosticInfo = ServerUtils.CreateDiagnosticInfo( m_server, context, errors[ii], m_logger); diagnosticsExist = true; diagnosticInfos!.Add(diagnosticInfo!); } continue; } IMonitoredItem monitoredItem = node.Value; monitoredItems.Add(monitoredItem); originalSamplingIntervals[ii] = monitoredItem.SamplingInterval; errors.Add(null!); validItems = true; // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos!.Add(null!); } } } // update items. if (validItems) { await m_server.NodeManager.ModifyMonitoredItemsAsync( context, timestampsToReturn, monitoredItems, itemsToModify, errors, filterResults, cancellationToken) .ConfigureAwait(false); } lock (m_lock) { // create results. for (int ii = 0; ii < errors.Count; ii++) { ServiceResult error = errors[ii]; MonitoredItemModifyResult? result = null; if (ServiceResult.IsGood(error)) { error = monitoredItems[ii].GetModifyResult(out result); } result ??= new MonitoredItemModifyResult(); if (error == null) { result.StatusCode = StatusCodes.Good; } else { result.StatusCode = error.StatusCode; } // update diagnostics. if (ServiceResult.IsGood(error)) { ModifyItemSamplingInterval( originalSamplingIntervals[ii], result.RevisedSamplingInterval, monitoredItems[ii].MonitoringMode); } if (filterResults[ii] != null) { result.FilterResult = new ExtensionObject(filterResults[ii]); } results.Add(result); if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0 && error != null && error.Code != StatusCodes.Good) { diagnosticInfos![ii] = ServerUtils.CreateDiagnosticInfo( m_server, context, error, m_logger)!; diagnosticsExist = true; } } // clear diagnostics if not required. if (!diagnosticsExist && diagnosticInfos != null) { diagnosticInfos.Clear(); } TraceState(LogLevel.Information, TraceStateId.Items, "ITEMS MODIFIED"); } return new ModifyMonitoredItemsResponse { Results = results, DiagnosticInfos = diagnosticInfos! }; } /// /// Deletes the monitored items in a subscription. /// public ValueTask DeleteMonitoredItemsAsync( OperationContext context, ArrayOf monitoredItemIds, CancellationToken cancellationToken = default) { return DeleteMonitoredItemsAsync( context, monitoredItemIds, false, cancellationToken); } /// /// Deletes the monitored items in a subscription. /// /// is null. private async ValueTask DeleteMonitoredItemsAsync( OperationContext context, ArrayOf monitoredItemIds, bool doNotCheckSession, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } int count = monitoredItemIds.Count; bool diagnosticsExist = false; var results = new List(count); List? diagnosticInfos = null; if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos = new List(count); } // build list of items to modify. var monitoredItems = new List(count); var errors = new List(count); double[] originalSamplingIntervals = new double[count]; var originalMonitoringModes = new MonitoringMode[count]; bool validItems = false; lock (m_lock) { // check session. if (!doNotCheckSession) { VerifySession(context); } // clear lifetime counter. ResetLifetimeCount(); for (int ii = 0; ii < count; ii++) { if (!m_monitoredItems.TryGetValue( monitoredItemIds[ii], out LinkedListNode? node)) { monitoredItems.Add(null!); errors.Add(StatusCodes.BadMonitoredItemIdInvalid); // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { DiagnosticInfo? diagnosticInfo = ServerUtils.CreateDiagnosticInfo( m_server, context, errors[ii], m_logger); diagnosticsExist = true; diagnosticInfos!.Add(diagnosticInfo!); } continue; } IMonitoredItem monitoredItem = node.Value; monitoredItems.Add(monitoredItem); // remove the item from the internal lists. m_monitoredItems.Remove(monitoredItemIds[ii]); m_itemsToTrigger.Remove(monitoredItemIds[ii]); //remove the links towards the deleted monitored item List? triggeredItems = null; foreach (KeyValuePair> item in m_itemsToTrigger) { triggeredItems = item.Value; for (int jj = 0; jj < triggeredItems.Count; jj++) { if (triggeredItems[jj].Id == monitoredItemIds[ii]) { triggeredItems.RemoveAt(jj); break; } } } node.List?.Remove(node); originalSamplingIntervals[ii] = monitoredItem.SamplingInterval; originalMonitoringModes[ii] = monitoredItem.MonitoringMode; errors.Add(null!); validItems = true; // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos!.Add(null!); } } } // update items. if (validItems) { await m_server.NodeManager.DeleteMonitoredItemsAsync(context, Id, monitoredItems, errors, cancellationToken) .ConfigureAwait(false); } //dispose monitored Items foreach (IMonitoredItem monitoredItem in monitoredItems) { monitoredItem?.Dispose(); } lock (m_lock) { // update diagnostics. for (int ii = 0; ii < errors.Count; ii++) { ServiceResult error = errors[ii]; if (error == null) { results.Add(StatusCodes.Good); } else { results.Add(error.StatusCode); } // update diagnostics. if (ServiceResult.IsGood(error)) { RemoveItemToSamplingInterval( originalSamplingIntervals[ii], originalMonitoringModes[ii]); } if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0 && error != null && error.Code != StatusCodes.Good) { diagnosticInfos![ii] = ServerUtils.CreateDiagnosticInfo( m_server, context, error, m_logger)!; diagnosticsExist = true; } } // clear diagnostics if not required. if (!diagnosticsExist && diagnosticInfos != null) { diagnosticInfos.Clear(); } TraceState(LogLevel.Information, TraceStateId.Items, "ITEMS DELETED"); } return new DeleteMonitoredItemsResponse { Results = results, DiagnosticInfos = diagnosticInfos! }; } /// /// Changes the monitoring mode for a set of items. /// /// is null. public async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> SetMonitoringModeAsync( OperationContext context, MonitoringMode monitoringMode, ArrayOf monitoredItemIds, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } int count = monitoredItemIds.Count; bool diagnosticsExist = false; var results = new List(count); List? diagnosticInfos = null; if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos = new List(count); } // build list of items to modify. var monitoredItems = new List(count); var errors = new List(count); var originalMonitoringModes = new MonitoringMode[count]; bool validItems = false; lock (m_lock) { // check session. VerifySession(context); // clear lifetime counter. ResetLifetimeCount(); for (int ii = 0; ii < count; ii++) { if (!m_monitoredItems.TryGetValue( monitoredItemIds[ii], out LinkedListNode? node)) { monitoredItems.Add(null!); errors.Add(StatusCodes.BadMonitoredItemIdInvalid); // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { DiagnosticInfo? diagnosticInfo = ServerUtils.CreateDiagnosticInfo( m_server, context, errors[ii], m_logger); diagnosticsExist = true; diagnosticInfos!.Add(diagnosticInfo!); } continue; } IMonitoredItem monitoredItem = node.Value; monitoredItems.Add(monitoredItem); originalMonitoringModes[ii] = monitoredItem.MonitoringMode; errors.Add(null!); validItems = true; // update diagnostics. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos!.Add(null!); } } } // update items. if (validItems) { await m_server.NodeManager .SetMonitoringModeAsync(context, monitoringMode, monitoredItems, errors, cancellationToken) .ConfigureAwait(false); } lock (m_lock) { // update diagnostics. for (int ii = 0; ii < errors.Count; ii++) { ServiceResult error = errors[ii]; if (error == null) { results.Add(StatusCodes.Good); } else { results.Add(error.StatusCode); } // update diagnostics. if (ServiceResult.IsGood(error)) { ModifyItemMonitoringMode( monitoredItems[ii].SamplingInterval, originalMonitoringModes[ii], monitoringMode); } if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0 && error != null && error.Code != StatusCodes.Good) { diagnosticInfos![ii] = ServerUtils.CreateDiagnosticInfo( m_server, context, error, m_logger)!; diagnosticsExist = true; } } // clear diagnostics if not required. if (!diagnosticsExist && diagnosticInfos != null) { diagnosticInfos.Clear(); } if (monitoringMode == MonitoringMode.Disabled) { TraceState(LogLevel.Information, TraceStateId.Monitor, "MONITORING DISABLED"); } else if (monitoringMode == MonitoringMode.Reporting) { TraceState(LogLevel.Information, TraceStateId.Monitor, "REPORTING"); } else { TraceState(LogLevel.Information, TraceStateId.Monitor, "SAMPLING"); } } return (results, diagnosticInfos!); } /// /// Verifies that a condition refresh operation is permitted. /// /// public void ValidateConditionRefresh(OperationContext context) { lock (m_lock) { VerifySession(context); if (m_refreshInProgress) { throw new ServiceResultException(StatusCodes.BadRefreshInProgress); } } } /// /// Verifies that a condition refresh operation is permitted. /// /// public void ValidateConditionRefresh2(OperationContext context, uint monitoredItemId) { ValidateConditionRefresh(context); lock (m_lock) { if (!m_monitoredItems.ContainsKey(monitoredItemId)) { throw new ServiceResultException( StatusCodes.BadMonitoredItemIdInvalid, "Cannot refresh conditions for a monitored item that does not exist."); } } } /// /// Refreshes the conditions. /// public async ValueTask ConditionRefreshAsync(CancellationToken cancellationToken = default) { var monitoredItems = new List(); lock (m_lock) { // build list of items to refresh. foreach (LinkedListNode monitoredItem in m_monitoredItems.Values) { if (monitoredItem.Value is IEventMonitoredItem eventMonitoredItem && eventMonitoredItem.EventFilter != null) { // add to list that gets reported to the NodeManagers. monitoredItems.Add(eventMonitoredItem); } } // nothing to do if no event subscriptions. if (monitoredItems.Count == 0) { return; } } await ConditionRefreshAsync(monitoredItems, 0, cancellationToken) .ConfigureAwait(false); } /// /// Refreshes the conditions. /// /// public async ValueTask ConditionRefresh2Async(uint monitoredItemId, CancellationToken cancellationToken = default) { var monitoredItems = new List(); lock (m_lock) { // build list of items to refresh. if (m_monitoredItems.TryGetValue( monitoredItemId, out LinkedListNode? monitoredItem)) { if (monitoredItem.Value is IEventMonitoredItem eventMonitoredItem && eventMonitoredItem.EventFilter != null) { // add to list that gets reported to the NodeManagers. monitoredItems.Add(eventMonitoredItem); } } else { throw new ServiceResultException( StatusCodes.BadMonitoredItemIdInvalid, "Cannot refresh conditions for a monitored item that does not exist."); } // nothing to do if no event subscriptions. if (monitoredItems.Count == 0) { return; } } await ConditionRefreshAsync(monitoredItems, monitoredItemId, cancellationToken) .ConfigureAwait(false); } /// /// Refreshes the conditions. Works for both ConditionRefresh and ConditionRefresh2 /// private async ValueTask ConditionRefreshAsync( List monitoredItems, uint monitoredItemId, CancellationToken cancellationToken = default) { ServerSystemContext systemContext = m_server.DefaultSystemContext.Copy(Session); string messageTemplate = Utils.Format( "Condition refresh {{0}} for subscription {0}.", Id); if (monitoredItemId > 0) { messageTemplate = Utils.Format( "Condition refresh {{0}} for subscription {0}, monitored item {1}.", Id, monitoredItemId); } lock (m_lock) { // generate start event. var e = new RefreshStartEventState(null); var message = new TranslationInfo( "RefreshStartEvent", "en-US", Utils.Format(messageTemplate, "started")); e.Initialize(systemContext, null, EventSeverity.Low, new LocalizedText(message)); e.SetChildValue(systemContext, BrowseNames.SourceNode, m_diagnosticsId, false); e.SetChildValue( systemContext, BrowseNames.SourceName, Utils.Format("Subscription/{0}", Id), false); e.SetChildValue(systemContext, BrowseNames.ReceiveTime, DateTimeUtc.Now, false); // build list of items to refresh. foreach (IEventMonitoredItem monitoredItem in monitoredItems) { IEventMonitoredItem eventMonitoredItem = monitoredItem; if (eventMonitoredItem != null && eventMonitoredItem.EventFilter != null) { // queue start refresh event. eventMonitoredItem.QueueEvent(e, true); } } // nothing to do if no event subscriptions. if (monitoredItems.Count == 0) { return; } } // tell the NodeManagers to report the current state of the conditions. try { m_refreshInProgress = true; var operationContext = new OperationContext(Session, DiagnosticsMasks.None); await m_server.NodeManager.ConditionRefreshAsync(operationContext, monitoredItems, cancellationToken) .ConfigureAwait(false); } finally { m_refreshInProgress = false; } lock (m_lock) { // generate start event. var e = new RefreshEndEventState(null); var message = new TranslationInfo( "RefreshEndEvent", "en-US", Utils.Format(messageTemplate, "completed")); e.Initialize(systemContext, null, EventSeverity.Low, new LocalizedText(message)); e.SetChildValue(systemContext, BrowseNames.SourceNode, m_diagnosticsId, false); e.SetChildValue( systemContext, BrowseNames.SourceName, Utils.Format("Subscription/{0}", Id), false); e.SetChildValue(systemContext, BrowseNames.ReceiveTime, DateTimeUtc.Now, false); // send refresh end event. for (int ii = 0; ii < monitoredItems.Count; ii++) { IEventMonitoredItem monitoredItem = monitoredItems[ii]; if (monitoredItem.EventFilter != null) { monitoredItem.QueueEvent(e, true); } } // TraceState("CONDITION REFRESH"); } } /// /// Sets the subscription to durable mode. /// public ServiceResult SetSubscriptionDurable(uint maxLifetimeCount) { lock (m_lock) { if (!m_supportsDurable) { m_logger.LogError( "SetSubscriptionDurable requested for subscription with id {SubscriptionId}, but no IMonitoredItemQueueFactory that supports durable queues was registered", Id); TraceState( LogLevel.Information, TraceStateId.Config, "SetSubscriptionDurable Failed"); return StatusCodes.BadNotSupported; } IsDurable = true; // clear lifetime counter. ResetLifetimeCount(); m_maxLifetimeCount = maxLifetimeCount; // update diagnostics lock (DiagnosticsWriteLock) { Diagnostics.ModifyCount++; Diagnostics.MaxLifetimeCount = m_maxLifetimeCount; } TraceState(LogLevel.Information, TraceStateId.Config, "SET DURABLE"); return StatusCodes.Good; } } /// /// Gets the monitored items for the subscription. /// public void GetMonitoredItems(out ArrayOf serverHandles, out ArrayOf clientHandles) { lock (m_lock) { uint[] serverHandleList = new uint[m_monitoredItems.Count]; uint[] clientHandleList = new uint[m_monitoredItems.Count]; int ii = 0; foreach (KeyValuePair> entry in m_monitoredItems) { serverHandleList[ii] = entry.Key; clientHandleList[ii] = entry.Value.Value.ClientHandle; ii++; } serverHandles = serverHandleList.ToArrayOf(); clientHandles = clientHandleList.ToArrayOf(); } } /// /// Return a StorableSubscription for restore after a server restart /// public IStoredSubscription ToStorableSubscription() { var monitoredItemsToStore = new List(); foreach (KeyValuePair> kvp in m_monitoredItems) { monitoredItemsToStore.Add(kvp.Value.Value.ToStorableMonitoredItem()); } return new StoredSubscription { SentMessages = m_sentMessages, Id = Id, SequenceNumber = m_sequenceNumber, LastSentMessage = m_lastSentMessage, LifetimeCounter = m_lifetimeCounter, MaxKeepaliveCount = m_maxKeepAliveCount, MaxLifetimeCount = m_maxLifetimeCount, MaxMessageCount = m_maxMessageCount, MaxNotificationsPerPublish = m_maxNotificationsPerPublish, Priority = Priority, PublishingInterval = PublishingInterval, UserIdentityToken = EffectiveIdentity?.TokenHandler.Token!, MonitoredItems = monitoredItemsToStore, IsDurable = IsDurable }; } /// /// Restore MonitoredItems after a Server restart /// protected virtual async ValueTask RestoreMonitoredItemsAsync( IEnumerable storedMonitoredItems, CancellationToken cancellationToken = default) { int count = storedMonitoredItems.Count(); // create the monitored items. var monitoredItems = new List(count); for (int ii = 0; ii < count; ii++) { monitoredItems.Add(null!); } await m_server.NodeManager.RestoreMonitoredItemsAsync( [.. storedMonitoredItems], monitoredItems, m_savedOwnerIdentity!, cancellationToken).ConfigureAwait(false); lock (m_lock) { foreach (IMonitoredItem monitoredItem in monitoredItems) { // skip MonitoredItem if recreation failed if (monitoredItem == null) { continue; } monitoredItem.SubscriptionCallback = this; LinkedListNode node = m_itemsToCheck.AddLast(monitoredItem); m_monitoredItems.Add(monitoredItem.Id, node); // update sampling interval diagnostics. AddItemToSamplingInterval( monitoredItem.SamplingInterval, monitoredItem.MonitoringMode); } TraceState(LogLevel.Information, TraceStateId.Items, "ITEMS RESTORED"); } } /// /// Returns a copy of the current diagnostics. /// private ServiceResult OnUpdateDiagnostics( ISystemContext context, NodeState node, ref Variant value) { lock (DiagnosticsLock) { value = Variant.FromStructure(Diagnostics); } return ServiceResult.Good; } /// /// Throws an exception if the session is not the owner. /// /// private void VerifySession(OperationContext context) { if (m_expired) { throw new ServiceResultException(StatusCodes.BadSubscriptionIdInvalid); } if (!ReferenceEquals(context.Session, Session)) { throw new ServiceResultException( StatusCodes.BadSubscriptionIdInvalid, "Subscription belongs to a different session."); } } /// /// The states to log. /// private enum TraceStateId { Config, Items, Monitor, Publish, Deleted } /// /// Dumps the current state of the session queue. /// private void TraceState(LogLevel logLevel, TraceStateId id, string context) { if (!m_logger.IsEnabled(logLevel)) { return; } // save counters Monitor.Enter(m_lock); long sequenceNumber = m_sequenceNumber; int itemsToCheck = m_itemsToCheck.Count; int monitoredItems = m_monitoredItems.Count; int itemsToPublish = m_itemsToPublish.Count; int sentMessages = m_sentMessages.Count; bool publishingEnabled = m_publishingEnabled; bool waitingForPublish = m_waitingForPublish; Monitor.Exit(m_lock); switch (id) { case TraceStateId.Deleted: m_logger.Log( logLevel, "Subscription {Subscription}, SessionId={SessionId}, Id={SubscriptionId}, SeqNo={SequenceNumber}, MessageCount={MessageCount}", context, Session?.Id, Id, sequenceNumber, sentMessages); break; case TraceStateId.Config: m_logger.Log( logLevel, "Subscription {Subscription}, SessionId={SessionId}, Id={SubscriptionId}, Priority={Priority}, Publishing={Publishing}, KeepAlive={KeepAlive}, LifeTime={LifeTime}, MaxNotifications={MaxNotifications}, Enabled={Enabled}", context, Session?.Id, Id, Priority, m_publishingInterval, m_maxKeepAliveCount, m_maxLifetimeCount, m_maxNotificationsPerPublish, publishingEnabled); break; case TraceStateId.Items: m_logger.Log( logLevel, "Subscription {Subscription}, Id={SubscriptionId}, ItemCount={ItemCount}, ItemsToCheck={ItemsToCheck}, ItemsToPublish={ItemsToPublish}", context, Id, monitoredItems, itemsToCheck, itemsToPublish); break; case TraceStateId.Publish: case TraceStateId.Monitor: m_logger.Log( logLevel, "Subscription {Subscription}, Id={SubscriptionId}, KeepAliveCounter={KeepAliveCounter}, LifeTimeCount={LifeTimeCount}, WaitingForPublish={WaitingForPublish}, SeqNo={SequenceNumber}, ItemCount={ItemCount}, ItemsToCheck={ItemsToCheck}, ItemsToPublish={ItemsToPublish}, MessageCount={MessageCount}", context, Id, m_keepAliveCounter, m_lifetimeCounter, waitingForPublish, sequenceNumber, monitoredItems, itemsToCheck, itemsToPublish, sentMessages); break; default: Debug.Fail($"Unexpected TraceStateId: {id}"); break; } } private readonly object m_lock = new(); private readonly IServerInternal m_server; private readonly TimeProvider m_timeProvider; private IUserIdentity? m_savedOwnerIdentity; private double m_publishingInterval; private uint m_maxLifetimeCount; private uint m_maxKeepAliveCount; private uint m_maxNotificationsPerPublish; private bool m_publishingEnabled; private long m_publishTimerExpiry; private uint m_keepAliveCounter; private uint m_lifetimeCounter; private bool m_waitingForPublish; private readonly List m_sentMessages; private int m_lastSentMessage; private uint m_sequenceNumber; private readonly uint m_maxMessageCount; private readonly Dictionary> m_monitoredItems; private readonly LinkedList m_itemsToCheck; private readonly LinkedList m_itemsToPublish; private readonly NodeId m_diagnosticsId; private bool m_refreshInProgress; private bool m_expired; private readonly Dictionary> m_itemsToTrigger; private readonly bool m_supportsDurable; private readonly ILogger m_logger; /// /// Walk a that is being /// discarded (acknowledged or evicted from the retransmission /// queue) and return every pooled notification payload to its /// activator's pool via . /// private static void ReuseNotificationPayloads(NotificationMessage message) { ReadOnlySpan data = message.NotificationData.Span; for (int i = 0; i < data.Length; i++) { if (data[i].TryGetValue(out DataChangeNotification? dcn)) { ReadOnlySpan items = dcn.MonitoredItems.Span; for (int j = 0; j < items.Length; j++) { (items[j] as IPooledEncodeable)?.Reuse(); } (dcn as IPooledEncodeable)?.Reuse(); } else if (data[i].TryGetValue(out EventNotificationList? enl)) { ReadOnlySpan events = enl.Events.Span; for (int j = 0; j < events.Length; j++) { (events[j] as IPooledEncodeable)?.Reuse(); } (enl as IPooledEncodeable)?.Reuse(); } } (message as IPooledEncodeable)?.Reuse(); } } }