/// <summary>
 ///   Initializes a new instance of the <see cref="PartitionContext"/> class.
 /// </summary>
 ///
 /// <param name="eventHubName">The name of the Event Hub that this context is associated with.</param>
 /// <param name="partitionId">The identifier of the Event Hub partition this context is associated with.</param>
 /// <param name="consumer">The <see cref="TransportConsumer" /> for this context to use as the source for information.</param>
 ///
 /// <remarks>
 ///   The <paramref name="consumer" />, if provided, will be held in a weak reference to ensure that it
 ///   does not impact resource use should the partition context be held beyond the lifespan of the
 ///   consumer instance.
 /// </remarks>
 ///
 internal PartitionContext(string eventHubName,
                           string partitionId,
                           TransportConsumer consumer) : this(eventHubName, partitionId)
 {
     Argument.AssertNotNull(consumer, nameof(consumer));
     SourceConsumer = new WeakReference <TransportConsumer>(consumer);
 }
        /// <summary>
        ///   Initializes a new instance of the <see cref="EventHubConsumerClient"/> class.
        /// </summary>
        ///
        /// <param name="consumerGroup">The name of the consumer group this consumer is associated with.  Events are read in the context of this group.</param>
        /// <param name="partitionId">The identifier of the Event Hub partition from which events will be received.</param>
        /// <param name="eventPosition">The position within the partition where the consumer should begin reading events.</param>
        /// <param name="fullyQualifiedNamespace">The fully qualified Event Hubs namespace to connect to.  This is likely to be similar to <c>{yournamespace}.servicebus.windows.net</c>.</param>
        /// <param name="eventHubName">The name of the specific Event Hub to associate the consumer with.</param>
        /// <param name="credential">The Azure managed identity credential to use for authorization.  Access controls may be specified by the Event Hubs namespace or the requested Event Hub, depending on Azure configuration.</param>
        /// <param name="consumerOptions">A set of options to apply when configuring the consumer.</param>
        ///
        public EventHubConsumerClient(string consumerGroup,
                                      string partitionId,
                                      EventPosition eventPosition,
                                      string fullyQualifiedNamespace,
                                      string eventHubName,
                                      TokenCredential credential,
                                      EventHubConsumerClientOptions consumerOptions = default)
        {
            Argument.AssertNotNullOrEmpty(consumerGroup, nameof(consumerGroup));
            Argument.AssertNotNullOrEmpty(partitionId, nameof(partitionId));
            Argument.AssertNotNull(eventPosition, nameof(eventPosition));
            Argument.AssertNotNullOrEmpty(fullyQualifiedNamespace, nameof(fullyQualifiedNamespace));
            Argument.AssertNotNullOrEmpty(eventHubName, nameof(eventHubName));
            Argument.AssertNotNull(credential, nameof(credential));

            consumerOptions = consumerOptions?.Clone() ?? new EventHubConsumerClientOptions();

            OwnsConnection = true;
            Connection     = new EventHubConnection(fullyQualifiedNamespace, eventHubName, credential, consumerOptions.ConnectionOptions);
            ConsumerGroup  = consumerGroup;
            Options        = consumerOptions;
            RetryPolicy    = consumerOptions.RetryOptions.ToRetryPolicy();
            InnerConsumer  = Connection.CreateTransportConsumer(consumerGroup, partitionId, eventPosition, consumerOptions);

            PartitionId      = partitionId;
            StartingPosition = eventPosition;
        }
        /// <summary>
        ///   Initializes a new instance of the <see cref="PartitionReceiver"/> class.
        /// </summary>
        ///
        /// <param name="consumerGroup">The name of the consumer group this client is associated with.  Events are read in the context of this group.</param>
        /// <param name="partitionId">The identifier of the Event Hub partition from which events will be received.</param>
        /// <param name="eventPosition">The position within the partition where the client should begin reading events.</param>
        /// <param name="fullyQualifiedNamespace">The fully qualified Event Hubs namespace to connect to.  This is likely to be similar to <c>{yournamespace}.servicebus.windows.net</c>.</param>
        /// <param name="eventHubName">The name of the specific Event Hub to associate the client with.</param>
        /// <param name="credential">The Azure managed identity credential to use for authorization.  Access controls may be specified by the Event Hubs namespace or the requested Event Hub, depending on Azure configuration.</param>
        /// <param name="options">A set of options to apply when configuring the client.</param>
        ///
        public PartitionReceiver(string consumerGroup,
                                 string partitionId,
                                 EventPosition eventPosition,
                                 string fullyQualifiedNamespace,
                                 string eventHubName,
                                 TokenCredential credential,
                                 PartitionReceiverOptions options = default)
        {
            Argument.AssertNotNullOrEmpty(consumerGroup, nameof(consumerGroup));
            Argument.AssertNotNullOrEmpty(partitionId, nameof(partitionId));
            Argument.AssertWellFormedEventHubsNamespace(fullyQualifiedNamespace, nameof(fullyQualifiedNamespace));
            Argument.AssertNotNullOrEmpty(eventHubName, nameof(eventHubName));
            Argument.AssertNotNull(credential, nameof(credential));

            options = options?.Clone() ?? new PartitionReceiverOptions();

            Connection             = new EventHubConnection(fullyQualifiedNamespace, eventHubName, credential, options.ConnectionOptions);
            ConsumerGroup          = consumerGroup;
            PartitionId            = partitionId;
            InitialPosition        = eventPosition;
            DefaultMaximumWaitTime = options.DefaultMaximumReceiveWaitTime;
            RetryPolicy            = options.RetryOptions.ToRetryPolicy();

#pragma warning disable CA2214 // Do not call overridable methods in constructors. This internal method is virtual for testing purposes.
            InnerConsumer = CreateTransportConsumer(consumerGroup, partitionId, eventPosition, RetryPolicy, options);
#pragma warning restore CA2214 // Do not call overridable methods in constructors.
        }
 internal ServiceBusSession(
     TransportConsumer consumer,
     ServiceBusRetryPolicy retryPolicy)
 {
     _consumer    = consumer;
     _retryPolicy = retryPolicy;
 }
Пример #5
0
 internal ServiceBusSession(
     TransportConsumer consumer,
     string sessionId)
 {
     _consumer = consumer;
     UserSpecifiedSessionId = sessionId;
 }
Пример #6
0
 /// <summary>
 ///   Initializes a new instance of the <see cref="PartitionContext"/> class.
 /// </summary>
 ///
 /// <param name="fullyQualifiedNamespace">The fully qualified Event Hubs namespace this context is associated with.</param>
 /// <param name="eventHubName">The name of the Event Hub partition this context is associated with.</param>
 /// <param name="consumerGroup">The name of the consumer group this context is associated with.</param>
 /// <param name="partitionId">The identifier of the Event Hub partition this context is associated with.</param>
 /// <param name="consumer">The <see cref="TransportConsumer" /> for this context to use as the source for information.</param>
 ///
 /// <remarks>
 ///   The <paramref name="consumer" />, if provided, will be held in a weak reference to ensure that it
 ///   does not impact resource use should the partition context be held beyond the lifespan of the
 ///   consumer instance.
 /// </remarks>
 ///
 internal PartitionContext(string fullyQualifiedNamespace,
                           string eventHubName,
                           string consumerGroup,
                           string partitionId,
                           TransportConsumer consumer) : this(fullyQualifiedNamespace, eventHubName, consumerGroup, partitionId)
 {
     Argument.AssertNotNull(consumer, nameof(consumer));
     SourceConsumer = new WeakReference <TransportConsumer>(consumer);
 }
        /// <summary>
        ///   Initializes a new instance of the <see cref="PartitionReceiver"/> class.
        /// </summary>
        ///
        /// <param name="consumerGroup">The name of the consumer group this receiver is associated with.  Events are read in the context of this group.</param>
        /// <param name="partitionId">The identifier of the Event Hub partition from which events will be received.</param>
        /// <param name="eventHubName">The name of the specific Event Hub to associate the receiver with.</param>
        /// <param name="trackingLastEnqueuedEvent">A flag indicating whether or not the <paramref name="transportConsumer" /> is tracking last enqueued event information.</param>
        /// <param name="defaultMaximumReceiveWaitTime">The default amount of time to wait for the requested amount of messages when receiving.</param>
        /// <param name="transportConsumer">The transport consumer that is used for operations performed against the Event Hubs service.</param>
        ///
        internal PartitionReceiver(string consumerGroup,
                                   string partitionId,
                                   string eventHubName,
                                   bool trackingLastEnqueuedEvent,
                                   TimeSpan defaultMaximumReceiveWaitTime,
                                   TransportConsumer transportConsumer) : this(consumerGroup, partitionId, eventHubName)
        {
            Argument.AssertNotNull(transportConsumer, nameof(transportConsumer));

            TrackingLastEnqueuedEvent     = trackingLastEnqueuedEvent;
            DefaultMaximumReceiveWaitTime = defaultMaximumReceiveWaitTime;
            InnerConsumer = transportConsumer;
        }
Пример #8
0
        /// <summary>
        ///   Initializes a new instance of the <see cref="ServiceBusReceiverClient"/> class.
        /// </summary>
        ///
        /// <param name="connection">The <see cref="ServiceBusConnection" /> connection to use for communication with the Service Bus service.</param>
        /// <param name="sessionId"></param>
        /// <param name="clientOptions">A set of options to apply when configuring the consumer.</param>
        ///
        internal ServiceBusReceiverClient(
            ServiceBusConnection connection,
            string sessionId,
            ServiceBusReceiverClientOptions clientOptions)
        {
            Argument.AssertNotNull(connection, nameof(connection));
            Argument.AssertNotNull(clientOptions, nameof(clientOptions));

            OwnsConnection = false;
            Connection     = connection;
            RetryPolicy    = clientOptions.RetryOptions.ToRetryPolicy();
            ReceiveMode    = clientOptions.ReceiveMode;
            Consumer       = Connection.CreateTransportConsumer(retryPolicy: RetryPolicy, sessionId: sessionId);
        }
Пример #9
0
        /// <summary>
        ///   Initializes a new instance of the <see cref="ServiceBusReceiverClient"/> class.
        /// </summary>
        ///
        /// <param name="connectionString">The connection string to use for connecting to the Service Bus namespace; it is expected that the shared key properties are contained in this connection string, but not the Service Bus entity name.</param>
        /// <param name="entityName">The name of the specific Service Bus entity to associate the consumer with.</param>
        /// <param name="clientOptions">A set of options to apply when configuring the consumer.</param>
        /// <param name="sessionId"></param>
        ///
        /// <remarks>
        ///   If the connection string is copied from the Service Bus entity itself, it will contain the name of the desired Service Bus entity,
        ///   and can be used directly without passing the <paramref name="entityName" />.  The name of the Service Bus entity should be
        ///   passed only once, either as part of the connection string or separately.
        /// </remarks>
        ///
        internal ServiceBusReceiverClient(
            string connectionString,
            string entityName,
            string sessionId,
            ServiceBusReceiverClientOptions clientOptions)
        {
            Argument.AssertNotNullOrEmpty(connectionString, nameof(connectionString));
            Argument.AssertNotNull(clientOptions, nameof(clientOptions));

            OwnsConnection = true;
            Connection     = new ServiceBusConnection(connectionString, entityName, clientOptions.ConnectionOptions);
            RetryPolicy    = clientOptions.RetryOptions.ToRetryPolicy();
            ReceiveMode    = clientOptions.ReceiveMode;
            Consumer       = Connection.CreateTransportConsumer(retryPolicy: RetryPolicy, sessionId: sessionId);
        }
Пример #10
0
        /// <summary>
        ///   Initializes a new instance of the <see cref="ServiceBusReceiverClient"/> class.
        /// </summary>
        ///
        /// <param name="fullyQualifiedNamespace">The fully qualified Service Bus namespace to connect to.  This is likely to be similar to <c>{yournamespace}.servicebus.windows.net</c>.</param>
        /// <param name="entityName">The name of the specific Service Bus entity to associate the consumer with.</param>
        /// <param name="credential">The Azure managed identity credential to use for authorization.  Access controls may be specified by the Service Bus namespace or the requested Service Bus entity, depending on Azure configuration.</param>
        /// <param name="sessionId"></param>
        /// <param name="clientOptions">A set of options to apply when configuring the consumer.</param>
        ///
        internal ServiceBusReceiverClient(
            string fullyQualifiedNamespace,
            string entityName,
            TokenCredential credential,
            string sessionId,
            ServiceBusReceiverClientOptions clientOptions)
        {
            Argument.AssertNotNullOrEmpty(fullyQualifiedNamespace, nameof(fullyQualifiedNamespace));
            Argument.AssertNotNullOrEmpty(entityName, nameof(entityName));
            Argument.AssertNotNull(credential, nameof(credential));
            Argument.AssertNotNull(clientOptions, nameof(clientOptions));

            OwnsConnection = true;
            Connection     = new ServiceBusConnection(fullyQualifiedNamespace, entityName, credential, clientOptions.ConnectionOptions);
            RetryPolicy    = clientOptions.RetryOptions.ToRetryPolicy();
            ReceiveMode    = clientOptions.ReceiveMode;
            Consumer       = Connection.CreateTransportConsumer(retryPolicy: RetryPolicy, sessionId: sessionId);
        }
Пример #11
0
        /// <summary>
        ///   Initializes a new instance of the <see cref="PartitionReceiver"/> class.
        /// </summary>
        ///
        /// <param name="consumerGroup">The name of the consumer group this client is associated with.  Events are read in the context of this group.</param>
        /// <param name="partitionId">The identifier of the Event Hub partition from which events will be received.</param>
        /// <param name="eventPosition">The position within the partition where the client should begin reading events.</param>
        /// <param name="connection">The <see cref="EventHubConnection" /> connection to use for communication with the Event Hubs service.</param>
        /// <param name="options">A set of options to apply when configuring the client.</param>
        ///
        public PartitionReceiver(string consumerGroup,
                                 string partitionId,
                                 EventPosition eventPosition,
                                 EventHubConnection connection,
                                 PartitionReceiverOptions options = default)
        {
            Argument.AssertNotNullOrEmpty(consumerGroup, nameof(consumerGroup));
            Argument.AssertNotNullOrEmpty(partitionId, nameof(partitionId));
            Argument.AssertNotNull(connection, nameof(connection));

            options = options?.Clone() ?? new PartitionReceiverOptions();

            OwnsConnection  = false;
            Connection      = connection;
            ConsumerGroup   = consumerGroup;
            PartitionId     = partitionId;
            InitialPosition = eventPosition;
            RetryPolicy     = options.RetryOptions.ToRetryPolicy();
            InnerConsumer   = CreateTransportConsumer(consumerGroup, partitionId, eventPosition, RetryPolicy, options);
        }
        /// <summary>
        ///   Initializes a new instance of the <see cref="PartitionReceiver"/> class.
        /// </summary>
        ///
        /// <param name="consumerGroup">The name of the consumer group this client is associated with.  Events are read in the context of this group.</param>
        /// <param name="partitionId">The identifier of the Event Hub partition from which events will be received.</param>
        /// <param name="eventPosition">The position within the partition where the client should begin reading events.</param>
        /// <param name="connectionString">The connection string to use for connecting to the Event Hubs namespace; it is expected that the shared key properties are contained in this connection string, but not the Event Hub name.</param>
        /// <param name="eventHubName">The name of the specific Event Hub to associate the client with.</param>
        /// <param name="options">A set of options to apply when configuring the client.</param>
        ///
        /// <remarks>
        ///   If the connection string is copied from the Event Hub itself, it will contain the name of the desired Event Hub,
        ///   and can be used directly without passing the <paramref name="eventHubName" />.  The name of the Event Hub should be
        ///   passed only once, either as part of the connection string or separately.
        /// </remarks>
        ///
        /// <seealso href="https://docs.microsoft.com/en-us/azure/event-hubs/event-hubs-get-connection-string"/>
        ///
        public PartitionReceiver(string consumerGroup,
                                 string partitionId,
                                 EventPosition eventPosition,
                                 string connectionString,
                                 string eventHubName,
                                 PartitionReceiverOptions options = default)
        {
            Argument.AssertNotNullOrEmpty(consumerGroup, nameof(consumerGroup));
            Argument.AssertNotNullOrEmpty(partitionId, nameof(partitionId));
            Argument.AssertNotNullOrEmpty(connectionString, nameof(connectionString));

            options = options?.Clone() ?? new PartitionReceiverOptions();

            Connection = new EventHubConnection(connectionString, eventHubName, options.ConnectionOptions);
            ConsumerGroup = consumerGroup;
            PartitionId = partitionId;
            InitialPosition = eventPosition;
            DefaultMaximumWaitTime = options.DefaultMaximumReceiveWaitTime;
            RetryPolicy = options.RetryOptions.ToRetryPolicy();
            InnerConsumer = CreateTransportConsumer(consumerGroup, partitionId, eventPosition, RetryPolicy, options);
        }
        /// <summary>
        ///   Initializes a new instance of the <see cref="EventHubConsumerClient"/> class.
        /// </summary>
        ///
        /// <param name="consumerGroup">The name of the consumer group this consumer is associated with.  Events are read in the context of this group.</param>
        /// <param name="partitionId">The identifier of the Event Hub partition from which events will be received.</param>
        /// <param name="eventPosition">The position within the partition where the consumer should begin reading events.</param>
        /// <param name="connection">The <see cref="EventHubConnection" /> connection to use for communication with the Event Hubs service.</param>
        /// <param name="consumerOptions">A set of options to apply when configuring the consumer.</param>
        ///
        public EventHubConsumerClient(string consumerGroup,
                                      string partitionId,
                                      EventPosition eventPosition,
                                      EventHubConnection connection,
                                      EventHubConsumerClientOptions consumerOptions = default)
        {
            Argument.AssertNotNullOrEmpty(consumerGroup, nameof(consumerGroup));
            Argument.AssertNotNullOrEmpty(partitionId, nameof(partitionId));
            Argument.AssertNotNull(eventPosition, nameof(eventPosition));
            Argument.AssertNotNull(connection, nameof(connection));
            consumerOptions = consumerOptions?.Clone() ?? new EventHubConsumerClientOptions();

            OwnsConnection = false;
            Connection     = connection;
            ConsumerGroup  = consumerGroup;
            Options        = consumerOptions;
            RetryPolicy    = consumerOptions.RetryOptions.ToRetryPolicy();
            InnerConsumer  = Connection.CreateTransportConsumer(consumerGroup, partitionId, eventPosition, consumerOptions);

            PartitionId      = partitionId;
            StartingPosition = eventPosition;
        }
Пример #14
0
        /// <summary>
        ///   Initializes a new instance of the <see cref="PartitionReceiver"/> class.
        /// </summary>
        ///
        /// <param name="consumerGroup">The name of the consumer group this client is associated with.  Events are read in the context of this group.</param>
        /// <param name="partitionId">The identifier of the Event Hub partition from which events will be received.</param>
        /// <param name="eventPosition">The position within the partition where the client should begin reading events.</param>
        /// <param name="fullyQualifiedNamespace">The fully qualified Event Hubs namespace to connect to.  This is likely to be similar to <c>{yournamespace}.servicebus.windows.net</c>.</param>
        /// <param name="eventHubName">The name of the specific Event Hub to associate the client with.</param>
        /// <param name="credential">The Azure managed identity credential to use for authorization.  Access controls may be specified by the Event Hubs namespace or the requested Event Hub, depending on Azure configuration.</param>
        /// <param name="options">A set of options to apply when configuring the client.</param>
        ///
        public PartitionReceiver(string consumerGroup,
                                 string partitionId,
                                 EventPosition eventPosition,
                                 string fullyQualifiedNamespace,
                                 string eventHubName,
                                 TokenCredential credential,
                                 PartitionReceiverOptions options = default)
        {
            Argument.AssertNotNullOrEmpty(consumerGroup, nameof(consumerGroup));
            Argument.AssertNotNullOrEmpty(partitionId, nameof(partitionId));
            Argument.AssertWellFormedEventHubsNamespace(fullyQualifiedNamespace, nameof(fullyQualifiedNamespace));
            Argument.AssertNotNullOrEmpty(eventHubName, nameof(eventHubName));
            Argument.AssertNotNull(credential, nameof(credential));

            options = options?.Clone() ?? new PartitionReceiverOptions();

            Connection      = new EventHubConnection(fullyQualifiedNamespace, eventHubName, credential, options.ConnectionOptions);
            ConsumerGroup   = consumerGroup;
            PartitionId     = partitionId;
            InitialPosition = eventPosition;
            RetryPolicy     = options.RetryOptions.ToRetryPolicy();
            InnerConsumer   = CreateTransportConsumer(consumerGroup, partitionId, eventPosition, RetryPolicy, options);
        }
Пример #15
0
        /// <summary>
        ///   Initializes a new instance of the <see cref="PartitionReceiver"/> class.
        /// </summary>
        ///
        /// <param name="consumerGroup">The name of the consumer group this client is associated with.  Events are read in the context of this group.</param>
        /// <param name="partitionId">The identifier of the Event Hub partition from which events will be received.</param>
        /// <param name="eventPosition">The position within the partition where the client should begin reading events.</param>
        /// <param name="connection">The <see cref="EventHubConnection" /> connection to use for communication with the Event Hubs service.</param>
        /// <param name="options">A set of options to apply when configuring the client.</param>
        ///
        public PartitionReceiver(string consumerGroup,
                                 string partitionId,
                                 EventPosition eventPosition,
                                 EventHubConnection connection,
                                 PartitionReceiverOptions options = default)
        {
            Argument.AssertNotNullOrEmpty(consumerGroup, nameof(consumerGroup));
            Argument.AssertNotNullOrEmpty(partitionId, nameof(partitionId));
            Argument.AssertNotNull(connection, nameof(connection));

            options = options?.Clone() ?? new PartitionReceiverOptions();

            OwnsConnection         = false;
            Connection             = connection;
            ConsumerGroup          = consumerGroup;
            PartitionId            = partitionId;
            InitialPosition        = eventPosition;
            DefaultMaximumWaitTime = options.DefaultMaximumReceiveWaitTime;
            RetryPolicy            = options.RetryOptions.ToRetryPolicy();

#pragma warning disable CA2214 // Do not call overridable methods in constructors. This internal method is virtual for testing purposes.
            InnerConsumer = CreateTransportConsumer(consumerGroup, partitionId, eventPosition, RetryPolicy, options);
#pragma warning restore CA2214 // Do not call overridable methods in constructors.
        }
        /// <summary>
        ///   Begins the background process responsible for receiving from the specified <see cref="TransportConsumer" />
        ///   and publishing to the requested <see cref="Channel{PartitionEvent}" />.
        /// </summary>
        ///
        /// <param name="transportConsumer">The consumer to use for receiving events.</param>
        /// <param name="channel">The channel to which received events should be published.</param>
        /// <param name="partitionContext">The context that represents the partition from which events being received.</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> to signal the request to cancel the background publishing.</param>
        ///
        /// <returns>A task to be resolved on when the operation has completed.</returns>
        ///
        private Task StartBackgroundChannelPublishingAsync(TransportConsumer transportConsumer,
                                                           Channel <PartitionEvent> channel,
                                                           PartitionContext partitionContext,
                                                           CancellationToken cancellationToken) =>
        Task.Run(async() =>
        {
            var failedAttemptCount = 0;
            var receivedItems      = default(IEnumerable <EventData>);
            var retryDelay         = default(TimeSpan?);
            var activeException    = default(Exception);

            while (!cancellationToken.IsCancellationRequested)
            {
                try
                {
                    // Receive items in batches and then write them to the subscribed channels.  The channels will naturally
                    // block if they reach their maximum queue size, so there is no need to throttle publishing.

                    receivedItems = await transportConsumer.ReceiveAsync(BackgroundPublishReceiveBatchSize, BackgroundPublishingWaitTime, cancellationToken).ConfigureAwait(false);

                    foreach (EventData item in receivedItems)
                    {
                        await channel.Writer.WriteAsync(new PartitionEvent(partitionContext, item), cancellationToken).ConfigureAwait(false);
                    }

                    failedAttemptCount = 0;
                }
                catch (TaskCanceledException ex)
                {
                    // In the case that a task was canceled, if cancellation was requested, then publishing should
                    // is already terminating.  Otherwise, something unexpected canceled the operation and it should
                    // be treated as an exception to ensure that the channels are marked final and consumers are made
                    // aware.

                    activeException = (cancellationToken.IsCancellationRequested) ? null : ex;
                    break;
                }
                catch (ConsumerDisconnectedException ex)
                {
                    // If the consumer was disconnected, it is known to be unrecoverable; do not offer the chance to retry.

                    activeException = ex;
                    break;
                }
                catch (Exception ex) when
                    (ex is OutOfMemoryException ||
                    ex is StackOverflowException ||
                    ex is ThreadAbortException)
                {
                    channel.Writer.TryComplete(ex);
                    throw;
                }
                catch (Exception ex)
                {
                    // Determine if there should be a retry for the next attempt; if so enforce the delay but do not quit the loop.
                    // Otherwise, mark the exception as active and break out of the loop.

                    ++failedAttemptCount;
                    retryDelay = RetryPolicy.CalculateRetryDelay(ex, failedAttemptCount);

                    if (retryDelay.HasValue)
                    {
                        await Task.Delay(retryDelay.Value).ConfigureAwait(false);
                        activeException = null;
                    }
                    else
                    {
                        activeException = ex;
                        break;
                    }
                }
            }

            // Publishing has terminated; if there was an active exception, then take the necessary steps to mark publishing as aborted rather
            // than completed normally.

            if (activeException != null)
            {
                channel.Writer.TryComplete(activeException);
            }
        }, cancellationToken);