/// <summary> /// Tries to claim ownership of the specified partition. /// </summary> /// /// <param name="partitionId">The identifier of the Event Hub partition the ownership is associated with.</param> /// <param name="completeOwnershipEnumerable">A complete enumerable of ownership obtained from the stored service provided by the user.</param> /// /// <returns>The claimed ownership. <c>null</c> if the claim attempt failed.</returns> /// private async Task <PartitionOwnership> ClaimOwnershipAsync(string partitionId, IEnumerable <PartitionOwnership> completeOwnershipEnumerable) { // We need the eTag from the most recent ownership of this partition, even if it's expired. We want to keep the offset and // the sequence number as well. var oldOwnership = completeOwnershipEnumerable.FirstOrDefault(ownership => ownership.PartitionId == partitionId); var newOwnership = new PartitionOwnership ( InnerClient.EventHubName, ConsumerGroup, Identifier, partitionId, oldOwnership?.Offset, oldOwnership?.SequenceNumber, DateTimeOffset.UtcNow, oldOwnership?.ETag ); // We are expecting an enumerable with a single element if the claim attempt succeeds. var claimedOwnership = (await Manager .ClaimOwnershipAsync(new List <PartitionOwnership> { newOwnership }) .ConfigureAwait(false)); return(claimedOwnership.FirstOrDefault()); }
/// <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.FullyQualifiedNamespace, 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. IEnumerable <PartitionOwnership> 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 PartitionPump 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. PartitionOwnership 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 = LoadBalanceUpdate - cycleDuration.Elapsed; 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); } } }