diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/src/EventProcessorClient.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/src/EventProcessorClient.cs old mode 100644 new mode 100755 index 383ff18a3d9c..d650b0add2c1 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/src/EventProcessorClient.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/src/EventProcessorClient.cs @@ -952,7 +952,7 @@ private async Task RunAsync(CancellationToken cancellationToken) } catch (EventHubsException ex) { - var errorEventArgs = new ProcessErrorEventArgs(null, ex.Message, ex, cancellationToken); + var errorEventArgs = new ProcessErrorEventArgs(null, ex.Message, ex.InnerException ?? ex, cancellationToken); _ = OnProcessErrorAsync(errorEventArgs); } catch (Exception ex) @@ -1040,7 +1040,7 @@ private async Task StartPartitionProcessingAsync(string partitionId, // partition. Logger.StartPartitionProcessingError(partitionId, ex.Message); - var errorEventArgs = new ProcessErrorEventArgs(null, Resources.OperationListCheckpoints, ex, cancellationToken); + var errorEventArgs = new ProcessErrorEventArgs(partitionId, Resources.OperationListCheckpoints, ex, cancellationToken); _ = OnProcessErrorAsync(errorEventArgs); return; diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/CheckpointStore/BlobsCheckpointStoreTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/CheckpointStore/BlobsCheckpointStoreTests.cs index 1aae94ceda20..2196f0c31027 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/CheckpointStore/BlobsCheckpointStoreTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/CheckpointStore/BlobsCheckpointStoreTests.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Azure.Messaging.EventHubs.Core; using Azure.Messaging.EventHubs.Processor.Diagnostics; +using Azure.Storage; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Moq; @@ -30,6 +32,29 @@ public class BlobsCheckpointStoreTests private const string PartitionId = "1"; private readonly string OwnershipIdentifier = Guid.NewGuid().ToString(); + /// + /// Provides the test cases for non-fatal exceptions that are not retriable + /// when encountered in a subscription. + /// + /// + public static IEnumerable NonFatalNotRetriableExceptionTestCases() + { + yield return new object[] { new InvalidOperationException() }; + yield return new object[] { new NotSupportedException() }; + yield return new object[] { new NullReferenceException() }; + yield return new object[] { new ObjectDisposedException("dummy") }; + } + + /// + /// Provides the test cases for non-fatal exceptions that are retriable + /// when encountered in a subscription. + /// + /// + public static IEnumerable NonFatalRetriableExceptionTestCases() + { + yield return new object[] { new TimeoutException() }; + } + /// /// Verifies functionality of the /// constructor. @@ -209,12 +234,12 @@ public async Task ListCheckpointsLogsStartAndComplete() /// /// [Test] - public void ListCheckointsForMissingPartitionThrowsAndLogsOwnershipNotClaimable() + public void ListCheckpointsForMissingPartitionThrowsAndLogsOwnershipNotClaimable() { var ex = new RequestFailedException(0, "foo", BlobErrorCode.ContainerNotFound.ToString(), null); var target = new BlobsCheckpointStore(new MockBlobContainerClient(getBlobsAsyncException: ex), - new BasicRetryPolicy(new EventHubsRetryOptions())); + new BasicRetryPolicy(new EventHubsRetryOptions())); var mockLog = new Mock(); target.Logger = mockLog.Object; @@ -265,6 +290,591 @@ public void UpdateCheckpointForMissingCheckpointThrowsAndLogsCheckpointUpdateErr mockLog.Verify(m => m.CheckpointUpdateError(PartitionId, It.Is(s => s.Contains(BlobErrorCode.ContainerNotFound.ToString())))); } + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCaseSource(nameof(NonFatalRetriableExceptionTestCases))] + public void ListOwnershipAsyncRetriesAndSurfacesRetriableExceptions(Exception exception) + { + const int maximumRetries = 2; + + var expectedServiceCalls = (maximumRetries + 1); + var serviceCalls = 0; + + var mockRetryPolicy = new Mock(); + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, mockRetryPolicy.Object); + + mockRetryPolicy + .Setup(policy => policy.CalculateRetryDelay(It.Is(value => value == exception), It.Is(value => value <= maximumRetries))) + .Returns(TimeSpan.FromMilliseconds(5)); + + mockContainerClient.GetBlobsAsyncCallback = (traits, states, prefix, token) => + { + serviceCalls++; + throw exception; + }; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + Assert.That(async () => await checkpointStore.ListOwnershipAsync("ns", "eh", "cg", cancellationSource.Token), Throws.TypeOf(exception.GetType()), "The method call should surface the exception."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The operation should have stopped without cancellation."); + Assert.That(serviceCalls, Is.EqualTo(expectedServiceCalls), "The retry policy should have been applied."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCaseSource(nameof(NonFatalNotRetriableExceptionTestCases))] + public void ListOwnershipAsyncSurfacesNonRetriableExceptions(Exception exception) + { + var expectedServiceCalls = 1; + var serviceCalls = 0; + + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, new BasicRetryPolicy(new EventHubsRetryOptions())); + + mockContainerClient.GetBlobsAsyncCallback = (traits, states, prefix, token) => + { + serviceCalls++; + throw exception; + }; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + Assert.That(async () => await checkpointStore.ListOwnershipAsync("ns", "eh", "cg", cancellationSource.Token), Throws.TypeOf(exception.GetType()), "The method call should surface the exception."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The operation should have stopped without cancellation."); + Assert.That(serviceCalls, Is.EqualTo(expectedServiceCalls), "The retry policy should not have been applied."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public async Task ListOwnershipAsyncDelegatesTheCancellationToken() + { + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, new BasicRetryPolicy(new EventHubsRetryOptions())); + + using var cancellationSource = new CancellationTokenSource(); + var stateBeforeCancellation = default(bool?); + var stateAfterCancellation = default(bool?); + + mockContainerClient.GetBlobsAsyncCallback = (traits, states, prefix, token) => + { + if (!stateBeforeCancellation.HasValue) + { + stateBeforeCancellation = token.IsCancellationRequested; + cancellationSource.Cancel(); + stateAfterCancellation = token.IsCancellationRequested; + } + }; + + await checkpointStore.ListOwnershipAsync("ns", "eh", "cg", cancellationSource.Token); + + Assert.That(stateBeforeCancellation.HasValue, Is.True, "State before cancellation should have been captured."); + Assert.That(stateBeforeCancellation.Value, Is.False, "The token should not have been canceled before cancellation request."); + Assert.That(stateAfterCancellation.HasValue, Is.True, "State after cancellation should have been captured."); + Assert.That(stateAfterCancellation.Value, Is.True, "The token should have been canceled after cancellation request."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public void AlreadyCanceledTokenMakesListOwnershipAsyncThrow() + { + var checkpointStore = new BlobsCheckpointStore(Mock.Of(), Mock.Of()); + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.Cancel(); + + Assert.That(async () => await checkpointStore.ListOwnershipAsync("ns", "eh", "cg", cancellationSource.Token), Throws.InstanceOf()); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCaseSource(nameof(NonFatalRetriableExceptionTestCases))] + public void ClaimOwnershipAsyncRetriesAndSurfacesRetriableExceptionsWhenETagIsNull(Exception exception) + { + const int maximumRetries = 2; + + var expectedServiceCalls = (maximumRetries + 1); + var serviceCalls = 0; + + var mockRetryPolicy = new Mock(); + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, mockRetryPolicy.Object); + var ownership = new PartitionOwnership("ns", "eh", "cg", "id", "pid", default, null); + + mockRetryPolicy + .Setup(policy => policy.CalculateRetryDelay(It.Is(value => value == exception), It.Is(value => value <= maximumRetries))) + .Returns(TimeSpan.FromMilliseconds(5)); + + mockContainerClient.BlobClientUploadAsyncCallback = (content, httpHeaders, metadata, conditions, progressHandler, accessTier, transferOptions, token) => + { + serviceCalls++; + throw exception; + }; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + Assert.That(async () => await checkpointStore.ClaimOwnershipAsync(new List() { ownership }, cancellationSource.Token), Throws.TypeOf(exception.GetType()), "The method call should surface the exception."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The operation should have stopped without cancellation."); + Assert.That(serviceCalls, Is.EqualTo(expectedServiceCalls), "The retry policy should have been applied."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCaseSource(nameof(NonFatalNotRetriableExceptionTestCases))] + public void ClaimOwnershipAsyncSurfacesNonRetriableExceptionsWhenETagIsNull(Exception exception) + { + var expectedServiceCalls = 1; + var serviceCalls = 0; + + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, new BasicRetryPolicy(new EventHubsRetryOptions())); + var ownership = new PartitionOwnership("ns", "eh", "cg", "id", "pid", default, null); + + mockContainerClient.BlobClientUploadAsyncCallback = (content, httpHeaders, metadata, conditions, progressHandler, accessTier, transferOptions, token) => + { + serviceCalls++; + throw exception; + }; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + Assert.That(async () => await checkpointStore.ClaimOwnershipAsync(new List() { ownership }, cancellationSource.Token), Throws.TypeOf(exception.GetType()), "The method call should surface the exception."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The operation should have stopped without cancellation."); + Assert.That(serviceCalls, Is.EqualTo(expectedServiceCalls), "The retry policy should not have been applied."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCaseSource(nameof(NonFatalRetriableExceptionTestCases))] + public void ClaimOwnershipAsyncRetriesAndSurfacesRetriableExceptionsWhenETagIsNotNull(Exception exception) + { + const int maximumRetries = 2; + + var expectedServiceCalls = (maximumRetries + 1); + var serviceCalls = 0; + + var mockRetryPolicy = new Mock(); + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, mockRetryPolicy.Object); + var ownership = new PartitionOwnership("ns", "eh", "cg", "id", "pid", default, "eTag"); + + mockRetryPolicy + .Setup(policy => policy.CalculateRetryDelay(It.Is(value => value == exception), It.Is(value => value <= maximumRetries))) + .Returns(TimeSpan.FromMilliseconds(5)); + + mockContainerClient.BlobInfo = Mock.Of(); + + mockContainerClient.BlobClientSetMetadataAsyncCallback = (metadata, conditions, token) => + { + serviceCalls++; + throw exception; + }; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + Assert.That(async () => await checkpointStore.ClaimOwnershipAsync(new List() { ownership }, cancellationSource.Token), Throws.TypeOf(exception.GetType()), "The method call should surface the exception."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The operation should have stopped without cancellation."); + Assert.That(serviceCalls, Is.EqualTo(expectedServiceCalls), "The retry policy should have been applied."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCaseSource(nameof(NonFatalNotRetriableExceptionTestCases))] + public void ClaimOwnershipAsyncSurfacesNonRetriableExceptionsWhenETagIsNotNull(Exception exception) + { + var expectedServiceCalls = 1; + var serviceCalls = 0; + + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, new BasicRetryPolicy(new EventHubsRetryOptions())); + var ownership = new PartitionOwnership("ns", "eh", "cg", "id", "pid", default, "eTag"); + + mockContainerClient.BlobInfo = Mock.Of(); + + mockContainerClient.BlobClientSetMetadataAsyncCallback = (metadata, conditions, token) => + { + serviceCalls++; + throw exception; + }; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + Assert.That(async () => await checkpointStore.ClaimOwnershipAsync(new List() { ownership }, cancellationSource.Token), Throws.TypeOf(exception.GetType()), "The method call should surface the exception."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The operation should have stopped without cancellation."); + Assert.That(serviceCalls, Is.EqualTo(expectedServiceCalls), "The retry policy should not have been applied."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCase(null)] + [TestCase("eTag")] + public async Task ClaimOwnershipAsyncDelegatesTheCancellationToken(string eTag) + { + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, new BasicRetryPolicy(new EventHubsRetryOptions())); + var ownership = new PartitionOwnership("ns", "eh", "cg", "id", "pid", default, eTag); + + using var cancellationSource = new CancellationTokenSource(); + var stateBeforeCancellation = default(bool?); + var stateAfterCancellation = default(bool?); + + // UploadAsync will be called if eTag is null; SetMetadataAsync is used otherwise. + + if (eTag == null) + { + mockContainerClient.BlobClientUploadAsyncCallback = (content, httpHeaders, metadata, conditions, progressHandler, accessTier, transferOptions, token) => + { + if (!stateBeforeCancellation.HasValue) + { + stateBeforeCancellation = token.IsCancellationRequested; + cancellationSource.Cancel(); + stateAfterCancellation = token.IsCancellationRequested; + } + }; + } + else + { + mockContainerClient.BlobInfo = Mock.Of(); + + mockContainerClient.BlobClientSetMetadataAsyncCallback = (metadata, conditions, token) => + { + if (!stateBeforeCancellation.HasValue) + { + stateBeforeCancellation = token.IsCancellationRequested; + cancellationSource.Cancel(); + stateAfterCancellation = token.IsCancellationRequested; + } + }; + } + + await checkpointStore.ClaimOwnershipAsync(new List() { ownership }, cancellationSource.Token); + + Assert.That(stateBeforeCancellation.HasValue, Is.True, "State before cancellation should have been captured."); + Assert.That(stateBeforeCancellation.Value, Is.False, "The token should not have been canceled before cancellation request."); + Assert.That(stateAfterCancellation.HasValue, Is.True, "State after cancellation should have been captured."); + Assert.That(stateAfterCancellation.Value, Is.True, "The token should have been canceled after cancellation request."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public void AlreadyCanceledTokenMakesClaimOwnershipAsyncThrow() + { + var checkpointStore = new BlobsCheckpointStore(Mock.Of(), Mock.Of()); + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.Cancel(); + + Assert.That(async () => await checkpointStore.ClaimOwnershipAsync(Mock.Of>(), cancellationSource.Token), Throws.InstanceOf()); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCaseSource(nameof(NonFatalRetriableExceptionTestCases))] + public void ListCheckpointsAsyncRetriesAndSurfacesRetriableExceptions(Exception exception) + { + const int maximumRetries = 2; + + var expectedServiceCalls = (maximumRetries + 1); + var serviceCalls = 0; + + var mockRetryPolicy = new Mock(); + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, mockRetryPolicy.Object); + + mockRetryPolicy + .Setup(policy => policy.CalculateRetryDelay(It.Is(value => value == exception), It.Is(value => value <= maximumRetries))) + .Returns(TimeSpan.FromMilliseconds(5)); + + mockContainerClient.GetBlobsAsyncCallback = (traits, states, prefix, token) => + { + serviceCalls++; + throw exception; + }; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + Assert.That(async () => await checkpointStore.ListCheckpointsAsync("ns", "eh", "cg", cancellationSource.Token), Throws.TypeOf(exception.GetType()), "The method call should surface the exception."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The operation should have stopped without cancellation."); + Assert.That(serviceCalls, Is.EqualTo(expectedServiceCalls), "The retry policy should have been applied."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCaseSource(nameof(NonFatalNotRetriableExceptionTestCases))] + public void ListCheckpointsAsyncSurfacesNonRetriableExceptions(Exception exception) + { + var expectedServiceCalls = 1; + var serviceCalls = 0; + + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, new BasicRetryPolicy(new EventHubsRetryOptions())); + + mockContainerClient.GetBlobsAsyncCallback = (traits, states, prefix, token) => + { + serviceCalls++; + throw exception; + }; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + Assert.That(async () => await checkpointStore.ListCheckpointsAsync("ns", "eh", "cg", cancellationSource.Token), Throws.TypeOf(exception.GetType()), "The method call should surface the exception."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The operation should have stopped without cancellation."); + Assert.That(serviceCalls, Is.EqualTo(expectedServiceCalls), "The retry policy should not have been applied."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public async Task ListCheckpointsAsyncDelegatesTheCancellationToken() + { + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, new BasicRetryPolicy(new EventHubsRetryOptions())); + + using var cancellationSource = new CancellationTokenSource(); + var stateBeforeCancellation = default(bool?); + var stateAfterCancellation = default(bool?); + + mockContainerClient.GetBlobsAsyncCallback = (traits, states, prefix, token) => + { + if (!stateBeforeCancellation.HasValue) + { + stateBeforeCancellation = token.IsCancellationRequested; + cancellationSource.Cancel(); + stateAfterCancellation = token.IsCancellationRequested; + } + }; + + await checkpointStore.ListCheckpointsAsync("ns", "eh", "cg", cancellationSource.Token); + + Assert.That(stateBeforeCancellation.HasValue, Is.True, "State before cancellation should have been captured."); + Assert.That(stateBeforeCancellation.Value, Is.False, "The token should not have been canceled before cancellation request."); + Assert.That(stateAfterCancellation.HasValue, Is.True, "State after cancellation should have been captured."); + Assert.That(stateAfterCancellation.Value, Is.True, "The token should have been canceled after cancellation request."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public void AlreadyCanceledTokenMakesListCheckpointsAsyncThrow() + { + var checkpointStore = new BlobsCheckpointStore(Mock.Of(), Mock.Of()); + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.Cancel(); + + Assert.That(async () => await checkpointStore.ListCheckpointsAsync("ns", "eh", "cg", cancellationSource.Token), Throws.InstanceOf()); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCaseSource(nameof(NonFatalRetriableExceptionTestCases))] + public void UpdateCheckpointAsyncRetriesAndSurfacesRetriableExceptions(Exception exception) + { + const int maximumRetries = 2; + + var expectedServiceCalls = (maximumRetries + 1); + var serviceCalls = 0; + + var mockRetryPolicy = new Mock(); + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, mockRetryPolicy.Object); + var checkpoint = new Checkpoint("ns", "eh", "cg", "pid", 0, 0); + + mockRetryPolicy + .Setup(policy => policy.CalculateRetryDelay(It.Is(value => value == exception), It.Is(value => value <= maximumRetries))) + .Returns(TimeSpan.FromMilliseconds(5)); + + mockContainerClient.BlobClientUploadAsyncCallback = (content, httpHeaders, metadata, conditions, progressHandler, accessTier, transferOptions, token) => + { + serviceCalls++; + throw exception; + }; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + Assert.That(async () => await checkpointStore.UpdateCheckpointAsync(checkpoint, cancellationSource.Token), Throws.TypeOf(exception.GetType()), "The method call should surface the exception."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The operation should have stopped without cancellation."); + Assert.That(serviceCalls, Is.EqualTo(expectedServiceCalls), "The retry policy should have been applied."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + [TestCaseSource(nameof(NonFatalNotRetriableExceptionTestCases))] + public void UpdateCheckpointAsyncSurfacesNonRetriableExceptions(Exception exception) + { + var expectedServiceCalls = 1; + var serviceCalls = 0; + + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, new BasicRetryPolicy(new EventHubsRetryOptions())); + var checkpoint = new Checkpoint("ns", "eh", "cg", "pid", 0, 0); + + mockContainerClient.BlobClientUploadAsyncCallback = (content, httpHeaders, metadata, conditions, progressHandler, accessTier, transferOptions, token) => + { + serviceCalls++; + throw exception; + }; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + Assert.That(async () => await checkpointStore.UpdateCheckpointAsync(checkpoint, cancellationSource.Token), Throws.TypeOf(exception.GetType()), "The method call should surface the exception."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The operation should have stopped without cancellation."); + Assert.That(serviceCalls, Is.EqualTo(expectedServiceCalls), "The retry policy should have been applied."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public async Task UpdateCheckpointAsyncDelegatesTheCancellationToken() + { + var mockContainerClient = new MockBlobContainerClient(); + var checkpointStore = new BlobsCheckpointStore(mockContainerClient, new BasicRetryPolicy(new EventHubsRetryOptions())); + var checkpoint = new Checkpoint("ns", "eh", "cg", "pid", 0, 0); + + using var cancellationSource = new CancellationTokenSource(); + var stateBeforeCancellation = default(bool?); + var stateAfterCancellation = default(bool?); + + mockContainerClient.BlobClientUploadAsyncCallback = (content, httpHeaders, metadata, conditions, progressHandler, accessTier, transferOptions, token) => + { + if (!stateBeforeCancellation.HasValue) + { + stateBeforeCancellation = token.IsCancellationRequested; + cancellationSource.Cancel(); + stateAfterCancellation = token.IsCancellationRequested; + } + }; + + await checkpointStore.UpdateCheckpointAsync(checkpoint, cancellationSource.Token); + + Assert.That(stateBeforeCancellation.HasValue, Is.True, "State before cancellation should have been captured."); + Assert.That(stateBeforeCancellation.Value, Is.False, "The token should not have been canceled before cancellation request."); + Assert.That(stateAfterCancellation.HasValue, Is.True, "State after cancellation should have been captured."); + Assert.That(stateAfterCancellation.Value, Is.True, "The token should have been canceled after cancellation request."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public void AlreadyCanceledTokenMakesUpdateCheckpointAsyncThrow() + { + var checkpointStore = new BlobsCheckpointStore(Mock.Of(), Mock.Of()); + var checkpoint = new Checkpoint("ns", "eh", "cg", "pid", 0, 0); + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.Cancel(); + + Assert.That(async () => await checkpointStore.UpdateCheckpointAsync(checkpoint, cancellationSource.Token), Throws.InstanceOf()); + } + private class MockBlobContainerClient : BlobContainerClient { public override Uri Uri { get; } @@ -274,8 +884,9 @@ private class MockBlobContainerClient : BlobContainerClient internal BlobInfo BlobInfo; internal Exception BlobClientUploadBlobException; internal Exception GetBlobsAsyncException; - - + internal Action GetBlobsAsyncCallback; + internal Action, BlobRequestConditions, IProgress, AccessTier?, StorageTransferOptions, CancellationToken> BlobClientUploadAsyncCallback; + internal Action, BlobRequestConditions, CancellationToken> BlobClientSetMetadataAsyncCallback; public MockBlobContainerClient(string accountName = "blobAccount", string containerName = "container", @@ -295,12 +906,15 @@ public override AsyncPageable GetBlobsAsync(BlobTraits traits = BlobTr { throw GetBlobsAsyncException; } + + GetBlobsAsyncCallback?.Invoke(traits, states, prefix, cancellationToken); + return new MockAsyncPageable(Blobs); } public override BlobClient GetBlobClient(string blobName) { - return new MockBlobClient(blobName, BlobInfo, BlobClientUploadBlobException); + return new MockBlobClient(blobName, BlobInfo, BlobClientUploadBlobException, BlobClientUploadAsyncCallback, BlobClientSetMetadataAsyncCallback); } } @@ -309,10 +923,18 @@ private class MockBlobClient : BlobClient public override string Name { get; } internal BlobInfo BlobInfo; internal Exception BlobClientUploadBlobException; - - public MockBlobClient(string blobName, BlobInfo blobInfo = null, Exception blobClientUploadBlobException = null) + private Action, BlobRequestConditions, IProgress, AccessTier?, StorageTransferOptions, CancellationToken> UploadAsyncCallback; + private Action, BlobRequestConditions, CancellationToken> SetMetadataAsyncCallback; + + public MockBlobClient(string blobName, + BlobInfo blobInfo = null, + Exception blobClientUploadBlobException = null, + Action, BlobRequestConditions, IProgress, AccessTier?, StorageTransferOptions, CancellationToken> uploadAsyncCallback = null, + Action, BlobRequestConditions, CancellationToken> setMetadataAsyncCallback = null) { BlobClientUploadBlobException = blobClientUploadBlobException; + UploadAsyncCallback = uploadAsyncCallback; + SetMetadataAsyncCallback = setMetadataAsyncCallback; Name = blobName; BlobInfo = blobInfo; } @@ -327,10 +949,13 @@ public MockBlobClient(string blobName, BlobInfo blobInfo = null, Exception blobC { return Task.FromResult(Response.FromValue(BlobInfo, Mock.Of())); } + + SetMetadataAsyncCallback?.Invoke(metadata, conditions, cancellationToken); + throw new RequestFailedException(412, BlobErrorCode.ConditionNotMet.ToString(), BlobErrorCode.ConditionNotMet.ToString(), default); } - public override Task> UploadAsync(System.IO.Stream content, BlobHttpHeaders httpHeaders = null, IDictionary metadata = null, BlobRequestConditions conditions = null, IProgress progressHandler = null, AccessTier? accessTier = null, Storage.StorageTransferOptions transferOptions = default, CancellationToken cancellationToken = default) + public override Task> UploadAsync(Stream content, BlobHttpHeaders httpHeaders = null, IDictionary metadata = null, BlobRequestConditions conditions = null, IProgress progressHandler = null, AccessTier? accessTier = null, StorageTransferOptions transferOptions = default, CancellationToken cancellationToken = default) { if (BlobClientUploadBlobException != null) { @@ -341,6 +966,8 @@ public override Task> UploadAsync(System.IO.Stream con throw new RequestFailedException(409, BlobErrorCode.BlobAlreadyExists.ToString(), BlobErrorCode.BlobAlreadyExists.ToString(), default); } + UploadAsyncCallback?.Invoke(content, httpHeaders, metadata, conditions, progressHandler, accessTier, transferOptions, cancellationToken); + return Task.FromResult( Response.FromValue( BlobsModelFactory.BlobContentInfo(new ETag("etag"), DateTime.UtcNow, new byte[] { }, string.Empty, 0L), diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientLiveTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientLiveTests.cs index fd7df1652e40..a8cf400cb003 100755 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientLiveTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientLiveTests.cs @@ -34,606 +34,6 @@ public class EventProcessorClientLiveTests /// The maximum number of times that the receive loop should iterate to collect the expected number of messages. private const int ReceiveRetryLimit = 10; - /// - /// Verifies that the is able to - /// connect to the Event Hubs service and perform operations. - /// - /// - [Test] - public async Task StartAsyncCallsPartitionProcessorInitializeAsync() - { - await using (EventHubScope scope = await EventHubScope.CreateAsync(2)) - { - var connectionString = TestEnvironment.BuildConnectionStringForEventHub(scope.EventHubName); - var consumerGroup = EventHubConsumerClient.DefaultConsumerGroupName; - - await using (var consumer = new EventHubConsumerClient(consumerGroup, connectionString)) - { - var initializeCalls = new ConcurrentDictionary(); - - // Create the event processor manager to manage our event processors. - - var eventProcessorManager = new EventProcessorManager - ( - consumerGroup, - connectionString, - onInitialize: eventArgs => - initializeCalls.AddOrUpdate(eventArgs.PartitionId, 1, (partitionId, value) => value + 1) - ); - - eventProcessorManager.AddEventProcessors(1); - - // InitializeAsync should have not been called when constructing the event processors. - - Assert.That(initializeCalls.Keys, Is.Empty); - - // Start the event processors. - - await eventProcessorManager.StartAllAsync(); - - // Make sure the event processors have enough time to stabilize. - - await eventProcessorManager.WaitStabilization(); - - // Validate results before calling stop. This way, we can make sure the initialize calls were - // triggered by start. - - var partitionIds = await consumer.GetPartitionIdsAsync(); - - foreach (var partitionId in partitionIds) - { - Assert.That(initializeCalls.TryGetValue(partitionId, out var calls), Is.True, $"{ partitionId }: InitializeAsync should have been called."); - Assert.That(calls, Is.EqualTo(1), $"{ partitionId }: InitializeAsync should have been called only once."); - } - - Assert.That(initializeCalls.Keys.Count, Is.EqualTo(partitionIds.Count())); - - // Stop the event processors. - - await eventProcessorManager.StopAllAsync(); - } - } - } - - /// - /// Verifies that the is able to - /// connect to the Event Hubs service and perform operations. - /// - /// - [Test] - public async Task StopAsyncCallsPartitionProcessorCloseAsyncWithShutdownReason() - { - await using (EventHubScope scope = await EventHubScope.CreateAsync(2)) - { - var connectionString = TestEnvironment.BuildConnectionStringForEventHub(scope.EventHubName); - var closeCalls = new ConcurrentDictionary(); - var stopReasons = new ConcurrentDictionary(); - - // Create the event processor manager to manage our event processors. - - var eventProcessorManager = new EventProcessorManager - ( - EventHubConsumerClient.DefaultConsumerGroupName, - connectionString, - onStop: eventArgs => - { - closeCalls.AddOrUpdate(eventArgs.PartitionId, 1, (partitionId, value) => value + 1); - stopReasons[eventArgs.PartitionId] = eventArgs.Reason; - } - ); - - eventProcessorManager.AddEventProcessors(1); - - // Start the event processors. - - await eventProcessorManager.StartAllAsync(); - - // Make sure the event processors have enough time to stabilize. - - await eventProcessorManager.WaitStabilization(); - - // CloseAsync should have not been called when constructing the event processor or during processing initialization. - - Assert.That(closeCalls.Keys, Is.Empty); - - // Stop the event processors. - - await eventProcessorManager.StopAllAsync(); - - // Validate results. - - await using (var producer = new EventHubProducerClient(connectionString)) - { - var partitionIds = await producer.GetPartitionIdsAsync(); - - foreach (var partitionId in partitionIds) - { - Assert.That(closeCalls.TryGetValue(partitionId, out var calls), Is.True, $"{ partitionId }: CloseAsync should have been called."); - Assert.That(calls, Is.EqualTo(1), $"{ partitionId }: CloseAsync should have been called only once."); - - Assert.That(stopReasons.TryGetValue(partitionId, out ProcessingStoppedReason reason), Is.True, $"{ partitionId }: processing stopped reason should have been set."); - Assert.That(reason, Is.EqualTo(ProcessingStoppedReason.Shutdown), $"{ partitionId }: unexpected processing stopped reason."); - } - - Assert.That(closeCalls.Keys.Count, Is.EqualTo(partitionIds.Count())); - } - } - } - - /// - /// Verifies that the is able to - /// connect to the Event Hubs service and perform operations. - /// - /// - [Test] - public async Task PartitionProcessorProcessEventsAsyncReceivesAllEvents() - { - await using (EventHubScope scope = await EventHubScope.CreateAsync(2)) - { - var connectionString = TestEnvironment.BuildConnectionStringForEventHub(scope.EventHubName); - - await using (var connection = new EventHubConnection(connectionString)) - { - var allReceivedEvents = new ConcurrentDictionary>(); - - // Create the event processor manager to manage our event processors. - - var eventProcessorManager = new EventProcessorManager - ( - EventHubConsumerClient.DefaultConsumerGroupName, - connectionString, - onProcessEvent: eventArgs => - { - if (eventArgs.Data != null) - { - allReceivedEvents.AddOrUpdate - ( - eventArgs.Partition.PartitionId, - partitionId => new List() { eventArgs.Data }, - (partitionId, list) => - { - list.Add(eventArgs.Data); - return list; - } - ); - } - } - ); - - eventProcessorManager.AddEventProcessors(1); - - // Send some events. - - string[] partitionIds; - var expectedEvents = new Dictionary>(); - - await using (var producer = new EventHubProducerClient(connection)) - { - partitionIds = await producer.GetPartitionIdsAsync(); - - foreach (var partitionId in partitionIds) - { - // Send a similar set of events for every partition. - - using (var partitionBatch = await producer.CreateBatchAsync(new CreateBatchOptions { PartitionId = partitionId })) - { - expectedEvents[partitionId] = new List - { - new EventData(Encoding.UTF8.GetBytes($"{ partitionId }: event processor tests are so long.")), - new EventData(Encoding.UTF8.GetBytes($"{ partitionId }: there are so many of them.")), - new EventData(Encoding.UTF8.GetBytes($"{ partitionId }: will they ever end?")), - new EventData(Encoding.UTF8.GetBytes($"{ partitionId }: let's add a few more messages.")), - new EventData(Encoding.UTF8.GetBytes($"{ partitionId }: this is a monologue.")), - new EventData(Encoding.UTF8.GetBytes($"{ partitionId }: loneliness is what I feel.")), - new EventData(Encoding.UTF8.GetBytes($"{ partitionId }: the end has come.")) - }; - - foreach (var expectedEvent in expectedEvents[partitionId]) - { - partitionBatch.TryAdd(expectedEvent); - } - - await producer.SendAsync(partitionBatch); - } - } - } - - // Start the event processors. - - await eventProcessorManager.StartAllAsync(); - - // Make sure the event processors have enough time to stabilize and receive events. - - await eventProcessorManager.WaitStabilization(); - - // Stop the event processors. - - await eventProcessorManager.StopAllAsync(); - - // Validate results. Make sure we received every event with the correct partition context, - // in the order they were sent. - - foreach (var partitionId in partitionIds) - { - Assert.That(allReceivedEvents.TryGetValue(partitionId, out List partitionReceivedEvents), Is.True, $"{ partitionId }: there should have been a set of events received."); - Assert.That(partitionReceivedEvents.Count, Is.EqualTo(expectedEvents[partitionId].Count), $"{ partitionId }: amount of received events should match."); - - var index = 0; - - foreach (EventData receivedEvent in partitionReceivedEvents) - { - Assert.That(receivedEvent.IsEquivalentTo(expectedEvents[partitionId][index]), Is.True, $"{ partitionId }: the received event at index { index } did not match the sent set of events."); - ++index; - } - } - - Assert.That(allReceivedEvents.Keys.Count, Is.EqualTo(partitionIds.Count())); - } - } - } - - /// - /// Verifies that the is able to - /// connect to the Event Hubs service and perform operations. - /// - /// - [Test] - public async Task PartitionProcessorProcessEventsAsyncIsCalledWithNoEvents() - { - await using (EventHubScope scope = await EventHubScope.CreateAsync(1)) - { - var connectionString = TestEnvironment.BuildConnectionStringForEventHub(scope.EventHubName); - - await using (var connection = new EventHubConnection(connectionString)) - { - var receivedEvents = new ConcurrentBag(); - - // Create the event processor manager to manage our event processors. - - var eventProcessorManager = new EventProcessorManager - ( - EventHubConsumerClient.DefaultConsumerGroupName, - connectionString, - onProcessEvent: eventArgs => - receivedEvents.Add(eventArgs.Data) - ); - - eventProcessorManager.AddEventProcessors(1); - - // Start the event processors. - - await eventProcessorManager.StartAllAsync(); - - // Make sure the event processors have enough time to stabilize. - - await eventProcessorManager.WaitStabilization(); - - // Stop the event processors. - - await eventProcessorManager.StopAllAsync(); - - // Validate results. - - Assert.That(receivedEvents, Is.Not.Empty); - Assert.That(receivedEvents.Any(eventData => eventData != null), Is.False); - } - } - } - - /// - /// Verifies that the is able to - /// connect to the Event Hubs service and perform operations. - /// - /// - [Test] - public async Task StartAsyncDoesNothingWhenEventProcessorIsRunning() - { - var partitions = 1; - - await using (EventHubScope scope = await EventHubScope.CreateAsync(partitions)) - { - var connectionString = TestEnvironment.BuildConnectionStringForEventHub(scope.EventHubName); - - await using (var connection = new EventHubConnection(connectionString)) - { - int initializeCallsCount = 0; - - // Create the event processor manager to manage our event processors. - - var eventProcessorManager = new EventProcessorManager - ( - EventHubConsumerClient.DefaultConsumerGroupName, - connectionString, - onInitialize: eventArgs => - Interlocked.Increment(ref initializeCallsCount) - ); - - eventProcessorManager.AddEventProcessors(1); - - // Start the event processors. - - await eventProcessorManager.StartAllAsync(); - - // Make sure the event processors have enough time to stabilize. - - await eventProcessorManager.WaitStabilization(); - - // We should be able to call StartAsync again without getting an exception. - - Assert.That(async () => await eventProcessorManager.StartAllAsync(), Throws.Nothing); - - // Give the event processors more time in case they try to initialize again, which shouldn't happen. - - await eventProcessorManager.WaitStabilization(); - - // Stop the event processors. - - await eventProcessorManager.StopAllAsync(); - - // Validate results. - - Assert.That(initializeCallsCount, Is.EqualTo(partitions)); - } - } - } - - /// - /// Verifies that the is able to - /// connect to the Event Hubs service and perform operations. - /// - /// - [Test] - public async Task StopAsyncDoesNothingWhenEventProcessorIsNotRunning() - { - var partitions = 1; - - await using (EventHubScope scope = await EventHubScope.CreateAsync(partitions)) - { - var connectionString = TestEnvironment.BuildConnectionStringForEventHub(scope.EventHubName); - - await using (var connection = new EventHubConnection(connectionString)) - { - int closeCallsCount = 0; - - // Create the event processor manager to manage our event processors. - - var eventProcessorManager = new EventProcessorManager - ( - EventHubConsumerClient.DefaultConsumerGroupName, - connectionString, - onStop: eventArgs => - Interlocked.Increment(ref closeCallsCount) - ); - - eventProcessorManager.AddEventProcessors(1); - - // Calling StopAsync before starting the event processors shouldn't have any effect. - - Assert.That(async () => await eventProcessorManager.StopAllAsync(), Throws.Nothing); - - Assert.That(closeCallsCount, Is.EqualTo(0)); - - // Start the event processors. - - await eventProcessorManager.StartAllAsync(); - - // Make sure the event processors have enough time to stabilize. - - await eventProcessorManager.WaitStabilization(); - - // Stop the event processors. - - await eventProcessorManager.StopAllAsync(); - - // We should be able to call StopAsync again without getting an exception. - - Assert.That(async () => await eventProcessorManager.StopAllAsync(), Throws.Nothing); - - // Validate results. - - Assert.That(closeCallsCount, Is.EqualTo(partitions)); - } - } - } - - /// - /// Verifies that the is able to - /// connect to the Event Hubs service and perform operations. - /// - /// - [Test] - [Ignore("Failing test: needs debugging (Tracked by: #7458)")] - public async Task EventProcessorCanStartAgainAfterStopping() - { - await using (EventHubScope scope = await EventHubScope.CreateAsync(2)) - { - var connectionString = TestEnvironment.BuildConnectionStringForEventHub(scope.EventHubName); - - await using (var connection = new EventHubConnection(connectionString)) - { - int receivedEventsCount = 0; - - // Create the event processor manager to manage our event processors. - - var eventProcessorManager = new EventProcessorManager - ( - EventHubConsumerClient.DefaultConsumerGroupName, - connectionString, - onProcessEvent: eventArgs => - { - if (eventArgs.Data != null) - { - Interlocked.Increment(ref receivedEventsCount); - } - } - ); - - eventProcessorManager.AddEventProcessors(1); - - // Send some events. - - var expectedEventsCount = 20; - - await using (var producer = new EventHubProducerClient(connection)) - { - using (var dummyBatch = await producer.CreateBatchAsync()) - { - for (int i = 0; i < expectedEventsCount; i++) - { - dummyBatch.TryAdd(new EventData(Encoding.UTF8.GetBytes("I'm dummy."))); - } - - await producer.SendAsync(dummyBatch); - } - } - - // We'll start and stop the event processors twice. This way, we can assert they will behave - // the same way both times, reprocessing all events in the second run. - - for (int i = 0; i < 2; i++) - { - receivedEventsCount = 0; - - // Start the event processors. - - await eventProcessorManager.StartAllAsync(); - - // Make sure the event processors have enough time to stabilize and receive events. - - await eventProcessorManager.WaitStabilization(); - - // Stop the event processors. - - await eventProcessorManager.StopAllAsync(); - - // Validate results. - - Assert.That(receivedEventsCount, Is.EqualTo(expectedEventsCount), $"Events should match in iteration { i + 1 }."); - } - } - } - } - - /// - /// Verifies that the is able to - /// connect to the Event Hubs service and perform operations. - /// - /// - [Test] - [Ignore("Unstable test. (Tracked by: #7458)")] - public async Task EventProcessorCanReceiveFromCheckpointedEventPosition() - { - await using (EventHubScope scope = await EventHubScope.CreateAsync(1)) - { - var connectionString = TestEnvironment.BuildConnectionStringForEventHub(scope.EventHubName); - - await using (var connection = new EventHubConnection(connectionString)) - { - string partitionId; - int receivedEventsCount = 0; - - // Send some events. - - var expectedEventsCount = 20; - long? checkpointedSequenceNumber = default; - - await using (var producer = new EventHubProducerClient(connectionString)) - await using (var consumer = new EventHubConsumerClient(EventHubConsumerClient.DefaultConsumerGroupName, connection)) - { - partitionId = (await consumer.GetPartitionIdsAsync()).First(); - - // Send a few dummy events. We are not expecting to receive these. - - var dummyEventsCount = 30; - - using (var dummyBatch = await producer.CreateBatchAsync()) - { - for (int i = 0; i < dummyEventsCount; i++) - { - dummyBatch.TryAdd(new EventData(Encoding.UTF8.GetBytes("I'm dummy."))); - } - - await producer.SendAsync(dummyBatch); - } - - // Receive the events; because there is some non-determinism in the messaging flow, the - // sent events may not be immediately available. Allow for a small number of attempts to receive, in order - // to account for availability delays. - - var receivedEvents = new List(); - var index = 0; - - while ((receivedEvents.Count < dummyEventsCount) && (++index < ReceiveRetryLimit)) - { - Assert.Fail("Convert to iterator"); - //receivedEvents.AddRange(await receiver.ReceiveAsync(dummyEventsCount + 10, TimeSpan.FromMilliseconds(25))); - } - - Assert.That(receivedEvents.Count, Is.EqualTo(dummyEventsCount)); - - checkpointedSequenceNumber = receivedEvents.Last().SequenceNumber; - - // Send the events we expect to receive. - - using (var dummyBatch = await producer.CreateBatchAsync()) - { - for (int i = 0; i < expectedEventsCount; i++) - { - dummyBatch.TryAdd(new EventData(Encoding.UTF8.GetBytes("I'm dummy."))); - } - - await producer.SendAsync(dummyBatch); - } - } - - // Create a storage manager and add an ownership with a checkpoint in it. - - var storageManager = new MockCheckPointStorage(); - - await storageManager.ClaimOwnershipAsync(new List() - { - new PartitionOwnership(connection.FullyQualifiedNamespace, connection.EventHubName, - EventHubConsumerClient.DefaultConsumerGroupName, "ownerIdentifier", partitionId, - lastModifiedTime: DateTimeOffset.UtcNow) - }); - - // Create the event processor manager to manage our event processors. - - var eventProcessorManager = new EventProcessorManager - ( - EventHubConsumerClient.DefaultConsumerGroupName, - connectionString, - storageManager, - onProcessEvent: eventArgs => - { - if (eventArgs.Data != null) - { - Interlocked.Increment(ref receivedEventsCount); - } - } - ); - - eventProcessorManager.AddEventProcessors(1); - - // Start the event processors. - - await eventProcessorManager.StartAllAsync(); - - // Make sure the event processors have enough time to stabilize and receive events. - - await eventProcessorManager.WaitStabilization(); - - // Stop the event processors. - - await eventProcessorManager.StopAllAsync(); - - // Validate results. - - Assert.That(receivedEventsCount, Is.EqualTo(expectedEventsCount)); - } - } - } - /// /// Verifies that the is able to /// connect to the Event Hubs service and perform operations. @@ -1054,5 +454,139 @@ public async Task LoadBalancingIsEnforcedWhenDistributionIsUneven() } } } + + /// + /// Verifies that the is able to + /// connect to the Event Hubs service and perform operations. + /// + /// + [Test] + public async Task ProcessorDoesNotProcessCheckpointedEventsAgain() + { + await using (EventHubScope scope = await EventHubScope.CreateAsync(1)) + { + var connectionString = TestEnvironment.BuildConnectionStringForEventHub(scope.EventHubName); + + var firstEventBatch = Enumerable + .Range(0, 20) + .Select(index => new EventData(Encoding.UTF8.GetBytes($"First event batch: { index }"))) + .ToList(); + + var secondEventBatch = Enumerable + .Range(0, 10) + .Select(index => new EventData(Encoding.UTF8.GetBytes($"Second event batch: { index }"))) + .ToList(); + + var completionSource = new TaskCompletionSource(); + var firstBatchReceivedEventsCount = 0; + + // Send the first batch of events and checkpoint after the last one. + + var checkpointStorage = new MockCheckPointStorage(); + var firstProcessor = new EventProcessorClient(checkpointStorage, EventHubConsumerClient.DefaultConsumerGroupName, + TestEnvironment.FullyQualifiedNamespace, scope.EventHubName, () => new EventHubConnection(connectionString), default); + + firstProcessor.ProcessEventAsync += async eventArgs => + { + if (++firstBatchReceivedEventsCount == firstEventBatch.Count) + { + await eventArgs.UpdateCheckpointAsync(); + completionSource.SetResult(true); + } + }; + + firstProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + await using (var producer = new EventHubProducerClient(connectionString)) + { + using var batch = await producer.CreateBatchAsync(); + + foreach (var eventData in firstEventBatch) + { + batch.TryAdd(eventData); + } + + await producer.SendAsync(batch); + } + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromMinutes(5)); + + await firstProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + await firstProcessor.StopProcessingAsync(cancellationSource.Token); + + // Send the second batch of events. Only the new events should be read by the second processor. + + await using (var producer = new EventHubProducerClient(connectionString)) + { + using var batch = await producer.CreateBatchAsync(); + + foreach (var eventData in secondEventBatch) + { + batch.TryAdd(eventData); + } + + await producer.SendAsync(batch); + } + + completionSource = new TaskCompletionSource(); + var secondBatchReceivedEvents = new List(); + + var secondProcessor = new EventProcessorClient(checkpointStorage, EventHubConsumerClient.DefaultConsumerGroupName, + TestEnvironment.FullyQualifiedNamespace, scope.EventHubName, () => new EventHubConnection(connectionString), default); + + secondProcessor.ProcessEventAsync += eventArgs => + { + secondBatchReceivedEvents.Add(eventArgs.Data); + + if (secondBatchReceivedEvents.Count == firstEventBatch.Count) + { + completionSource.SetResult(true); + } + + return Task.CompletedTask; + }; + + var wasErrorHandlerCalled = false; + + secondProcessor.ProcessErrorAsync += eventArgs => + { + wasErrorHandlerCalled = true; + return Task.CompletedTask; + }; + + await secondProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + await secondProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processors should have stopped without cancellation."); + Assert.That(wasErrorHandlerCalled, Is.False, "No errors should have happened while resuming from checkpoint."); + + var index = 0; + + foreach (var eventData in secondBatchReceivedEvents) + { + Assert.That(eventData.IsEquivalentTo(secondEventBatch[index]), Is.True, "The received and sent event datas do not match."); + index++; + } + + Assert.That(index, Is.EqualTo(secondEventBatch.Count), $"The second processor did not receive the expected amount of events."); + } + } } } diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientTests.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientTests.cs index 8dec969c8e77..2706c1ff0b62 100755 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientTests.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Processor/EventProcessorClientTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -124,36 +125,6 @@ public void ConstructorValidatesTheCredential() Assert.That(() => new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hubName", default(TokenCredential)), Throws.ArgumentNullException); } - /// - /// Verifies functionality of the constructor. - /// - /// - [Test] - public void ConnectionStringConstructorSetsTheRetryPolicy() - { - var expected = Mock.Of(); - var options = new EventProcessorClientOptions { RetryOptions = new EventHubsRetryOptions { CustomRetryPolicy = expected } }; - var connectionString = "Endpoint=sb://somehost.com;SharedAccessKeyName=ABC;SharedAccessKey=123;EntityPath=somehub"; - var processor = new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, connectionString, options); - - Assert.That(GetRetryPolicy(processor), Is.SameAs(expected)); - } - - /// - /// Verifies functionality of the constructor. - /// - /// - [Test] - public void NamespaceConstructorSetsTheRetryPolicy() - { - var expected = Mock.Of(); - var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); - var options = new EventProcessorClientOptions { RetryOptions = new EventHubsRetryOptions { CustomRetryPolicy = expected } }; - var processor = new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, "namespace", "hubName", credential.Object, options); - - Assert.That(GetRetryPolicy(processor), Is.SameAs(expected)); - } - /// /// Verifies functionality of the /// constructor. @@ -440,12 +411,50 @@ public void NamespaceConstructorCopiesTheIdentifier() } /// - /// Verifies functionality of the + /// Verifies functionality of the + /// constructor. + /// + /// + [Test] + public void ConnectionStringConstructorPassesTheRetryPolicyToStorageManager() + { + var expected = Mock.Of(); + var clientOptions = new EventProcessorClientOptions { RetryOptions = new EventHubsRetryOptions { CustomRetryPolicy = expected } }; + var connectionString = "Endpoint=sb://somehost.com;SharedAccessKeyName=ABC;SharedAccessKey=123;EntityPath=somehub"; + var eventProcessor = new EventProcessorClient(Mock.Of(), EventHubConsumerClient.DefaultConsumerGroupName, connectionString, clientOptions); + + var storageManager = GetStorageManager(eventProcessor); + var retryPolicy = GetStorageManagerRetryPolicy(storageManager); + + Assert.That(retryPolicy, Is.EqualTo(expected)); + } + + /// + /// Verifies functionality of the + /// constructor. + /// + /// + [Test] + public void NamespaceConstructorPassesTheRetryPolicyToStorageManager() + { + var expected = Mock.Of(); + var clientOptions = new EventProcessorClientOptions { RetryOptions = new EventHubsRetryOptions { CustomRetryPolicy = expected } }; + var credential = new Mock(Mock.Of(), "{namespace}.servicebus.windows.net"); + var eventProcessor = new EventProcessorClient(Mock.Of(), "consumerGroup", "namespace", "hub", credential.Object, clientOptions); + + var storageManager = GetStorageManager(eventProcessor); + var retryPolicy = GetStorageManagerRetryPolicy(storageManager); + + Assert.That(retryPolicy, Is.EqualTo(expected)); + } + + /// + /// Verifies functionality of the /// method. /// /// [Test] - public void StartAsyncValidatesProcessEventsAsync() + public void StartProcessingAsyncValidatesProcessEventsAsync() { var processor = new EventProcessorClient(Mock.Of(), "consumerGroup", "namespace", "eventHub", () => new MockConnection(), default); processor.ProcessErrorAsync += eventArgs => Task.CompletedTask; @@ -454,12 +463,12 @@ public void StartAsyncValidatesProcessEventsAsync() } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// [Test] - public void StartAsyncValidatesProcessExceptionAsync() + public void StartProcessingAsyncValidatesProcessExceptionAsync() { var processor = new EventProcessorClient(Mock.Of(), "consumerGroup", "namespace", "eventHub", () => new MockConnection(), default); processor.ProcessEventAsync += eventArgs => Task.CompletedTask; @@ -468,12 +477,12 @@ public void StartAsyncValidatesProcessExceptionAsync() } /// - /// Verifies functionality of the + /// Verifies functionality of the /// method. /// /// [Test] - public async Task StartAsyncStartsTheEventProcessorWhenProcessingHandlerPropertiesAreSet() + public async Task StartProcessingAsyncStartsTheEventProcessorWhenProcessingHandlerPropertiesAreSet() { var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; @@ -499,107 +508,102 @@ public async Task StartAsyncStartsTheEventProcessorWhenProcessingHandlerProperti } /// - /// Verifies functionality of the properties. + /// Verifies functionality of the + /// method. /// /// [Test] - public void CannotAddNullHandler() + public async Task StartProcessingAsyncDoesNothingWhenProcessorIsAlreadyRunning() { - var processor = new EventProcessorClient(Mock.Of(), "consumerGroup", "namespace", "eventHub", () => new MockConnection(), default); + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; - Assert.That(() => processor.PartitionInitializingAsync += null, Throws.InstanceOf()); - Assert.That(() => processor.PartitionClosingAsync += null, Throws.InstanceOf()); - Assert.That(() => processor.ProcessEventAsync += null, Throws.InstanceOf()); - Assert.That(() => processor.ProcessErrorAsync += null, Throws.InstanceOf()); - } + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(Array.Empty())); - /// - /// Verifies functionality of the properties. - /// - /// - [Test] - public void CannotAddTwoHandlersToTheSameEvent() - { - var processor = new EventProcessorClient(Mock.Of(), "consumerGroup", "namespace", "eventHub", () => new MockConnection(), default); + // CreateConsumer is called as soon as the background processor running task starts. After the first call, + // we'll try calling StartProcessingAsync again and make sure no other task has started by monitoring + // CreateConsumer calls. - processor.PartitionInitializingAsync += eventArgs => Task.CompletedTask; - processor.PartitionClosingAsync += eventArgs => Task.CompletedTask; - processor.ProcessEventAsync += eventArgs => Task.CompletedTask; - processor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + var completionSource = new TaskCompletionSource(); - Assert.That(() => processor.PartitionInitializingAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); - Assert.That(() => processor.PartitionClosingAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); - Assert.That(() => processor.ProcessEventAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); - Assert.That(() => processor.ProcessErrorAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); - } + mockProcessor + .Setup(processor => processor.CreateConsumer( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback(() => + { + completionSource.SetResult(true); + }) + .Returns(mockConsumer.Object); - /// - /// Verifies functionality of the properties. - /// - /// - [Test] - public void CannotRemoveHandlerThatHasNotBeenAdded() - { - var processor = new EventProcessorClient(Mock.Of(), "consumerGroup", "namespace", "eventHub", () => new MockConnection(), default); + mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; + mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; - // First scenario: no handler has been set. + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. - Assert.That(() => processor.PartitionInitializingAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); - Assert.That(() => processor.PartitionClosingAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); - Assert.That(() => processor.ProcessEventAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); - Assert.That(() => processor.ProcessErrorAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); - // Second scenario: there is a handler set, but it's not the one we are trying to remove. + await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); - processor.PartitionInitializingAsync += eventArgs => Task.CompletedTask; - processor.PartitionClosingAsync += eventArgs => Task.CompletedTask; - processor.ProcessEventAsync += eventArgs => Task.CompletedTask; - processor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } - Assert.That(() => processor.PartitionInitializingAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); - Assert.That(() => processor.PartitionClosingAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); - Assert.That(() => processor.ProcessEventAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); - Assert.That(() => processor.ProcessErrorAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); + await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); + await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + + mockProcessor + .Verify(processor => processor.CreateConsumer( + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Once(), "Only one background running task should have been spawned."); } /// - /// Verifies functionality of the properties. + /// Verifies that an invokes partition load balancing + /// after is called. /// /// [Test] - public void CanRemoveHandlerThatHasBeenAdded() + public async Task StartProcessingAsyncStartsLoadbalancer() { - var processor = new EventProcessorClient(Mock.Of(), "consumerGroup", "namespace", "eventHub", () => new MockConnection(), default); - - Func initHandler = eventArgs => Task.CompletedTask; - Func closeHandler = eventArgs => Task.CompletedTask; - Func eventHandler = eventArgs => Task.CompletedTask; - Func errorHandler = eventArgs => Task.CompletedTask; + const int NumberOfPartitions = 3; + var mockLoadbalancer = new Mock(); + mockLoadbalancer.SetupAllProperties(); + mockLoadbalancer.Object.LoadBalanceInterval = TimeSpan.FromSeconds(1); + Func connectionFactory = () => new MockConnection(); + var connection = connectionFactory(); + var storageManager = new MockCheckPointStorage((s) => Console.WriteLine(s)); + var processor = new MockEventProcessorClient( + storageManager, + connectionFactory: connectionFactory, + loadBalancer: mockLoadbalancer.Object); - processor.PartitionInitializingAsync += initHandler; - processor.PartitionClosingAsync += closeHandler; - processor.ProcessEventAsync += eventHandler; - processor.ProcessErrorAsync += errorHandler; + await processor.StartProcessingAsync(); - Assert.That(() => processor.PartitionInitializingAsync -= initHandler, Throws.Nothing); - Assert.That(() => processor.PartitionClosingAsync -= closeHandler, Throws.Nothing); - Assert.That(() => processor.ProcessEventAsync -= eventHandler, Throws.Nothing); - Assert.That(() => processor.ProcessErrorAsync -= errorHandler, Throws.Nothing); + // Starting the processor should call the PartitionLoadBalancer. - // Assert that handlers can be added again. + mockLoadbalancer.Verify(m => m.RunLoadBalancingAsync(It.Is(p => p.Length == NumberOfPartitions), It.IsAny())); - Assert.That(() => processor.PartitionInitializingAsync += initHandler, Throws.Nothing); - Assert.That(() => processor.PartitionClosingAsync += closeHandler, Throws.Nothing); - Assert.That(() => processor.ProcessEventAsync += eventHandler, Throws.Nothing); - Assert.That(() => processor.ProcessErrorAsync += errorHandler, Throws.Nothing); + await processor.StopProcessingAsync(CancellationToken.None); } /// - /// Verifies functionality of the properties. + /// Verifies functionality of the + /// method. /// /// [Test] - public async Task CannotAddHandlerWhileProcessorIsRunning() + public void AlreadyCancelledTokenMakesStartProcessingAsyncThrow() { var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; @@ -619,27 +623,18 @@ public async Task CannotAddHandlerWhileProcessorIsRunning() mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; using var cancellationSource = new CancellationTokenSource(); - cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); - - await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); - - Assert.That(() => mockProcessor.Object.PartitionInitializingAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); - Assert.That(() => mockProcessor.Object.PartitionClosingAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); - - await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token); - - // Once stopped, the processor should allow handlers to be added again. + cancellationSource.Cancel(); - Assert.That(() => mockProcessor.Object.PartitionInitializingAsync += eventArgs => Task.CompletedTask, Throws.Nothing); - Assert.That(() => mockProcessor.Object.PartitionClosingAsync += eventArgs => Task.CompletedTask, Throws.Nothing); + Assert.That(async () => await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token), Throws.InstanceOf()); } /// - /// Verifies functionality of the properties. + /// Verifies functionality of the + /// method. /// /// [Test] - public async Task CannotRemoveHandlerWhileProcessorIsRunning() + public void AlreadyCancelledTokenMakesStartProcessingThrow() { var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; @@ -655,43 +650,22 @@ public async Task CannotRemoveHandlerWhileProcessorIsRunning() It.IsAny())) .Returns(mockConsumer.Object); + mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; + mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; using var cancellationSource = new CancellationTokenSource(); - cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); - - Func initHandler = eventArgs => Task.CompletedTask; - Func closeHandler = eventArgs => Task.CompletedTask; - Func eventHandler = eventArgs => Task.CompletedTask; - Func errorHandler = eventArgs => Task.CompletedTask; - - mockProcessor.Object.PartitionInitializingAsync += initHandler; - mockProcessor.Object.PartitionClosingAsync += closeHandler; - mockProcessor.Object.ProcessEventAsync += eventHandler; - mockProcessor.Object.ProcessErrorAsync += errorHandler; - - await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); - - Assert.That(() => mockProcessor.Object.PartitionInitializingAsync -= initHandler, Throws.InstanceOf()); - Assert.That(() => mockProcessor.Object.PartitionClosingAsync -= closeHandler, Throws.InstanceOf()); - Assert.That(() => mockProcessor.Object.ProcessEventAsync -= eventHandler, Throws.InstanceOf()); - Assert.That(() => mockProcessor.Object.ProcessErrorAsync -= errorHandler, Throws.InstanceOf()); - - await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token); - - // Once stopped, the processor should allow handlers to be removed again. + cancellationSource.Cancel(); - Assert.That(() => mockProcessor.Object.PartitionInitializingAsync -= initHandler, Throws.Nothing); - Assert.That(() => mockProcessor.Object.PartitionClosingAsync -= closeHandler, Throws.Nothing); - Assert.That(() => mockProcessor.Object.ProcessEventAsync -= eventHandler, Throws.Nothing); - Assert.That(() => mockProcessor.Object.ProcessErrorAsync -= errorHandler, Throws.Nothing); + Assert.That(() => mockProcessor.Object.StartProcessing(cancellationSource.Token), Throws.InstanceOf()); } /// - /// Verifies functionality of the properties. + /// Verifies functionality of the + /// method. /// /// [Test] - public async Task IsRunningReturnsTrueWhileStopProcessingAsyncIsNotCalled() + public async Task StopProcessingAsyncResetsFailedState() { var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; @@ -700,113 +674,2006 @@ public async Task IsRunningReturnsTrueWhileStopProcessingAsyncIsNotCalled() .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) .Returns(Task.FromResult(Array.Empty())); + // Force an exception to be thrown as soon as the background running task starts. + + var firstRun = true; + var expectedException = new Exception(); + mockProcessor .Setup(processor => processor.CreateConsumer( It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(mockConsumer.Object); + .Returns(() => + { + if (firstRun) + { + firstRun = false; + throw expectedException; + } + + return mockConsumer.Object; + }); mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + using var cancellationSource = new CancellationTokenSource(); - cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); - Assert.That(mockProcessor.Object.IsRunning, Is.False); + await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); + + Assert.That(mockProcessor.Object.IsRunning, Is.False, "The failed processor should not be running anymore."); await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); - Assert.That(mockProcessor.Object.IsRunning, Is.True); + Assert.That(mockProcessor.Object.IsRunning, Is.False, "StartProcessingAsync should not be able to reset processor state."); + + var capturedException = default(Exception); + + try + { + await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token); + } + catch (Exception ex) + { + capturedException = ex; + } + + Assert.That(capturedException, Is.EqualTo(expectedException), "The captured and expected exceptions do not match."); + + await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); + + Assert.That(mockProcessor.Object.IsRunning, Is.True, "The processor state should have been reset by StopProcessingAsync."); await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token); - Assert.That(mockProcessor.Object.IsRunning, Is.False); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); } /// - /// Verifies functionality of the properties. + /// Verifies functionality of the + /// method. /// /// [Test] - public async Task IsRunningReturnsFalseWhenLoadBalancingTaskFails() + public async Task StopProcessingAsyncDoesNothingWhenProcessorIsNotRunning() { - var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; - var completionSource = new TaskCompletionSource(); - - // This should be called right before the first load balancing cycle. + var mockStorage = new Mock(default(Action)) { CallBase = true }; + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(mockStorage.Object, "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); - mockProcessor - .Setup(processor => processor.CreateConsumer( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Callback(() => completionSource.SetResult(true)) - .Throws(new Exception()); + // Ownership claim attempts with an empty owner identifier are actually relinquish requests. This should happen once + // when the processor is stopping. We don't really care whether the relinquish attempt has succeeded or not, so just + // return a null. - mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; - mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; + var relinquishAttempts = 0; + + mockStorage + .Setup(storage => storage.ClaimOwnershipAsync( + It.Is>(ownershipEnumerable => + ownershipEnumerable.Any(ownership => string.IsNullOrEmpty(ownership.OwnerIdentifier))), + It.IsAny())) + .Callback, CancellationToken>((ownershipEnumerable, token) => + { + Interlocked.Increment(ref relinquishAttempts); + }) + .Returns(Task.FromResult(default(IEnumerable))); + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { "0" })); + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((partition, position, options, token) => + MockEmptyPartitionEventEnumerable(1, token)); + + var closingHandlerCalls = 0; + + mockProcessor.PartitionClosingAsync += eventArgs => + { + Interlocked.Increment(ref closingHandlerCalls); + return Task.CompletedTask; + }; + + // Wait until processing has started before stopping the processor. This way we can ensure the closing + // handler will be triggered. + + var completionSource = new TaskCompletionSource(); + + mockProcessor.ProcessEventAsync += eventArgs => + { + completionSource.SetResult(true); + return Task.CompletedTask; + }; + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(closingHandlerCalls, Is.EqualTo(1), "The closing handler should not have been called again."); + Assert.That(relinquishAttempts, Is.EqualTo(1), "No more relinquish attempts should have been made after the first one."); + } + + /// + /// Verifies that partitions owned by an are immediately available to be claimed by another processor + /// after is called. + /// + /// + [Test] + public async Task StopProcessingAsyncStopsLoadbalancer() + { + var mockLoadbalancer = new Mock(); + mockLoadbalancer.SetupAllProperties(); + mockLoadbalancer.Object.LoadBalanceInterval = TimeSpan.FromSeconds(1); + Func connectionFactory = () => new MockConnection(); + var connection = connectionFactory(); + var storageManager = new MockCheckPointStorage((s) => Console.WriteLine(s)); + var processor = new MockEventProcessorClient( + storageManager, + connectionFactory: connectionFactory, + loadBalancer: mockLoadbalancer.Object); + + await processor.StartProcessingAsync(); + await processor.StopProcessingAsync(); + + // Stopping the processor should stop the PartitionLoadBalancer. + + mockLoadbalancer.Verify(m => m.RelinquishOwnershipAsync(It.IsAny())); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public async Task StopProcessingAsyncStopsProcessingForEveryPartition() + { + var maximumWaitTimeInMilli = 100; + var processorOptions = new EventProcessorClientOptions { MaximumWaitTime = TimeSpan.FromMilliseconds(maximumWaitTimeInMilli) }; + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), processorOptions, mockConsumer.Object); + + var partitionIds = new[] { "0", "1" }; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(partitionIds)); + + var completionSource = new TaskCompletionSource(); + var partitionsBeingProcessed = 0; + + // Use the ReadEventsFromPartitionAsync method to notify when all partitions have started being processed. + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback(() => + { + if (Interlocked.Increment(ref partitionsBeingProcessed) >= partitionIds.Length) + { + completionSource.TrySetResult(true); + } + }) + .Returns((partition, position, options, token) => + MockEndlessPartitionEventEnumerable(options.MaximumWaitTime, token)); + + var hasReceivedEventsWhileNotRunning = false; + + // Use the process event handler to track events received after the processor has stopped. + + mockProcessor.ProcessEventAsync += eventArgs => + { + if (!mockProcessor.IsRunning) + { + hasReceivedEventsWhileNotRunning = true; + } + + return Task.CompletedTask; + }; + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + + // Wait a bit to give the stopped processor the chance to trigger the event handler. This scenario is not expected. + + await Task.Delay(5 * maximumWaitTimeInMilli); + + Assert.That(hasReceivedEventsWhileNotRunning, Is.False, "The processor should not have received events while not running."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public async Task StopProcessingAsyncShouldSurfaceBackgroundRunningTaskException() + { + var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; + var completionSource = new TaskCompletionSource(); + + mockProcessor + .Setup(processor => processor.CreateConsumer( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback(() => completionSource.SetResult(true)) + .Throws(new Exception()); + + mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; + mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; // To ensure that the test does not hang for the duration, set a timeout to force completion // after a shorter period of time. using var cancellationSource = new CancellationTokenSource(); - cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); + await Task.WhenAny(Task.Delay(-1, cancellationSource.Token), completionSource.Task); + + Assert.That(async () => await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token), Throws.Exception, "An exception should have been thrown when creating the consumer."); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public async Task AlreadyCancelledTokenMakesStopProcessingAsyncThrow() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(Array.Empty())); + + mockProcessor + .Setup(processor => processor.CreateConsumer( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockConsumer.Object); + + mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; + mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + await mockProcessor.Object.StartProcessingAsync(); + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.Cancel(); + + Assert.That(async () => await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token), Throws.InstanceOf()); + } + + /// + /// Verifies functionality of the + /// method. + /// + /// + [Test] + public void AlreadyCancelledTokenMakesStopProcessingThrow() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(Array.Empty())); + + mockProcessor + .Setup(processor => processor.CreateConsumer( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockConsumer.Object); + + mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; + mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + mockProcessor.Object.StartProcessing(); + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.Cancel(); + + Assert.That(() => mockProcessor.Object.StopProcessing(cancellationSource.Token), Throws.InstanceOf()); + } + + /// + /// Verifies functionality of the + /// and methods. + /// + /// + [Test] + public async Task SupportsStartProcessingAfterStop() + { + var partitionId = "expectedPartition"; + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { partitionId })); + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((partition, position, options, token) => MockEmptyPartitionEventEnumerable(1, token)); + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + var completionSource = new TaskCompletionSource(); + var isProcessEventHandlerInvoked = false; + + mockProcessor.ProcessEventAsync += eventArgs => + { + isProcessEventHandlerInvoked = true; + completionSource.SetResult(true); + + return Task.CompletedTask; + }; + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + Assert.That(mockProcessor.IsRunning, Is.False); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + await completionSource.Task; + + Assert.That(mockProcessor.IsRunning, Is.True); + Assert.That(isProcessEventHandlerInvoked, Is.EqualTo(true)); + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + isProcessEventHandlerInvoked = false; + completionSource = new TaskCompletionSource(); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(mockProcessor.IsRunning, Is.False); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + await completionSource.Task; + + Assert.That(mockProcessor.IsRunning, Is.True); + Assert.That(isProcessEventHandlerInvoked, Is.EqualTo(true)); + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(mockProcessor.IsRunning, Is.False); + } + + /// + /// Verifies functionality of the + /// and methods. + /// + /// + [Test] + public void StartAndStopProcessingShouldStartAndStopProcessors() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(Array.Empty())); + + mockProcessor + .Setup(processor => processor.CreateConsumer( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockConsumer.Object); + + mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; + mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + Assert.That(mockProcessor.Object.IsRunning, Is.False); + + mockProcessor.Object.StartProcessing(cancellationSource.Token); + + Assert.That(mockProcessor.Object.IsRunning, Is.True); + + mockProcessor.Object.StopProcessing(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(mockProcessor.Object.IsRunning, Is.False); + } + + /// + /// Verifies functionality of the events. + /// + /// + [Test] + public void CannotAddNullHandler() + { + var processor = new EventProcessorClient(Mock.Of(), "consumerGroup", "namespace", "eventHub", () => new MockConnection(), default); + + Assert.That(() => processor.PartitionInitializingAsync += null, Throws.InstanceOf()); + Assert.That(() => processor.PartitionClosingAsync += null, Throws.InstanceOf()); + Assert.That(() => processor.ProcessEventAsync += null, Throws.InstanceOf()); + Assert.That(() => processor.ProcessErrorAsync += null, Throws.InstanceOf()); + } + + /// + /// Verifies functionality of the events. + /// + /// + [Test] + public void CannotAddTwoHandlersToTheSameEvent() + { + var processor = new EventProcessorClient(Mock.Of(), "consumerGroup", "namespace", "eventHub", () => new MockConnection(), default); + + processor.PartitionInitializingAsync += eventArgs => Task.CompletedTask; + processor.PartitionClosingAsync += eventArgs => Task.CompletedTask; + processor.ProcessEventAsync += eventArgs => Task.CompletedTask; + processor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + Assert.That(() => processor.PartitionInitializingAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); + Assert.That(() => processor.PartitionClosingAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); + Assert.That(() => processor.ProcessEventAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); + Assert.That(() => processor.ProcessErrorAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); + } + + /// + /// Verifies functionality of the events. + /// + /// + [Test] + public void CannotRemoveHandlerThatHasNotBeenAdded() + { + var processor = new EventProcessorClient(Mock.Of(), "consumerGroup", "namespace", "eventHub", () => new MockConnection(), default); + + // First scenario: no handler has been set. + + Assert.That(() => processor.PartitionInitializingAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); + Assert.That(() => processor.PartitionClosingAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); + Assert.That(() => processor.ProcessEventAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); + Assert.That(() => processor.ProcessErrorAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); + + // Second scenario: there is a handler set, but it's not the one we are trying to remove. + + processor.PartitionInitializingAsync += eventArgs => Task.CompletedTask; + processor.PartitionClosingAsync += eventArgs => Task.CompletedTask; + processor.ProcessEventAsync += eventArgs => Task.CompletedTask; + processor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + Assert.That(() => processor.PartitionInitializingAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); + Assert.That(() => processor.PartitionClosingAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); + Assert.That(() => processor.ProcessEventAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); + Assert.That(() => processor.ProcessErrorAsync -= eventArgs => Task.CompletedTask, Throws.InstanceOf()); + } + + /// + /// Verifies functionality of the events. + /// + /// + [Test] + public void CanRemoveHandlerThatHasBeenAdded() + { + var processor = new EventProcessorClient(Mock.Of(), "consumerGroup", "namespace", "eventHub", () => new MockConnection(), default); + + Func initHandler = eventArgs => Task.CompletedTask; + Func closeHandler = eventArgs => Task.CompletedTask; + Func eventHandler = eventArgs => Task.CompletedTask; + Func errorHandler = eventArgs => Task.CompletedTask; + + processor.PartitionInitializingAsync += initHandler; + processor.PartitionClosingAsync += closeHandler; + processor.ProcessEventAsync += eventHandler; + processor.ProcessErrorAsync += errorHandler; + + Assert.That(() => processor.PartitionInitializingAsync -= initHandler, Throws.Nothing); + Assert.That(() => processor.PartitionClosingAsync -= closeHandler, Throws.Nothing); + Assert.That(() => processor.ProcessEventAsync -= eventHandler, Throws.Nothing); + Assert.That(() => processor.ProcessErrorAsync -= errorHandler, Throws.Nothing); + + // Assert that handlers can be added again. + + Assert.That(() => processor.PartitionInitializingAsync += initHandler, Throws.Nothing); + Assert.That(() => processor.PartitionClosingAsync += closeHandler, Throws.Nothing); + Assert.That(() => processor.ProcessEventAsync += eventHandler, Throws.Nothing); + Assert.That(() => processor.ProcessErrorAsync += errorHandler, Throws.Nothing); + } + + /// + /// Verifies functionality of the events. + /// + /// + [Test] + public async Task CannotAddHandlerWhileProcessorIsRunning() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(Array.Empty())); + + mockProcessor + .Setup(processor => processor.CreateConsumer( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockConsumer.Object); + + mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; + mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); + + Assert.That(() => mockProcessor.Object.PartitionInitializingAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); + Assert.That(() => mockProcessor.Object.PartitionClosingAsync += eventArgs => Task.CompletedTask, Throws.InstanceOf()); + + await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token); + + // Once stopped, the processor should allow handlers to be added again. + + Assert.That(() => mockProcessor.Object.PartitionInitializingAsync += eventArgs => Task.CompletedTask, Throws.Nothing); + Assert.That(() => mockProcessor.Object.PartitionClosingAsync += eventArgs => Task.CompletedTask, Throws.Nothing); + } + + /// + /// Verifies functionality of the events. + /// + /// + [Test] + public async Task CannotRemoveHandlerWhileProcessorIsRunning() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(Array.Empty())); + + mockProcessor + .Setup(processor => processor.CreateConsumer( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockConsumer.Object); + + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + Func initHandler = eventArgs => Task.CompletedTask; + Func closeHandler = eventArgs => Task.CompletedTask; + Func eventHandler = eventArgs => Task.CompletedTask; + Func errorHandler = eventArgs => Task.CompletedTask; + + mockProcessor.Object.PartitionInitializingAsync += initHandler; + mockProcessor.Object.PartitionClosingAsync += closeHandler; + mockProcessor.Object.ProcessEventAsync += eventHandler; + mockProcessor.Object.ProcessErrorAsync += errorHandler; + + await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); + + Assert.That(() => mockProcessor.Object.PartitionInitializingAsync -= initHandler, Throws.InstanceOf()); + Assert.That(() => mockProcessor.Object.PartitionClosingAsync -= closeHandler, Throws.InstanceOf()); + Assert.That(() => mockProcessor.Object.ProcessEventAsync -= eventHandler, Throws.InstanceOf()); + Assert.That(() => mockProcessor.Object.ProcessErrorAsync -= errorHandler, Throws.InstanceOf()); + + await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token); + + // Once stopped, the processor should allow handlers to be removed again. + + Assert.That(() => mockProcessor.Object.PartitionInitializingAsync -= initHandler, Throws.Nothing); + Assert.That(() => mockProcessor.Object.PartitionClosingAsync -= closeHandler, Throws.Nothing); + Assert.That(() => mockProcessor.Object.ProcessEventAsync -= eventHandler, Throws.Nothing); + Assert.That(() => mockProcessor.Object.ProcessErrorAsync -= errorHandler, Throws.Nothing); + } + + /// + /// Verifies functionality of the + /// property. + /// + /// + [Test] + public async Task IsRunningReturnsTrueWhileStopProcessingAsyncIsNotCalled() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(Array.Empty())); + + mockProcessor + .Setup(processor => processor.CreateConsumer( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockConsumer.Object); + + mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; + mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + Assert.That(mockProcessor.Object.IsRunning, Is.False); + + await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); + + Assert.That(mockProcessor.Object.IsRunning, Is.True); + + await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token); + + Assert.That(mockProcessor.Object.IsRunning, Is.False); + } + + /// + /// Verifies functionality of the + /// property. + /// + /// + [Test] + public async Task IsRunningReturnsFalseWhenLoadBalancingTaskFails() + { + var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; + var completionSource = new TaskCompletionSource(); + + // This should be called right before the first load balancing cycle. + + mockProcessor + .Setup(processor => processor.CreateConsumer( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback(() => completionSource.SetResult(true)) + .Throws(new Exception()); + + mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; + mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + // To ensure that the test does not hang for the duration, set a timeout to force completion + // after a shorter period of time. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + + await mockProcessor.Object.StartProcessingAsync(); + await Task.WhenAny(Task.Delay(-1, cancellationSource.Token), completionSource.Task); + + // Capture the value of IsRunning before stopping the processor. We are doing this to make sure + // we stop the processor properly even in case of failure. + + var isRunning = mockProcessor.Object.IsRunning; + + // Stop the processor and ensure that it does not block on the handler. + + Assert.That(async () => await mockProcessor.Object.StopProcessingAsync(), Throws.Exception, "An exception should have been thrown when creating the consumer."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + + Assert.That(isRunning, Is.False, "IsRunning should return false when the load balancing task fails."); + } + + /// + /// Verify logs for the . + /// + /// + [Test] + public async Task VerifiesEventProcessorLogs() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + string[] partitionIds = { "0" }; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(partitionIds)); + + var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default); + + var mockLog = new Mock(); + mockProcessor.CallBase = true; + mockProcessor.Object.Logger = mockLog.Object; + + mockProcessor + .Setup(processor => processor.CreateConsumer( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockConsumer.Object); + + mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; + mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + using var cancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + + Assert.That(async () => await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token), Throws.Nothing); + await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token); + + mockLog.Verify(m => m.EventProcessorStart(mockProcessor.Object.Identifier)); + mockLog.Verify(m => m.EventProcessorStopStart(mockProcessor.Object.Identifier)); + mockLog.Verify(m => m.EventProcessorStopComplete(mockProcessor.Object.Identifier)); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task PartitionInitializingAsyncIsTriggeredWhenPartitionProcessingIsStarting() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + var partitionIds = new[] { "0", "1" }; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(partitionIds)); + + var completionSource = new TaskCompletionSource(); + var partitionsBeingProcessed = 0; + + // Use the ReadEventsFromPartitionAsync method to notify when all partitions have started being processed. + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback(() => + { + if (Interlocked.Increment(ref partitionsBeingProcessed) == partitionIds.Length) + { + completionSource.SetResult(true); + } + }) + .Returns((partition, position, options, token) => + MockEndlessPartitionEventEnumerable(options.MaximumWaitTime, token)); + + // Use the init handler to keep track of the partitions that have been initialized. + + var initializingEventArgs = new ConcurrentBag(); + + mockProcessor.PartitionInitializingAsync += eventArgs => + { + initializingEventArgs.Add(eventArgs); + return Task.CompletedTask; + }; + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + + var initializingEventArgsList = initializingEventArgs.ToList(); + + Assert.That(initializingEventArgsList.Count, Is.EqualTo(partitionIds.Length), $"The initializing handler should have been called { partitionIds.Length } times."); + + foreach (var partitionId in partitionIds) + { + Assert.That(initializingEventArgsList.Any(args => args.PartitionId == partitionId), Is.True, $"The initializing handler should have been triggered with partitionId = '{ partitionId }'."); + } + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task PartitionInitializingAsyncIsCalledWhenPartitionProcessingTaskFails() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + var partitionId = "0"; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { partitionId })); + + // We will force an exception when running a partition processing task. + + mockProcessor.RunPartitionProcessingException = new Exception(); + + // Keep track of how many times we call the init handler. If we call it for a second time, + // it means a new processing task has started. + + var completionSource = new TaskCompletionSource(); + var initHandlerCalls = 0; + var capturedPartitionId = default(string); + + mockProcessor.PartitionInitializingAsync += eventArgs => + { + if (Interlocked.Increment(ref initHandlerCalls) == 2) + { + capturedPartitionId = eventArgs.PartitionId; + completionSource.SetResult(true); + } + + return Task.CompletedTask; + }; + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(capturedPartitionId, Is.EqualTo(partitionId), "The associated partition id does not match."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task PartitionInitializingAsyncTokenIsCanceledWhenStopProcessingAsyncIsCalled() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { "0" })); + + // We'll wait until the init handler is called and capture its token. + + var completionSource = new TaskCompletionSource(); + var capturedToken = default(CancellationToken); + + mockProcessor.PartitionInitializingAsync += eventArgs => + { + capturedToken = eventArgs.CancellationToken; + completionSource.SetResult(true); + + return Task.CompletedTask; + }; + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + Assert.That(capturedToken.IsCancellationRequested, Is.False, "The token in the handler should not have been canceled yet."); + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(capturedToken.IsCancellationRequested, Is.True, "The token in the handler should have been canceled."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task PartitionClosingAsyncIsCalledWithShutdownReasonWhenStoppingTheProcessor() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + var partitionIds = new[] { "0", "1" }; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(partitionIds)); + + var completionSource = new TaskCompletionSource(); + var partitionsBeingProcessed = 0; + + // Use the ReadEventsFromPartitionAsync method to notify when all partitions have started being processed. + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback(() => + { + if (Interlocked.Increment(ref partitionsBeingProcessed) == partitionIds.Length) + { + completionSource.SetResult(true); + } + }) + .Returns((partition, position, options, token) => + MockEndlessPartitionEventEnumerable(options.MaximumWaitTime, token)); + + // Use the close handler to keep track of the partitions that have been closed. + + var closingEventArgs = new ConcurrentBag(); + + mockProcessor.PartitionClosingAsync += eventArgs => + { + closingEventArgs.Add(eventArgs); + return Task.CompletedTask; + }; + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + + var closingEventArgsList = closingEventArgs.ToList(); + + Assert.That(closingEventArgsList.Count, Is.EqualTo(partitionIds.Length), $"The closing handler should have been called { partitionIds.Length } times."); + + foreach (var partitionId in partitionIds) + { + Assert.That(closingEventArgsList.Any(args => args.PartitionId == partitionId), Is.True, $"The closing handler should have been triggered with partitionId = '{ partitionId }'."); + } + + foreach (var eventArgs in closingEventArgsList) + { + Assert.That(eventArgs.Reason, Is.EqualTo(ProcessingStoppedReason.Shutdown), $"The partition '{ eventArgs.PartitionId }' should have stopped processing with the correct reason."); + } + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + [Ignore("Flaky test. (Tracked by: #10015)")] + public async Task PartitionClosingAsyncIsCalledWithOwnershipLostReasonWhenStoppingTheFailedProcessor() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + var partitionIds = new[] { "0", "1" }; + var faultedPartitionId = partitionIds.Last(); + + mockProcessor.ShouldIgnoreTestRunnerException = (partitionId, position, token) => partitionId != faultedPartitionId; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(partitionIds)); + + var completionSource = new TaskCompletionSource(); + var partitionsBeingProcessed = 0; + + // Use the ReadEventsFromPartitionAsync method to notify when all partitions have started being processed. If the partition is faulted, + // throw. + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback(() => + { + if (Interlocked.Increment(ref partitionsBeingProcessed) == partitionIds.Length) + { + completionSource.SetResult(true); + } + }) + .Returns((partition, position, options, token) => + { + if (partition == faultedPartitionId) + { + throw new Exception(); + } + + return MockEndlessPartitionEventEnumerable(options.MaximumWaitTime, token); + }); + + // Use the close handler to keep track of the partitions that have been closed. + + var closingEventArgs = new ConcurrentDictionary(); + + mockProcessor.PartitionClosingAsync += eventArgs => + { + closingEventArgs[eventArgs.PartitionId] = eventArgs; + return Task.CompletedTask; + }; + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + await completionSource.Task; + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + // Wait until we have data for every partition. Give up after token cancellation so the test + // doesn't hang. + + while (closingEventArgs.Count < partitionIds.Length + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + + foreach (var partitionId in partitionIds) + { + var eventArgs = default(PartitionClosingEventArgs); + var expectedReason = (partitionId == faultedPartitionId) ? ProcessingStoppedReason.OwnershipLost : ProcessingStoppedReason.Shutdown; + + Assert.That(closingEventArgs.TryGetValue(partitionId, out eventArgs), Is.True, $"The partition '{ partitionId }' should have triggered the closing handler."); + Assert.That(eventArgs.Reason, Is.EqualTo(expectedReason), $"Stop reason in '{ partitionId }' is incorrect."); + } + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task PartitionClosingAsyncIsCalledWhenPartitionProcessingTaskFails() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + var partitionId = "0"; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { partitionId })); + + // We will force an exception when running a partition processing task. + + mockProcessor.RunPartitionProcessingException = new Exception(); + + // We'll wait until the closing handler is called. If it's called before we stop + // the processor, it means the failed task has stopped as expected. + + var completionSource = new TaskCompletionSource(); + var capturedEventArgs = default(PartitionClosingEventArgs); + + mockProcessor.PartitionClosingAsync += eventArgs => + { + if (completionSource.TrySetResult(true)) + { + capturedEventArgs = eventArgs; + } + + return Task.CompletedTask; + }; + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(capturedEventArgs, Is.Not.Null, "The error event args should have been captured."); + Assert.That(capturedEventArgs.PartitionId, Is.EqualTo(partitionId), "The associated partition id does not match."); + Assert.That(capturedEventArgs.Reason, Is.EqualTo(ProcessingStoppedReason.OwnershipLost), "Stop reason is incorrect."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + [Ignore("Failing test. (Tracked by: #10015)")] + public async Task PartitionClosingAsyncTokenIsCanceledWhenStopProcessingAsyncIsCalled() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { "0" })); + + // We will force an exception when running a partition processing task. + + mockProcessor.RunPartitionProcessingException = new Exception(); + + // We'll wait until the closing handler is called and capture its token. + + var completionSource = new TaskCompletionSource(); + var capturedToken = default(CancellationToken); + + mockProcessor.PartitionClosingAsync += eventArgs => + { + if (completionSource.TrySetResult(true)) + { + capturedToken = eventArgs.CancellationToken; + } + + return Task.CompletedTask; + }; + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + Assert.That(capturedToken.IsCancellationRequested, Is.False, "The token in the handler should not have been canceled yet."); + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(capturedToken.IsCancellationRequested, Is.True, "The token in the handler should have been canceled."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task ProcessEventAsyncReceivesAnEmptyPartitionContextForNoData() + { + var partitionId = "expectedPartition"; + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { partitionId })); + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((partition, position, options, token) => MockEmptyPartitionEventEnumerable(1, token)); + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + var completionSource = new TaskCompletionSource(); + var emptyEventArgs = default(ProcessEventArgs); + + mockProcessor.ProcessEventAsync += eventArgs => + { + emptyEventArgs = eventArgs; + completionSource.TrySetResult(true); + + return Task.CompletedTask; + }; + + // Start the processor and wait for the event handler to be triggered. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + await completionSource.Task; + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + // Validate the empty event arguments. + + Assert.That(emptyEventArgs, Is.Not.Null, "The event arguments should have been populated."); + Assert.That(emptyEventArgs.Data, Is.Null, "The event arguments should not have an event available."); + Assert.That(emptyEventArgs.Partition, Is.Not.Null, "The event arguments should have a partition context."); + Assert.That(emptyEventArgs.Partition.PartitionId, Is.EqualTo(partitionId), "The partition identifier should match."); + Assert.That(() => emptyEventArgs.Partition.ReadLastEnqueuedEventProperties(), Throws.InstanceOf(), "The last event properties should not be available."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task ProcessEventAsyncIsNotTriggeredWhenThereIsNoMaximumWaitTime() + { + const int testDurationInCycles = 8; + + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockStorage = new Mock(default(Action)) { CallBase = true }; + var mockProcessor = new InjectableEventSourceProcessorMock(mockStorage.Object, "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + // We'll run multiple cycles, so keeping the default load balance interval (10s) would take too much time. + + mockProcessor.LoadBalancer.LoadBalanceInterval = TimeSpan.FromMilliseconds(500); + + // We are making the assumption that a single GetPartitionIds call is made every cycle, so we'll + // use it to count the total of cycles. + + var cycles = 0; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Callback(() => cycles++) + .Returns(Task.FromResult(new[] { "0" })); + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((partition, position, options, token) => MockEndlessPartitionEventEnumerable(options.MaximumWaitTime, token)); + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + var wasHandlerCalled = false; + + mockProcessor.ProcessEventAsync += eventArgs => + { + wasHandlerCalled = true; + return Task.CompletedTask; + }; + + // Start the processor and wait for the specified amount of cycles. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (cycles < testDurationInCycles + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + // The handler should not have been called. + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(wasHandlerCalled, Is.False, "The handler should not have been called."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task ProcessHandlerTriggersForEveryReceivedEvent() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { "0" })); + + var numberOfEvents = 5; + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((partition, position, options, token) => MockPartitionEventEnumerable(numberOfEvents, token)); + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + var completionSource = new TaskCompletionSource(); + + var processEventTriggerCount = 0; + + mockProcessor.ProcessEventAsync += eventArgs => + { + processEventTriggerCount++; + + if (processEventTriggerCount == numberOfEvents) + { + completionSource.SetResult(true); + } + + return Task.CompletedTask; + }; + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + // Start the processor and wait for the event handler to be triggered. + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + await completionSource.Task; + + Assert.That(numberOfEvents, Is.EqualTo(processEventTriggerCount)); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task ProcessEventAsyncTokenIsCanceledWhenStopProcessingAsyncIsCalled() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { "0" })); + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((partition, position, options, token) => + MockEmptyPartitionEventEnumerable(1, token)); + + // We'll wait until the process handler is called and capture its token. + + var completionSource = new TaskCompletionSource(); + var capturedToken = default(CancellationToken); + + mockProcessor.ProcessEventAsync += eventArgs => + { + capturedToken = eventArgs.CancellationToken; + completionSource.SetResult(true); + + return Task.CompletedTask; + }; + + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + Assert.That(capturedToken.IsCancellationRequested, Is.False, "The token in the handler should not have been canceled yet."); + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(capturedToken.IsCancellationRequested, Is.True, "The token in the handler should have been canceled."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + [Ignore("Failing test. (Tracked by: #10015)")] + public async Task ProcessErrorAsyncIsTriggeredWithCorrectArgumentsWhenOwnershipClaimFails() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockStorage = new Mock(default(Action)) { CallBase = true }; + var mockProcessor = new InjectableEventSourceProcessorMock(mockStorage.Object, "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + var partitionId = "0"; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { partitionId })); + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((partition, position, options, token) => + MockEndlessPartitionEventEnumerable(options.MaximumWaitTime, token)); + + var expectedExceptionReference = new Exception(); + + // When the mock storage intercepts a call to ClaimOwnershipAsync with a single object, it means we have found a claim attempt. + // We will force an exception and catch it with the error handler. + + mockStorage + .Setup(storage => storage.ClaimOwnershipAsync( + It.Is>(ownershipEnumerable => ownershipEnumerable.Count() == 1), + It.IsAny())) + .Throws(expectedExceptionReference); + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + var capturedEventArgs = default(ProcessErrorEventArgs); + var completionSource = new TaskCompletionSource(); + + mockProcessor.ProcessErrorAsync += eventArgs => + { + capturedEventArgs = eventArgs; + completionSource.TrySetResult(true); + + return Task.CompletedTask; + }; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + Assert.That(capturedEventArgs, Is.Not.Null, "The error event args should have been captured."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.False, "The token in the handler should not have been canceled yet."); + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(capturedEventArgs.Operation, Is.EqualTo(Resources.OperationClaimOwnership), "The captured Operation string is not correct."); + Assert.That(capturedEventArgs.Exception, Is.EqualTo(expectedExceptionReference), "The captured and expected exceptions do not match."); + Assert.That(capturedEventArgs.PartitionId, Is.EqualTo(partitionId), "The associated partition id does not match."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.True, "The token in the handler should have been canceled."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task ProcessErrorAsyncIsTriggeredWithCorrectArgumentsWhenOwnershipRenewalFails() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockStorage = new Mock(default(Action)) { CallBase = true }; + var mockProcessor = new InjectableEventSourceProcessorMock(mockStorage.Object, "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { "0" })); + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((partition, position, options, token) => + MockEndlessPartitionEventEnumerable(options.MaximumWaitTime, token)); + + var partitionHasBeenClaimed = false; + var expectedExceptionReference = new Exception(); + + // When the mock storage intercepts a call to ClaimOwnershipAsync after the partition has been claimed, it means we have found a renewal attempt. + // We will force an exception and catch it with the error handler. + + mockStorage + .Setup(storage => storage.ClaimOwnershipAsync( + It.Is>(ownershipEnumerable => partitionHasBeenClaimed && ownershipEnumerable.All(ownership => ownership.OwnerIdentifier == mockProcessor.Identifier)), + It.IsAny())) + .Throws(expectedExceptionReference); + + mockProcessor.PartitionInitializingAsync += eventArgs => + { + partitionHasBeenClaimed = true; + return Task.CompletedTask; + }; + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + var capturedEventArgs = default(ProcessErrorEventArgs); + var completionSource = new TaskCompletionSource(); + + mockProcessor.ProcessErrorAsync += eventArgs => + { + capturedEventArgs = eventArgs; + completionSource.TrySetResult(true); + + return Task.CompletedTask; + }; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + Assert.That(capturedEventArgs, Is.Not.Null, "The error event args should have been captured."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.False, "The token in the handler should not have been canceled yet."); + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(capturedEventArgs.Operation, Is.EqualTo(Resources.OperationRenewOwnership), "The captured Operation string is not correct."); + Assert.That(capturedEventArgs.Exception, Is.EqualTo(expectedExceptionReference), "The captured and expected exceptions do not match."); + Assert.That(capturedEventArgs.PartitionId, Is.Null, "There should be no partition id associated with the renewal attempt failure."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.True, "The token in the handler should have been canceled."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task ProcessErrorAsyncIsTriggeredWithCorrectArgumentsWhenListOwnershipFails() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockStorage = new Mock(default(Action)) { CallBase = true }; + var mockProcessor = new InjectableEventSourceProcessorMock(mockStorage.Object, "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { "0" })); + + var expectedExceptionReference = new Exception(); + + // We will force an exception and catch it with the error handler. + + mockStorage + .Setup(storage => storage.ListOwnershipAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Throws(expectedExceptionReference); + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + var capturedEventArgs = default(ProcessErrorEventArgs); + var completionSource = new TaskCompletionSource(); + + mockProcessor.ProcessErrorAsync += eventArgs => + { + capturedEventArgs = eventArgs; + completionSource.TrySetResult(true); + + return Task.CompletedTask; + }; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + Assert.That(capturedEventArgs, Is.Not.Null, "The error event args should have been captured."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.False, "The token in the handler should not have been canceled yet."); + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(capturedEventArgs.Operation, Is.EqualTo(Resources.OperationListOwnership), "The captured Operation string is not correct."); + Assert.That(capturedEventArgs.Exception, Is.EqualTo(expectedExceptionReference), "The captured and expected exceptions do not match."); + Assert.That(capturedEventArgs.PartitionId, Is.Null, "There should be no partition id associated with the list ownership failure."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.True, "The token in the handler should have been canceled."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task ProcessErrorAsyncIsTriggeredWithCorrectArgumentsWhenGetPartitionIdsFails() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + var expectedExceptionReference = new Exception(); + + // We will force an exception and catch it with the error handler. + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Throws(expectedExceptionReference); + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + var capturedEventArgs = default(ProcessErrorEventArgs); + var completionSource = new TaskCompletionSource(); + + mockProcessor.ProcessErrorAsync += eventArgs => + { + capturedEventArgs = eventArgs; + completionSource.TrySetResult(true); + + return Task.CompletedTask; + }; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + Assert.That(capturedEventArgs, Is.Not.Null, "The error event args should have been captured."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.False, "The token in the handler should not have been canceled yet."); + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(capturedEventArgs.Operation, Is.EqualTo(Resources.OperationGetPartitionIds), "The captured Operation string is not correct."); + Assert.That(capturedEventArgs.Exception, Is.EqualTo(expectedExceptionReference), "The captured and expected exceptions do not match."); + Assert.That(capturedEventArgs.PartitionId, Is.Null, "There should be no partition id associated with the get partition ids failure."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.True, "The token in the handler should have been canceled."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task ProcessErrorAsyncIsTriggeredWithCorrectArgumentsWhenListCheckpointsFails() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockStorage = new Mock(default(Action)) { CallBase = true }; + var mockProcessor = new InjectableEventSourceProcessorMock(mockStorage.Object, "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + var partitionId = "0"; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { partitionId })); + + var expectedExceptionReference = new Exception(); + + // We will force an exception and catch it with the error handler. + + mockStorage + .Setup(storage => storage.ListCheckpointsAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Throws(expectedExceptionReference); + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + var capturedEventArgs = default(ProcessErrorEventArgs); + var completionSource = new TaskCompletionSource(); + + mockProcessor.ProcessErrorAsync += eventArgs => + { + capturedEventArgs = eventArgs; + completionSource.TrySetResult(true); + + return Task.CompletedTask; + }; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + Assert.That(capturedEventArgs, Is.Not.Null, "The error event args should have been captured."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.False, "The token in the handler should not have been canceled yet."); + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(capturedEventArgs.Operation, Is.EqualTo(Resources.OperationListCheckpoints), "The captured Operation string is not correct."); + Assert.That(capturedEventArgs.Exception, Is.EqualTo(expectedExceptionReference), "The captured and expected exceptions do not match."); + Assert.That(capturedEventArgs.PartitionId, Is.EqualTo(partitionId), "The associated partition id does not match."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.True, "The token in the handler should have been canceled."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task ProcessErrorAsyncIsTriggeredWithCorrectArgumentsWhenPartitionProcessingFails() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + var partitionId = "0"; + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { partitionId })); + + var expectedExceptionReference = new Exception(); + + // We will force an exception and catch it with the error handler. + + mockProcessor.RunPartitionProcessingException = expectedExceptionReference; + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + var capturedEventArgs = default(ProcessErrorEventArgs); + var completionSource = new TaskCompletionSource(); + + mockProcessor.ProcessErrorAsync += eventArgs => + { + if (completionSource.TrySetResult(true)) + { + capturedEventArgs = eventArgs; + } + + return Task.CompletedTask; + }; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); - await mockProcessor.Object.StartProcessingAsync(); - await Task.WhenAny(Task.Delay(-1, cancellationSource.Token), completionSource.Task); + await mockProcessor.StartProcessingAsync(cancellationSource.Token); - // Capture the value of IsRunning before stopping the processor. We are doing this to make sure - // we stop the processor properly even in case of failure. + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } - var isRunning = mockProcessor.Object.IsRunning; + Assert.That(capturedEventArgs, Is.Not.Null, "The error event args should have been captured."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.False, "The token in the handler should not have been canceled yet."); - // Stop the processor and ensure that it does not block on the handler. + await mockProcessor.StopProcessingAsync(cancellationSource.Token); - Assert.That(async () => await mockProcessor.Object.StopProcessingAsync(), Throws.Exception, "An exception should have been thrown when creating the consumer."); Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); - - Assert.That(isRunning, Is.False, "IsRunning should return false when the load balancing task fails."); + Assert.That(capturedEventArgs.Operation, Is.EqualTo(Resources.OperationReadEvents), "The captured Operation string is not correct."); + Assert.That(capturedEventArgs.Exception, Is.EqualTo(expectedExceptionReference), "The captured and expected exceptions do not match."); + Assert.That(capturedEventArgs.PartitionId, Is.EqualTo(partitionId), "The associated partition id does not match."); + Assert.That(capturedEventArgs.CancellationToken.IsCancellationRequested, Is.True, "The token in the handler should have been canceled."); } /// - /// Verify logs for the . + /// Verifies functionality of the + /// event. /// /// [Test] - public async Task VerifiesEventProcessorLogs() + public async Task ProcessErrorAsyncIsNotTriggeredWhenRelinquishingOwnershipFails() { var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); - string[] partitionIds = { "0" }; + var mockStorage = new Mock(default(Action)) { CallBase = true }; + var mockProcessor = new InjectableEventSourceProcessorMock(mockStorage.Object, "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); mockConsumer .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) - .Returns(Task.FromResult(partitionIds)); + .Returns(Task.FromResult(new[] { "0" })); - var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default); + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((partition, position, options, token) => + MockEndlessPartitionEventEnumerable(options.MaximumWaitTime, token)); - var mockLog = new Mock(); - mockProcessor.CallBase = true; - mockProcessor.Object.Logger = mockLog.Object; + var expectedExceptionReference = new Exception(); - mockProcessor - .Setup(processor => processor.CreateConsumer( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(mockConsumer.Object); + // When the mock storage intercepts a call to ClaimOwnershipAsync with no owner id, it means we have found a relinquish attempt. + // We will force an exception and catch it with the error handler. - mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; - mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; + mockStorage + .Setup(storage => storage.ClaimOwnershipAsync( + It.Is>(ownershipEnumerable => ownershipEnumerable.Any(ownership => string.IsNullOrEmpty(ownership.OwnerIdentifier))), + It.IsAny())) + .Throws(expectedExceptionReference); - using var cancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + var completionSource = new TaskCompletionSource(); - Assert.That(async () => await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token), Throws.Nothing); - await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token); + // We can stop processing once the partition has been claimed. - mockLog.Verify(m => m.EventProcessorStart(mockProcessor.Object.Identifier)); - mockLog.Verify(m => m.EventProcessorStopStart(mockProcessor.Object.Identifier)); - mockLog.Verify(m => m.EventProcessorStopComplete(mockProcessor.Object.Identifier)); + mockProcessor.PartitionInitializingAsync += eventArgs => + { + completionSource.SetResult(true); + return Task.CompletedTask; + }; + + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + var hasCalledProcessError = false; + + mockProcessor.ProcessErrorAsync += eventArgs => + { + hasCalledProcessError = true; + return Task.CompletedTask; + }; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + var capturedException = default(Exception); + + try + { + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + } + catch (Exception ex) + { + capturedException = ex; + } + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + + Assert.That(capturedException, Is.Not.Null, "An exception should have be thrown when stopping the processor."); + Assert.That(capturedException, Is.EqualTo(expectedExceptionReference), "The captured and expected exceptions do not match."); + Assert.That(hasCalledProcessError, Is.False, "The error handler should not have been triggered."); } /// @@ -815,14 +2682,15 @@ public async Task VerifiesEventProcessorLogs() /// /// [Test] - public async Task ProcessErrorAsyncDoesNotBlockStopping() + public async Task ProcessErrorAsyncIsNotTriggeredWhenUpdateCheckpointFails() { var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); - var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + var mockStorage = new Mock(default(Action)) { CallBase = true }; + var mockProcessor = new InjectableEventSourceProcessorMock(mockStorage.Object, "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); mockConsumer .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) - .ThrowsAsync(new InvalidCastException()); + .Returns(Task.FromResult(new[] { "0" })); mockConsumer .Setup(consumer => consumer.ReadEventsFromPartitionAsync( @@ -830,36 +2698,203 @@ public async Task ProcessErrorAsyncDoesNotBlockStopping() It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((partition, position, options, token) => MockPartitionEventEnumerable(20, token)); + .Returns((partition, position, options, token) => MockPartitionEventEnumerable(1, token)); + + var expectedExceptionReference = new Exception(); + + // We will force an exception and catch it with a catch block. + + mockStorage + .Setup(storage => storage.UpdateCheckpointAsync( + It.IsAny(), + It.IsAny())) + .Throws(expectedExceptionReference); + + var completionSource = new TaskCompletionSource(); + var capturedException = default(Exception); + + mockProcessor.ProcessEventAsync += async eventArgs => + { + try + { + await eventArgs.UpdateCheckpointAsync(); + } + catch (Exception ex) + { + capturedException = ex; + } + + completionSource.SetResult(true); + }; + + var hasCalledProcessError = false; + + mockProcessor.ProcessErrorAsync += eventArgs => + { + hasCalledProcessError = true; + return Task.CompletedTask; + }; + + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + Assert.That(mockProcessor.IsRunning, Is.True, "The processor should not have stopped working."); + + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + + Assert.That(capturedException, Is.Not.Null, "An exception should have be thrown when updating the checkpoint."); + Assert.That(capturedException, Is.EqualTo(expectedExceptionReference), "The captured and expected exceptions do not match."); + Assert.That(hasCalledProcessError, Is.False, "The error handler should not have been triggered."); + } + + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + public async Task ProcessErrorAsyncIsNotTriggeredWhenPartitionInitializingAsyncFails() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { "0" })); + + var expectedExceptionReference = new Exception(); + var completionSource = new TaskCompletionSource(); + + mockProcessor.PartitionInitializingAsync += eventArgs => + { + completionSource.SetResult(true); + throw expectedExceptionReference; + }; mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; - // Create a handler that does not complete in a reasonable amount of time. To ensure that the - // test does not hang for the duration, set a timeout to force completion after a shorter period - // of time. + var hasCalledProcessError = false; + + mockProcessor.ProcessErrorAsync += eventArgs => + { + hasCalledProcessError = true; + return Task.CompletedTask; + }; + + // Establish timed cancellation to ensure that the test doesn't hang. using var cancellationSource = new CancellationTokenSource(); - cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + var capturedException = default(Exception); + + try + { + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + } + catch (Exception ex) + { + capturedException = ex; + } + + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + + Assert.That(capturedException, Is.Not.Null, "An exception should have be thrown when stopping the processor."); + Assert.That(capturedException, Is.EqualTo(expectedExceptionReference), "The captured and expected exceptions do not match."); + Assert.That(hasCalledProcessError, Is.False, "The error handler should not have been triggered."); + } + /// + /// Verifies functionality of the + /// event. + /// + /// + [Test] + [Ignore("Not implemented yet. (Tracked by #9228)")] + public async Task ProcessErrorAsyncIsNotTriggeredWhenProcessEventAsyncFails() + { + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { "0" })); + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((partition, position, options, token) => + MockEndlessPartitionEventEnumerable(options.MaximumWaitTime, token)); + + var expectedExceptionReference = new Exception(); var completionSource = new TaskCompletionSource(); - mockProcessor.ProcessErrorAsync += async eventArgs => + mockProcessor.ProcessEventAsync += eventArgs => { completionSource.SetResult(true); - await Task.Delay(TimeSpan.FromMinutes(3), cancellationSource.Token); + throw expectedExceptionReference; }; - // Start the processor and wait for the event handler to be triggered. + var hasCalledProcessError = false; - await mockProcessor.StartProcessingAsync(); - await completionSource.Task; + mockProcessor.ProcessErrorAsync += eventArgs => + { + hasCalledProcessError = true; + return Task.CompletedTask; + }; - // Stop the processor and ensure that it does not block on the handler. + // Establish timed cancellation to ensure that the test doesn't hang. + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + + await mockProcessor.StartProcessingAsync(cancellationSource.Token); + + while (!completionSource.Task.IsCompleted + && !cancellationSource.IsCancellationRequested) + { + await Task.Delay(25); + } + + var capturedException = default(Exception); + + try + { + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + } + catch (Exception ex) + { + capturedException = ex; + } - Assert.That(async () => await mockProcessor.StopProcessingAsync(cancellationSource.Token), Throws.Nothing, "The processor should stop without a problem."); Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); - cancellationSource.Cancel(); + Assert.That(capturedException, Is.Not.Null, "An exception should have be thrown when stopping the processor."); + Assert.That(capturedException, Is.EqualTo(expectedExceptionReference), "The captured and expected exceptions do not match."); + Assert.That(hasCalledProcessError, Is.False, "The error handler should not have been triggered."); } /// @@ -868,7 +2903,7 @@ public async Task ProcessErrorAsyncDoesNotBlockStopping() /// /// [Test] - public async Task ProcessErrorAsyncCanStopTheEventProcessorClient() + public async Task ProcessErrorAsyncDoesNotBlockStopping() { var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); @@ -887,42 +2922,48 @@ public async Task ProcessErrorAsyncCanStopTheEventProcessorClient() mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + // Create a handler that does not complete in a reasonable amount of time. To ensure that the + // test does not hang for the duration, set a timeout to force completion after a shorter period + // of time. + using var cancellationSource = new CancellationTokenSource(); - cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); var completionSource = new TaskCompletionSource(); mockProcessor.ProcessErrorAsync += async eventArgs => { - await mockProcessor.StopProcessingAsync(cancellationSource.Token); completionSource.SetResult(true); + await Task.Delay(TimeSpan.FromMinutes(3), cancellationSource.Token); }; // Start the processor and wait for the event handler to be triggered. - await mockProcessor.StartProcessingAsync(cancellationSource.Token); + await mockProcessor.StartProcessingAsync(); await completionSource.Task; - // Ensure that the processor has been stopped. + // Stop the processor and ensure that it does not block on the handler. + + Assert.That(async () => await mockProcessor.StopProcessingAsync(cancellationSource.Token), Throws.Nothing, "The processor should stop without a problem."); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); - Assert.That(mockProcessor.IsRunning, Is.False, "The processor should have stopped."); + cancellationSource.Cancel(); } /// - /// Verifies functionality of the + /// Verifies functionality of the /// event. /// /// [Test] - public async Task ProcessEventAsyncReceivesAnEmptyPartitionContextForNoData() + public async Task ProcessErrorAsyncCanStopTheEventProcessorClient() { - var partitionId = "expectedPartition"; var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); mockConsumer .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) - .Returns(Task.FromResult(new[] { partitionId })); + .ThrowsAsync(new InvalidCastException()); mockConsumer .Setup(consumer => consumer.ReadEventsFromPartitionAsync( @@ -930,94 +2971,29 @@ public async Task ProcessEventAsyncReceivesAnEmptyPartitionContextForNoData() It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((partition, position, options, token) => MockEmptyPartitionEventEnumerable(1, token)); + .Returns((partition, position, options, token) => MockPartitionEventEnumerable(20, token)); - mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); var completionSource = new TaskCompletionSource(); - var emptyEventArgs = default(ProcessEventArgs); - mockProcessor.ProcessEventAsync += eventArgs => + mockProcessor.ProcessErrorAsync += async eventArgs => { - emptyEventArgs = eventArgs; - completionSource.TrySetResult(true); - - return Task.CompletedTask; + await mockProcessor.StopProcessingAsync(cancellationSource.Token); + completionSource.SetResult(true); }; // Start the processor and wait for the event handler to be triggered. - using var cancellationSource = new CancellationTokenSource(); - cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); - - await mockProcessor.StartProcessingAsync(cancellationSource.Token); await completionSource.Task; - await mockProcessor.StopProcessingAsync(cancellationSource.Token); - - // Validate the empty event arguments. - - Assert.That(emptyEventArgs, Is.Not.Null, "The event arguments should have been populated."); - Assert.That(emptyEventArgs.Data, Is.Null, "The event arguments should not have an event available."); - Assert.That(emptyEventArgs.Partition, Is.Not.Null, "The event arguments should have a partition context."); - Assert.That(emptyEventArgs.Partition.PartitionId, Is.EqualTo(partitionId), "The partition identifier should match."); - Assert.That(() => emptyEventArgs.Partition.ReadLastEnqueuedEventProperties(), Throws.InstanceOf(), "The last event properties should not be available."); - } - - /// - /// Verifies that partitions owned by an are immediately available to be claimed by another processor - /// after is called. - /// - /// - [Test] - public async Task StopProcessingAsyncStopsLoadbalancer() - { - var mockLoadbalancer = new Mock(); - mockLoadbalancer.SetupAllProperties(); - mockLoadbalancer.Object.LoadBalanceInterval = TimeSpan.FromSeconds(1); - Func connectionFactory = () => new MockConnection(); - var connection = connectionFactory(); - var storageManager = new MockCheckPointStorage((s) => Console.WriteLine(s)); - var processor = new MockEventProcessorClient( - storageManager, - connectionFactory: connectionFactory, - loadBalancer: mockLoadbalancer.Object); - - await processor.StartProcessingAsync(); - await processor.StopProcessingAsync(); - - // Stopping the processor should stop the PartitionLoadBalancer. - mockLoadbalancer.Verify(m => m.RelinquishOwnershipAsync(It.IsAny())); - } - - /// - /// Verifies that an invokes partition load balancing - /// after is called. - /// - /// - [Test] - public async Task StartProcessingAsyncStartsLoadbalancer() - { - const int NumberOfPartitions = 3; - var mockLoadbalancer = new Mock(); - mockLoadbalancer.SetupAllProperties(); - mockLoadbalancer.Object.LoadBalanceInterval = TimeSpan.FromSeconds(1); - Func connectionFactory = () => new MockConnection(); - var connection = connectionFactory(); - var storageManager = new MockCheckPointStorage((s) => Console.WriteLine(s)); - var processor = new MockEventProcessorClient( - storageManager, - connectionFactory: connectionFactory, - loadBalancer: mockLoadbalancer.Object); - - await processor.StartProcessingAsync(); - - // Starting the processor should call the PartitionLoadBalancer. - - mockLoadbalancer.Verify(m => m.RunLoadBalancingAsync(It.Is(p => p.Length == NumberOfPartitions), It.IsAny())); + // Ensure that the processor has been stopped. - await processor.StopProcessingAsync(CancellationToken.None); + Assert.That(mockProcessor.IsRunning, Is.False, "The processor should have stopped."); } /// @@ -1132,185 +3108,17 @@ public async Task ProcessingStartsWithTheExactDefaultPositionWithNoCheckpoint() Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); mockConsumer.VerifyAll(); - - cancellationSource.Cancel(); - } - - /// - /// Verifies functionality of the - /// method. - /// - /// - [Test] - public void AlreadyCancelledTokenMakesStartProcessingAsyncThrow() - { - var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); - var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; - - mockConsumer - .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) - .Returns(Task.FromResult(Array.Empty())); - - mockProcessor - .Setup(processor => processor.CreateConsumer( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(mockConsumer.Object); - - mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; - mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; - - using var cancellationSource = new CancellationTokenSource(); - cancellationSource.Cancel(); - - Assert.That(async () => await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token), Throws.InstanceOf()); - } - - /// - /// Verifies functionality of the - /// method. - /// - /// - [Test] - public async Task AlreadyCancelledTokenMakesStopProcessingAsyncThrow() - { - var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); - var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; - - mockConsumer - .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) - .Returns(Task.FromResult(Array.Empty())); - - mockProcessor - .Setup(processor => processor.CreateConsumer( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(mockConsumer.Object); - - mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; - mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; - - await mockProcessor.Object.StartProcessingAsync(); - - using var cancellationSource = new CancellationTokenSource(); - cancellationSource.Cancel(); - - Assert.That(async () => await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token), Throws.InstanceOf()); - } - - /// - /// Verifies functionality of the - /// methods. - /// - /// - [Test] - public void AlreadyCancelledTokenMakesStartProcessingThrow() - { - var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); - var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; - - mockConsumer - .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) - .Returns(Task.FromResult(Array.Empty())); - - mockProcessor - .Setup(processor => processor.CreateConsumer( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(mockConsumer.Object); - - mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; - mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; - - using var cancellationSource = new CancellationTokenSource(); - cancellationSource.Cancel(); - - Assert.That(() => mockProcessor.Object.StartProcessing(cancellationSource.Token), Throws.InstanceOf()); - } - - /// - /// Verifies functionality of the - /// methods. - /// - /// - [Test] - public void AlreadyCancelledTokenMakesStopProcessingThrow() - { - var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); - var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; - - mockConsumer - .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) - .Returns(Task.FromResult(Array.Empty())); - - mockProcessor - .Setup(processor => processor.CreateConsumer( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(mockConsumer.Object); - - mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; - mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; - - mockProcessor.Object.StartProcessing(); - - using var cancellationSource = new CancellationTokenSource(); - cancellationSource.Cancel(); - - Assert.That(() => mockProcessor.Object.StopProcessing(cancellationSource.Token), Throws.InstanceOf()); - } - - /// - /// Verifies functionality of the - /// and methods. - /// - /// - [Test] - public void StartAndStopProcessingShouldStartAndStopProcessors() - { - var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); - var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; - - mockConsumer - .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) - .Returns(Task.FromResult(Array.Empty())); - - mockProcessor - .Setup(processor => processor.CreateConsumer( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(mockConsumer.Object); - - mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; - mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; - - using var cancellationSource = new CancellationTokenSource(); - cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); - - Assert.That(mockProcessor.Object.IsRunning, Is.False); - - mockProcessor.Object.StartProcessing(cancellationSource.Token); - - Assert.That(mockProcessor.Object.IsRunning, Is.True); - - mockProcessor.Object.StopProcessing(cancellationSource.Token); - - Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); - Assert.That(mockProcessor.Object.IsRunning, Is.False); + + cancellationSource.Cancel(); } /// - /// Verifies functionality of the - /// and methods. + /// Verifies functionality of the + /// handler's method. /// /// [Test] - public async Task SupportsStartProcessingAfterStop() + public async Task WhenProcessEventTriggersWithNoDataUpdateCheckpointThrows() { var partitionId = "expectedPartition"; var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); @@ -1326,16 +3134,19 @@ public async Task SupportsStartProcessingAfterStop() It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((partition, position, options, token) => MockEmptyPartitionEventEnumerable(1, token)); + .Returns((partition, position, options, token) => MockEmptyPartitionEventEnumerable(5, token)); mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; var completionSource = new TaskCompletionSource(); - var isProcessEventHandlerInvoked = false; + var emptyEventArgs = default(ProcessEventArgs); mockProcessor.ProcessEventAsync += eventArgs => { - isProcessEventHandlerInvoked = true; + emptyEventArgs = eventArgs; + + Assert.That(async () => await eventArgs.UpdateCheckpointAsync(), Throws.InstanceOf(), "An exception should have been thrown when ProcessEventAsync triggers with no data."); + completionSource.SetResult(true); return Task.CompletedTask; @@ -1344,69 +3155,66 @@ public async Task SupportsStartProcessingAfterStop() using var cancellationSource = new CancellationTokenSource(); cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); - Assert.That(mockProcessor.IsRunning, Is.False); - await mockProcessor.StartProcessingAsync(cancellationSource.Token); await completionSource.Task; - - Assert.That(mockProcessor.IsRunning, Is.True); - Assert.That(isProcessEventHandlerInvoked, Is.EqualTo(true)); - await mockProcessor.StopProcessingAsync(cancellationSource.Token); - isProcessEventHandlerInvoked = false; - completionSource = new TaskCompletionSource(); - - Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); - Assert.That(mockProcessor.IsRunning, Is.False); - - await mockProcessor.StartProcessingAsync(cancellationSource.Token); - await completionSource.Task; - - Assert.That(mockProcessor.IsRunning, Is.True); - Assert.That(isProcessEventHandlerInvoked, Is.EqualTo(true)); - - await mockProcessor.StopProcessingAsync(cancellationSource.Token); + // Validate the empty event arguments. - Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); - Assert.That(mockProcessor.IsRunning, Is.False); + Assert.That(emptyEventArgs, Is.Not.Null, "The event arguments should have been populated."); + Assert.That(emptyEventArgs.Data, Is.Null, "The event arguments should not have an event available."); + Assert.That(emptyEventArgs.Partition, Is.Not.Null, "The event arguments should have a partition context."); + Assert.That(emptyEventArgs.Partition.PartitionId, Is.EqualTo(partitionId), "The partition identifier should match."); + Assert.That(() => emptyEventArgs.Partition.ReadLastEnqueuedEventProperties(), Throws.InstanceOf(), "The last event properties should not be available."); } /// - /// Verifies functionality of the method. + /// Verifies functionality of the + /// handler's method. /// /// [Test] - public async Task StopProcessingShouldSurfaceLoadBalancingException() + public async Task AlreadyCancelledTokenMakesUpdateCheckpointThrow() { - var mockProcessor = new Mock(Mock.Of(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, default) { CallBase = true }; - var completionSource = new TaskCompletionSource(); + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); + var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); - mockProcessor - .Setup(processor => processor.CreateConsumer( + mockConsumer + .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) + .Returns(Task.FromResult(new[] { "0", "1" })); + + mockConsumer + .Setup(consumer => consumer.ReadEventsFromPartitionAsync( It.IsAny(), - It.IsAny(), - It.IsAny())) - .Callback(() => completionSource.SetResult(true)) - .Throws(new Exception()); + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((partition, position, options, token) => MockPartitionEventEnumerable(5, token)); - mockProcessor.Object.ProcessEventAsync += eventArgs => Task.CompletedTask; - mockProcessor.Object.ProcessErrorAsync += eventArgs => Task.CompletedTask; + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; - // To ensure that the test does not hang for the duration, set a timeout to force completion - // after a shorter period of time. + var completionSource = new TaskCompletionSource(); - using var cancellationSource = new CancellationTokenSource(); - cancellationSource.CancelAfter(TimeSpan.FromSeconds(15)); + mockProcessor.ProcessEventAsync += eventArgs => + { + using var cancellationSource = new CancellationTokenSource(); + cancellationSource.Cancel(); - await mockProcessor.Object.StartProcessingAsync(cancellationSource.Token); - await Task.WhenAny(Task.Delay(-1, cancellationSource.Token), completionSource.Task); + Assert.That(async () => await eventArgs.UpdateCheckpointAsync(cancellationSource.Token), Throws.InstanceOf()); - Assert.That(async () => await mockProcessor.Object.StopProcessingAsync(cancellationSource.Token), Throws.Exception, "An exception should have been thrown when creating the consumer."); + completionSource.SetResult(true); + + return Task.CompletedTask; + }; + + await mockProcessor.StartProcessingAsync(); + await completionSource.Task; + await mockProcessor.StopProcessingAsync(); } /// - /// Verifies functionality of the . + /// Verifies functionality of the + /// method. /// /// [Test] @@ -1423,7 +3231,7 @@ public void ToStringReturnsStringContainingProcessorIdentifier() /// /// [Test] - public async Task ProcessorStopsProcessingParitionItDoesNotOwnAnymore() + public async Task ProcessorStopsProcessingPartitionItDoesNotOwnAnymore() { const int NumberOfPartitions = 2; Func connectionFactory = () => new MockConnection(); @@ -1462,37 +3270,49 @@ public async Task ProcessorStopsProcessingParitionItDoesNotOwnAnymore() Assert.That(completeOwnership.Count(p => p.OwnerIdentifier.Equals(processor1.Identifier)), Is.EqualTo(NumberOfPartitions)); - // Start Processor2 so that the it will steal 1 partition from processor1. + // Start Processor2 so that it will steal 1 partition from processor1. await processor2.StartProcessingAsync(cancellationSource.Token); await processor2.WaitStabilization(); completeOwnership = await storageManager.ListOwnershipAsync(processor1.FullyQualifiedNamespace, processor1.EventHubName, processor1.ConsumerGroup, cancellationSource.Token); - // Now both processors own 1 partition + // Now both processors own 1 partition. Assert.That(completeOwnership.ElementAt(0).OwnerIdentifier, Is.Not.EqualTo(completeOwnership.ElementAt(1).OwnerIdentifier)); - // processor1 stopped processing partition it donesn't own anymore with OwnershipLost reason. + // processor1 stopped processing partition it doesn't own anymore with OwnershipLost reason. Assert.That(processor1.StopReasons.Values.First, Is.EqualTo(ProcessingStoppedReason.OwnershipLost)); } /// - /// Verifies functionality of the - /// handler UpdateCheckpointAsync method. + /// Verifies functionality of the + /// load balance cycle. /// /// [Test] - public async Task WhenProcessEventTriggersWithNoDataUpdateCheckpointThrow() + public async Task ProcessorRenewsItsOwnershipEveryCycle() { - var partitionId = "expectedPartition"; + const int testDurationInCycles = 4; + var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); - var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + var mockStorage = new Mock(default(Action)) { CallBase = true }; + var mockProcessor = new InjectableEventSourceProcessorMock(mockStorage.Object, "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); + + // We'll run multiple cycles, so keeping the default load balance interval (10s) would take too much time. + + mockProcessor.LoadBalancer.LoadBalanceInterval = TimeSpan.FromMilliseconds(500); + + // We are making the assumption that a single GetPartitionIds call is made every cycle, so we'll + // use it to count the total of cycles. + + var cycles = 0; mockConsumer .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) - .Returns(Task.FromResult(new[] { partitionId })); + .Callback(() => cycles++) + .Returns(Task.FromResult(new[] { "0" })); mockConsumer .Setup(consumer => consumer.ReadEventsFromPartitionAsync( @@ -1500,147 +3320,61 @@ public async Task WhenProcessEventTriggersWithNoDataUpdateCheckpointThrow() It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((partition, position, options, token) => MockEmptyPartitionEventEnumerable(5, token)); - - mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; + .Returns((partition, position, options, token) => + MockEndlessPartitionEventEnumerable(options.MaximumWaitTime, token)); - var completionSource = new TaskCompletionSource(); - var emptyEventArgs = default(ProcessEventArgs); + // When the mock storage intercepts a call to ClaimOwnershipAsync with at least one object, it means we have found a claim attempt. + // The processor could also be relinquishing ownership, so make sure it has a valid owner id. - mockProcessor.ProcessEventAsync += eventArgs => - { - emptyEventArgs = eventArgs; + var renewals = 0; - Assert.That(async () => await eventArgs.UpdateCheckpointAsync(), Throws.InstanceOf(), "An exception should have been thrown When ProcessEventAsync triggers with no data."); + mockStorage + .Setup(storage => storage.ClaimOwnershipAsync( + It.Is>(ownershipEnumerable => ownershipEnumerable.Any(ownership => !string.IsNullOrEmpty(ownership.OwnerIdentifier))), + It.IsAny())) + .Callback(() => renewals++); - completionSource.SetResult(true); + mockProcessor.ProcessEventAsync += eventArgs => Task.CompletedTask; + mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; - return Task.CompletedTask; - }; + // Establish timed cancellation to ensure that the test doesn't hang. using var cancellationSource = new CancellationTokenSource(); cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); await mockProcessor.StartProcessingAsync(cancellationSource.Token); - await completionSource.Task; - await mockProcessor.StopProcessingAsync(cancellationSource.Token); - - // Validate the empty event arguments. - - Assert.That(emptyEventArgs, Is.Not.Null, "The event arguments should have been populated."); - Assert.That(emptyEventArgs.Data, Is.Null, "The event arguments should not have an event available."); - Assert.That(emptyEventArgs.Partition, Is.Not.Null, "The event arguments should have a partition context."); - Assert.That(emptyEventArgs.Partition.PartitionId, Is.EqualTo(partitionId), "The partition identifier should match."); - Assert.That(() => emptyEventArgs.Partition.ReadLastEnqueuedEventProperties(), Throws.InstanceOf(), "The last event properties should not be available."); - } - - /// - /// Verifies functionality of the - /// handler UpdateCheckpointAsync method. - /// - /// - [Test] - public async Task AlreadyCancelledTokenMakesUpdateCheckpointThrow() - { - var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); - var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); - - mockConsumer - .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) - .Returns(Task.FromResult(new[] { "0", "1" })); - - mockConsumer - .Setup(consumer => consumer.ReadEventsFromPartitionAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns((partition, position, options, token) => MockPartitionEventEnumerable(5, token)); - - mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; - var completionSource = new TaskCompletionSource(); - - mockProcessor.ProcessEventAsync += eventArgs => + while (cycles < testDurationInCycles + && !cancellationSource.IsCancellationRequested) { - using var cancellationSource = new CancellationTokenSource(); - cancellationSource.Cancel(); - - Assert.That(async () => await eventArgs.UpdateCheckpointAsync(cancellationSource.Token), Throws.InstanceOf()); - - completionSource.SetResult(true); + await Task.Delay(25); + } - return Task.CompletedTask; - }; + await mockProcessor.StopProcessingAsync(cancellationSource.Token); - await mockProcessor.StartProcessingAsync(); - await completionSource.Task; - await mockProcessor.StopProcessingAsync(); + Assert.That(cancellationSource.IsCancellationRequested, Is.False, "The processor should have stopped without cancellation."); + Assert.That(renewals, Is.EqualTo(cycles).Within(1)); } /// - /// Verifies functionality of the - /// event. + /// Retrieves the StorageManager for the processor client using its private accessor. /// /// - [Test] - public async Task ProcessHanderTriggersForEveryReceivedEvent() - { - var mockConsumer = new Mock("consumerGroup", Mock.Of(), default); - var mockProcessor = new InjectableEventSourceProcessorMock(new MockCheckPointStorage(), "consumerGroup", "namespace", "eventHub", Mock.Of>(), default, mockConsumer.Object); - - mockConsumer - .Setup(consumer => consumer.GetPartitionIdsAsync(It.IsAny())) - .Returns(Task.FromResult(new[] { "0" })); - - var numberOfEvents = 5; - - mockConsumer - .Setup(consumer => consumer.ReadEventsFromPartitionAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns((partition, position, options, token) => MockPartitionEventEnumerable(numberOfEvents, token)); - - mockProcessor.ProcessErrorAsync += eventArgs => Task.CompletedTask; - - var completionSource = new TaskCompletionSource(); - - var processEventTriggerCount = 0; - - mockProcessor.ProcessEventAsync += eventArgs => - { - processEventTriggerCount++; - - if (processEventTriggerCount == numberOfEvents) - { - completionSource.SetResult(true); - } - - return Task.CompletedTask; - }; - - using var cancellationSource = new CancellationTokenSource(); - cancellationSource.CancelAfter(TimeSpan.FromSeconds(30)); - - // Start the processor and wait for the event handler to be triggered. - - await mockProcessor.StartProcessingAsync(cancellationSource.Token); - await completionSource.Task; - - Assert.That(numberOfEvents, Is.EqualTo(processEventTriggerCount)); - } + private static BlobsCheckpointStore GetStorageManager(EventProcessorClient client) => + (BlobsCheckpointStore) + typeof(EventProcessorClient) + .GetProperty("StorageManager", BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(client); /// - /// Retrieves the RetryPolicy for the processor client using its private accessor. + /// Retrieves the RetryPolicy for the storage manager using its private accessor. /// /// - private static EventHubsRetryPolicy GetRetryPolicy(EventProcessorClient client) => + private static EventHubsRetryPolicy GetStorageManagerRetryPolicy(BlobsCheckpointStore storageManager) => (EventHubsRetryPolicy) - typeof(EventProcessorClient) - .GetProperty("RetryPolicy", BindingFlags.Instance | BindingFlags.NonPublic) - .GetValue(client); + typeof(BlobsCheckpointStore) + .GetProperty("RetryPolicy", BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(storageManager); /// /// Retrieves the ProcessingConsumerOptions for the processor client using its private accessor. @@ -1690,7 +3424,12 @@ private static async IAsyncEnumerable MockPartitionEventEnumerab { await Task.Delay(5); cancellationToken.ThrowIfCancellationRequested(); - yield return new PartitionEvent(new MockPartitionContext("fake"), new EventData(Encoding.UTF8.GetBytes($"Event { index }"))); + + // If offset and sequence number are not set, the processor will throw an exception when updating + // checkpoint. + + var eventData = new EventDataMock(Encoding.UTF8.GetBytes($"Event { index }"), index, index); + yield return new PartitionEvent(new MockPartitionContext("fake"), eventData); } await Task.CompletedTask; @@ -1713,14 +3452,30 @@ private static async IAsyncEnumerable MockEmptyPartitionEventEnu await Task.CompletedTask; } + /// + /// Allows for injecting an offset and a sequence number to the Event Data creation. + /// + /// + private class EventDataMock : EventData + { + public EventDataMock(ReadOnlyMemory eventBody, + long sequenceNumber, + long offset) : base(eventBody, sequenceNumber: sequenceNumber, offset: offset) + { + } + } + /// /// Allows for injecting a consumer client to use as the source of events to be processed. /// /// private class InjectableEventSourceProcessorMock : EventProcessorClient { + public Func ShouldIgnoreTestRunnerException = (id, pos, token) => true; + + public Exception RunPartitionProcessingException; + private EventHubConsumerClient _consumer; - private bool _stopCalled; public InjectableEventSourceProcessorMock(StorageManager storageManager, string consumerGroup, @@ -1734,24 +3489,28 @@ public InjectableEventSourceProcessorMock(StorageManager storageManager, _consumer = eventSourceConsumer; } - public override Task StopProcessingAsync(CancellationToken cancellationToken = default) - { - _stopCalled = true; - return base.StopProcessingAsync(cancellationToken); - } - internal override EventHubConsumerClient CreateConsumer(string consumerGroup, EventHubConnection connection, EventHubConsumerClientOptions options) => _consumer; internal override async Task RunPartitionProcessingAsync(string partitionId, EventPosition startingPosition, CancellationToken cancellationToken) { + // There are a few scenarios in which we want to throw from RunPartitionProcessingAsync. + // The NullReferenceException thrown by the test runner overwrites the exception we are + // expecting, so this workaround is necessary. + + if (RunPartitionProcessingException != null) + { + throw RunPartitionProcessingException; + } + try { await base.RunPartitionProcessingAsync(partitionId, startingPosition, cancellationToken).ConfigureAwait(false); } catch (NullReferenceException ex) - when ((_stopCalled) && (string.Equals(ex.Source, "Microsoft.Bcl.AsyncInterfaces", StringComparison.OrdinalIgnoreCase))) + when ((ShouldIgnoreTestRunnerException(partitionId, startingPosition, cancellationToken)) + && (string.Equals(ex.Source, "Microsoft.Bcl.AsyncInterfaces", StringComparison.OrdinalIgnoreCase))) { // This is a test-specific error that occurs when stopping and using a mocked event source. // This scenario does not occur outside of the NUnit runner environment. To ensure that @@ -1760,6 +3519,27 @@ internal override async Task RunPartitionProcessingAsync(string partitionId, Eve } } + /// + /// Creates a mock async enumerable to simulate reading events from a partition. + /// + /// + private static async IAsyncEnumerable MockEndlessPartitionEventEnumerable(TimeSpan? maximumWaitTime, + [EnumeratorCancellation]CancellationToken cancellationToken) + { + if (!maximumWaitTime.HasValue) + { + await Task.Delay(Timeout.Infinite, cancellationToken).ConfigureAwait(false); + } + + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(maximumWaitTime.Value, cancellationToken).ConfigureAwait(false); + yield return new PartitionEvent(); + } + + throw new TaskCanceledException(); + } + /// /// Serves as a non-functional connection for testing processor functionality. /// @@ -1777,18 +3557,6 @@ private static EventHubTokenCredential CreateCredentials() } } - /// - /// Serves as a mock . - /// - /// - private class MockEventHubProperties : EventHubProperties - { - public MockEventHubProperties(string name, - DateTimeOffset createdOn, - string[] partitionIds) : base(name, createdOn, partitionIds) - { } - } - /// /// Serves as a mock . /// diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Properties/AssemblyInfo.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Properties/AssemblyInfo.cs index b5f9716641f8..b4d848c1fb80 100755 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Properties/AssemblyInfo.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Processor/tests/Properties/AssemblyInfo.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Runtime.CompilerServices; using NUnit.Framework; [assembly: Parallelizable(ParallelScope.All)] + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Processor/PartitionLoadBalancer.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Processor/PartitionLoadBalancer.cs index bc359886ac04..08c386ed41d0 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Processor/PartitionLoadBalancer.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Processor/PartitionLoadBalancer.cs @@ -167,9 +167,11 @@ public virtual async ValueTask RunLoadBalancingAsync(string[ cancellationToken.ThrowIfCancellationRequested(); // If ownership list retrieval fails, give up on the current cycle. There's nothing more we can do - // without an updated ownership list. + // without an updated ownership list. Set the EventHubName to null so it doesn't modify the exception + // message. This exception message is used so the processor can retrieve the raw Operation string, and + // adding the EventHubName would append unwanted info to it. - throw new EventHubsException(true, EventHubName, Resources.OperationListOwnership, ex); + throw new EventHubsException(true, null, Resources.OperationListOwnership, ex); } // There's no point in continuing the current cycle if we failed to fetch the completeOwnershipList. @@ -192,7 +194,7 @@ public virtual async ValueTask RunLoadBalancingAsync(string[ foreach (PartitionOwnership ownership in completeOwnershipList) { - if (utcNow.Subtract(ownership.LastModifiedTime.Value) < OwnershipExpiration && !string.IsNullOrWhiteSpace(ownership.OwnerIdentifier)) + if (utcNow.Subtract(ownership.LastModifiedTime.Value) < OwnershipExpiration && !string.IsNullOrEmpty(ownership.OwnerIdentifier)) { if (ActiveOwnershipWithDistribution.ContainsKey(ownership.OwnerIdentifier)) { @@ -416,7 +418,12 @@ private async Task RenewOwnershipAsync(CancellationToken cancellationToken) // end up losing some of its ownership. Logger.RenewOwnershipError(OwnerIdentifier, ex.Message); - throw new EventHubsException(true, EventHubName, Resources.OperationRenewOwnership, ex); + + // Set the EventHubName to null so it doesn't modify the exception message. This exception message is + // used so the processor can retrieve the raw Operation string, and adding the EventHubName would append + // unwanted info to it. + + throw new EventHubsException(true, null, Resources.OperationRenewOwnership, ex); } finally { @@ -472,7 +479,11 @@ private async Task ClaimOwnershipAsync(string partitionId, Logger.ClaimOwnershipError(partitionId, ex.Message); - throw new EventHubsException(true, EventHubName, Resources.OperationClaimOwnership, ex); + // Set the EventHubName to null so it doesn't modify the exception message. This exception message is + // used so the processor can retrieve the raw Operation string, and adding the EventHubName would append + // unwanted info to it. + + throw new EventHubsException(true, null, Resources.OperationClaimOwnership, ex); } // We are expecting an enumerable with a single element if the claim attempt succeeds. diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/EventHubScope.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/EventHubScope.cs index e2fedbb4245a..4bef4a6a30fd 100755 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/EventHubScope.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/EventHubScope.cs @@ -10,7 +10,6 @@ using Microsoft.Azure.Management.EventHub.Models; using Microsoft.Azure.Management.ResourceManager; using Microsoft.Rest; -using Microsoft.Rest.Azure; namespace Azure.Messaging.EventHubs.Tests { diff --git a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/MockCheckPointStorage.cs b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/MockCheckPointStorage.cs index a804f1ff8df6..20dec6cc269e 100644 --- a/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/MockCheckPointStorage.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Testing/MockCheckPointStorage.cs @@ -20,7 +20,7 @@ namespace Azure.Messaging.EventHubs.Tests /// store the checkpoints and partition ownership to a persistent store instead. /// /// - internal sealed class MockCheckPointStorage : StorageManager + internal class MockCheckPointStorage : StorageManager { /// The primitive for synchronizing access during ownership update. private readonly object _ownershipLock = new object(); diff --git a/sdk/eventhub/Azure.Messaging.EventHubs/src/EventData.cs b/sdk/eventhub/Azure.Messaging.EventHubs/src/EventData.cs index 0e30b90e48f1..cab6f8f3cf1f 100755 --- a/sdk/eventhub/Azure.Messaging.EventHubs/src/EventData.cs +++ b/sdk/eventhub/Azure.Messaging.EventHubs/src/EventData.cs @@ -182,7 +182,6 @@ public Stream BodyAsStream /// The raw data to use as the body of the event. /// public EventData(ReadOnlyMemory eventBody) : this(eventBody, lastPartitionSequenceNumber: null) - { }