public async Task RunLoadBalancingAsyncClaimsAllClaimablePartitions()
        {
            const int NumberOfPartitions = 3;
            var       partitionIds       = Enumerable.Range(1, NumberOfPartitions).Select(p => p.ToString()).ToArray();
            var       storageManager     = new MockCheckPointStorage((s) => Console.WriteLine(s));
            var       loadbalancer       = new PartitionLoadBalancer(
                storageManager, Guid.NewGuid().ToString(), ConsumerGroup, FullyQualifiedNamespace, EventHubName, TimeSpan.FromMinutes(1));

            // Ownership should start empty.

            var completeOwnership = await storageManager.ListOwnershipAsync(FullyQualifiedNamespace, EventHubName, ConsumerGroup);

            Assert.That(completeOwnership.Count(), Is.EqualTo(0));

            // Start the load balancer so that it claims a random partition until none are left.

            for (int i = 0; i < NumberOfPartitions; i++)
            {
                await loadbalancer.RunLoadBalancingAsync(partitionIds, CancellationToken.None);
            }

            completeOwnership = await storageManager.ListOwnershipAsync(FullyQualifiedNamespace, EventHubName, ConsumerGroup);

            // All partitions are owned by load balancer.

            Assert.That(completeOwnership.Count(), Is.EqualTo(NumberOfPartitions));
        }
        public async Task RelinquishOwnershipAsyncRelinquishesPartitionOwnershipOtherClientsConsiderThemClaimableImmediately()
        {
            const int NumberOfPartitions = 3;
            var       partitionIds       = Enumerable.Range(1, NumberOfPartitions).Select(p => p.ToString()).ToArray();
            var       storageManager     = new MockCheckPointStorage((s) => Console.WriteLine(s));
            var       loadbalancer1      = new PartitionLoadBalancer(
                storageManager, Guid.NewGuid().ToString(), ConsumerGroup, FullyQualifiedNamespace, EventHubName, TimeSpan.FromMinutes(1));
            var loadbalancer2 = new PartitionLoadBalancer(
                storageManager, Guid.NewGuid().ToString(), ConsumerGroup, FullyQualifiedNamespace, EventHubName, TimeSpan.FromMinutes(1));

            // Ownership should start empty.

            var completeOwnership = await storageManager.ListOwnershipAsync(FullyQualifiedNamespace, EventHubName, ConsumerGroup);

            Assert.That(completeOwnership.Count(), Is.EqualTo(0));

            // Start the load balancer so that it claims a random partition until none are left.

            for (int i = 0; i < NumberOfPartitions; i++)
            {
                await loadbalancer1.RunLoadBalancingAsync(partitionIds, CancellationToken.None);
            }

            completeOwnership = await storageManager.ListOwnershipAsync(FullyQualifiedNamespace, EventHubName, ConsumerGroup);

            // All partitions are owned by loadbalancer1.

            Assert.That(completeOwnership.Count(p => p.OwnerIdentifier.Equals(loadbalancer1.OwnerIdentifier)), Is.EqualTo(NumberOfPartitions));

            // Stopping the load balancer should relinquish all partition ownership.

            await loadbalancer1.RelinquishOwnershipAsync(CancellationToken.None);

            completeOwnership = await storageManager.ListOwnershipAsync(loadbalancer1.FullyQualifiedNamespace, loadbalancer1.EventHubName, loadbalancer1.ConsumerGroup);

            // No partitions are owned by loadbalancer1.

            Assert.That(completeOwnership.Count(p => p.OwnerIdentifier.Equals(loadbalancer1.OwnerIdentifier)), Is.EqualTo(0));

            // Start loadbalancer2 so that the load balancer claims a random partition until none are left.
            // All partitions should be immediately claimable even though they were just claimed by the loadbalancer1.

            for (int i = 0; i < NumberOfPartitions; i++)
            {
                await loadbalancer2.RunLoadBalancingAsync(partitionIds, CancellationToken.None);
            }

            completeOwnership = await storageManager.ListOwnershipAsync(loadbalancer1.FullyQualifiedNamespace, loadbalancer1.EventHubName, loadbalancer1.ConsumerGroup);

            // All partitions are owned by loadbalancer2.

            Assert.That(completeOwnership.Count(p => p.OwnerIdentifier.Equals(loadbalancer2.OwnerIdentifier)), Is.EqualTo(NumberOfPartitions));
        }
        public async Task VerifiesEventProcessorLogs()
        {
            const int NumberOfPartitions    = 4;
            const int MinimumpartitionCount = NumberOfPartitions / 2;
            var       partitionIds          = Enumerable.Range(1, NumberOfPartitions).Select(p => p.ToString()).ToArray();
            var       storageManager        = new MockCheckPointStorage((s) => Console.WriteLine(s));
            var       loadbalancer          = new PartitionLoadBalancer(
                storageManager, Guid.NewGuid().ToString(), ConsumerGroup, FullyQualifiedNamespace, EventHubName, TimeSpan.FromMinutes(1));

            // Create more partitions owned by a different load balancer.

            var loadbalancer2Id   = Guid.NewGuid().ToString();
            var completeOwnership = CreatePartitionOwnership(partitionIds.Skip(1), loadbalancer2Id);

            // Seed the storageManager with the owned partitions.

            await storageManager.ClaimOwnershipAsync(completeOwnership);

            var mockLog = new Mock <PartitionLoadBalancerEventSource>();

            loadbalancer.Logger = mockLog.Object;

            for (int i = 0; i < NumberOfPartitions; i++)
            {
                await loadbalancer.RunLoadBalancingAsync(partitionIds, CancellationToken.None);
            }

            await loadbalancer.RelinquishOwnershipAsync(CancellationToken.None);

            mockLog.Verify(m => m.RenewOwnershipStart(loadbalancer.OwnerIdentifier));
            mockLog.Verify(m => m.RenewOwnershipComplete(loadbalancer.OwnerIdentifier));
            mockLog.Verify(m => m.ClaimOwnershipStart(It.Is <string>(p => partitionIds.Contains(p))));
            mockLog.Verify(m => m.MinimumPartitionsPerEventProcessor(MinimumpartitionCount));
            mockLog.Verify(m => m.CurrentOwnershipCount(MinimumpartitionCount, loadbalancer.OwnerIdentifier));
            mockLog.Verify(m => m.StealPartition(loadbalancer.OwnerIdentifier));
            mockLog.Verify(m => m.ShouldStealPartition(loadbalancer.OwnerIdentifier));
            mockLog.Verify(m => m.UnclaimedPartitions(It.Is <HashSet <string> >(p => p.Overlaps(partitionIds))));
        }
示例#4
0
        /// <summary>
        ///   Runs the sample using the specified Event Hubs connection information.
        /// </summary>
        ///
        /// <param name="connectionString">The connection string for the Event Hubs namespace that the sample should target.</param>
        /// <param name="eventHubName">The name of the Event Hub, sometimes known as its path, that she sample should run against.</param>
        ///
        public async Task RunAsync(string connectionString,
                                   string eventHubName)
        {
            // An event processor is associated with a specific Event Hub and a consumer group.  It receives events from
            // multiple partitions in the Event Hub, passing them to a handler delegate for processing using code that you
            // provide.
            //
            // These handler delegates are invoked for each event or error that occurs during operation of the event processor.  For
            // a given partition, only a single event will be dispatched for processing at a time so that the order of events within a
            // partition is preserved.  Partitions, however, are processed concurrently.  As a result, your handlers are potentially processing
            // multiple events or errors at any given time.

            // A partition manager may create checkpoints and list/claim partition ownership.  A developer may implement their
            // own partition manager by creating a subclass from the PartitionManager abstract class.  Here we are creating
            // a new instance of a MockCheckPointStorage to get started with using the `EventProcessor` but in production,
            // you should choose an implementation of the PartitionManager interface that will
            // store the checkpoints and partition ownership to a persistent store instead.
            // This isn't relevant to understanding this sample, but is required by the event processor constructor.

            var partitionManager = new MockCheckPointStorage();

            // It's also possible to specify custom options upon event processor creation.  We don't want to wait
            // more than 1 second for every set of events.

            var eventProcessorOptions = new EventProcessorClientOptions
            {
                MaximumWaitTime = TimeSpan.FromSeconds(1)
            };

            // Let's finally create our event processor.  We're using the default consumer group that was created with the Event Hub.

            var eventProcessor = new EventProcessorClient(partitionManager, EventHubConsumerClient.DefaultConsumerGroupName, connectionString, eventHubName, eventProcessorOptions);

            int totalEventsCount = 0;
            int partitionsBeingProcessedCount = 0;

            eventProcessor.PartitionInitializingAsync += (eventArgs) =>
            {
                // This is the last piece of code guaranteed to run before event processing, so all initialization
                // must be done by the moment this method returns.

                // We want to receive events from the latest available position so older events don't interfere with our sample.

                eventArgs.DefaultStartingPosition = EventPosition.Latest;

                Interlocked.Increment(ref partitionsBeingProcessedCount);

                Console.WriteLine($"\tPartition '{ eventArgs.PartitionId }': partition processing has started.");

                // This method is asynchronous, which means it's expected to return a Task.

                return(Task.CompletedTask);
            };

            eventProcessor.PartitionClosingAsync += (eventArgs) =>
            {
                // The code to be run just before stopping processing events for a partition.  This is the right place to dispose
                // of objects that will no longer be used.

                Interlocked.Decrement(ref partitionsBeingProcessedCount);

                Console.WriteLine($"\tPartition '{ eventArgs.Partition.PartitionId }': partition processing has stopped. Reason: { eventArgs.Reason }.");

                // This method is asynchronous, which means it's expected to return a Task.

                return(Task.CompletedTask);
            };

            eventProcessor.ProcessEventAsync += (eventArgs) =>
            {
                // Here the user can specify what to do with the event received from the event processor.  We are counting how
                // many events were received across all partitions so we can check whether all sent events were received.
                //
                // It's important to notice that this method is called even when no event is received after the maximum wait time, which
                // can be specified by the user in the event processor options.  In this case, the received event is null.

                if (eventArgs.Data != null)
                {
                    Interlocked.Increment(ref totalEventsCount);
                    Console.WriteLine($"\tPartition '{ eventArgs.Partition.PartitionId }': event received.");
                }

                // This method is asynchronous, which means it's expected to return a Task.

                return(Task.CompletedTask);
            };

            eventProcessor.ProcessErrorAsync += (eventArgs) =>
            {
                // Any exception which occurs as a result of the event processor itself will be passed to
                // this delegate so it may be handled.  The processor will continue to process events if
                // it is able to unless this handler explicitly requests that it stop doing so.
                //
                // It is important to note that this does not include exceptions during event processing; those
                // are considered responsibility of the developer implementing the event processing handler.  It
                // is, therefore, highly encouraged that best practices for exception handling practices are
                // followed with that delegate.
                //
                // This piece of code is not supposed to be reached by this sample.  If the following message has been printed
                // to the Console, then something unexpected has happened.

                Console.WriteLine($"\tPartition '{ eventArgs.PartitionId }': an unhandled exception was encountered. This was not expected to happen.");

                // This method is asynchronous, which means it's expected to return a Task.

                return(Task.CompletedTask);
            };

            // Once started, the event processor will start to claim partitions and receive events from them.

            Console.WriteLine("Starting the event processor.");
            Console.WriteLine();

            await eventProcessor.StartProcessingAsync();

            Console.WriteLine("Event processor started.");
            Console.WriteLine();

            // Wait until the event processor has claimed ownership of all partitions in the Event Hub.  This may take some time
            // as there's a 10 seconds interval between claims.  To be sure that we do not block forever in case the event processor
            // fails, we will specify a fairly long time to wait and then cancel waiting.

            CancellationTokenSource cancellationSource = new CancellationTokenSource();

            cancellationSource.CancelAfter(TimeSpan.FromSeconds(400));

            await using (var producerClient = new EventHubProducerClient(connectionString, eventHubName))
            {
                var partitionsCount = (await producerClient.GetPartitionIdsAsync()).Length;

                while (partitionsBeingProcessedCount < partitionsCount)
                {
                    await Task.Delay(500, cancellationSource.Token);
                }

                // The processor may take some time to connect to the Event Hubs service.  Let's wait 1 second before sending
                // events so we don't end up missing events.

                await Task.Delay(1000);

                // To test our event processor, we are publishing 10 sets of events to the Event Hub.  Notice that we are not
                // specifying a partition to send events to, so these sets may end up in different partitions.

                int amountOfSets           = 10;
                int eventsPerSet           = 2;
                int expectedAmountOfEvents = amountOfSets * eventsPerSet;

                Console.WriteLine();
                Console.WriteLine("Sending events to the Event Hub.");
                Console.WriteLine();

                for (int i = 0; i < amountOfSets; i++)
                {
                    using EventDataBatch eventBatch = await producerClient.CreateBatchAsync();

                    eventBatch.TryAdd(new EventData(Encoding.UTF8.GetBytes("I am not the second event.")));
                    eventBatch.TryAdd(new EventData(Encoding.UTF8.GetBytes("I am not the first event.")));

                    await producerClient.SendAsync(eventBatch);
                }

                // Because there is some non-determinism in the messaging flow, the sent events may not be immediately
                // available.  For this reason, we wait 500 ms before resuming.

                await Task.Delay(500);

                // Once stopped, the event processor won't receive events anymore.  In case there are still events being
                // processed when the stop method is called, the processing will complete before the corresponding partition
                // processor is closed.

                Console.WriteLine();
                Console.WriteLine("Stopping the event processor.");
                Console.WriteLine();

                await eventProcessor.StopProcessingAsync();

                // Print out the amount of events that we received.

                Console.WriteLine();
                Console.WriteLine($"Amount of events received: { totalEventsCount }. Expected: { expectedAmountOfEvents }.");
            }

            // At this point, our clients have passed their "using" scope and have safely been disposed of.  We
            // have no further obligations.

            Console.WriteLine();
        }
        public async Task RunLoadBalancingAsyncStealsPartitionsWhenThisLoadbalancerOwnsLessThanMinPartitionsAndOtherLoadbalancerOwnsMaxPartitions()
        {
            const int MinimumpartitionCount = 4;
            const int MaximumpartitionCount = 5;
            const int NumberOfPartitions    = 12;
            var       partitionIds          = Enumerable.Range(1, NumberOfPartitions).Select(p => p.ToString()).ToArray();
            var       storageManager        = new MockCheckPointStorage((s) => Console.WriteLine(s));
            var       loadbalancer          = new PartitionLoadBalancer(
                storageManager, Guid.NewGuid().ToString(), ConsumerGroup, FullyQualifiedNamespace, EventHubName, TimeSpan.FromMinutes(1));

            // Create more partitions owned by this load balancer.

            var loadbalancer1PartitionIds = Enumerable.Range(1, MinimumpartitionCount - 1);
            var completeOwnership         = CreatePartitionOwnership(loadbalancer1PartitionIds.Select(i => i.ToString()), loadbalancer.OwnerIdentifier);

            // Create more partitions owned by a different load balancer.

            var loadbalancer2Id           = Guid.NewGuid().ToString();
            var loadbalancer2PartitionIds = Enumerable.Range(loadbalancer1PartitionIds.Max() + 1, MinimumpartitionCount);

            completeOwnership = completeOwnership
                                .Concat(CreatePartitionOwnership(loadbalancer2PartitionIds.Select(i => i.ToString()), loadbalancer2Id));

            // Create more partitions owned by a different load balancer above the MaximumPartitionCount.

            var loadbalancer3Id       = Guid.NewGuid().ToString();
            var stealablePartitionIds = Enumerable.Range(loadbalancer2PartitionIds.Max() + 1, MaximumpartitionCount);

            completeOwnership = completeOwnership
                                .Concat(CreatePartitionOwnership(stealablePartitionIds.Select(i => i.ToString()), loadbalancer3Id));

            // Seed the storageManager with the owned partitions.

            await storageManager.ClaimOwnershipAsync(completeOwnership);

            // Get owned partitions.

            var totalOwnedPartitions = await storageManager.ListOwnershipAsync(loadbalancer.FullyQualifiedNamespace, loadbalancer.EventHubName, loadbalancer.ConsumerGroup);

            var ownedByloadbalancer1 = totalOwnedPartitions.Where(p => p.OwnerIdentifier == loadbalancer.OwnerIdentifier);
            var ownedByloadbalancer3 = totalOwnedPartitions.Where(p => p.OwnerIdentifier == loadbalancer3Id);

            // Verify owned partitionIds match the owned partitions.

            Assert.That(ownedByloadbalancer1.Any(owned => stealablePartitionIds.Contains(int.Parse(owned.PartitionId))), Is.False);

            // Verify load balancer 3 has stealable partitions.

            Assert.That(ownedByloadbalancer3.Count(), Is.EqualTo(MaximumpartitionCount));

            // Start the load balancer to steal ownership from of a when ownedPartitionCount == MinimumOwnedPartitionsCount but a load balancer owns > MaximumPartitionCount.

            await loadbalancer.RunLoadBalancingAsync(partitionIds, CancellationToken.None);

            // Get owned partitions.

            totalOwnedPartitions = await storageManager.ListOwnershipAsync(loadbalancer.FullyQualifiedNamespace, loadbalancer.EventHubName, loadbalancer.ConsumerGroup);

            ownedByloadbalancer1 = totalOwnedPartitions.Where(p => p.OwnerIdentifier == loadbalancer.OwnerIdentifier);
            ownedByloadbalancer3 = totalOwnedPartitions.Where(p => p.OwnerIdentifier == loadbalancer3Id);

            // Verify that we took ownership of the additional partition.

            Assert.That(ownedByloadbalancer1.Any(owned => stealablePartitionIds.Contains(int.Parse(owned.PartitionId))), Is.True);

            // Verify load balancer 3 now does not own > MaximumPartitionCount.

            Assert.That(ownedByloadbalancer3.Count(), Is.LessThan(MaximumpartitionCount));
        }
        public async Task RunLoadBalancingAsyncClaimsPartitionsWhenOwnedEqualsMinimumOwnedPartitionsCount()
        {
            const int MinimumpartitionCount = 4;
            const int NumberOfPartitions    = 13;
            var       partitionIds          = Enumerable.Range(1, NumberOfPartitions).Select(p => p.ToString()).ToArray();
            var       storageManager        = new MockCheckPointStorage((s) => Console.WriteLine(s));
            var       loadbalancer          = new PartitionLoadBalancer(
                storageManager, Guid.NewGuid().ToString(), ConsumerGroup, FullyQualifiedNamespace, EventHubName, TimeSpan.FromMinutes(1));

            // Create partitions owned by this load balancer.

            var loadbalancer1PartitionIds = Enumerable.Range(1, MinimumpartitionCount);
            var completeOwnership         = CreatePartitionOwnership(loadbalancer1PartitionIds.Select(i => i.ToString()), loadbalancer.OwnerIdentifier);

            // Create partitions owned by a different load balancer.

            var loadbalancer2Id           = Guid.NewGuid().ToString();
            var loadbalancer2PartitionIds = Enumerable.Range(loadbalancer1PartitionIds.Max() + 1, MinimumpartitionCount);

            completeOwnership = completeOwnership
                                .Concat(CreatePartitionOwnership(loadbalancer2PartitionIds.Select(i => i.ToString()), loadbalancer2Id));

            // Create partitions owned by a different load balancer.

            var loadbalancer3Id           = Guid.NewGuid().ToString();
            var loadbalancer3PartitionIds = Enumerable.Range(loadbalancer2PartitionIds.Max() + 1, MinimumpartitionCount);

            completeOwnership = completeOwnership
                                .Concat(CreatePartitionOwnership(loadbalancer3PartitionIds.Select(i => i.ToString()), loadbalancer3Id));

            // Seed the storageManager with all partitions.

            await storageManager.ClaimOwnershipAsync(completeOwnership);

            var claimablePartitionIds = partitionIds.Except(completeOwnership.Select(p => p.PartitionId));

            // Get owned partitions.

            var totalOwnedPartitions = await storageManager.ListOwnershipAsync(loadbalancer.FullyQualifiedNamespace, loadbalancer.EventHubName, loadbalancer.ConsumerGroup);

            var ownedByloadbalancer1 = totalOwnedPartitions.Where(p => p.OwnerIdentifier == loadbalancer.OwnerIdentifier);

            // Verify owned partitionIds match the owned partitions.

            Assert.That(ownedByloadbalancer1.Count(), Is.EqualTo(MinimumpartitionCount));
            Assert.That(ownedByloadbalancer1.Any(owned => claimablePartitionIds.Contains(owned.PartitionId)), Is.False);

            // Start the load balancer to claim ownership from of a Partition even though ownedPartitionCount == MinimumOwnedPartitionsCount.

            await loadbalancer.RunLoadBalancingAsync(partitionIds, CancellationToken.None);

            // Get owned partitions.

            totalOwnedPartitions = await storageManager.ListOwnershipAsync(loadbalancer.FullyQualifiedNamespace, loadbalancer.EventHubName, loadbalancer.ConsumerGroup);

            ownedByloadbalancer1 = totalOwnedPartitions.Where(p => p.OwnerIdentifier == loadbalancer.OwnerIdentifier);

            // Verify that we took ownership of the additional partition.

            Assert.That(ownedByloadbalancer1.Count(), Is.GreaterThan(MinimumpartitionCount));
            Assert.That(ownedByloadbalancer1.Any(owned => claimablePartitionIds.Contains(owned.PartitionId)), Is.True);
        }