/// <summary> /// Stops the partition pump. In case it isn't running, nothing happens. /// </summary> /// /// <param name="reason">The reason why the processing for the associated partition is being stopped. In case it's <c>null</c>, the internal close reason set by this pump is used.</param> /// /// <returns>A task to be resolved on when the operation has completed.</returns> /// public async Task StopAsync(CloseReason?reason) { if (RunningTask != null) { await RunningTaskSemaphore.WaitAsync().ConfigureAwait(false); try { if (RunningTask != null) { RunningTaskTokenSource.Cancel(); RunningTaskTokenSource = null; try { // RunningTask is only expected to fail when the event processor throws while processing // an error, but unforeseen scenarios might happen. await RunningTask.ConfigureAwait(false); } catch (Exception) { // TODO: delegate the exception handling to an Exception Callback. } RunningTask = null; // It's important to close the consumer as soon as possible. Failing to do so multiple times // would make it impossible to create more consumers for the associated partition as there's a // limit per client. await InnerConsumer.CloseAsync().ConfigureAwait(false); // In case an exception is encountered while the processing is stopping, don't catch it and let // the event processor handle it. The pump has no way to guess when a partition was lost or when // a shutdown request was sent to the event processor, so it expects a "reason" parameter to provide // this information. However, in case of pump failure, the external event processor does not have // enough information to figure out what failure reason to use, as this information is only known // by the pump. In this case, we expect the processor-provided reason to be null, and the private // CloseReason is used instead. if (OwnerEventProcessor.ProcessingForPartitionStoppedAsync != null) { var stopContext = new PartitionProcessingStoppedContext(Context, reason ?? CloseReason); await OwnerEventProcessor.ProcessingForPartitionStoppedAsync(stopContext).ConfigureAwait(false); } } } finally { RunningTaskSemaphore.Release(); } } }
/// <summary> /// Starts the partition pump. In case it's already running, nothing happens. /// </summary> /// /// <returns>A task to be resolved on when the operation has completed.</returns> /// public async Task StartAsync() { if (RunningTask == null) { await RunningTaskSemaphore.WaitAsync().ConfigureAwait(false); try { if (RunningTask == null) { // We expect the token source to be null, but we are playing safe. RunningTaskTokenSource?.Cancel(); RunningTaskTokenSource = new CancellationTokenSource(); // In case an exception is encountered while the processing is initializing, don't catch it // and let the event processor handle it. EventPosition startingPosition = StartingPosition; if (OwnerEventProcessor.InitializeProcessingForPartitionAsync != null) { var initializationContext = new InitializePartitionProcessingContext(Context); await OwnerEventProcessor.InitializeProcessingForPartitionAsync(initializationContext).ConfigureAwait(false); startingPosition = startingPosition ?? initializationContext.DefaultStartingPosition; } InnerConsumer = new EventHubConsumerClient(ConsumerGroup, Context.PartitionId, startingPosition ?? EventPosition.Earliest, Connection); // Before closing, the running task will set the close reason in case of failure. When something // unexpected happens and it's not set, the default value (Unknown) is kept. RunningTask = RunAsync(RunningTaskTokenSource.Token); } } finally { RunningTaskSemaphore.Release(); } } }
/// <summary> /// Starts the partition pump. In case it's already running, nothing happens. /// </summary> /// /// <returns>A task to be resolved on when the operation has completed.</returns> /// public async Task StartAsync() { if (RunningTask == null) { await RunningTaskSemaphore.WaitAsync().ConfigureAwait(false); try { if (RunningTask == null) { // We expect the token source to be null, but we are playing safe. RunningTaskTokenSource?.Cancel(); RunningTaskTokenSource = new CancellationTokenSource(); InnerConsumer = new EventHubConsumerClient(ConsumerGroup, Context.PartitionId, DefaultEventPosition, Connection); // In case an exception is encountered while partition processor is initializing, don't catch it // and let the event processor handle it. The inner consumer hasn't connected to the service yet, // so there's no need to close it. if (OwnerEventProcessor.InitializeProcessingForPartitionAsync != null) { await OwnerEventProcessor.InitializeProcessingForPartitionAsync(Context).ConfigureAwait(false); } // Before closing, the running task will set the close reason in case of failure. When something // unexpected happens and it's not set, the default value (Unknown) is kept. RunningTask = RunAsync(RunningTaskTokenSource.Token); } } finally { RunningTaskSemaphore.Release(); } } }
/// <summary> /// The main loop of a partition pump. It receives events from the Azure Event Hubs service /// and delegates their processing to the event processor processing handlers. /// </summary> /// /// <param name="cancellationToken">A <see cref="CancellationToken"/> instance to signal the request to cancel the operation.</param> /// /// <returns>A task to be resolved on when the operation has completed.</returns> /// private async Task RunAsync(CancellationToken cancellationToken) { List <EventData> receivedEvents; Exception unrecoverableException = null; // We'll break from the loop upon encountering a non-retriable exception. The event processor periodically // checks its pumps' status, so it should be aware of when one of them stops working. while (!cancellationToken.IsCancellationRequested && unrecoverableException == null) { try { receivedEvents = (await InnerConsumer.ReceiveAsync(MaximumMessageCount, Options.MaximumReceiveWaitTime, cancellationToken).ConfigureAwait(false)).ToList(); using DiagnosticScope diagnosticScope = EventDataInstrumentation.ClientDiagnostics.CreateScope(DiagnosticProperty.EventProcessorProcessingActivityName); diagnosticScope.AddAttribute("kind", "server"); if (diagnosticScope.IsEnabled) { foreach (var eventData in receivedEvents) { if (EventDataInstrumentation.TryExtractDiagnosticId(eventData, out string diagnosticId)) { diagnosticScope.AddLink(diagnosticId); } } } // Small workaround to make sure we call ProcessEvent with EventData = null when no events have been received. // The code is expected to get simpler when we start using the async enumerator internally to receive events. if (receivedEvents.Count == 0) { receivedEvents.Add(null); } diagnosticScope.Start(); foreach (var eventData in receivedEvents) { try { var processorEvent = new EventProcessorEvent(Context, eventData, OwnerEventProcessor.UpdateCheckpointAsync); await OwnerEventProcessor.ProcessEventAsync(processorEvent).ConfigureAwait(false); } catch (Exception eventProcessingException) { diagnosticScope.Failed(eventProcessingException); unrecoverableException = eventProcessingException; CloseReason = CloseReason.Exception; break; } } } catch (Exception eventHubException) { // Stop running only if it's not a retriable exception. if (RetryPolicy.CalculateRetryDelay(eventHubException, 1) == null) { unrecoverableException = eventHubException; CloseReason = CloseReason.Exception; break; } } } if (unrecoverableException != null) { // In case an exception is thrown by ProcessExceptionAsync, don't catch it and // let the calling method (StopAsync) handle it. var errorContext = new ProcessorErrorContext(Context.PartitionId, unrecoverableException); await OwnerEventProcessor.ProcessExceptionAsync(errorContext).ConfigureAwait(false); } }
/// <summary> /// The main loop of a partition pump. It receives events from the Azure Event Hubs service /// and delegates their processing to the inner partition processor. /// </summary> /// /// <param name="cancellationToken">A <see cref="CancellationToken"/> instance to signal the request to cancel the operation.</param> /// /// <returns>A task to be resolved on when the operation has completed.</returns> /// private async Task RunAsync(CancellationToken cancellationToken) { IEnumerable <EventData> receivedEvents; Exception unrecoverableException = null; // We'll break from the loop upon encountering a non-retriable exception. The event processor periodically // checks its pumps' status, so it should be aware of when one of them stops working. while (!cancellationToken.IsCancellationRequested) { try { receivedEvents = await InnerConsumer.ReceiveAsync(MaximumMessageCount, Options.MaximumReceiveWaitTime, cancellationToken).ConfigureAwait(false); using DiagnosticScope diagnosticScope = EventDataInstrumentation.ClientDiagnostics.CreateScope(DiagnosticProperty.EventProcessorProcessingActivityName); diagnosticScope.AddAttribute("kind", "server"); if (diagnosticScope.IsEnabled) { foreach (var eventData in receivedEvents) { if (EventDataInstrumentation.TryExtractDiagnosticId(eventData, out string diagnosticId)) { diagnosticScope.AddLink(diagnosticId); } } } diagnosticScope.Start(); try { await OwnerEventProcessor.ProcessEventsAsync(Context, receivedEvents).ConfigureAwait(false); } catch (Exception partitionProcessorException) { diagnosticScope.Failed(partitionProcessorException); unrecoverableException = partitionProcessorException; CloseReason = PartitionProcessorCloseReason.PartitionProcessorException; break; } } catch (Exception eventHubException) { // Stop running only if it's not a retriable exception. if (RetryPolicy.CalculateRetryDelay(eventHubException, 1) == null) { unrecoverableException = eventHubException; CloseReason = PartitionProcessorCloseReason.EventHubException; break; } } } if (unrecoverableException != null) { // In case an exception is encountered while partition processor is processing the error, don't // catch it and let the calling method (StopAsync) handle it. await OwnerEventProcessor.ProcessExceptionAsync(Context, unrecoverableException).ConfigureAwait(false); } }