/// <summary> /// Stops the event processor. In case it hasn't been started, nothing happens. /// </summary> /// /// <returns>A task to be resolved on when the operation has completed.</returns> /// public async Task StopAsync() { if (RunningTask != null) { await RunningTaskSemaphore.WaitAsync().ConfigureAwait(false); try { if (RunningTask != null) { RunningTaskTokenSource.Cancel(); RunningTaskTokenSource = null; try { await RunningTask.ConfigureAwait(false); } finally { RunningTask = null; } await Task.WhenAll(PartitionPumps.Select(kvp => kvp.Value.StopAsync())).ConfigureAwait(false); } } finally { RunningTaskSemaphore.Release(); } } }
/// <summary> /// Stops an owned partition pump instance in case it exists. It is also removed from the pumps dictionary. /// </summary> /// /// <param name="partitionId">The identifier of the Event Hub partition the partition pump is associated with.</param> /// <param name="reason">The reason why the partition processor is being closed.</param> /// /// <returns>A task to be resolved on when the operation has completed.</returns> /// private async Task RemovePartitionPumpIfItExistsAsync(string partitionId, PartitionProcessorCloseReason?reason = null) { if (PartitionPumps.TryRemove(partitionId, out var pump)) { try { await pump.StopAsync(reason).ConfigureAwait(false); } catch (Exception) { // TODO: delegate the exception handling to an Exception Callback. } } }
/// <summary> /// The main loop of an event processor. It loops through every owned <see cref="PartitionPump" />, checking /// its status and creating a new one if necessary. /// </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> /// /// <remarks> /// The actual goal of this method is to perform load balancing between multiple <see cref="EventProcessor" /> /// instances, but this feature is currently out of the scope of the current preview. /// </remarks> /// private async Task RunAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { await Task.WhenAll(PartitionPumps .Where(kvp => !kvp.Value.IsRunning) .Select(async kvp => { try { await kvp.Value.StopAsync(); } catch (Exception) { // We're catching every possible unhandled exception that may have happened during Partition Pump execution. // TODO: delegate the exception handling to an Exception Callback. } var partitionId = kvp.Key; var partitionContext = new PartitionContext(InnerClient.EventHubName, ConsumerGroup, partitionId); var checkpointManager = new CheckpointManager(partitionContext, Manager, Identifier); var partitionProcessor = PartitionProcessorFactory(partitionContext, checkpointManager); var partitionPump = new PartitionPump(InnerClient, ConsumerGroup, partitionId, partitionProcessor, Options); PartitionPumps.TryUpdate(partitionId, partitionPump, partitionPump); await partitionPump.StartAsync(); })).ConfigureAwait(false); try { // Wait 1 second before the next verification. await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } catch (TaskCanceledException) { } } }
/// <summary> /// Starts the event processor. 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) { RunningTaskTokenSource?.Cancel(); RunningTaskTokenSource = new CancellationTokenSource(); PartitionPumps.Clear(); var partitionIds = await InnerClient.GetPartitionIdsAsync().ConfigureAwait(false); await Task.WhenAll(partitionIds .Select(partitionId => { var partitionContext = new PartitionContext(InnerClient.EventHubName, ConsumerGroup, partitionId); var checkpointManager = new CheckpointManager(partitionContext, Manager, Identifier); var partitionProcessor = PartitionProcessorFactory(partitionContext, checkpointManager); var partitionPump = new PartitionPump(InnerClient, ConsumerGroup, partitionId, partitionProcessor, Options); PartitionPumps.TryAdd(partitionId, partitionPump); return(partitionPump.StartAsync()); })).ConfigureAwait(false); RunningTask = RunAsync(RunningTaskTokenSource.Token); } } finally { RunningTaskSemaphore.Release(); } } }
/// <summary> /// Performs load balancing between multiple <see cref="EventProcessor{T}" /> instances, claiming others' partitions to enforce /// a more equal distribution when necessary. It also manages its own partition pumps and ownership. /// </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) { while (!cancellationToken.IsCancellationRequested) { Stopwatch cycleDuration = Stopwatch.StartNew(); // Renew this instance's ownership so they don't expire. await RenewOwnershipAsync().ConfigureAwait(false); // From the storage service provided by the user, obtain a complete list of ownership, including expired ones. We may still need // their eTags to claim orphan partitions. var completeOwnershipList = (await Manager .ListOwnershipAsync(InnerClient.EventHubName, ConsumerGroup) .ConfigureAwait(false)) .ToList(); // Filter the complete ownership list to obtain only the ones that are still active. The expiration time defaults to 30 seconds, // but it may be overriden by a derived class. var activeOwnership = completeOwnershipList .Where(ownership => DateTimeOffset.UtcNow.Subtract(ownership.LastModifiedTime.Value) < OwnershipExpiration); // Dispose of all previous partition ownership instances and get a whole new dictionary. InstanceOwnership = activeOwnership .Where(ownership => ownership.OwnerIdentifier == Identifier) .ToDictionary(ownership => ownership.PartitionId); // Some previously owned partitions might have had their ownership expired or might have been stolen, so we need to stop // the pumps we don't need anymore. await Task.WhenAll(PartitionPumps.Keys .Except(InstanceOwnership.Keys) .Select(partitionId => RemovePartitionPumpIfItExistsAsync(partitionId, PartitionProcessorCloseReason.OwnershipLost))) .ConfigureAwait(false); // Now that we are left with pumps that should be running, check their status. If any has stopped, it means an // unexpected failure has happened, so try closing it and starting a new one. In case we don't have a pump that // should exist, create it. This might happen when pump creation has failed in a previous cycle. await Task.WhenAll(InstanceOwnership .Where(kvp => { if (PartitionPumps.TryGetValue(kvp.Key, out var pump)) { return(!pump.IsRunning); } return(true); }) .Select(kvp => AddOrOverwritePartitionPumpAsync(kvp.Key, kvp.Value.SequenceNumber))) .ConfigureAwait(false); // Find an ownership to claim and try to claim it. The method will return null if this instance was not eligible to // increase its ownership list, if no claimable ownership could be found or if a claim attempt failed. var claimedOwnership = await FindAndClaimOwnershipAsync(completeOwnershipList, activeOwnership).ConfigureAwait(false); if (claimedOwnership != null) { InstanceOwnership[claimedOwnership.PartitionId] = claimedOwnership; await AddOrOverwritePartitionPumpAsync(claimedOwnership.PartitionId, claimedOwnership.SequenceNumber).ConfigureAwait(false); } // Wait the remaining time, if any, to start the next cycle. The total time of a cycle defaults to 10 seconds, // but it may be overriden by a derived class. TimeSpan remainingTimeUntilNextCycle = cycleDuration.Elapsed - LoadBalanceUpdate; if (remainingTimeUntilNextCycle > TimeSpan.Zero) { // If a stop request has been issued, Task.Delay will throw a TaskCanceledException. This is expected and it // will be caught by the StopAsync method. await Task.Delay(remainingTimeUntilNextCycle, cancellationToken).ConfigureAwait(false); } } }
/// <summary> /// Performs load balancing between multiple <see cref="EventProcessor" /> instances, claiming others' partitions to enforce /// a more equal distribution when necessary. It also manages its own partition pumps and ownership. /// </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) { while (!cancellationToken.IsCancellationRequested) { Stopwatch cycleDuration = Stopwatch.StartNew(); // Renew this instance's ownership so they don't expire. This method call will fill the InstanceOwnership dictionary // with the renewed ownership information. await RenewOwnershipAsync().ConfigureAwait(false); // Some previously owned partitions might have had their ownership expired or might have been stolen, so we need to stop // the pumps we don't need anymore. await Task.WhenAll(PartitionPumps.Keys .Except(InstanceOwnership.Keys) .Select(partitionId => RemovePartitionPumpIfItExistsAsync(partitionId, PartitionProcessorCloseReason.OwnershipLost))) .ConfigureAwait(false); // Now that we are left with pumps that should be running, check their status. If any has stopped, it means an // unexpected failure has happened, so try closing it and starting a new one. In case we don't have a pump that // should exist, create it. This might happen when pump creation has failed in a previous cycle. await Task.WhenAll(InstanceOwnership.Keys .Where(partitionId => { if (PartitionPumps.TryGetValue(partitionId, out var pump)) { return(!pump.IsRunning); } return(true); }) .Select(partitionId => AddOrOverwritePartitionPumpAsync(partitionId))) .ConfigureAwait(false); // Find an ownership to claim and try to claim it. The method will return null if this instance was not eligible to // increase its ownership list, if no claimable ownership could be found or if a claim attempt failed. var claimedOwnership = await FindAndClaimOwnershipAsync().ConfigureAwait(false); if (claimedOwnership != null) { InstanceOwnership[claimedOwnership.PartitionId] = claimedOwnership; await AddOrOverwritePartitionPumpAsync(claimedOwnership.PartitionId).ConfigureAwait(false); } // Wait the remaining time, if any, to start the next cycle. The total time of a cycle defaults to 10 seconds, // but it may be overriden by a derived class. TimeSpan remainingTimeUntilNextCycle = cycleDuration.Elapsed - LoadBalanceUpdate; if (remainingTimeUntilNextCycle > TimeSpan.Zero) { // If a stop request has been issued, Task.Delay will throw a TaskCanceledException. This is expected and it // will be caught by the StopAsync method. await Task.Delay(remainingTimeUntilNextCycle, cancellationToken).ConfigureAwait(false); } } }