/* ======================================================================== * 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.Client { /// /// The classic (V1) subscription engine implementation. /// Uses fire-and-forget publish requests with task continuations. /// public class ClassicSubscriptionEngine : ISubscriptionEngine { /// /// Initializes a new instance of the /// class. /// /// The session context that provides /// access to session state and services. /// Optional /// used for throttling and back-off delays. Defaults to /// when null. public ClassicSubscriptionEngine( ISubscriptionEngineContext context, TimeProvider? timeProvider = null) { m_context = context ?? throw new ArgumentNullException(nameof(context)); m_logger = context.Telemetry .CreateLogger(); m_minPublishRequestCount = kDefaultPublishRequestCount; m_maxPublishRequestCount = kMaxPublishRequestCountMax; m_timeProvider = timeProvider ?? TimeProvider.System; } /// public int GoodPublishRequestCount => m_context.GoodPublishRequestCount; /// public int BadPublishRequestCount => 0; /// public int PublishWorkerCount => 0; /// public int MinPublishRequestCount { get => m_minPublishRequestCount; set { if (value is >= kDefaultPublishRequestCount and <= kMinPublishRequestCountMax) { m_minPublishRequestCount = value; } else { throw new ArgumentOutOfRangeException( nameof(MinPublishRequestCount), "Minimum publish request count must be between" + $" {kDefaultPublishRequestCount} and {kMinPublishRequestCountMax}."); } } } /// public int MaxPublishRequestCount { get => Math.Max( m_minPublishRequestCount, m_maxPublishRequestCount); set { if (value is >= kDefaultPublishRequestCount and <= kMaxPublishRequestCountMax) { m_maxPublishRequestCount = value; } else { throw new ArgumentOutOfRangeException( nameof(MaxPublishRequestCount), "Maximum publish request count must be between " + $"{kDefaultPublishRequestCount} and {kMaxPublishRequestCountMax}."); } } } /// public void StartPublishing(int timeout, bool fullQueue) { int publishCount = GetDesiredPublishRequestCount(true); // refill pipeline. Send at least one publish request // if subscriptions are active. if (publishCount > 0 && BeginPublish(timeout)) { int startCount = fullQueue ? 1 : GoodPublishRequestCount + 1; for (int ii = startCount; ii < publishCount; ii++) { if (!BeginPublish(timeout)) { break; } } } } /// public ValueTask StopPublishingAsync( CancellationToken ct = default) { return default; } /// public void PausePublishing() { // The classic engine relies on the reconnect lock in // OnPublishComplete to gate processing during reconnect. } /// public void ResumePublishing() { // The classic engine relies on the reconnect lock in // OnPublishComplete to gate processing during reconnect. } /// public void NotifySubscriptionsChanged() { QueueBeginPublish(); } /// public ValueTask RecreateSubscriptionsAsync( NodeId? previousSessionId, CancellationToken ct = default) { // The classic engine does not own subscription state; the // host Session drives recreate via subscription templates // through Session.RecreateSubscriptionsAsync. No-op here. return default; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases the unmanaged resources used by the engine and /// optionally releases the managed resources. /// /// True to release both managed and /// unmanaged resources. protected virtual void Dispose(bool disposing) { m_disposed = true; } /// /// Sends a publish request to the server. /// /// The timeout for publish requests /// in milliseconds. /// True if the request was sent successfully. internal bool BeginPublish(int timeout) { // do not publish if reconnecting or the session is in closed state. if (!m_context.Connected) { m_logger.LogWarning("Publish skipped due to session not connected"); return false; } if (m_context.Reconnecting) { m_logger.LogWarning("Publish skipped due to session reconnect"); return false; } if (m_context.Closing) { m_logger.LogWarning("Publish cancelled due to session closed"); return false; } if (m_context.KeepAliveStopped) { m_logger.LogWarning( "Publish skipped due to session lost connection. " + "Last successful keepalive: {LastKeepAlive}", m_context.LastKeepAliveTime); return false; } // collect the current set of acknowledgements. List? acknowledgementsToSend = null; lock (m_acknowledgementsToSendLock) { (List toSend, List updatedPending) = m_context.PrepareAcknowledgementsToSend(m_acknowledgementsToSend); acknowledgementsToSend = toSend; m_acknowledgementsToSend = updatedPending; #if DEBUG_SEQUENTIALPUBLISHING foreach (var toSend in acknowledgementsToSend) { m_latestAcknowledgementsSent[toSend.SubscriptionId] = toSend.SequenceNumber; } #endif } uint timeoutHint = timeout > 0 ? (uint)timeout : uint.MaxValue; timeoutHint = Math.Min( (uint)(m_context.OperationTimeout / 2), timeoutHint); // send publish request. var requestHeader = new RequestHeader { // ensure the publish request is discarded before the // timeout occurs to ensure the channel is dropped. TimeoutHint = timeoutHint, ReturnDiagnostics = (uint)(int)m_context.ReturnDiagnostics, RequestHandle = Utils.IncrementIdentifier(ref PublishCounter) }; m_logger.LogTrace( "PUBLISH #{RequestHandle} SENT", requestHeader.RequestHandle); CoreClientUtils.EventLog.PublishStart( (int)requestHeader.RequestHandle); try { Activity? activity = m_context.Telemetry .StartActivity(); Task task = m_context.PublishAsync( requestHeader, acknowledgementsToSend, default).AsTask(); m_context.AsyncRequestStarted( task, activity, requestHeader.RequestHandle, DataTypes.PublishRequest); task.ConfigureAwait(false) .GetAwaiter() .OnCompleted(() => OnPublishComplete( task, m_context.SessionId, acknowledgementsToSend, requestHeader)); return true; } catch (Exception e) { m_logger.LogError( e, "Unexpected error sending publish request."); return false; } } /// /// Completes an asynchronous publish operation. /// private void OnPublishComplete( Task task, NodeId sessionId, List? acknowledgementsToSend, RequestHeader requestHeader) { // extract state information. uint subscriptionId = 0; m_context.AsyncRequestCompleted( task, requestHeader.RequestHandle, DataTypes.PublishRequest); m_logger.LogTrace( "PUBLISH #{RequestHandle} RECEIVED", requestHeader.RequestHandle); CoreClientUtils.EventLog.PublishStop( (int)requestHeader.RequestHandle); // Bail out early if the session has been disposed. if (m_context.Disposed) { return; } try { // gate entry if transfer/reactivate is busy try { m_context.ReconnectLock.Wait(); } catch (ObjectDisposedException) { return; } bool reconnecting = m_context.Reconnecting; try { m_context.ReconnectLock.Release(); } catch (ObjectDisposedException) { return; } // complete publish. PublishResponse response = task.Result; ResponseHeader responseHeader = response.ResponseHeader; subscriptionId = response.SubscriptionId; ArrayOf availableSequenceNumbers = response.AvailableSequenceNumbers; bool moreNotifications = response.MoreNotifications; NotificationMessage notificationMessage = response.NotificationMessage; ArrayOf acknowledgeResults = response.Results; LogLevel logLevel = LogLevel.Warning; foreach (StatusCode code in acknowledgeResults) { if (StatusCode.IsBad(code) && code != StatusCodes.BadSequenceNumberUnknown) { m_logger.Log( logLevel, "Publish Ack Response. ResultCode={StatusCode}; " + "SubscriptionId={SubscriptionId}", code, subscriptionId); // only show the first error as warning logLevel = LogLevel.Trace; } } // nothing more to do if we were never connected if (sessionId.IsNull) { return; } // nothing more to do if session changed. if (sessionId != m_context.SessionId) { m_logger.LogWarning( "Publish response discarded because session " + "id changed: Old {PreviousSessionId} != " + "New {SessionId}", sessionId, m_context.SessionId); return; } m_logger.LogTrace( "NOTIFICATION RECEIVED: SubId={SubscriptionId}" + ", SeqNo={SequenceNumber}", subscriptionId, notificationMessage.SequenceNumber); CoreClientUtils.EventLog.NotificationReceived( (int)subscriptionId, (int)notificationMessage.SequenceNumber); // process response. ProcessPublishResponse( responseHeader, subscriptionId, availableSequenceNumbers, moreNotifications, notificationMessage); // nothing more to do if reconnecting. if (reconnecting) { m_logger.LogWarning( "No new publish sent because of " + "reconnect in progress."); return; } } catch (Exception e) { IReadOnlyList subscriptions = m_context.Subscriptions; if (subscriptions.Count == 0) { m_logger.LogWarning( "Publish #{RequestHandle}, " + "Subscription count = 0, Error: {Message}", requestHeader.RequestHandle, e.Message); } else { m_logger.LogError( "Publish #{RequestHandle}, " + "Reconnecting={Reconnecting}, " + "Error: {Message}", requestHeader.RequestHandle, m_context.Reconnecting, e.Message); } // raise an error event. var error = new ServiceResult(e); // raise publish error even for BadNoSubscription // if there are active subscriptions. if (error.Code != StatusCodes.BadNoSubscription || subscriptions.Any(s => s.Created)) { m_context.OnPublishError( error, subscriptionId, 0); } // ignore errors if reconnecting if (m_context.Reconnecting) { m_logger.LogInformation( "Publish abandoned after error {Message} " + "due to session {SessionId} reconnecting", e.Message, sessionId); return; } // nothing more to do if session changed. if (sessionId != m_context.SessionId) { if (m_context.Connected) { m_logger.LogError( "Publish abandoned after error " + "{Message} because session id changed:" + " Old {PreviousSessionId} != " + "New {SessionId}", e.Message, sessionId, m_context.SessionId); } else { m_logger.LogInformation( "Publish abandoned after error " + "{Message} because session {SessionId}" + " was closed.", e.Message, sessionId); } return; } // try to acknowledge the notifications again in // the next publish. if (acknowledgementsToSend != null) { lock (m_acknowledgementsToSendLock) { m_acknowledgementsToSend.AddRange( acknowledgementsToSend); } } // don't send another publish for these errors, // or throttle to avoid server overload. if (error.StatusCode == StatusCodes.BadTooManyPublishRequests) { int tooManyPublishRequests = GoodPublishRequestCount; if (BelowPublishRequestLimit( tooManyPublishRequests)) { m_tooManyPublishRequests = tooManyPublishRequests; m_logger.LogInformation( "PUBLISH - Too many requests, " + "set limit to " + "GoodPublishRequestCount=" + "{GoodRequestCount}.", m_tooManyPublishRequests); } return; } if (error.StatusCode == StatusCodes.BadNoSubscription || error.StatusCode == StatusCodes.BadSessionClosed || error.StatusCode == StatusCodes.BadSecurityChecksFailed || error.StatusCode == StatusCodes.BadCertificateInvalid || error.StatusCode == StatusCodes.BadServerHalted) { return; } if (error.StatusCode == StatusCodes.BadSessionIdInvalid || error.StatusCode == StatusCodes.BadSecureChannelIdInvalid || error.StatusCode == StatusCodes.BadSecureChannelClosed) { m_context.OnKeepAliveError(error); return; } // Servers may return this error when overloaded if (error.StatusCode != StatusCodes.BadTimeout && error.StatusCode != StatusCodes.BadRequestTimeout) { if (error.StatusCode != StatusCodes.BadTooManyOperations && error.StatusCode != StatusCodes.BadTcpServerTooBusy && error.StatusCode != StatusCodes.BadServerTooBusy) { m_logger.LogError( e, "PUBLISH #{RequestHandle} - " + "Unhandled error {StatusCode} " + "during Publish.", requestHeader.RequestHandle, error.StatusCode); } // throttle the next publish to reduce // server load _ = Task.Run(async () => { await m_timeProvider.Delay(TimeSpan.FromMilliseconds(100)) .ConfigureAwait(false); QueueBeginPublish(); }); return; } } QueueBeginPublish(); } /// /// Processes the response from a publish request. /// internal void ProcessPublishResponse( ResponseHeader responseHeader, uint subscriptionId, ArrayOf availableSequenceNumbers, bool moreNotifications, NotificationMessage notificationMessage) { Subscription? subscription = null; var availableSequenceNumberList = availableSequenceNumbers.ToList(); // send notification that the server is alive. m_context.OnKeepAlive( m_context.ServerState, (DateTime)responseHeader.Timestamp); // collect the current set of acknowledgements. lock (m_acknowledgementsToSendLock) { // clear out acknowledgements for messages that the // server does not have any more. var acknowledgementsToSend = new List(); uint latestSequenceNumberToSend = 0; // create an acknowledgement to be sent back to the // server. if (notificationMessage.NotificationData.Count > 0) { AddAcknowledgementToSend( acknowledgementsToSend, subscriptionId, notificationMessage.SequenceNumber); UpdateLatestSequenceNumberToSend( ref latestSequenceNumberToSend, notificationMessage.SequenceNumber); availableSequenceNumberList.Remove(notificationMessage.SequenceNumber); } // match an acknowledgement to be sent back to the // server. for (int ii = 0; ii < m_acknowledgementsToSend.Count; ii++) { SubscriptionAcknowledgement acknowledgement = m_acknowledgementsToSend[ii]; if (acknowledgement.SubscriptionId != subscriptionId) { acknowledgementsToSend.Add(acknowledgement); } else if (availableSequenceNumberList.Remove(acknowledgement.SequenceNumber)) { acknowledgementsToSend.Add(acknowledgement); UpdateLatestSequenceNumberToSend( ref latestSequenceNumberToSend, acknowledgement.SequenceNumber); } // a publish response may be processed out of // order, allow for a tolerance until the // sequence number is removed. else if (Math.Abs((int)(acknowledgement.SequenceNumber - latestSequenceNumberToSend)) < kPublishRequestSequenceNumberOutOfOrderThreshold) { acknowledgementsToSend.Add(acknowledgement); } else { m_logger.LogWarning( "SessionId {SessionId}, SubscriptionId {SubscriptionId}, Sequence number=" + "{SequenceNumber} was not received in the available sequence numbers.", m_context.SessionId, subscriptionId, acknowledgement.SequenceNumber); } } // Check for outdated sequence numbers. May have // been not acked due to a network glitch. if (latestSequenceNumberToSend != 0 && availableSequenceNumberList.Count > 0) { foreach (uint sequenceNumber in availableSequenceNumberList) { if ((int)(latestSequenceNumberToSend - sequenceNumber) > kPublishRequestSequenceNumberOutdatedThreshold) { AddAcknowledgementToSend( acknowledgementsToSend, subscriptionId, sequenceNumber); m_logger.LogWarning( "SessionId {SessionId}, SubscriptionId {SubscriptionId}, " + "Sequence number={SequenceNumber} was outdated, acknowledged.", m_context.SessionId, subscriptionId, sequenceNumber); } } } m_acknowledgementsToSend = acknowledgementsToSend; if (notificationMessage.IsEmpty) { m_logger.LogTrace( "Empty notification message received " + "for SessionId {SessionId} with " + "PublishTime {PublishTime}", m_context.SessionId, notificationMessage.PublishTime .ToDateTime().ToLocalTime()); } } bool subscriptionCreationInProgress = false; // find the subscription. foreach (Subscription current in m_context.Subscriptions) { if (current.Id == subscriptionId) { subscription = current; break; } if (current.Id == default) { // Subscription is being created, disable // cleanup mechanism subscriptionCreationInProgress = true; } } // ignore messages with a subscription that has been // deleted. if (subscription != null) { #if DEBUG // Validate publish time and reject old values. if (notificationMessage.PublishTime.AddMilliseconds( subscription.CurrentPublishingInterval * subscription.CurrentLifetimeCount) < DateTimeUtc.Now) { m_logger.LogTrace( "PublishTime {PublishTime} in publish response is too old for " + "SubscriptionId {SubscriptionId}.", notificationMessage.PublishTime.ToLocalTime(), subscription.Id); } // Validate publish time and reject future values. if (notificationMessage.PublishTime > DateTimeUtc.Now.AddMilliseconds( subscription.CurrentPublishingInterval * subscription.CurrentLifetimeCount)) { m_logger.LogTrace( "PublishTime {PublishTime} in publish response is newer than actual time " + "for SubscriptionId {SubscriptionId}.", notificationMessage.PublishTime.ToLocalTime(), subscription.Id); } #endif // save the information that more notifications // are expected notificationMessage.MoreNotifications = moreNotifications; // save the string table that came with the // notification. notificationMessage.StringTable = responseHeader.StringTable; // update subscription cache. subscription.SaveMessageInCache(availableSequenceNumberList, notificationMessage); // raise the notification. var args = new NotificationEventArgs( subscription, notificationMessage, responseHeader.StringTable); m_context.OnPublishNotification(subscription, args); } else if (m_context.DeleteSubscriptionsOnClose && !m_context.Reconnecting && !subscriptionCreationInProgress) { // Delete abandoned subscription from server. m_logger.LogWarning( "Received Publish Response for Unknown SubscriptionId={SubscriptionId}. " + "Deleting abandoned subscription from server.", subscriptionId); _ = Task.Run( () => m_context.DeleteOrphanedSubscriptionAsync(subscriptionId)); } else { // Do not delete publish requests of stale // subscriptions m_logger.LogWarning( "Received Publish Response for Unknown SubscriptionId={SubscriptionId}. Ignored.", subscriptionId); } } /// /// Queues a publish request if there are not enough /// outstanding requests. /// private void QueueBeginPublish() { if (m_context.Disposed || m_disposed) { return; } int requestCount = GoodPublishRequestCount; int minPublishRequestCount = GetDesiredPublishRequestCount(false); if (requestCount < minPublishRequestCount) { BeginPublish(m_context.OperationTimeout); } else { m_logger.LogDebug( "PUBLISH - Did not send another publish request. GoodPublishRequestCount=" + "{GoodRequestCount}, MinPublishRequestCount={MinRequestCount}", requestCount, minPublishRequestCount); } } /// /// Returns the desired number of active publish requests /// that should be used. /// /// /// Returns 0 if there are no subscriptions. /// /// False if called when re-queuing. /// The number of desired publish requests for the session. protected virtual int GetDesiredPublishRequestCount(bool createdOnly) { IReadOnlyList subscriptions = m_context.Subscriptions; if (subscriptions.Count == 0) { return 0; } int publishCount; if (createdOnly) { int count = 0; foreach (Subscription subscription in subscriptions) { if (subscription.Created) { count++; } } if (count == 0) { return 0; } publishCount = count; } else { publishCount = subscriptions.Count; } // If a dynamic limit was set because of // BadTooManyPublishRequest error, limit the number // of publish requests to this value. if (m_tooManyPublishRequests > 0 && publishCount > m_tooManyPublishRequests) { publishCount = m_tooManyPublishRequests; } // Limit resulting to a number between min and max // request count. If max is below min, we honor the // min publish request count. if (publishCount > m_maxPublishRequestCount) { publishCount = m_maxPublishRequestCount; } if (publishCount < m_minPublishRequestCount) { publishCount = m_minPublishRequestCount; } return publishCount; } /// /// Adds an acknowledgement to the list to send. /// /// /// is null. private void AddAcknowledgementToSend( List acknowledgementsToSend, uint subscriptionId, uint sequenceNumber) { if (acknowledgementsToSend == null) { throw new ArgumentNullException(nameof(acknowledgementsToSend)); } Debug.Assert(Monitor.IsEntered(m_acknowledgementsToSendLock)); acknowledgementsToSend.Add(new SubscriptionAcknowledgement { SubscriptionId = subscriptionId, SequenceNumber = sequenceNumber }); } /// /// Adds an acknowledgement to the pending list. Called /// by Session when re-creating subscriptions after /// transfer. /// /// The subscription ID to /// acknowledge. /// The sequence number to /// acknowledge. internal void AddPendingAcknowledgement( uint subscriptionId, uint sequenceNumber) { lock (m_acknowledgementsToSendLock) { AddAcknowledgementToSend( m_acknowledgementsToSend, subscriptionId, sequenceNumber); } } /// /// Drops every queued acknowledgement targeting the given /// . Used by the /// recovery-on-unsolicited-transfer path to prevent /// BadSubscriptionIdInvalid ack responses from /// reaching the server after the local subscription has /// been invalidated and is about to be recreated. Servers /// that re-use subscription identifiers across generations /// (e.g. Kepware always starting at 1) require this /// to run while the old id is still "uniquely dead" — i.e. /// before a recreate assigns a new id. /// /// The subscription id whose /// queued acknowledgements should be dropped. /// The number of queued acknowledgements that /// were dropped. internal int RemoveAcknowledgementsForSubscription(uint subscriptionId) { lock (m_acknowledgementsToSendLock) { int before = m_acknowledgementsToSend.Count; if (before == 0) { return 0; } m_acknowledgementsToSend.RemoveAll( ack => ack.SubscriptionId == subscriptionId); return before - m_acknowledgementsToSend.Count; } } /// /// Helper to update the latest sequence number to send. /// Handles wrap around of sequence numbers. /// private static void UpdateLatestSequenceNumberToSend( ref uint latestSequenceNumberToSend, uint sequenceNumber) { // Handle wrap around with subtraction and test // result is int. Assume sequence numbers to ack // do not differ by more than uint.Max / 2 if (latestSequenceNumberToSend == 0 || ((int)(sequenceNumber - latestSequenceNumberToSend)) > 0) { latestSequenceNumberToSend = sequenceNumber; } } /// /// Returns true if the Bad_TooManyPublishRequests limit /// has not been reached. /// /// The actual number of /// publish requests. /// If the publish request limit was reached. private bool BelowPublishRequestLimit(int requestCount) { return (m_tooManyPublishRequests == 0) || (requestCount < m_tooManyPublishRequests); } /// /// Processes an error from a republish response. /// /// The exception. /// The subscription identifier. /// The sequence number. /// A tuple indicating whether the error was /// handled and the service result. internal (bool handled, ServiceResult error) ProcessRepublishResponseError( Exception e, uint subscriptionId, uint sequenceNumber) { var error = new ServiceResult(e); bool result = true; if (error.StatusCode == StatusCodes.BadSubscriptionIdInvalid || error.StatusCode == StatusCodes.BadMessageNotAvailable) { m_logger.LogWarning( "Message {SubscriptionId}-{SequenceNumber} no longer available.", subscriptionId, sequenceNumber); } else if (error.StatusCode == StatusCodes.BadEncodingLimitsExceeded) { m_logger.LogError( e, "Message {SubscriptionId}-{SequenceNumber} exceeded size limits, ignored.", subscriptionId, sequenceNumber); lock (m_acknowledgementsToSendLock) { AddAcknowledgementToSend( m_acknowledgementsToSend, subscriptionId, sequenceNumber); } } else { result = false; m_logger.LogError(e, "Unexpected error sending republish request."); } // raise an error event. m_context.OnPublishError( error, subscriptionId, sequenceNumber); return (result, error); } private const int kMinPublishRequestCountMax = 100; private const int kMaxPublishRequestCountMax = ushort.MaxValue; private const int kDefaultPublishRequestCount = 1; private const int kPublishRequestSequenceNumberOutOfOrderThreshold = 10; private const int kPublishRequestSequenceNumberOutdatedThreshold = 100; private readonly ISubscriptionEngineContext m_context; private readonly ILogger m_logger; private readonly TimeProvider m_timeProvider; private readonly object m_acknowledgementsToSendLock = new(); private List m_acknowledgementsToSend = []; internal uint PublishCounter; private int m_tooManyPublishRequests; private int m_minPublishRequestCount; private int m_maxPublishRequestCount; private bool m_disposed; } }