public async Task <T> RunOperation <T>(Func <Task <T> > operation) { var failedAttemptCount = 0; var stopWatch = Stopwatch.StartNew(); try { TimeSpan tryTimeout = RetryPolicy.CalculateTryTimeout(0); while (!CancellationToken.IsCancellationRequested) { try { return(await operation().ConfigureAwait(false)); } catch (Exception ex) { Exception activeEx = ex.TranslateServiceException(EntityName); // 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; TimeSpan?retryDelay = RetryPolicy.CalculateRetryDelay(ex, failedAttemptCount); if (retryDelay.HasValue && !Scope.IsDisposed && !CancellationToken.IsCancellationRequested) { //EventHubsEventSource.Log.GetPropertiesError(EventHubName, activeEx.Message); await Task.Delay(retryDelay.Value, CancellationToken).ConfigureAwait(false); tryTimeout = RetryPolicy.CalculateTryTimeout(failedAttemptCount); stopWatch.Reset(); } else if (ex is AmqpException) { throw activeEx; } else { throw; } } } // If no value has been returned nor exception thrown by this point, // then cancellation has been requested. throw new TaskCanceledException(); } catch (Exception exception) { throw exception; //TODO through correct exception throw AmqpExceptionHelper.GetClientException(exception); } finally { stopWatch.Stop(); //TODO log correct completion event ServiceBusEventSource.Log.PeekMessagesComplete(EntityName); } }
/// <summary> /// Reads the set of partition publishing properties active for this producer at the time it was initialized. /// </summary> /// /// <param name="cancellationToken">The cancellation token to consider when creating the link.</param> /// /// <returns>The set of <see cref="PartitionPublishingProperties" /> observed when the producer was initialized.</returns> /// /// <remarks> /// It is important to note that these properties are a snapshot of the service state at the time when the /// producer was initialized; they do not necessarily represent the current state of the service. /// </remarks> /// public override async ValueTask <PartitionPublishingProperties> ReadInitializationPublishingPropertiesAsync(CancellationToken cancellationToken) { Argument.AssertNotClosed(_closed, nameof(AmqpProducer)); Argument.AssertNotClosed(ConnectionScope.IsDisposed, nameof(EventHubConnection)); // If the properties were already initialized, use them. if (InitializedPartitionProperties != null) { return(InitializedPartitionProperties); } // Initialize the properties by forcing the link to be opened. var failedAttemptCount = 0; var tryTimeout = RetryPolicy.CalculateTryTimeout(0); while ((!cancellationToken.IsCancellationRequested) && (!_closed)) { try { if (!SendLink.TryGetOpenedObject(out _)) { await SendLink.GetOrCreateAsync(UseMinimum(ConnectionScope.SessionTimeout, tryTimeout), cancellationToken).ConfigureAwait(false); } break; } catch (Exception ex) { ++failedAttemptCount; // Determine if there should be a retry for the next attempt; if so enforce the delay but do not quit the loop. // Otherwise, bubble the exception. var activeEx = ex.TranslateServiceException(EventHubName); var retryDelay = RetryPolicy.CalculateRetryDelay(activeEx, failedAttemptCount); if ((retryDelay.HasValue) && (!ConnectionScope.IsDisposed) && (!cancellationToken.IsCancellationRequested)) { await Task.Delay(retryDelay.Value, cancellationToken).ConfigureAwait(false); tryTimeout = RetryPolicy.CalculateTryTimeout(failedAttemptCount); } else if (ex is AmqpException) { ExceptionDispatchInfo.Capture(activeEx).Throw(); } else { throw; } } } cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); return(InitializedPartitionProperties); }
/// <summary> /// Applies the checkpoint store's <see cref="RetryPolicy" /> to a specified function. /// </summary> /// /// <param name="functionToRetry">The function to which the retry policy should be applied.</param> /// <param name="cancellationToken">A <see cref="CancellationToken" /> instance to signal the request to cancel the operation.</param> /// /// <returns>The value returned by the function to which the retry policy has been applied.</returns> /// private async Task ApplyRetryPolicy(Func <CancellationToken, Task> functionToRetry, CancellationToken cancellationToken) { TimeSpan?retryDelay; var failedAttemptCount = 0; var tryTimeout = RetryPolicy.CalculateTryTimeout(0); var timeoutTokenSource = default(CancellationTokenSource); var linkedTokenSource = default(CancellationTokenSource); while (!cancellationToken.IsCancellationRequested) { try { timeoutTokenSource = new CancellationTokenSource(tryTimeout); linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutTokenSource.Token); await functionToRetry(linkedTokenSource.Token).ConfigureAwait(false); return; } 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) && (!cancellationToken.IsCancellationRequested)) { await Task.Delay(retryDelay.Value, cancellationToken).ConfigureAwait(false); tryTimeout = RetryPolicy.CalculateTryTimeout(failedAttemptCount); } else { timeoutTokenSource?.Token.ThrowIfCancellationRequested <TimeoutException>(); throw; } } finally { timeoutTokenSource?.Dispose(); linkedTokenSource?.Dispose(); } } // If no value has been returned nor exception thrown by this point, // then cancellation has been requested. throw new TaskCanceledException(); }
/// <summary> /// Creates the AMQP link to be used for producer-related operations and ensures /// that the corresponding state for the producer has been updated based on the link /// configuration. /// </summary> /// /// <param name="partitionId">The identifier of the Event Hub partition to which it is bound; if unbound, <c>null</c>.</param> /// <param name="producerIdentifier">The identifier associated with the producer.</param> /// <param name="partitionOptions">The set of options, if any, that should be considered when initializing the producer.</param> /// <param name="timeout">The timeout to apply for creating the link.</param> /// <param name="cancellationToken">The cancellation token to consider when creating the link.</param> /// /// <returns>The AMQP link to use for producer-related operations.</returns> /// /// <remarks> /// This method will modify class-level state, setting those attributes that depend on the AMQP /// link configuration. There exists a benign race condition in doing so, as there may be multiple /// concurrent callers. In this case, the attributes may be set multiple times but the resulting /// value will be the same. /// </remarks> /// protected virtual async Task <SendingAmqpLink> CreateLinkAndEnsureProducerStateAsync(string partitionId, string producerIdentifier, PartitionPublishingOptionsInternal partitionOptions, TimeSpan timeout, CancellationToken cancellationToken) { var link = default(SendingAmqpLink); var operationTimeout = RetryPolicy.CalculateTryTimeout(0); try { link = await ConnectionScope.OpenProducerLinkAsync(partitionId, ActiveFeatures, partitionOptions, operationTimeout, timeout, producerIdentifier, cancellationToken).ConfigureAwait(false); if (!MaximumMessageSize.HasValue) { // This delay is necessary to prevent the link from causing issues for subsequent // operations after creating a batch. Without it, operations using the link consistently // timeout. The length of the delay does not appear significant, just the act of introducing // an asynchronous delay. // // For consistency the value used by the legacy Event Hubs client has been brought forward and // used here. await Task.Delay(15, cancellationToken).ConfigureAwait(false); MaximumMessageSize = (long)link.Settings.MaxMessageSize; } if (InitializedPartitionProperties == null) { var producerGroup = link.ExtractSettingPropertyValueOrDefault(AmqpProperty.ProducerGroupId, default(long?)); var ownerLevel = link.ExtractSettingPropertyValueOrDefault(AmqpProperty.ProducerOwnerLevel, default(short?)); var sequence = link.ExtractSettingPropertyValueOrDefault(AmqpProperty.ProducerSequenceNumber, default(int?)); // Once the properties are initialized, clear the starting sequence number to ensure that the current // sequence tracked by the service is used should the link need to be recreated; this avoids the need for // the transport producer to have awareness of the sequence numbers of events being sent. InitializedPartitionProperties = new PartitionPublishingPropertiesInternal(false, producerGroup, ownerLevel, sequence); partitionOptions.StartingSequenceNumber = null; } } catch (Exception ex) { ExceptionDispatchInfo.Capture(ex.TranslateConnectionCloseDuringLinkCreationException(EventHubName)).Throw(); } return(link); }
/// <summary> /// Receives a batch of <see cref="EventData" /> from the Event Hub partition. /// </summary> /// /// <param name="maximumEventCount">The maximum number of messages to receive in this batch.</param> /// <param name="maximumWaitTime">The maximum amount of time to wait for events to become available, if no events can be read from the prefetch queue. If not specified, the per-try timeout specified by the retry policy will be used.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> instance to signal the request to cancel the operation.</param> /// /// <returns>The batch of <see cref="EventData" /> from the Event Hub partition this consumer is associated with. If no events are present, an empty set is returned.</returns> /// /// <remarks> /// When events are available in the prefetch queue, they will be used to form the batch as quickly as possible without waiting for additional events from the /// Event Hubs service to try and meet the requested <paramref name="maximumEventCount" />. When no events are available in prefetch, the receiver will wait up /// to the <paramref name="maximumWaitTime"/> for events to be read from the service. Once any events are available, they will be used to form the batch immediately. /// </remarks> /// public override async Task <IReadOnlyList <EventData> > ReceiveAsync(int maximumEventCount, TimeSpan?maximumWaitTime, CancellationToken cancellationToken) { Argument.AssertNotClosed(_closed, nameof(AmqpConsumer)); Argument.AssertNotClosed(ConnectionScope.IsDisposed, nameof(EventHubConnection)); Argument.AssertAtLeast(maximumEventCount, 1, nameof(maximumEventCount)); var receivedEventCount = 0; var failedAttemptCount = 0; var tryTimeout = RetryPolicy.CalculateTryTimeout(0); var waitTime = (maximumWaitTime ?? tryTimeout); var operationId = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture); var link = default(ReceivingAmqpLink); var retryDelay = default(TimeSpan?); var receivedEvents = default(List <EventData>); var lastReceivedEvent = default(EventData); var stopWatch = ValueStopwatch.StartNew(); try { while ((!cancellationToken.IsCancellationRequested) && (!_closed)) { try { // Creation of the link happens without explicit knowledge of the cancellation token // used for this operation; validate the token state before attempting link creation and // again after the operation completes to provide best efforts in respecting it. EventHubsEventSource.Log.EventReceiveStart(EventHubName, ConsumerGroup, PartitionId, operationId); cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); link = await ReceiveLink.GetOrCreateAsync(UseMinimum(ConnectionScope.SessionTimeout, tryTimeout)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); var messagesReceived = await Task.Factory.FromAsync <(ReceivingAmqpLink, int, TimeSpan), IEnumerable <AmqpMessage> > ( static (arguments, callback, state) => { var(link, maximumEventCount, waitTime) = arguments; return(link.BeginReceiveMessages(maximumEventCount, waitTime, callback, link)); },
/// <summary> /// Creates a size-constraint batch to which <see cref="EventData" /> may be added using a try-based pattern. If an event would /// exceed the maximum allowable size of the batch, the batch will not allow adding the event and signal that scenario using its /// return value. /// /// Because events that would violate the size constraint cannot be added, publishing a batch will not trigger an exception when /// attempting to send the events to the Event Hubs service. /// </summary> /// /// <param name="options">The set of options to consider when creating this batch.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> instance to signal the request to cancel the operation.</param> /// /// <returns>An <see cref="EventDataBatch" /> with the requested <paramref name="options"/>.</returns> /// public override async ValueTask <TransportEventBatch> CreateBatchAsync(CreateBatchOptions options, CancellationToken cancellationToken) { Argument.AssertNotNull(options, nameof(options)); // Ensure that maximum message size has been determined; this depends on the underlying // AMQP link, so if not set, requesting the link will ensure that it is populated. if (!MaximumMessageSize.HasValue) { await SendLink.GetOrCreateAsync(RetryPolicy.CalculateTryTimeout(0)).ConfigureAwait(false); } // Ensure that there was a maximum size populated; if none was provided, // default to the maximum size allowed by the link. options.MaximumSizeInBytes ??= MaximumMessageSize; Argument.AssertInRange(options.MaximumSizeInBytes.Value, EventHubProducerClient.MinimumBatchSizeLimit, MaximumMessageSize.Value, nameof(options.MaximumSizeInBytes)); return(new AmqpEventBatch(MessageConverter, options)); }
/// <summary> /// Creates a <see cref="PartitionReceiver" /> which allows events from a specific partition of an Event Hub to be read /// with a greater level of control over communication with the Event Hubs service than is offered by other ways of consuming /// events. /// </summary> /// /// <param name="partitionId">The identifier of the Event Hub partition from which events will be received.</param> /// <param name="startingPosition">The position within the partition where the consumer should begin reading events.</param> /// /// <returns>A <see cref="PartitionReceiver"/> to be used for iterating over events in the partition.</returns> /// /// <remarks> /// It is recommended that the <see cref="EventProcessorClient" /> or <see cref="EventHubConsumerClient" /> /// be used for reading and processing events for the majority of scenarios. The partition receiver is /// intended to enable scenarios with special needs which require more direct control. /// </remarks> /// /// <seealso cref="EventProcessorClient" /> /// <seealso cref="ReadEventsFromPartitionAsync(string, EventPosition, CancellationToken)"/> /// <seealso cref="ReadEventsFromPartitionAsync(string, EventPosition, TimeSpan?, CancellationToken)"/> /// public virtual PartitionReceiver CreatePartitionReceiver(string partitionId, EventPosition startingPosition) { var transportConsumer = Connection.CreateTransportConsumer(ConsumerGroup, partitionId, startingPosition, Options); if (!ActiveConsumers.TryAdd(Guid.NewGuid().ToString(), transportConsumer)) { transportConsumer.CloseAsync(CancellationToken.None).GetAwaiter().GetResult(); throw new EventHubsException(false, EventHubName, String.Format(CultureInfo.CurrentCulture, Resources.FailedToCreateReader, EventHubName, partitionId, ConsumerGroup)); } return(new PartitionReceiver ( ConsumerGroup, partitionId, EventHubName, Options.TrackLastEnqueuedEventInformation, Options.DefaultMaximumReceiveWaitTime ?? RetryPolicy.CalculateTryTimeout(0), transportConsumer )); }
/// <summary> /// Sends an AMQP message that contains a batch of events to the associated Event Hub. If the size of events exceed the /// maximum size of a single batch, an exception will be triggered and the send will fail. /// </summary> /// /// <param name="messageFactory">A factory which can be used to produce an AMQP message containing the batch of events to be sent.</param> /// <param name="partitionKey">The hashing key to use for influencing the partition to which events should be routed.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> instance to signal the request to cancel the operation.</param> /// protected virtual async Task SendAsync(Func <AmqpMessage> messageFactory, string partitionKey, CancellationToken cancellationToken) { var failedAttemptCount = 0; var logPartition = PartitionId ?? partitionKey; var retryDelay = default(TimeSpan?); var messageHash = default(string); var stopWatch = Stopwatch.StartNew(); SendingAmqpLink link; try { var tryTimeout = RetryPolicy.CalculateTryTimeout(0); while (!cancellationToken.IsCancellationRequested) { try { using AmqpMessage batchMessage = messageFactory(); messageHash = batchMessage.GetHashCode().ToString(); EventHubsEventSource.Log.EventPublishStart(EventHubName, logPartition, messageHash); link = await SendLink.GetOrCreateAsync(UseMinimum(ConnectionScope.SessionTimeout, tryTimeout)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); // Validate that the batch of messages is not too large to send. This is done after the link is created to ensure // that the maximum message size is known, as it is dictated by the service using the link. if (batchMessage.SerializedMessageSize > MaximumMessageSize) { throw new EventHubsException(EventHubName, string.Format(Resources.MessageSizeExceeded, messageHash, batchMessage.SerializedMessageSize, MaximumMessageSize), EventHubsException.FailureReason.MessageSizeExceeded); } // Attempt to send the message batch. var deliveryTag = new ArraySegment <byte>(BitConverter.GetBytes(Interlocked.Increment(ref _deliveryCount))); var outcome = await link.SendMessageAsync(batchMessage, deliveryTag, AmqpConstants.NullBinary, tryTimeout.CalculateRemaining(stopWatch.Elapsed)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); if (outcome.DescriptorCode != Accepted.Code) { throw AmqpError.CreateExceptionForError((outcome as Rejected)?.Error, EventHubName); } // The send operation should be considered successful; return to // exit the retry loop. return; } catch (Exception ex) { Exception activeEx = ex.TranslateServiceException(EventHubName); // Determine if there should be a retry for the next attempt; if so enforce the delay but do not quit the loop. // Otherwise, bubble the exception. ++failedAttemptCount; retryDelay = RetryPolicy.CalculateRetryDelay(activeEx, failedAttemptCount); if ((retryDelay.HasValue) && (!ConnectionScope.IsDisposed) && (!cancellationToken.IsCancellationRequested)) { EventHubsEventSource.Log.EventPublishError(EventHubName, logPartition, messageHash, activeEx.Message); await Task.Delay(retryDelay.Value, cancellationToken).ConfigureAwait(false); tryTimeout = RetryPolicy.CalculateTryTimeout(failedAttemptCount); stopWatch.Reset(); } else if (ex is AmqpException) { throw activeEx; } else { throw; } } } // If no value has been returned nor exception thrown by this point, // then cancellation has been requested. throw new TaskCanceledException(); } catch (Exception ex) { EventHubsEventSource.Log.EventPublishError(EventHubName, logPartition, messageHash, ex.Message); throw; } finally { stopWatch.Stop(); EventHubsEventSource.Log.EventPublishComplete(EventHubName, logPartition, messageHash); } }
/// <summary> /// Creates a size-constraint batch to which <see cref="EventData" /> may be added using a try-based pattern. If an event would /// exceed the maximum allowable size of the batch, the batch will not allow adding the event and signal that scenario using its /// return value. /// /// Because events that would violate the size constraint cannot be added, publishing a batch will not trigger an exception when /// attempting to send the events to the Event Hubs service. /// </summary> /// /// <param name="options">The set of options to consider when creating this batch.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> instance to signal the request to cancel the operation.</param> /// /// <returns>An <see cref="EventDataBatch" /> with the requested <paramref name="options"/>.</returns> /// public override async ValueTask <TransportEventBatch> CreateBatchAsync(CreateBatchOptions options, CancellationToken cancellationToken) { Argument.AssertNotNull(options, nameof(options)); Argument.AssertNotClosed(_closed, nameof(AmqpProducer)); cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); // Ensure that maximum message size has been determined; this depends on the underlying // AMQP link, so if not set, requesting the link will ensure that it is populated. if (!MaximumMessageSize.HasValue) { var failedAttemptCount = 0; var retryDelay = default(TimeSpan?); var tryTimeout = RetryPolicy.CalculateTryTimeout(0); while (!cancellationToken.IsCancellationRequested) { try { await SendLink.GetOrCreateAsync(UseMinimum(ConnectionScope.SessionTimeout, tryTimeout)).ConfigureAwait(false); break; } catch (Exception ex) { Exception activeEx = ex.TranslateServiceException(EventHubName); // Determine if there should be a retry for the next attempt; if so enforce the delay but do not quit the loop. // Otherwise, bubble the exception. ++failedAttemptCount; retryDelay = RetryPolicy.CalculateRetryDelay(activeEx, failedAttemptCount); if ((retryDelay.HasValue) && (!ConnectionScope.IsDisposed) && (!cancellationToken.IsCancellationRequested)) { await Task.Delay(retryDelay.Value, cancellationToken).ConfigureAwait(false); tryTimeout = RetryPolicy.CalculateTryTimeout(failedAttemptCount); } else if (ex is AmqpException) { throw activeEx; } else { throw; } } } // If MaximumMessageSize has not been populated nor exception thrown // by this point, then cancellation has been requested. if (!MaximumMessageSize.HasValue) { throw new TaskCanceledException(); } } // Ensure that there was a maximum size populated; if none was provided, // default to the maximum size allowed by the link. options.MaximumSizeInBytes ??= MaximumMessageSize; Argument.AssertInRange(options.MaximumSizeInBytes.Value, EventHubProducerClient.MinimumBatchSizeLimit, MaximumMessageSize.Value, nameof(options.MaximumSizeInBytes)); return(new AmqpEventBatch(MessageConverter, options)); }
/// <summary> /// Receives a batch of <see cref="EventData" /> from the Event Hub partition. /// </summary> /// /// <param name="maximumMessageCount">The maximum number of messages to receive in this batch.</param> /// <param name="maximumWaitTime">The maximum amount of time to wait to build up the requested message count for the batch; if not specified, the per-try timeout specified by the retry policy will be used.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> instance to signal the request to cancel the operation.</param> /// /// <returns>The batch of <see cref="EventData" /> from the Event Hub partition this consumer is associated with. If no events are present, an empty set is returned.</returns> /// public override async Task <IReadOnlyList <EventData> > ReceiveAsync(int maximumMessageCount, TimeSpan?maximumWaitTime, CancellationToken cancellationToken) { Argument.AssertNotClosed(_closed, nameof(AmqpConsumer)); Argument.AssertAtLeast(maximumMessageCount, 1, nameof(maximumMessageCount)); var receivedEventCount = 0; var failedAttemptCount = 0; var tryTimeout = RetryPolicy.CalculateTryTimeout(0); var waitTime = (maximumWaitTime ?? tryTimeout); var link = default(ReceivingAmqpLink); var retryDelay = default(TimeSpan?); var amqpMessages = default(IEnumerable <AmqpMessage>); var receivedEvents = default(List <EventData>); var lastReceivedEvent = default(EventData); var stopWatch = ValueStopwatch.StartNew(); try { while ((!cancellationToken.IsCancellationRequested) && (!_closed)) { try { // Creation of the link happens without explicit knowledge of the cancellation token // used for this operation; validate the token state before attempting link creation and // again after the operation completes to provide best efforts in respecting it. EventHubsEventSource.Log.EventReceiveStart(EventHubName, ConsumerGroup, PartitionId); cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); link = await ReceiveLink.GetOrCreateAsync(UseMinimum(ConnectionScope.SessionTimeout, tryTimeout)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); var messagesReceived = await Task.Factory.FromAsync ( (callback, state) => link.BeginReceiveMessages(maximumMessageCount, waitTime, callback, state), (asyncResult) => link.EndReceiveMessages(asyncResult, out amqpMessages), TaskCreationOptions.RunContinuationsAsynchronously ).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); // If event messages were received, then package them for consumption and // return them. if ((messagesReceived) && (amqpMessages != null)) { receivedEvents ??= new List <EventData>(); foreach (AmqpMessage message in amqpMessages) { link.DisposeDelivery(message, true, AmqpConstants.AcceptedOutcome); receivedEvents.Add(MessageConverter.CreateEventFromMessage(message)); message.Dispose(); } receivedEventCount = receivedEvents.Count; if (receivedEventCount > 0) { lastReceivedEvent = receivedEvents[receivedEventCount - 1]; if (lastReceivedEvent.Offset > long.MinValue) { CurrentEventPosition = EventPosition.FromOffset(lastReceivedEvent.Offset, false); } if (TrackLastEnqueuedEventProperties) { LastReceivedEvent = lastReceivedEvent; } } return(receivedEvents); } // No events were available. return(EmptyEventSet); } catch (EventHubsException ex) when(ex.Reason == EventHubsException.FailureReason.ServiceTimeout) { // Because the timeout specified with the request is intended to be the maximum // amount of time to wait for events, a timeout isn't considered an error condition, // rather a sign that no events were available in the requested period. return(EmptyEventSet); } catch (Exception ex) { Exception activeEx = ex.TranslateServiceException(EventHubName); // Determine if there should be a retry for the next attempt; if so enforce the delay but do not quit the loop. // Otherwise, bubble the exception. ++failedAttemptCount; retryDelay = RetryPolicy.CalculateRetryDelay(activeEx, failedAttemptCount); if ((retryDelay.HasValue) && (!ConnectionScope.IsDisposed) && (!cancellationToken.IsCancellationRequested)) { EventHubsEventSource.Log.EventReceiveError(EventHubName, ConsumerGroup, PartitionId, activeEx.Message); await Task.Delay(UseMinimum(retryDelay.Value, waitTime.CalculateRemaining(stopWatch.GetElapsedTime())), cancellationToken).ConfigureAwait(false); tryTimeout = RetryPolicy.CalculateTryTimeout(failedAttemptCount); } else if (ex is AmqpException) { ExceptionDispatchInfo.Capture(activeEx).Throw(); } else { throw; } } } // If no value has been returned nor exception thrown by this point, // then cancellation has been requested. throw new TaskCanceledException(); } catch (TaskCanceledException) { throw; } catch (Exception ex) { EventHubsEventSource.Log.EventReceiveError(EventHubName, ConsumerGroup, PartitionId, ex.Message); throw; } finally { EventHubsEventSource.Log.EventReceiveComplete(EventHubName, ConsumerGroup, PartitionId, receivedEventCount); } }
/// <summary> /// Receives a batch of <see cref="EventData" /> from the Event Hub partition. /// </summary> /// /// <param name="maximumMessageCount">The maximum number of messages to receive in this batch.</param> /// <param name="maximumWaitTime">The maximum amount of time to wait to build up the requested message count for the batch; if not specified, the per-try timeout specified by the retry policy will be used.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> instance to signal the request to cancel the operation.</param> /// /// <returns>The batch of <see cref="EventData" /> from the Event Hub partition this consumer is associated with. If no events are present, an empty enumerable is returned.</returns> /// public override async Task <IEnumerable <EventData> > ReceiveAsync(int maximumMessageCount, TimeSpan?maximumWaitTime, CancellationToken cancellationToken) { Argument.AssertNotClosed(_closed, nameof(AmqpConsumer)); Argument.AssertAtLeast(maximumMessageCount, 1, nameof(maximumMessageCount)); var receivedEventCount = 0; var failedAttemptCount = 0; var tryTimeout = RetryPolicy.CalculateTryTimeout(0); var waitTime = (maximumWaitTime ?? tryTimeout); var link = default(ReceivingAmqpLink); var retryDelay = default(TimeSpan?); var amqpMessages = default(IEnumerable <AmqpMessage>); var receivedEvents = default(List <EventData>); var stopWatch = Stopwatch.StartNew(); try { while (!cancellationToken.IsCancellationRequested) { try { EventHubsEventSource.Log.EventReceiveStart(EventHubName, ConsumerGroup, PartitionId); link = await ReceiveLink.GetOrCreateAsync(UseMinimum(ConnectionScope.SessionTimeout, tryTimeout)).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); var messagesReceived = await Task.Factory.FromAsync ( (callback, state) => link.BeginReceiveMessages(maximumMessageCount, waitTime, callback, state), (asyncResult) => link.EndReceiveMessages(asyncResult, out amqpMessages), TaskCreationOptions.RunContinuationsAsynchronously ).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); // If event messages were received, then package them for consumption and // return them. if ((messagesReceived) && (amqpMessages != null)) { receivedEvents ??= new List <EventData>(); foreach (AmqpMessage message in amqpMessages) { link.DisposeDelivery(message, true, AmqpConstants.AcceptedOutcome); receivedEvents.Add(MessageConverter.CreateEventFromMessage(message)); message.Dispose(); } receivedEventCount = receivedEvents.Count; if ((TrackLastEnqueuedEventInformation) && (receivedEventCount > 0)) { LastReceivedEvent = receivedEvents[receivedEventCount - 1]; } return(receivedEvents); } // No events were available. return(Enumerable.Empty <EventData>()); } catch (EventHubsTimeoutException) { // Because the timeout specified with the request is intended to be the maximum // amount of time to wait for events, a timeout isn't considered an error condition, // rather a sign that no events were available in the requested period. return(Enumerable.Empty <EventData>()); } catch (AmqpException amqpException) { throw AmqpError.CreateExceptionForError(amqpException.Error, EventHubName); } 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, bubble the exception. ++failedAttemptCount; retryDelay = RetryPolicy.CalculateRetryDelay(ex, failedAttemptCount); if ((retryDelay.HasValue) && (!ConnectionScope.IsDisposed) && (!cancellationToken.IsCancellationRequested)) { EventHubsEventSource.Log.EventReceiveError(EventHubName, ConsumerGroup, PartitionId, ex.Message); await Task.Delay(UseMinimum(retryDelay.Value, waitTime.CalculateRemaining(stopWatch.Elapsed)), cancellationToken).ConfigureAwait(false); tryTimeout = RetryPolicy.CalculateTryTimeout(failedAttemptCount); } else { throw; } } } // If no value has been returned nor exception thrown by this point, // then cancellation has been requested. throw new TaskCanceledException(); } catch (Exception ex) { EventHubsEventSource.Log.EventReceiveError(EventHubName, ConsumerGroup, PartitionId, ex.Message); throw; } finally { stopWatch.Stop(); EventHubsEventSource.Log.EventReceiveComplete(EventHubName, ConsumerGroup, PartitionId, receivedEventCount); } }
/// <summary> /// Sends an AMQP message that contains a batch of events to the associated Event Hub. If the size of events exceed the /// maximum size of a single batch, an exception will be triggered and the send will fail. /// </summary> /// /// <param name="messages">The set of AMQP messages to packaged in a batch envelope and sent.</param> /// <param name="partitionKey">The hashing key to use for influencing the partition to which events should be routed.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> instance to signal the request to cancel the operation.</param> /// /// <remarks> /// Callers retain ownership of the <paramref name="messages" /> passed and hold responsibility for /// ensuring that they are disposed. /// </remarks> /// protected virtual async Task SendAsync(IReadOnlyCollection <AmqpMessage> messages, string partitionKey, CancellationToken cancellationToken) { var failedAttemptCount = 0; var logPartition = PartitionId ?? partitionKey; var operationId = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture); TimeSpan? retryDelay; SendingAmqpLink link; try { var tryTimeout = RetryPolicy.CalculateTryTimeout(0); while (!cancellationToken.IsCancellationRequested) { EventHubsEventSource.Log.EventPublishStart(EventHubName, logPartition, operationId); try { using AmqpMessage batchMessage = MessageConverter.CreateBatchFromMessages(messages); if (!SendLink.TryGetOpenedObject(out link)) { link = await SendLink.GetOrCreateAsync(UseMinimum(ConnectionScope.SessionTimeout, tryTimeout), cancellationToken).ConfigureAwait(false); } // Validate that the batch of messages is not too large to send. This is done after the link is created to ensure // that the maximum message size is known, as it is dictated by the service using the link. if (batchMessage.SerializedMessageSize > MaximumMessageSize) { throw new EventHubsException(EventHubName, string.Format(CultureInfo.CurrentCulture, Resources.MessageSizeExceeded, operationId, batchMessage.SerializedMessageSize, MaximumMessageSize), EventHubsException.FailureReason.MessageSizeExceeded); } // Attempt to send the message batch. var deliveryTag = new ArraySegment <byte>(BitConverter.GetBytes(Interlocked.Increment(ref _deliveryCount))); var outcome = await link.SendMessageAsync(batchMessage, deliveryTag, AmqpConstants.NullBinary, cancellationToken).ConfigureAwait(false); if (outcome.DescriptorCode != Accepted.Code) { throw AmqpError.CreateExceptionForError((outcome as Rejected)?.Error, EventHubName); } // The send operation should be considered successful; return to // exit the retry loop. return; } catch (Exception ex) { Exception activeEx = ex.TranslateServiceException(EventHubName); // Determine if there should be a retry for the next attempt; if so enforce the delay but do not quit the loop. // Otherwise, bubble the exception. ++failedAttemptCount; retryDelay = RetryPolicy.CalculateRetryDelay(activeEx, failedAttemptCount); if ((retryDelay.HasValue) && (!ConnectionScope.IsDisposed) && (!_closed) && (!cancellationToken.IsCancellationRequested)) { EventHubsEventSource.Log.EventPublishError(EventHubName, logPartition, operationId, activeEx.Message); await Task.Delay(retryDelay.Value, cancellationToken).ConfigureAwait(false); tryTimeout = RetryPolicy.CalculateTryTimeout(failedAttemptCount); } else if (ex is AmqpException) { ExceptionDispatchInfo.Capture(activeEx).Throw(); } else { throw; } } } // If no value has been returned nor exception thrown by this point, // then cancellation has been requested. throw new TaskCanceledException(); } catch (TaskCanceledException) { throw; } catch (Exception ex) { EventHubsEventSource.Log.EventPublishError(EventHubName, logPartition, operationId, ex.Message); throw; } finally { EventHubsEventSource.Log.EventPublishComplete(EventHubName, logPartition, operationId, failedAttemptCount); } }
/// <summary> /// Creates the AMQP link to be used for consumer-related operations. /// </summary> /// /// <param name="consumerGroup">The consumer group of the Event Hub to which the link is bound.</param> /// <param name="partitionId">The identifier of the Event Hub partition to which the link is bound.</param> /// <param name="consumerIdentifier">The identifier associated with the consumer.</param> /// <param name="eventStartingPosition">The place within the partition's event stream to begin consuming events.</param> /// <param name="prefetchCount">Controls the number of events received and queued locally without regard to whether an operation was requested.</param> /// <param name="prefetchSizeInBytes">The cache size of the prefetch queue. When set, the link makes a best effort to ensure prefetched messages fit into the specified size.</param> /// <param name="ownerLevel">The relative priority to associate with the link; for a non-exclusive link, this value should be <c>null</c>.</param> /// <param name="trackLastEnqueuedEventProperties">Indicates whether information on the last enqueued event on the partition is sent as events are received.</param> /// <param name="timeout">The timeout to apply for creating the link.</param> /// <param name="cancellationToken">The cancellation token to consider when creating the link.</param> /// /// <returns>The AMQP link to use for consumer-related operations.</returns> /// protected async Task <ReceivingAmqpLink> CreateConsumerLinkAsync(string consumerGroup, string partitionId, string consumerIdentifier, EventPosition eventStartingPosition, uint prefetchCount, long?prefetchSizeInBytes, long?ownerLevel, bool trackLastEnqueuedEventProperties, TimeSpan timeout, CancellationToken cancellationToken) { // Determine if there is an active is an active exception that needs to be surfaced, // do so now. // // Note that this is a benign race; it is possible that multiple threads // will observe the active exception and surface it, even if the consumer is not // considered invalid. This is reasonable behavior for this scenario as the underlying // condition is itself a race condition between multiple consumers. var activeException = _activePartitionStolenException; if (activeException != null) { // If the consumer should not be considered invalid, clear the active exception if // it hasn't already been reset. if (!InvalidateConsumerWhenPartitionStolen) { Interlocked.CompareExchange(ref _activePartitionStolenException, null, activeException); } EventHubsEventSource.Log.AmqpConsumerLinkCreateCapturedErrorThrow(EventHubName, consumerGroup, partitionId, ownerLevel?.ToString(CultureInfo.InvariantCulture), eventStartingPosition.ToString(), activeException.Message); ExceptionDispatchInfo.Capture(activeException).Throw(); } // Create and open the consumer link. var link = default(ReceivingAmqpLink); var tryTimeout = RetryPolicy.CalculateTryTimeout(0); try { link = await ConnectionScope.OpenConsumerLinkAsync( consumerGroup, partitionId, eventStartingPosition, tryTimeout, timeout, prefetchCount, prefetchSizeInBytes, ownerLevel, trackLastEnqueuedEventProperties, consumerIdentifier, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { ExceptionDispatchInfo.Capture(ex.TranslateConnectionCloseDuringLinkCreationException(EventHubName)).Throw(); } return(link); }
/// <summary> /// Receives a batch of <see cref="EventData" /> from the Event Hub partition. /// </summary> /// /// <param name="maximumEventCount">The maximum number of messages to receive in this batch.</param> /// <param name="maximumWaitTime">The maximum amount of time to wait for events to become available, if no events can be read from the prefetch queue. If not specified, the per-try timeout specified by the retry policy will be used.</param> /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> instance to signal the request to cancel the operation.</param> /// /// <returns>The batch of <see cref="EventData" /> from the Event Hub partition this consumer is associated with. If no events are present, an empty set is returned.</returns> /// /// <remarks> /// When events are available in the prefetch queue, they will be used to form the batch as quickly as possible without waiting for additional events from the /// Event Hubs service to try and meet the requested <paramref name="maximumEventCount" />. When no events are available in prefetch, the receiver will wait up /// to the <paramref name="maximumWaitTime"/> for events to be read from the service. Once any events are available, they will be used to form the batch immediately. /// </remarks> /// public override async Task <IReadOnlyList <EventData> > ReceiveAsync(int maximumEventCount, TimeSpan?maximumWaitTime, CancellationToken cancellationToken) { Argument.AssertNotClosed(_closed, nameof(AmqpConsumer)); Argument.AssertNotClosed(ConnectionScope.IsDisposed, nameof(EventHubConnection)); Argument.AssertAtLeast(maximumEventCount, 1, nameof(maximumEventCount)); var receivedEventCount = 0; var failedAttemptCount = 0; var tryTimeout = RetryPolicy.CalculateTryTimeout(0); var waitTime = (maximumWaitTime ?? tryTimeout); var operationId = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture); var link = default(ReceivingAmqpLink); var retryDelay = default(TimeSpan?); var receivedEvents = default(List <EventData>); var lastReceivedEvent = default(EventData); var stopWatch = ValueStopwatch.StartNew(); try { while ((!cancellationToken.IsCancellationRequested) && (!_closed)) { try { // Creation of the link happens without explicit knowledge of the cancellation token // used for this operation; validate the token state before attempting link creation and // again after the operation completes to provide best efforts in respecting it. EventHubsEventSource.Log.EventReceiveStart(EventHubName, ConsumerGroup, PartitionId, operationId); link = await ReceiveLink.GetOrCreateAsync(UseMinimum(ConnectionScope.SessionTimeout, tryTimeout), cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested <TaskCanceledException>(); var messagesReceived = await link.ReceiveMessagesAsync(maximumEventCount, ReceiveBuildBatchInterval, waitTime, cancellationToken).ConfigureAwait(false); // If no messages were received, then just return the empty set. if (messagesReceived == null) { return(EmptyEventSet); } // If event messages were received, then package them for consumption and // return them. foreach (AmqpMessage message in messagesReceived) { receivedEvents ??= new List <EventData>(); link.DisposeDelivery(message, true, AmqpConstants.AcceptedOutcome); receivedEvents.Add(MessageConverter.CreateEventFromMessage(message)); message.Dispose(); receivedEventCount = receivedEvents.Count; } if (receivedEventCount > 0) { lastReceivedEvent = receivedEvents[receivedEventCount - 1]; if (lastReceivedEvent.Offset > long.MinValue) { CurrentEventPosition = EventPosition.FromOffset(lastReceivedEvent.Offset, false); } if (TrackLastEnqueuedEventProperties) { LastReceivedEvent = lastReceivedEvent; } } return(receivedEvents ?? EmptyEventSet); } catch (EventHubsException ex) when(ex.Reason == EventHubsException.FailureReason.ServiceTimeout) { // Because the timeout specified with the request is intended to be the maximum // amount of time to wait for events, a timeout isn't considered an error condition, // rather a sign that no events were available in the requested period. return(EmptyEventSet); } catch (Exception ex) { Exception activeEx = ex.TranslateServiceException(EventHubName); // If the partition was stolen determine the correct action to take for // capturing it with respect to whether the consumer should be invalidated. // // In either case, it is a terminal exception and will not trigger a retry; // allow the normal error handling flow to surface the exception. if (ex.IsConsumerPartitionStolenException()) { // If the consumer should be invalidated, capture the exception // and force-close the link. This will ensure that the next operation // will surface it. if (InvalidateConsumerWhenPartitionStolen) { _activePartitionStolenException = ex; CloseConsumerLink(link); } else { // If the consumer should not be invalidated, clear any previously captured exception to avoid // surfacing the failure multiple times. If the link is stolen after this operation, it will // be intercepted and handled as needed. _activePartitionStolenException = null; } } // Determine if there should be a retry for the next attempt; if so enforce the delay but do not quit the loop. // Otherwise, bubble the exception. ++failedAttemptCount; retryDelay = RetryPolicy.CalculateRetryDelay(activeEx, failedAttemptCount); if ((retryDelay.HasValue) && (!ConnectionScope.IsDisposed) && (!_closed) && (!cancellationToken.IsCancellationRequested)) { EventHubsEventSource.Log.EventReceiveError(EventHubName, ConsumerGroup, PartitionId, operationId, activeEx.Message); await Task.Delay(UseMinimum(retryDelay.Value, waitTime.CalculateRemaining(stopWatch.GetElapsedTime())), cancellationToken).ConfigureAwait(false); tryTimeout = RetryPolicy.CalculateTryTimeout(failedAttemptCount); } else if (ex is AmqpException) { ExceptionDispatchInfo.Capture(activeEx).Throw(); } else { throw; } } } // If no value has been returned nor exception thrown by this point, // then cancellation has been requested. throw new TaskCanceledException(); } catch (TaskCanceledException) { throw; } catch (Exception ex) { EventHubsEventSource.Log.EventReceiveError(EventHubName, ConsumerGroup, PartitionId, operationId, ex.Message); throw; } finally { EventHubsEventSource.Log.EventReceiveComplete(EventHubName, ConsumerGroup, PartitionId, operationId, failedAttemptCount, receivedEventCount); } }