From 4a097bd1347479319c4298c5887d5c9189dc6097 Mon Sep 17 00:00:00 2001 From: Serkant Karaca Date: Fri, 15 Feb 2019 12:54:49 -0800 Subject: [PATCH] Merge dev to master for 3.0.0 release (#368) * Use Azure Storage Account NameValidator to check LeaseContainerName (#327) * Create EventHubsTimeoutException for consistency (#328) * Mark readonly fields, complete cancellation token and remove useless Where in LINQ (#326) * Implementing Plugins for EventHub (#324) * Implement Plugin to Process each event when client is sending telemetry * Microsoft copyright header * Fix Typo * Changes for https://github.com/Azure/azure-event-hubs-dotnet/pull/324#pullrequestreview-162989181 * Implement AfterEventsReceive for EventHubsPlugin * Implement Plugin Tests * Sort usings * changes for https://github.com/Azure/azure-event-hubs-dotnet/pull/324#issuecomment-428688265 * Fix Resources * Changes for https://github.com/Azure/azure-event-hubs-dotnet/pull/324#pullrequestreview-163616505 * Change for https://github.com/Azure/azure-event-hubs-dotnet/pull/324#issuecomment-429417199 * Move Using to Namespace block * Copy Plugins for InnerSender in AmqpEventHubClient (#329) * Prevent event data being over writed when multiple plugins called (#330) * Parallelize expired lease check in processor host (#333) * Parallelize expired lease check * - * Remove unit test and rename FirstPlugin (#335) * Using Lazy instead of static initialization for ExceptionUtility (#337) * Using Lazy instead of static initialization * Use default ctor * Complete Missing CancellationToken (#338) * Fix LazyLoad Ctor (#341) * Nullify Task when The Stop is complete (#342) * Nullfy Task when The Stop is complete * Test for Re Register event processor * Reset CancellationTokenSource * Replace Locks in AmqpEventHubClient and Code Clean ups in AmqpEventHubClient (#345) * Replace Double lock patterns by using Lazy * Code CleanUps in AmqpEventHubClient * Check client management address when it is being created * Remove Result for AzureStorageCheckpointLeaseManager GetAllLeases (#346) * Remove Result for async call * Get awaiter get result for GetAllLeases * Remove useless using * Remove useless initializator * Replace Task Run Call * Remove Task Run * Fix Tests Moving Lazy initialization at the top of the ctor (#347) * Max message size is 1MB now. Updating the test accordingly. (#344) * Use Guard Class to improve code legibility and avoid lines (#339) * Using Guard to improve code reading and avoid lines * Using ArgumentNotNullOrEmpty * Complete More validations with Guard * Replace All ArgumentNullException * Fix Namespace * Change a few expecting exceptions (#351) * Code CleanUps in Primitives / General (#350) * EventHubCode Clean ups * Code CleanUps in Primitives * Rename variable * Leave string cast * Some Code Cleanups in Ampq Implementation (#349) * Omit failures in receive pump (#354) * Clean more results in sync context (#348) * Clean more results in sync context * Order using * Changes for https://github.com/Azure/azure-event-hubs-dotnet/pull/348#pullrequestreview-176550010 * Remove using * Support partition-empty in runtime metrics (#352) * Adding partition IsEmpty support to runtime metrics * Use AMQP client constants * Move to correct name * is_partition_empty is the correct name * Reduce the number of storage calls in lease manager (#357) * Couple improvements in Azure Lease Manager to reduce numberof storage calls. * N/A as partition id * Go with default timeout * Moving to most recent AMQP release * Fix flaky EPH test * Adding 30 seconds default operation timeout back to tests. * Reducing EPH to storage IO calls. * Couple more fixes * . * Set token for owned leases. * Refresh lease before acquiring in processor host. * Fix metada removal order during lease release. * Update lease token only for already running pumps to avoid resetting receiver position data. * FetchAttributesAsync of blob as part of GetAllLeasesAsync() call. * Refresh lease before attempting to steal * Don't retry if we already lost the lease during receiver open. * Don't attempt to steal if owner has changed from the calculation time to refresh time. * - * Partition pump to close when hit ReceiverDisconnectedException since this is not recoverable. * - * Ignore any failure during releasing the lease * Don't update pump token if token is empty * Nullify the owner on the lease in case this host lost it. * Increment ourLeaseCount when a lease is acquired. * Correcting task list * No need to assign pump lease token to downloaded lease. * comment update * comment update * Clear ownership on partial acquisition. * Clear ownership on partial acquisition. * Make sure we don't leave the lease as owned if acquisition failed. * Adding logs to debug lease corruption bug * Adding logs to debug lease corruption bug * Small fix at steal lease check * Protect subject iterator variable during task creation in for loops. * . * Renew lease right after ChangeLease call * Don't create pump if partition expired or already moved to some other host. * Use refreshed lease while creating partition pump. * Remove temporary debug logs. * Addressing SJ's comments * Remove obsolete * Moving AD SDK 4.5 and Xamarin.iOS10 support (#365) * Making SystemProperties public addressing testability issues. (#332) * Provide batch object's event data enumerator publicly. (#356) * Bumping up the SDK version (#366) * ServiceFabricProcessor preview (#262) This is the code that built and released as preview version 0.5.2 https://www.nuget.org/packages/Microsoft.Azure.EventHubs.ServiceFabricProcessor/0.5.2 At the time it couldn't be merged with dev due to test issues from unrelated work, so we did the release from the SFprocessor branch. Those issues have been resolved, and we expect that future preview releases will come from dev branch. * Client changes to support SFP (#367) Three changes in the client needed to support SFP or SFP testing: 1) A previous PR added the ability to set EventData.SystemProperties, but it is not much use without the ability to create a new SystemPropertiesCollection instance. SFP testing does not need to set individual values on a SystemPropertiesCollection, just create new instances with values that do not change after creation time, so I added a public constructor which sets all the values. 2) SFP was previously creating EventHubClients with connection strings, but there is no string syntax for setting the operation timeout, and the message pump feature on PartitionReceiver uses the operation timeout. The easiest way to let SFP set the operation timeout is to make public the existing EventHubClient.Create call which takes a ConnectionStringBuilder. 3) Originally, SFP was a friend assembly of the client, the same as EPH, but it was decided that that was not the best design. Supporting the EnableReceiverRuntimeMetric option requires the ability to create new ReceiverRuntimeInformation instances (made constructor public) and update the values from a received EventData. Copying the values in SFP code would require making a bunch of properties on EventData public get, and the corresponding properties on ReceiverRuntimeInformation public set. Instead, I added an Update method which takes an EventData and performs the copy within the client assembly so no visibility change is required. Also modified the EPH code to use the new Update method. * delaysign=false --- Microsoft.Azure.EventHubs.sln | 7 + .../AzureBlobLease.cs | 22 +- .../AzureStorageCheckpointLeaseManager.cs | 271 +++++----- .../EventHubPartitionPump.cs | 9 +- .../EventProcessorHost.cs | 75 +-- .../EventProcessorHostActionStrings.cs | 2 + .../ICheckpointManager.cs | 7 - .../ILeaseManager.cs | 2 +- .../LeaseLostException.cs | 13 +- ...Microsoft.Azure.EventHubs.Processor.csproj | 10 +- .../PartitionContext.cs | 34 +- .../PartitionManager.cs | 384 ++++++++++----- .../PartitionPump.cs | 25 +- .../ProcessorEventSource.cs | 1 + .../Checkpoint.cs | 151 ++++++ .../CloseReason.cs | 21 + .../Constants.cs | 24 + .../EventHubMocks.cs | 209 ++++++++ .../EventHubWrappers.cs | 140 ++++++ .../EventProcessorConfigurationException.cs | 21 + .../EventProcessorEventSource.cs | 124 +++++ .../EventProcessorOptions.cs | 92 ++++ .../ICheckpointMananger.cs | 53 ++ .../IEventProcessor.cs | 65 +++ ...re.EventHubs.ServiceFabricProcessor.csproj | 47 ++ .../PartitionContext.cs | 102 ++++ .../ProgrammersGuide.md | 271 ++++++++++ .../ReliableDictionaryCheckpointMananger.cs | 159 ++++++ .../ServiceFabricProcessor.cs | 464 ++++++++++++++++++ .../Amqp/ActiveClientLink.cs | 16 +- .../Amqp/ActiveClientLinkManager.cs | 5 +- .../Amqp/ActiveClientLinkObject.cs | 45 +- .../Amqp/ActiveClientRequestResponseLink.cs | 16 +- .../Amqp/AmqpClientConstants.cs | 1 + .../Amqp/AmqpEventDataSender.cs | 29 +- .../Amqp/AmqpEventHubClient.cs | 132 +++-- .../Amqp/AmqpExceptionHelper.cs | 67 ++- .../Amqp/AmqpMessageConverter.cs | 22 +- .../Amqp/AmqpPartitionReceiver.cs | 59 ++- .../Amqp/AmqpSelectorFilter.cs | 5 +- .../Amqp/AmqpServiceClient.cs | 25 +- .../Amqp/SerializationUtilities.cs | 4 +- .../Core/EventHubsPlugin.cs | 34 ++ src/Microsoft.Azure.EventHubs/EventData.cs | 57 ++- .../EventDataBatch.cs | 31 +- .../EventDataDiagnosticExtensions.cs | 6 +- .../EventDataSender.cs | 55 ++- .../EventHubClient.cs | 141 ++---- .../EventHubPartitionRuntimeInformation.cs | 3 + .../EventHubsDiagnosticSource.cs | 31 +- .../EventHubsEventSource.cs | 31 ++ .../EventPosition.cs | 40 +- .../Microsoft.Azure.EventHubs.csproj | 30 +- .../PartitionReceiver.cs | 36 +- .../PartitionSender.cs | 22 +- .../Primitives/AsyncLock.cs | 5 +- .../AzureActiveDirectoryTokenProvider.cs | 8 +- .../Primitives/ClientEntity.cs | 56 ++- .../Primitives/ClientInfo.cs | 2 + .../EventHubsConnectionStringBuilder.cs | 27 +- .../Primitives/EventHubsTimeoutException.cs | 27 + .../Primitives/ExceptionUtility.cs | 18 +- .../Primitives/Fx.cs | 16 +- .../Primitives/Guard.cs | 24 + .../Primitives/ITokenProvider.cs | 3 - .../Primitives/JsonSecurityToken.cs | 1 - .../ManagedServiceIdentityTokenProvider.cs | 4 +- .../Primitives/RetryPolicy.cs | 58 +-- .../Primitives/SecurityToken.cs | 8 +- .../Primitives/SharedAccessSignatureToken.cs | 18 +- .../SharedAccessSignatureTokenProvider.cs | 13 +- .../Primitives/TaskExtensions.cs | 117 +++++ .../Primitives/Ticks.cs | 2 +- .../Primitives/TimeoutHelper.cs | 86 +--- .../Primitives/TokenProvider.cs | 51 +- .../Properties/AssemblyInfo.cs | 2 +- .../ReceiverOptions.cs | 7 +- .../ReceiverRuntimeInfo.cs | 44 +- .../Resources.Designer.cs | 19 + src/Microsoft.Azure.EventHubs/Resources.resx | 6 + .../Amqp/AmqpMEssageCoverterTests.cs | 6 +- .../Client/ClientTestBase.cs | 4 +- .../Client/DataBatchTests.cs | 2 +- .../Client/NegativeCases.cs | 10 +- .../Client/PluginTests.cs | 122 +++++ .../Client/ReceiverRuntimeMetricsTests.cs | 1 + .../Client/TimeoutTests.cs | 4 +- .../Processor/ProcessorTestBase.cs | 46 +- .../Processor/TestEventProcessor.cs | 23 + .../TestUtility.cs | 3 +- 90 files changed, 3444 insertions(+), 1147 deletions(-) create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Checkpoint.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/CloseReason.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Constants.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventHubMocks.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventHubWrappers.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorConfigurationException.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorEventSource.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorOptions.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ICheckpointMananger.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/IEventProcessor.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Microsoft.Azure.EventHubs.ServiceFabricProcessor.csproj create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/PartitionContext.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ProgrammersGuide.md create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ReliableDictionaryCheckpointMananger.cs create mode 100644 src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ServiceFabricProcessor.cs create mode 100644 src/Microsoft.Azure.EventHubs/Core/EventHubsPlugin.cs create mode 100644 src/Microsoft.Azure.EventHubs/Primitives/EventHubsTimeoutException.cs create mode 100644 src/Microsoft.Azure.EventHubs/Primitives/Guard.cs create mode 100644 src/Microsoft.Azure.EventHubs/Primitives/TaskExtensions.cs create mode 100644 test/Microsoft.Azure.EventHubs.Tests/Client/PluginTests.cs diff --git a/Microsoft.Azure.EventHubs.sln b/Microsoft.Azure.EventHubs.sln index 3edf9f2..3f83517 100644 --- a/Microsoft.Azure.EventHubs.sln +++ b/Microsoft.Azure.EventHubs.sln @@ -18,6 +18,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.EventHubs.T EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.EventHubs.Processor", "src\Microsoft.Azure.EventHubs.Processor\Microsoft.Azure.EventHubs.Processor.csproj", "{8C89967A-4E1F-46B0-8458-81B545C822B2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.EventHubs.ServiceFabricProcessor", "src\Microsoft.Azure.EventHubs.ServiceFabricProcessor\Microsoft.Azure.EventHubs.ServiceFabricProcessor.csproj", "{D96BCC8F-D5EE-464A-9C15-EF59613F9F82}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,6 +38,10 @@ Global {8C89967A-4E1F-46B0-8458-81B545C822B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C89967A-4E1F-46B0-8458-81B545C822B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C89967A-4E1F-46B0-8458-81B545C822B2}.Release|Any CPU.Build.0 = Release|Any CPU + {D96BCC8F-D5EE-464A-9C15-EF59613F9F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D96BCC8F-D5EE-464A-9C15-EF59613F9F82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D96BCC8F-D5EE-464A-9C15-EF59613F9F82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D96BCC8F-D5EE-464A-9C15-EF59613F9F82}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -44,5 +50,6 @@ Global {126D946D-CE0F-4F14-9F13-8FD7098B81D8} = {08E028C1-29E7-42B5-871F-C911DB93E78A} {154F7B4C-B998-4FA0-933F-F34DB0CA9B88} = {AF49C862-CB78-4110-A275-8111B387805D} {8C89967A-4E1F-46B0-8458-81B545C822B2} = {08E028C1-29E7-42B5-871F-C911DB93E78A} + {D96BCC8F-D5EE-464A-9C15-EF59613F9F82} = {08E028C1-29E7-42B5-871F-C911DB93E78A} EndGlobalSection EndGlobal diff --git a/src/Microsoft.Azure.EventHubs.Processor/AzureBlobLease.cs b/src/Microsoft.Azure.EventHubs.Processor/AzureBlobLease.cs index f1c54cc..0bf9090 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/AzureBlobLease.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/AzureBlobLease.cs @@ -9,6 +9,8 @@ namespace Microsoft.Azure.EventHubs.Processor class AzureBlobLease : Lease { + readonly bool isOwned; + // ctor needed for deserialization internal AzureBlobLease() { @@ -17,14 +19,23 @@ internal AzureBlobLease() internal AzureBlobLease(string partitionId, CloudBlockBlob blob) : base(partitionId) { this.Blob = blob; - } + this.isOwned = blob.Properties.LeaseState == LeaseState.Leased; + } + + internal AzureBlobLease(string partitionId, string owner, CloudBlockBlob blob) : base(partitionId) + { + this.Blob = blob; + this.Owner = owner; + this.isOwned = blob.Properties.LeaseState == LeaseState.Leased; + } - internal AzureBlobLease(AzureBlobLease source) + internal AzureBlobLease(AzureBlobLease source) : base(source) { this.Offset = source.Offset; this.SequenceNumber = source.SequenceNumber; this.Blob = source.Blob; + this.isOwned = source.isOwned; } internal AzureBlobLease(AzureBlobLease source, CloudBlockBlob blob) : base(source) @@ -32,17 +43,16 @@ internal AzureBlobLease(AzureBlobLease source, CloudBlockBlob blob) : base(sourc this.Offset = source.Offset; this.SequenceNumber = source.SequenceNumber; this.Blob = blob; + this.isOwned = blob.Properties.LeaseState == LeaseState.Leased; } // do not serialize [JsonIgnore] public CloudBlockBlob Blob { get; } - public override async Task IsExpired() + public override Task IsExpired() { - await this.Blob.FetchAttributesAsync().ConfigureAwait(false); // Get the latest metadata - var currentState = this.Blob.Properties.LeaseState; - return currentState != LeaseState.Leased; + return Task.FromResult(!this.isOwned); } } } \ No newline at end of file diff --git a/src/Microsoft.Azure.EventHubs.Processor/AzureStorageCheckpointLeaseManager.cs b/src/Microsoft.Azure.EventHubs.Processor/AzureStorageCheckpointLeaseManager.cs index 1a31648..1e37ea3 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/AzureStorageCheckpointLeaseManager.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/AzureStorageCheckpointLeaseManager.cs @@ -5,21 +5,24 @@ namespace Microsoft.Azure.EventHubs.Processor { using System; using System.Collections.Generic; - using System.Text.RegularExpressions; + using System.Linq; using System.Threading.Tasks; + using Microsoft.Azure.EventHubs.Primitives; using Newtonsoft.Json; using WindowsAzure.Storage; using WindowsAzure.Storage.Blob; class AzureStorageCheckpointLeaseManager : ICheckpointManager, ILeaseManager { + static string MetaDataOwnerName = "OWNINGHOST"; + EventProcessorHost host; TimeSpan leaseDuration; TimeSpan leaseRenewInterval; static readonly TimeSpan storageMaximumExecutionTime = TimeSpan.FromMinutes(2); readonly CloudStorageAccount cloudStorageAccount; - readonly string leaseContainerName = null; + readonly string leaseContainerName; readonly string storageBlobPrefix; BlobRequestOptions renewRequestOptions; OperationContext operationContext = null; @@ -33,17 +36,17 @@ internal AzureStorageCheckpointLeaseManager(string storageConnectionString, stri internal AzureStorageCheckpointLeaseManager(CloudStorageAccount cloudStorageAccount, string leaseContainerName, string storageBlobPrefix) { - if (cloudStorageAccount == null) + Guard.ArgumentNotNull(nameof(cloudStorageAccount), cloudStorageAccount); + + try { - throw new ArgumentNullException(nameof(cloudStorageAccount)); + NameValidator.ValidateContainerName(leaseContainerName); } - - // Validate lease container name. - if (!Regex.IsMatch(leaseContainerName, @"^[a-z0-9](([a-z0-9\-[^\-])){1,61}[a-z0-9]$")) + catch (ArgumentException) { throw new ArgumentException( "Azure Storage lease container name is invalid. Please check naming conventions at https://msdn.microsoft.com/en-us/library/azure/dd135715.aspx", - nameof(leaseContainerName)); + nameof(leaseContainerName)); } this.cloudStorageAccount = cloudStorageAccount; @@ -77,7 +80,7 @@ internal void Initialize(EventProcessorHost host) // Proxy enabled? if (this.host.EventProcessorOptions != null && this.host.EventProcessorOptions.WebProxy != null) { - this.operationContext = new OperationContext() + this.operationContext = new OperationContext { Proxy = this.host.EventProcessorOptions.WebProxy }; @@ -87,7 +90,7 @@ internal void Initialize(EventProcessorHost host) // Create storage client and configure max execution time. // Max execution time will apply to any storage calls except renew. var storageClient = this.cloudStorageAccount.CreateCloudBlobClient(); - storageClient.DefaultRequestOptions = new BlobRequestOptions() + storageClient.DefaultRequestOptions = new BlobRequestOptions { MaximumExecutionTime = AzureStorageCheckpointLeaseManager.storageMaximumExecutionTime }; @@ -107,12 +110,14 @@ public Task CheckpointStoreExistsAsync() public Task CreateCheckpointStoreIfNotExistsAsync() { - return CreateLeaseStoreIfNotExistsAsync(); + // Because we control the caller, we know that this method will only be called after createLeaseStoreIfNotExists. + // In this implementation, it's the same store, so the store will always exist if execution reaches here. + return Task.FromResult(true); } public async Task GetCheckpointAsync(string partitionId) { - AzureBlobLease lease = (AzureBlobLease)await GetLeaseAsync(partitionId).ConfigureAwait(false); + AzureBlobLease lease = (AzureBlobLease)await GetLeaseAsync(partitionId).ConfigureAwait(false); Checkpoint checkpoint = null; if (lease != null && !string.IsNullOrEmpty(lease.Offset)) { @@ -123,29 +128,22 @@ public async Task GetCheckpointAsync(string partitionId) }; } - return checkpoint; - } - - [Obsolete("Use UpdateCheckpointAsync(Lease lease, Checkpoint checkpoint) instead", true)] - public Task UpdateCheckpointAsync(Checkpoint checkpoint) - { - throw new NotImplementedException(); + return checkpoint; } - public async Task CreateCheckpointIfNotExistsAsync(string partitionId) + public Task CreateCheckpointIfNotExistsAsync(string partitionId) { - // Normally the lease will already be created, checkpoint store is initialized after lease store. - AzureBlobLease lease = (AzureBlobLease)await CreateLeaseIfNotExistsAsync(partitionId).ConfigureAwait(false); - Checkpoint checkpoint = new Checkpoint(partitionId, lease.Offset, lease.SequenceNumber); - - return checkpoint; + // Normally the lease will already be created, checkpoint store is initialized after lease store. + return Task.FromResult(null); } public async Task UpdateCheckpointAsync(Lease lease, Checkpoint checkpoint) { - AzureBlobLease newLease = new AzureBlobLease((AzureBlobLease)lease); - newLease.Offset = checkpoint.Offset; - newLease.SequenceNumber = checkpoint.SequenceNumber; + AzureBlobLease newLease = new AzureBlobLease((AzureBlobLease) lease) + { + Offset = checkpoint.Offset, + SequenceNumber = checkpoint.SequenceNumber + }; await this.UpdateLeaseAsync(newLease).ConfigureAwait(false); } @@ -158,21 +156,9 @@ public Task DeleteCheckpointAsync(string partitionId) // // Lease operations. // - public TimeSpan LeaseRenewInterval - { - get - { - return this.leaseRenewInterval; - } - } + public TimeSpan LeaseRenewInterval => this.leaseRenewInterval; - public TimeSpan LeaseDuration - { - get - { - return this.leaseDuration; - } - } + public TimeSpan LeaseDuration => this.leaseDuration; public Task LeaseStoreExistsAsync() { @@ -217,7 +203,7 @@ public async Task DeleteLeaseStoreAsync() } while (innerContinuationToken != null); } - else if (blob is CloudBlockBlob) + else if (blob is CloudBlockBlob) { try { @@ -238,35 +224,52 @@ public async Task DeleteLeaseStoreAsync() public async Task GetLeaseAsync(string partitionId) // throws URISyntaxException, IOException, StorageException { - AzureBlobLease retval = null; - CloudBlockBlob leaseBlob = GetBlockBlobReference(partitionId); - if (await leaseBlob.ExistsAsync(null, this.operationContext).ConfigureAwait(false)) - { - retval = await DownloadLeaseAsync(partitionId, leaseBlob).ConfigureAwait(false); - } + await leaseBlob.FetchAttributesAsync().ConfigureAwait(false); - return retval; + return await DownloadLeaseAsync(partitionId, leaseBlob).ConfigureAwait(false); } - public IEnumerable> GetAllLeases() + public async Task> GetAllLeasesAsync() { - List> leaseFutures = new List>(); - IEnumerable partitionIds = this.host.PartitionManager.GetPartitionIdsAsync().Result; - foreach (string id in partitionIds) + var leaseList = new List(); + BlobContinuationToken continuationToken = null; + + do { - leaseFutures.Add(GetLeaseAsync(id)); - } + var leaseBlobsResult = await this.consumerGroupDirectory.ListBlobsSegmentedAsync( + true, + BlobListingDetails.Metadata, + null, + continuationToken, + null, + this.operationContext); + + foreach (CloudBlockBlob leaseBlob in leaseBlobsResult.Results) + { + // Try getting owner name from existing blob. + // This might return null when run on the existing lease after SDK upgrade. + leaseBlob.Metadata.TryGetValue(MetaDataOwnerName, out var owner); + + // Discover partition id from URI path of the blob. + var partitionId = leaseBlob.Uri.AbsolutePath.Split('/').Last(); - return leaseFutures; + leaseList.Add(new AzureBlobLease(partitionId, owner, leaseBlob)); + } + + continuationToken = leaseBlobsResult.ContinuationToken; + + } while (continuationToken != null); + + return leaseList; } public async Task CreateLeaseIfNotExistsAsync(string partitionId) // throws URISyntaxException, IOException, StorageException { - AzureBlobLease returnLease; - try - { + AzureBlobLease returnLease; + try + { CloudBlockBlob leaseBlob = GetBlockBlobReference(partitionId); returnLease = new AzureBlobLease(partitionId, leaseBlob); string jsonLease = JsonConvert.SerializeObject(returnLease); @@ -274,37 +277,37 @@ public async Task CreateLeaseIfNotExistsAsync(string partitionId) // thro ProcessorEventSource.Log.AzureStorageManagerInfo( this.host.HostName, partitionId, - "CreateLeaseIfNotExist - leaseContainerName: " + this.leaseContainerName + + "CreateLeaseIfNotExist - leaseContainerName: " + this.leaseContainerName + " consumerGroupName: " + this.host.ConsumerGroupName + " storageBlobPrefix: " + this.storageBlobPrefix); await leaseBlob.UploadTextAsync( - jsonLease, - null, - AccessCondition.GenerateIfNoneMatchCondition("*"), - null, + jsonLease, + null, + AccessCondition.GenerateIfNoneMatchCondition("*"), + null, this.operationContext).ConfigureAwait(false); } - catch (StorageException se) - { - if (se.RequestInformation.ErrorCode == BlobErrorCodeStrings.BlobAlreadyExists || + catch (StorageException se) + { + if (se.RequestInformation.ErrorCode == BlobErrorCodeStrings.BlobAlreadyExists || se.RequestInformation.ErrorCode == BlobErrorCodeStrings.LeaseIdMissing) // occurs when somebody else already has leased the blob - { + { // The blob already exists. ProcessorEventSource.Log.AzureStorageManagerInfo(this.host.HostName, partitionId, "Lease already exists"); returnLease = (AzureBlobLease)await GetLeaseAsync(partitionId).ConfigureAwait(false); } - else - { + else + { ProcessorEventSource.Log.AzureStorageManagerError( this.host.HostName, partitionId, "CreateLeaseIfNotExist StorageException - leaseContainerName: " + this.leaseContainerName + " consumerGroupName: " + this.host.ConsumerGroupName + " storageBlobPrefix: " + this.storageBlobPrefix, se.ToString()); - throw; + throw; } - } - - return returnLease; + } + + return returnLease; } public Task DeleteLeaseAsync(Lease lease) @@ -325,8 +328,9 @@ async Task AcquireLeaseCoreAsync(AzureBlobLease lease) bool retval = true; string newLeaseId = Guid.NewGuid().ToString(); string partitionId = lease.PartitionId; - try + try { + bool renewLease = false; string newToken; await leaseBlob.FetchAttributesAsync(null, null, this.operationContext).ConfigureAwait(false); if (leaseBlob.Properties.LeaseState == LeaseState.Leased) @@ -344,45 +348,55 @@ async Task AcquireLeaseCoreAsync(AzureBlobLease lease) } ProcessorEventSource.Log.AzureStorageManagerInfo(this.host.HostName, lease.PartitionId, "Need to ChangeLease"); + renewLease = true; newToken = await leaseBlob.ChangeLeaseAsync( - newLeaseId, - AccessCondition.GenerateLeaseCondition(lease.Token), - null, + newLeaseId, + AccessCondition.GenerateLeaseCondition(lease.Token), + null, this.operationContext).ConfigureAwait(false); } else { ProcessorEventSource.Log.AzureStorageManagerInfo(this.host.HostName, lease.PartitionId, "Need to AcquireLease"); - - try - { - newToken = await leaseBlob.AcquireLeaseAsync(leaseDuration, newLeaseId, null, null, this.operationContext).ConfigureAwait(false); - } - catch (StorageException se) - when (se.RequestInformation != null - && se.RequestInformation.ErrorCode.Equals(BlobErrorCodeStrings.LeaseAlreadyPresent, StringComparison.OrdinalIgnoreCase)) - { - // Either some other host grabbed the lease or checkpoint call renewed it. - return false; - } + newToken = await leaseBlob.AcquireLeaseAsync( + leaseDuration, + newLeaseId, + null, + null, + this.operationContext).ConfigureAwait(false); } lease.Token = newToken; lease.Owner = this.host.HostName; lease.IncrementEpoch(); // Increment epoch each time lease is acquired or stolen by a new host + + // Renew lease here if needed? + // ChangeLease doesn't renew so we should avoid lease expiring before next renew interval. + if (renewLease) + { + await this.RenewLeaseCoreAsync(lease).ConfigureAwait(false); + } + await leaseBlob.UploadTextAsync( JsonConvert.SerializeObject(lease), null, AccessCondition.GenerateLeaseCondition(lease.Token), null, this.operationContext).ConfigureAwait(false); + + // Update owner in the metadata. + lease.Blob.Metadata[MetaDataOwnerName] = lease.Owner; + await lease.Blob.SetMetadataAsync( + AccessCondition.GenerateLeaseCondition(lease.Token), + null, + this.operationContext).ConfigureAwait(false); } - catch (StorageException se) + catch (StorageException se) { throw HandleStorageException(partitionId, se); } - - return retval; + + return retval; } public Task RenewLeaseAsync(Lease lease) @@ -395,19 +409,19 @@ async Task RenewLeaseCoreAsync(AzureBlobLease lease) CloudBlockBlob leaseBlob = lease.Blob; string partitionId = lease.PartitionId; - try + try { await leaseBlob.RenewLeaseAsync( AccessCondition.GenerateLeaseCondition(lease.Token), this.renewRequestOptions, this.operationContext).ConfigureAwait(false); } - catch (StorageException se) + catch (StorageException se) { throw HandleStorageException(partitionId, se); } - - return true; + + return true; } public Task ReleaseLeaseAsync(Lease lease) @@ -422,7 +436,7 @@ async Task ReleaseLeaseCoreAsync(AzureBlobLease lease) CloudBlockBlob leaseBlob = lease.Blob; string partitionId = lease.PartitionId; - try + try { string leaseId = lease.Token; AzureBlobLease releasedCopy = new AzureBlobLease(lease) @@ -430,20 +444,28 @@ async Task ReleaseLeaseCoreAsync(AzureBlobLease lease) Token = string.Empty, Owner = string.Empty }; + + // Remove owner in the metadata. + leaseBlob.Metadata.Remove(MetaDataOwnerName); + await leaseBlob.SetMetadataAsync( + AccessCondition.GenerateLeaseCondition(leaseId), + null, + this.operationContext); + await leaseBlob.UploadTextAsync( - JsonConvert.SerializeObject(releasedCopy), - null, - AccessCondition.GenerateLeaseCondition(leaseId), + JsonConvert.SerializeObject(releasedCopy), + null, + AccessCondition.GenerateLeaseCondition(leaseId), null, this.operationContext).ConfigureAwait(false); await leaseBlob.ReleaseLeaseAsync(AccessCondition.GenerateLeaseCondition(leaseId)).ConfigureAwait(false); } - catch (StorageException se) + catch (StorageException se) { throw HandleStorageException(partitionId, se); } - - return true; + + return true; } public Task UpdateLeaseAsync(Lease lease) @@ -453,7 +475,7 @@ public Task UpdateLeaseAsync(Lease lease) async Task UpdateLeaseCoreAsync(AzureBlobLease lease) { - if (lease == null) + if (lease == null) { return false; } @@ -462,7 +484,7 @@ async Task UpdateLeaseCoreAsync(AzureBlobLease lease) ProcessorEventSource.Log.AzureStorageManagerInfo(this.host.HostName, partitionId, "Updating lease"); string token = lease.Token; - if (string.IsNullOrEmpty(token)) + if (string.IsNullOrEmpty(token)) { return false; } @@ -471,33 +493,33 @@ async Task UpdateLeaseCoreAsync(AzureBlobLease lease) await this.RenewLeaseAsync(lease).ConfigureAwait(false); CloudBlockBlob leaseBlob = lease.Blob; - try + try { string jsonToUpload = JsonConvert.SerializeObject(lease); ProcessorEventSource.Log.AzureStorageManagerInfo(this.host.HostName, lease.PartitionId, $"Raw JSON uploading: {jsonToUpload}"); await leaseBlob.UploadTextAsync( - jsonToUpload, - null, - AccessCondition.GenerateLeaseCondition(token), + jsonToUpload, + null, + AccessCondition.GenerateLeaseCondition(token), null, this.operationContext).ConfigureAwait(false); } - catch (StorageException se) - { + catch (StorageException se) + { throw HandleStorageException(partitionId, se); - } - - return true; + } + + return true; } - async Task DownloadLeaseAsync(string partitionId, CloudBlockBlob blob) // throws StorageException, IOException + async Task DownloadLeaseAsync(string partitionId, CloudBlockBlob blob) // throws StorageException, IOException { string jsonLease = await blob.DownloadTextAsync().ConfigureAwait(false); ProcessorEventSource.Log.AzureStorageManagerInfo(this.host.HostName, partitionId, "Raw JSON downloaded: " + jsonLease); AzureBlobLease rehydrated = (AzureBlobLease)JsonConvert.DeserializeObject(jsonLease, typeof(AzureBlobLease)); - AzureBlobLease blobLease = new AzureBlobLease(rehydrated, blob); - return blobLease; + AzureBlobLease blobLease = new AzureBlobLease(rehydrated, blob); + return blobLease; } Exception HandleStorageException(string partitionId, StorageException se) @@ -525,16 +547,7 @@ Exception HandleStorageException(string partitionId, StorageException se) CloudBlockBlob GetBlockBlobReference(string partitionId) { - CloudBlockBlob leaseBlob = this.consumerGroupDirectory.GetBlockBlobReference(partitionId); - - // Fixed, keeping workaround commented until full validation. - // GetBlockBlobReference creates a new ServiceClient thus resets options. - // Because of this we lose settings like MaximumExecutionTime on the client. - // Until storage addresses the issue we need to override it here once more. - // Tracking bug: https://github.com/Azure/azure-storage-net/issues/398 - // leaseBlob.ServiceClient.DefaultRequestOptions = this.storageClient.DefaultRequestOptions; - - return leaseBlob; + return this.consumerGroupDirectory.GetBlockBlobReference(partitionId); } } } \ No newline at end of file diff --git a/src/Microsoft.Azure.EventHubs.Processor/EventHubPartitionPump.cs b/src/Microsoft.Azure.EventHubs.Processor/EventHubPartitionPump.cs index 9436faf..fec94d5 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/EventHubPartitionPump.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/EventHubPartitionPump.cs @@ -35,6 +35,13 @@ protected override async Task OnOpenAsync() lastException = e; ProcessorEventSource.Log.PartitionPumpWarning( this.Host.HostName, this.PartitionContext.PartitionId, "Failure creating client or receiver, retrying", e.ToString()); + + // Don't retry if we already lost the lease. + if (e is ReceiverDisconnectedException) + { + break; + } + retryCount++; } } @@ -93,7 +100,7 @@ async Task OpenClientsAsync() // throws EventHubsException, IOException, Interru receiverOptions); this.partitionReceiver.PrefetchCount = this.Host.EventProcessorOptions.PrefetchCount; - + ProcessorEventSource.Log.PartitionPumpCreateClientsStop(this.Host.HostName, this.PartitionContext.PartitionId); } diff --git a/src/Microsoft.Azure.EventHubs.Processor/EventProcessorHost.cs b/src/Microsoft.Azure.EventHubs.Processor/EventProcessorHost.cs index 01820fc..e6e1efb 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/EventProcessorHost.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/EventProcessorHost.cs @@ -5,6 +5,7 @@ namespace Microsoft.Azure.EventHubs.Processor { using System; using System.Threading.Tasks; + using Microsoft.Azure.EventHubs.Primitives; using Microsoft.WindowsAzure.Storage; /// @@ -13,7 +14,7 @@ namespace Microsoft.Azure.EventHubs.Processor public sealed class EventProcessorHost { // A processor host will work on either the token provider or the connection string. - ITokenProvider tokenProvider; + readonly ITokenProvider tokenProvider; string eventHubConnectionString; /// @@ -99,14 +100,9 @@ public EventProcessorHost( ICheckpointManager checkpointManager, ILeaseManager leaseManager) { - if (string.IsNullOrEmpty(consumerGroupName)) - { - throw new ArgumentNullException(nameof(consumerGroupName)); - } - else if (checkpointManager == null || leaseManager == null) - { - throw new ArgumentNullException(checkpointManager == null ? nameof(checkpointManager) : nameof(leaseManager)); - } + Guard.ArgumentNotNullOrWhiteSpace(nameof(consumerGroupName), consumerGroupName); + Guard.ArgumentNotNull(nameof(checkpointManager), checkpointManager); + Guard.ArgumentNotNull(nameof(leaseManager), leaseManager); var csb = new EventHubsConnectionStringBuilder(eventHubConnectionString); if (string.IsNullOrEmpty(eventHubPath)) @@ -195,40 +191,13 @@ public EventProcessorHost( TimeSpan? operationTimeout = null, TransportType transportType = TransportType.Amqp) { - if (string.IsNullOrWhiteSpace(hostName)) - { - throw new ArgumentNullException(nameof(hostName)); - } - - if (endpointAddress == null) - { - throw new ArgumentNullException(nameof(endpointAddress)); - } - - if (string.IsNullOrWhiteSpace(eventHubPath)) - { - throw new ArgumentNullException(nameof(eventHubPath)); - } - - if (string.IsNullOrWhiteSpace(consumerGroupName)) - { - throw new ArgumentNullException(nameof(consumerGroupName)); - } - - if (tokenProvider == null) - { - throw new ArgumentNullException(nameof(tokenProvider)); - } - - if (cloudStorageAccount == null) - { - throw new ArgumentNullException(nameof(cloudStorageAccount)); - } - - if (string.IsNullOrWhiteSpace(leaseContainerName)) - { - throw new ArgumentNullException(nameof(leaseContainerName)); - } + Guard.ArgumentNotNullOrWhiteSpace(nameof(hostName), hostName); + Guard.ArgumentNotNull(nameof(endpointAddress), endpointAddress); + Guard.ArgumentNotNullOrWhiteSpace(nameof(eventHubPath), eventHubPath); + Guard.ArgumentNotNullOrWhiteSpace(nameof(consumerGroupName), consumerGroupName); + Guard.ArgumentNotNull(nameof(tokenProvider), tokenProvider); + Guard.ArgumentNotNull(nameof(cloudStorageAccount), cloudStorageAccount); + Guard.ArgumentNotNullOrWhiteSpace(nameof(leaseContainerName), leaseContainerName); this.HostName = hostName; this.EndpointAddress = endpointAddress; @@ -298,7 +267,7 @@ public EventProcessorHost( /// object. /// The instance. public PartitionManagerOptions PartitionManagerOptions { get; set; } - + // All of these accessors are for internal use only. internal ICheckpointManager CheckpointManager { get; } @@ -360,10 +329,8 @@ public Task RegisterEventProcessorFactoryAsync(IEventProcessorFactory factory) /// A task to indicate EventProcessorHost instance is started. public async Task RegisterEventProcessorFactoryAsync(IEventProcessorFactory factory, EventProcessorOptions processorOptions) { - if (factory == null || processorOptions == null) - { - throw new ArgumentNullException(factory == null ? nameof(factory) : nameof(processorOptions)); - } + Guard.ArgumentNotNull(nameof(factory), factory); + Guard.ArgumentNotNull(nameof(processorOptions), processorOptions); // Initialize partition manager options with default values if not already set by the client. if (this.PartitionManagerOptions == null) @@ -415,7 +382,7 @@ public async Task RegisterEventProcessorFactoryAsync(IEventProcessorFactory fact /// public async Task UnregisterEventProcessorAsync() // throws InterruptedException, ExecutionException { - ProcessorEventSource.Log.EventProcessorHostCloseStart(this.HostName); + ProcessorEventSource.Log.EventProcessorHostCloseStart(this.HostName); try { await this.PartitionManager.StopAsync().ConfigureAwait(false); @@ -449,7 +416,7 @@ static string CreateHostName(string prefix) prefix = "host"; } - return prefix + "-" + Guid.NewGuid().ToString(); + return prefix + "-" + Guid.NewGuid(); } internal EventHubClient CreateEventHubClient() @@ -462,10 +429,10 @@ internal EventHubClient CreateEventHubClient() else { return EventHubClient.Create( - this.EndpointAddress, - this.EventHubPath, - this.tokenProvider, - this.OperationTimeout, + this.EndpointAddress, + this.EventHubPath, + this.tokenProvider, + this.OperationTimeout, this.TransportType); } } diff --git a/src/Microsoft.Azure.EventHubs.Processor/EventProcessorHostActionStrings.cs b/src/Microsoft.Azure.EventHubs.Processor/EventProcessorHostActionStrings.cs index a070254..d974996 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/EventProcessorHostActionStrings.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/EventProcessorHostActionStrings.cs @@ -5,8 +5,10 @@ namespace Microsoft.Azure.EventHubs.Processor { internal static class EventProcessorHostActionStrings { + internal static readonly string DownloadingLeases = "Downloading Leases"; internal static readonly string CheckingLeases = "Checking Leases"; internal static readonly string RenewingLease = "Renewing Lease"; + internal static readonly string ReleasingLease = "Releasing Lease"; internal static readonly string StealingLease = "Stealing Lease"; internal static readonly string CreatingLease = "Creating Lease"; internal static readonly string ClosingEventProcessor = "Closing Event Processor"; diff --git a/src/Microsoft.Azure.EventHubs.Processor/ICheckpointManager.cs b/src/Microsoft.Azure.EventHubs.Processor/ICheckpointManager.cs index b606950..d2372f4 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/ICheckpointManager.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/ICheckpointManager.cs @@ -38,13 +38,6 @@ public interface ICheckpointManager /// Checkpoint info for the given partition, or null if none has been previously stored. Task GetCheckpointAsync(string partitionId); - /// - /// Update the checkpoint in the store with the offset/sequenceNumber in the provided checkpoint. - /// - /// offset/sequeceNumber to update the store with. - [System.Obsolete("Use UpdateCheckpointAsync(Lease lease, Checkpoint checkpoint) instead", true)] - Task UpdateCheckpointAsync(Checkpoint checkpoint); - /// /// Create the checkpoint for the given partition if it doesn't exist. Do nothing if it does exist. /// The offset/sequenceNumber for a freshly-created checkpoint should be set to StartOfStream/0. diff --git a/src/Microsoft.Azure.EventHubs.Processor/ILeaseManager.cs b/src/Microsoft.Azure.EventHubs.Processor/ILeaseManager.cs index da98948..a6a207e 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/ILeaseManager.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/ILeaseManager.cs @@ -65,7 +65,7 @@ public interface ILeaseManager /// A typical implementation could just call GetLeaseAsync() on all partitions. /// /// list of lease info. - IEnumerable> GetAllLeases(); + Task> GetAllLeasesAsync(); /// /// Create in the store the lease info for the given partition, if it does not exist. Do nothing if it does exist diff --git a/src/Microsoft.Azure.EventHubs.Processor/LeaseLostException.cs b/src/Microsoft.Azure.EventHubs.Processor/LeaseLostException.cs index fbfc0a3..31e5317 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/LeaseLostException.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/LeaseLostException.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.Azure.EventHubs.Primitives; + namespace Microsoft.Azure.EventHubs.Processor { using System; @@ -15,20 +17,13 @@ public class LeaseLostException : Exception internal LeaseLostException(string partitionId, Exception innerException) : base(innerException.Message, innerException) { - if (partitionId == null) - { - throw new ArgumentNullException(nameof(partitionId)); - } - + Guard.ArgumentNotNullOrWhiteSpace(nameof(partitionId), partitionId); this.partitionId = partitionId; } /// /// Gets the partition ID where the exception occured. /// - public string PartitionId - { - get { return this.partitionId; } - } + public string PartitionId => this.partitionId; } } \ No newline at end of file diff --git a/src/Microsoft.Azure.EventHubs.Processor/Microsoft.Azure.EventHubs.Processor.csproj b/src/Microsoft.Azure.EventHubs.Processor/Microsoft.Azure.EventHubs.Processor.csproj index 093c054..78fb12e 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/Microsoft.Azure.EventHubs.Processor.csproj +++ b/src/Microsoft.Azure.EventHubs.Processor/Microsoft.Azure.EventHubs.Processor.csproj @@ -3,7 +3,7 @@ This is the next generation Azure Event Hubs .NET Standard Event Processor Host library. For more information about Event Hubs, see https://azure.microsoft.com/en-us/services/event-hubs/ Microsoft.Azure.EventHubs.Processor - 2.2.1 + 3.0.0 Microsoft net461;netstandard2.0;uap10.0; true @@ -23,9 +23,9 @@ false full bin\$(Configuration)\$(TargetFramework)\Microsoft.Azure.EventHubs.Processor.xml - 2.2.1 - 2.2.1.0 - 2.2.1.0 + 3.0.0 + 3.0.0.0 + 3.0.0.0 false © Microsoft Corporation. All rights reserved. @@ -45,7 +45,7 @@ - + diff --git a/src/Microsoft.Azure.EventHubs.Processor/PartitionContext.cs b/src/Microsoft.Azure.EventHubs.Processor/PartitionContext.cs index ae69115..25c8a64 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/PartitionContext.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/PartitionContext.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.EventHubs.Processor using System; using System.Threading; using System.Threading.Tasks; + using Microsoft.Azure.EventHubs.Primitives; /// /// Encapsulates information related to an Event Hubs partition used by . @@ -50,23 +51,13 @@ internal PartitionContext(EventProcessorHost host, string partitionId, string ev /// /// Gets the host owner for the partition. /// - public string Owner - { - get - { - return this.Lease.Owner; - } - } + public string Owner => this.Lease.Owner; /// /// Gets the approximate receiver runtime information for a logical partition of an Event Hub. /// To enable the setting, refer to /// - public ReceiverRuntimeInformation RuntimeInformation - { - get; - private set; - } + public ReceiverRuntimeInformation RuntimeInformation { get; } internal string Offset { get; set; } @@ -82,10 +73,7 @@ public ReceiverRuntimeInformation RuntimeInformation internal void SetOffsetAndSequenceNumber(EventData eventData) { - if (eventData == null) - { - throw new ArgumentNullException(nameof(eventData)); - } + Guard.ArgumentNotNull(nameof(eventData), eventData); lock (this.ThisLock) { @@ -104,13 +92,18 @@ internal async Task GetInitialOffsetAsync() // throws Interrupted // No checkpoint was ever stored. Use the initialOffsetProvider instead. ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, this.PartitionId, "Calling user-provided initial offset provider"); eventPosition = this.host.EventProcessorOptions.InitialOffsetProvider(this.PartitionId); - ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, this.PartitionId, $"Initial Position Provider. Offset:{eventPosition.Offset}, SequenceNumber:{eventPosition.SequenceNumber}, DateTime:{eventPosition.EnqueuedTimeUtc}"); + ProcessorEventSource.Log.PartitionPumpInfo( + this.host.HostName, + this.PartitionId, + $"Initial Position Provider. Offset:{eventPosition.Offset}, SequenceNumber:{eventPosition.SequenceNumber}, DateTime:{eventPosition.EnqueuedTimeUtc}"); } else { this.Offset = startingCheckpoint.Offset; this.SequenceNumber = startingCheckpoint.SequenceNumber; - ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, this.PartitionId, $"Retrieved starting offset/sequenceNumber: {this.Offset}/{this.SequenceNumber}"); + ProcessorEventSource.Log.PartitionPumpInfo( + this.host.HostName, + this.PartitionId, $"Retrieved starting offset/sequenceNumber: {this.Offset}/{this.SequenceNumber}"); eventPosition = EventPosition.FromOffset(this.Offset); } @@ -144,10 +137,7 @@ public Task CheckpointAsync() /// If the sequenceNumber is less than the last checkpointed value public Task CheckpointAsync(EventData eventData) { - if (eventData == null) - { - throw new ArgumentNullException("eventData"); - } + Guard.ArgumentNotNull(nameof(eventData), eventData); // We have never seen this sequence number yet if (eventData.SystemProperties.SequenceNumber > this.SequenceNumber) diff --git a/src/Microsoft.Azure.EventHubs.Processor/PartitionManager.cs b/src/Microsoft.Azure.EventHubs.Processor/PartitionManager.cs index ed149e6..dbdf80b 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/PartitionManager.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/PartitionManager.cs @@ -10,13 +10,15 @@ namespace Microsoft.Azure.EventHubs.Processor using System.Linq; using System.Threading; using System.Threading.Tasks; + using Microsoft.Azure.EventHubs.Primitives; class PartitionManager { readonly EventProcessorHost host; - readonly CancellationTokenSource cancellationTokenSource; readonly ConcurrentDictionary partitionPumps; + IList partitionIds; + CancellationTokenSource cancellationTokenSource; Task runTask; internal PartitionManager(EventProcessorHost host) @@ -39,7 +41,7 @@ public async Task> GetPartitionIdsAsync() this.partitionIds = runtimeInfo.PartitionIds.ToList(); } catch (Exception e) - { + { throw new EventProcessorConfigurationException("Encountered error while fetching the list of EventHub PartitionIds", e); } finally @@ -76,6 +78,10 @@ public async Task StopAsync() { await localRunTask.ConfigureAwait(false); } + + // once it is closed let's reset the task + this.runTask = null; + this.cancellationTokenSource = new CancellationTokenSource(); } async Task RunAsync() @@ -86,6 +92,7 @@ async Task RunAsync() } catch (Exception e) { + // Ideally RunLoop should never throw. ProcessorEventSource.Log.EventProcessorHostError(this.host.HostName, "Exception from partition manager main loop, shutting down", e.ToString()); this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, "N/A", e, EventProcessorHostActionStrings.PartitionManagerMainLoop); } @@ -97,7 +104,7 @@ async Task RunAsync() await this.RemoveAllPumpsAsync(CloseReason.Shutdown).ConfigureAwait(false); } catch (Exception e) - { + { ProcessorEventSource.Log.EventProcessorHostError(this.host.HostName, "Failure during shutdown", e.ToString()); this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, "N/A", e, EventProcessorHostActionStrings.PartitionManagerCleanup); } @@ -110,16 +117,16 @@ async Task InitializeStoresAsync() //throws InterruptedException, ExecutionExcep if (!await leaseManager.LeaseStoreExistsAsync().ConfigureAwait(false)) { await RetryAsync(() => leaseManager.CreateLeaseStoreIfNotExistsAsync(), null, "Failure creating lease store for this Event Hub, retrying", - "Out of retries creating lease store for this Event Hub", EventProcessorHostActionStrings.CreatingLeaseStore, 5).ConfigureAwait(false); + "Out of retries creating lease store for this Event Hub", EventProcessorHostActionStrings.CreatingLeaseStore, 5).ConfigureAwait(false); } // else // lease store already exists, no work needed - + // Now make sure the leases exist foreach (string id in await this.GetPartitionIdsAsync().ConfigureAwait(false)) { - await RetryAsync(() => leaseManager.CreateLeaseIfNotExistsAsync(id), id, "Failure creating lease for partition, retrying", - "Out of retries creating lease for partition", EventProcessorHostActionStrings.CreatingLease, 5).ConfigureAwait(false); + await RetryAsync(() => leaseManager.CreateLeaseIfNotExistsAsync(id), id, $"Failure creating lease for partition {id}, retrying", + $"Out of retries creating lease for partition {id}", EventProcessorHostActionStrings.CreatingLease, 5).ConfigureAwait(false); } // Make sure the checkpoint store exists @@ -127,26 +134,26 @@ async Task InitializeStoresAsync() //throws InterruptedException, ExecutionExcep if (!await checkpointManager.CheckpointStoreExistsAsync().ConfigureAwait(false)) { await RetryAsync(() => checkpointManager.CreateCheckpointStoreIfNotExistsAsync(), null, "Failure creating checkpoint store for this Event Hub, retrying", - "Out of retries creating checkpoint store for this Event Hub", EventProcessorHostActionStrings.CreatingCheckpointStore, 5).ConfigureAwait(false); + "Out of retries creating checkpoint store for this Event Hub", EventProcessorHostActionStrings.CreatingCheckpointStore, 5).ConfigureAwait(false); } // else // checkpoint store already exists, no work needed - + // Now make sure the checkpoints exist foreach (string id in await this.GetPartitionIdsAsync().ConfigureAwait(false)) { - await RetryAsync(() => checkpointManager.CreateCheckpointIfNotExistsAsync(id), id, "Failure creating checkpoint for partition, retrying", - "Out of retries creating checkpoint blob for partition", EventProcessorHostActionStrings.CreatingCheckpoint, 5).ConfigureAwait(false); + await RetryAsync(() => checkpointManager.CreateCheckpointIfNotExistsAsync(id), id, $"Failure creating checkpoint for partition {id}, retrying", + $"Out of retries creating checkpoint for partition {id}", EventProcessorHostActionStrings.CreatingCheckpoint, 5).ConfigureAwait(false); } } - + // Throws if it runs out of retries. If it returns, action succeeded. async Task RetryAsync(Func lambda, string partitionId, string retryMessage, string finalFailureMessage, string action, int maxRetries) // throws ExceptionWithAction { Exception finalException = null; bool createdOK = false; - int retryCount = 0; - do + int retryCount = 0; + do { try { @@ -195,138 +202,232 @@ async Task RunLoopAsync(CancellationToken cancellationToken) // throws Exception loopStopwatch.Restart(); ILeaseManager leaseManager = this.host.LeaseManager; - Dictionary allLeases = new Dictionary(); + var allLeases = new ConcurrentDictionary(); + var leasesOwnedByOthers = new ConcurrentDictionary(); // Inspect all leases. // Acquire any expired leases. // Renew any leases that currently belong to us. - IEnumerable> gettingAllLeases = leaseManager.GetAllLeases(); - List leasesOwnedByOthers = new List(); + IEnumerable downloadedLeases; var renewLeaseTasks = new List(); int ourLeaseCount = 0; - // First thing is first, renew owned leases. - foreach (Task getLeaseTask in gettingAllLeases) - { + try + { try - { - var lease = await getLeaseTask.ConfigureAwait(false); - allLeases[lease.PartitionId] = lease; - if (lease.Owner == this.host.HostName) + { + downloadedLeases = await leaseManager.GetAllLeasesAsync(); + } + catch (Exception e) + { + ProcessorEventSource.Log.EventProcessorHostError(this.host.HostName, "Exception during downloading leases", e.Message); + this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, "N/A", e, EventProcessorHostActionStrings.DownloadingLeases); + + // Avoid tight spin if getallleases call keeps failing. + await Task.Delay(1000); + + continue; + } + + // First things first, renew owned leases. + foreach (var lease in downloadedLeases) + { + var subjectLease = lease; + + try { - ourLeaseCount++; - ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, lease.PartitionId, "Trying to renew lease."); - renewLeaseTasks.Add(leaseManager.RenewLeaseAsync(lease).ContinueWith(renewResult => + allLeases[subjectLease.PartitionId] = subjectLease; + if (subjectLease.Owner == this.host.HostName) { - if (renewResult.IsFaulted || !renewResult.Result) + ourLeaseCount++; + + // Get lease from partition since we need the token at this point. + if (!this.partitionPumps.TryGetValue(subjectLease.PartitionId, out var capturedPump)) { - // Might have failed due to intermittent error or lease-lost. - // Just log here, expired leases will be picked by same or another host anyway. - ProcessorEventSource.Log.PartitionPumpError(this.host.HostName, lease.PartitionId, "Failed to renew lease.", renewResult.Exception?.Message); - this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, lease.PartitionId, renewResult.Exception, EventProcessorHostActionStrings.RenewingLease); + continue; } - })); + + var capturedLease = capturedPump.Lease; + + ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, capturedLease.PartitionId, "Trying to renew lease."); + renewLeaseTasks.Add(leaseManager.RenewLeaseAsync(capturedLease).ContinueWith(renewResult => + { + if (renewResult.IsFaulted) + { + // Might have failed due to intermittent error or lease-lost. + // Just log here, expired leases will be picked by same or another host anyway. + ProcessorEventSource.Log.PartitionPumpError( + this.host.HostName, + capturedLease.PartitionId, + "Failed to renew lease.", + renewResult.Exception?.Message); + + this.host.EventProcessorOptions.NotifyOfException( + this.host.HostName, + capturedLease.PartitionId, + renewResult.Exception, + EventProcessorHostActionStrings.RenewingLease); + + // Nullify the owner on the lease in case this host lost it. + // This helps to remove pump earlier reducing duplicate receives. + if (renewResult.Exception?.GetBaseException() is LeaseLostException) + { + allLeases[capturedLease.PartitionId].Owner = null; + } + } + }, cancellationToken)); + } + else if (!await subjectLease.IsExpired().ConfigureAwait(false)) + { + leasesOwnedByOthers[subjectLease.PartitionId] = subjectLease; + } } - else + catch (Exception e) { - leasesOwnedByOthers.Add(lease); + ProcessorEventSource.Log.EventProcessorHostError(this.host.HostName, "Failure during checking lease.", e.ToString()); + this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, "N/A", e, EventProcessorHostActionStrings.CheckingLeases); } } - catch (Exception e) - { - ProcessorEventSource.Log.EventProcessorHostError(this.host.HostName, "Failure during checking lease.", e.ToString()); - this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, "N/A", e, EventProcessorHostActionStrings.CheckingLeases); - } - } - // Wait until we are done with renewing our own leases here. - // In theory, this should never throw, error are logged and notified in the renew tasks. - await Task.WhenAll(renewLeaseTasks.ToArray()).ConfigureAwait(false); - ProcessorEventSource.Log.EventProcessorHostInfo(this.host.HostName, "Lease renewal is finished."); + // Wait until we are done with renewing our own leases here. + // In theory, this should never throw, error are logged and notified in the renew tasks. + await Task.WhenAll(renewLeaseTasks).ConfigureAwait(false); + ProcessorEventSource.Log.EventProcessorHostInfo(this.host.HostName, "Lease renewal is finished."); - // Check any expired leases that we can grab here. - foreach (var possibleLease in allLeases.Values) - { - try + // Check any expired leases that we can grab here. + var checkLeaseTasks = new List(); + foreach (var possibleLease in allLeases.Values.Where(lease => lease.Owner != this.host.HostName)) { - if (await possibleLease.IsExpired().ConfigureAwait(false)) + var subjectLease = possibleLease; + + checkLeaseTasks.Add(Task.Run(async () => { - bool isExpiredLeaseOwned = possibleLease.Owner == this.host.HostName; - ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, possibleLease.PartitionId, "Trying to acquire lease."); - if (await leaseManager.AcquireLeaseAsync(possibleLease).ConfigureAwait(false)) + try { - ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, possibleLease.PartitionId, "Acquired lease."); - - // Don't double count if we have already counted this lease at the beginning of the loop. - if (!isExpiredLeaseOwned) + if (await subjectLease.IsExpired().ConfigureAwait(false)) { - ourLeaseCount++; + // Get fresh content of lease subject to acquire. + var downloadedLease = await leaseManager.GetLeaseAsync(subjectLease.PartitionId).ConfigureAwait(false); + allLeases[subjectLease.PartitionId] = downloadedLease; + + // Check expired once more here incase another host have already leased this since we populated the list. + if (await downloadedLease.IsExpired().ConfigureAwait(false)) + { + ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, downloadedLease.PartitionId, "Trying to acquire lease."); + if (await leaseManager.AcquireLeaseAsync(downloadedLease).ConfigureAwait(false)) + { + ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, downloadedLease.PartitionId, "Acquired lease."); + leasesOwnedByOthers.TryRemove(downloadedLease.PartitionId, out var removedLease); + Interlocked.Increment(ref ourLeaseCount); + } + else + { + // Acquisition failed. Make sure we don't leave the lease as owned. + allLeases[subjectLease.PartitionId].Owner = null; + + ProcessorEventSource.Log.EventProcessorHostWarning(this.host.HostName, + "Failed to acquire lease for partition " + downloadedLease.PartitionId, null); + } + } } } - } - } - catch (Exception e) - { - ProcessorEventSource.Log.PartitionPumpError(this.host.HostName, possibleLease.PartitionId, "Failure during acquiring lease", e.ToString()); - this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, possibleLease.PartitionId, e, EventProcessorHostActionStrings.CheckingLeases); + catch (Exception e) + { + ProcessorEventSource.Log.PartitionPumpError(this.host.HostName, subjectLease.PartitionId, "Failure during acquiring lease", e.ToString()); + this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, subjectLease.PartitionId, e, EventProcessorHostActionStrings.CheckingLeases); + + // Acquisition failed. Make sure we don't leave the lease as owned. + allLeases[subjectLease.PartitionId].Owner = null; + } + }, cancellationToken)); } - } - // Grab more leases if available and needed for load balancing - if (leasesOwnedByOthers.Count > 0) - { - Lease stealThisLease = WhichLeaseToSteal(leasesOwnedByOthers, ourLeaseCount); - if (stealThisLease != null) + await Task.WhenAll(checkLeaseTasks).ConfigureAwait(false); + ProcessorEventSource.Log.EventProcessorHostInfo(this.host.HostName, "Expired lease check is finished."); + + // Grab more leases if available and needed for load balancing + if (leasesOwnedByOthers.Count > 0) { - try + Lease stealThisLease = WhichLeaseToSteal(leasesOwnedByOthers.Values, ourLeaseCount); + + // Don't attempt to steal the lease if current host has a pump for this partition id + // This is possible when current pump is in failed state due to lease moved to some other host. + if (stealThisLease != null && !this.partitionPumps.ContainsKey(stealThisLease.PartitionId)) { - ProcessorEventSource.Log.PartitionPumpStealLeaseStart(this.host.HostName, stealThisLease.PartitionId); - if (await leaseManager.AcquireLeaseAsync(stealThisLease).ConfigureAwait(false)) + try { - // Succeeded in stealing lease - ProcessorEventSource.Log.PartitionPumpStealLeaseStop(this.host.HostName, stealThisLease.PartitionId); + // Get fresh content of lease subject to acquire. + var downloadedLease = await leaseManager.GetLeaseAsync(stealThisLease.PartitionId).ConfigureAwait(false); + allLeases[stealThisLease.PartitionId] = downloadedLease; + + // Don't attempt to steal if lease is already expired. + // Expired leases are picked up by other hosts quickly. + // Don't attempt to steal if owner has changed from the calculation time to refresh time. + if (!await downloadedLease.IsExpired().ConfigureAwait(false) + && downloadedLease.Owner == stealThisLease.Owner) + { + ProcessorEventSource.Log.PartitionPumpStealLeaseStart(this.host.HostName, downloadedLease.PartitionId); + if (await leaseManager.AcquireLeaseAsync(downloadedLease).ConfigureAwait(false)) + { + // Succeeded in stealing lease + ProcessorEventSource.Log.PartitionPumpStealLeaseStop(this.host.HostName, downloadedLease.PartitionId); + ourLeaseCount++; + } + else + { + // Acquisition failed. Make sure we don't leave the lease as owned. + allLeases[stealThisLease.PartitionId].Owner = null; + + ProcessorEventSource.Log.EventProcessorHostWarning(this.host.HostName, + "Failed to steal lease for partition " + downloadedLease.PartitionId, null); + } + } } - else + catch (Exception e) { - ProcessorEventSource.Log.EventProcessorHostWarning(this.host.HostName, - "Failed to steal lease for partition " + stealThisLease.PartitionId, null); + ProcessorEventSource.Log.EventProcessorHostError(this.host.HostName, + "Exception during stealing lease for partition " + stealThisLease.PartitionId, e.ToString()); + this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, + stealThisLease.PartitionId, e, EventProcessorHostActionStrings.StealingLease); + + // Acquisition failed. Make sure we don't leave the lease as owned. + allLeases[stealThisLease.PartitionId].Owner = null; } } - catch (Exception e) - { - ProcessorEventSource.Log.EventProcessorHostError(this.host.HostName, - "Exception during stealing lease for partition " + stealThisLease.PartitionId, e.ToString()); - this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, - stealThisLease.PartitionId, e, EventProcessorHostActionStrings.StealingLease); - } } - } - // Update pump with new state of leases. - foreach (string partitionId in allLeases.Keys) - { - try + // Update pump with new state of leases on owned partitions in parallel. + var createRemovePumpTasks = new List(); + foreach (string partitionId in allLeases.Keys) { - Lease updatedLease = allLeases[partitionId]; - ProcessorEventSource.Log.EventProcessorHostInfo(this.host.HostName, $"Lease on partition {updatedLease.PartitionId} owned by {updatedLease.Owner}"); - if (updatedLease.Owner == this.host.HostName) - { - await this.CheckAndAddPumpAsync(partitionId, updatedLease).ConfigureAwait(false); - } - else + var subjectPartitionId = partitionId; + + createRemovePumpTasks.Add(Task.Run(async () => { - await this.RemovePumpAsync(partitionId, CloseReason.LeaseLost).ConfigureAwait(false); - } - } - catch (Exception e) - { - ProcessorEventSource.Log.EventProcessorHostError(this.host.HostName, $"Exception during add/remove pump on partition {partitionId}", e.Message); - this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, partitionId, e, EventProcessorHostActionStrings.PartitionPumpManagement); + try + { + Lease updatedLease = allLeases[subjectPartitionId]; + ProcessorEventSource.Log.EventProcessorHostInfo(this.host.HostName, $"Lease on partition {updatedLease.PartitionId} owned by {updatedLease.Owner}"); + if (updatedLease.Owner == this.host.HostName) + { + await this.CheckAndAddPumpAsync(subjectPartitionId, updatedLease).ConfigureAwait(false); + } + else + { + await this.TryRemovePumpAsync(subjectPartitionId, CloseReason.LeaseLost).ConfigureAwait(false); + } + } + catch (Exception e) + { + ProcessorEventSource.Log.EventProcessorHostError(this.host.HostName, $"Exception during add/remove pump on partition {subjectPartitionId}", e.Message); + this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, subjectPartitionId, e, EventProcessorHostActionStrings.PartitionPumpManagement); + } + }, cancellationToken)); } - } - try - { + await Task.WhenAll(createRemovePumpTasks).ConfigureAwait(false); + ProcessorEventSource.Log.EventProcessorHostInfo(this.host.HostName, "Pump update is finished."); + // Consider reducing the wait time with last lease-walkthrough's time taken. var elapsedTime = loopStopwatch.Elapsed; if (leaseManager.LeaseRenewInterval > elapsedTime) @@ -334,9 +435,17 @@ async Task RunLoopAsync(CancellationToken cancellationToken) // throws Exception await Task.Delay(leaseManager.LeaseRenewInterval.Subtract(elapsedTime), cancellationToken).ConfigureAwait(false); } } - catch (TaskCanceledException) + catch (Exception e) { - // Bail on the async work if we are canceled. + // TaskCancelledException is expected furing host unregister. + if (e is TaskCanceledException) + { + continue; + } + + // Loop should not exit unless signalled via cancellation token. Log any failures and continue. + ProcessorEventSource.Log.EventProcessorHostError(this.host.HostName, "Exception from partition manager main loop, continuing", e.Message); + this.host.EventProcessorOptions.NotifyOfException(this.host.HostName, "N/A", e, EventProcessorHostActionStrings.PartitionPumpManagement); } } } @@ -350,31 +459,50 @@ async Task CheckAndAddPumpAsync(string partitionId, Lease lease) if (capturedPump.PumpStatus == PartitionPumpStatus.Errored || capturedPump.IsClosing) { // The existing pump is bad. Remove it. - await RemovePumpAsync(partitionId, CloseReason.Shutdown).ConfigureAwait(false); + await TryRemovePumpAsync(partitionId, CloseReason.Shutdown).ConfigureAwait(false); } else { - // Pump is working, just replace the lease. - ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, partitionId, "Updating lease for pump"); - capturedPump.SetLease(lease); + // Lease token can show up empty here if lease content download has failed or not recently acquired. + // Don't update the token if so. + if (!string.IsNullOrWhiteSpace(lease.Token)) + { + // Pump is working, just replace the lease token. + ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, partitionId, "Updating lease token for pump"); + capturedPump.SetLeaseToken(lease.Token); + } + else + { + ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, partitionId, "Skipping to update lease token for pump"); + } } } else { // No existing pump, create a new one. - await CreateNewPumpAsync(partitionId, lease).ConfigureAwait(false); + await CreateNewPumpAsync(partitionId).ConfigureAwait(false); } } - async Task CreateNewPumpAsync(string partitionId, Lease lease) + async Task CreateNewPumpAsync(string partitionId) { - PartitionPump newPartitionPump = new EventHubPartitionPump(this.host, lease); + // Refresh lease content and do last minute check to reduce partition moves. + var refreshedLease = await this.host.LeaseManager.GetLeaseAsync(partitionId); + if (refreshedLease.Owner != this.host.HostName || await refreshedLease.IsExpired().ConfigureAwait(false)) + { + // Partition moved to some other node after lease acquisition. + // Return w/o creating the pump. + ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, partitionId, $"Partition moved to another host or expired after acquisition."); + return; + } + + PartitionPump newPartitionPump = new EventHubPartitionPump(this.host, refreshedLease); await newPartitionPump.OpenAsync().ConfigureAwait(false); this.partitionPumps.TryAdd(partitionId, newPartitionPump); // do the put after start, if the start fails then put doesn't happen ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, partitionId, "Created new PartitionPump"); } - async Task RemovePumpAsync(string partitionId, CloseReason reason) + async Task TryRemovePumpAsync(string partitionId, CloseReason reason) { PartitionPump capturedPump; if (this.partitionPumps.TryRemove(partitionId, out capturedPump)) @@ -385,12 +513,6 @@ async Task RemovePumpAsync(string partitionId, CloseReason reason) } // else, pump is already closing/closed, don't need to try to shut it down again } - else - { - // PartitionManager main loop tries to remove pump for every partition that the host does not own, just to be sure. - // Not finding a pump for a partition is normal and expected most of the time. - ProcessorEventSource.Log.PartitionPumpInfo(this.host.HostName, partitionId, "No pump found to remove for this partition"); - } } Task RemoveAllPumpsAsync(CloseReason reason) @@ -399,15 +521,22 @@ Task RemoveAllPumpsAsync(CloseReason reason) var keys = new List(this.partitionPumps.Keys); foreach (string partitionId in keys) { - tasks.Add(this.RemovePumpAsync(partitionId, reason)); + tasks.Add(this.TryRemovePumpAsync(partitionId, reason)); } return Task.WhenAll(tasks); } - Lease WhichLeaseToSteal(List stealableLeases, int haveLeaseCount) + Lease WhichLeaseToSteal(IEnumerable stealableLeases, int haveLeaseCount) { IDictionary countsByOwner = CountLeasesByOwner(stealableLeases); + + // Consider all leases might be already released where we won't have any entry in the return counts map. + if (countsByOwner.Count == 0) + { + return null; + } + var biggestOwner = countsByOwner.OrderByDescending(o => o.Value).First(); Lease stealThisLease = null; @@ -432,7 +561,7 @@ Lease WhichLeaseToSteal(List stealableLeases, int haveLeaseCount) if ((biggestOwner.Value - haveLeaseCount) >= 2) { - stealThisLease = stealableLeases.Where(l => l.Owner == biggestOwner.Key).First(); + stealThisLease = stealableLeases.First(l => l.Owner == biggestOwner.Key); ProcessorEventSource.Log.EventProcessorHostInfo(this.host.HostName, $"Proposed to steal lease for partition {stealThisLease.PartitionId} from {biggestOwner.Key}"); } @@ -441,7 +570,8 @@ Lease WhichLeaseToSteal(List stealableLeases, int haveLeaseCount) Dictionary CountLeasesByOwner(IEnumerable leases) { - var counts = leases.GroupBy(lease => lease.Owner).Select(group => new { + var counts = leases.Where(lease => lease.Owner != null).GroupBy(lease => lease.Owner).Select(group => new + { Owner = group.Key, Count = group.Count() }); diff --git a/src/Microsoft.Azure.EventHubs.Processor/PartitionPump.cs b/src/Microsoft.Azure.EventHubs.Processor/PartitionPump.cs index cf9f396..e725304 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/PartitionPump.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/PartitionPump.cs @@ -23,7 +23,7 @@ protected PartitionPump(EventProcessorHost host, Lease lease) protected EventProcessorHost Host { get; } - protected Lease Lease { get; } + protected internal Lease Lease { get; } protected IEventProcessor Processor { get; private set; } @@ -31,9 +31,9 @@ protected PartitionPump(EventProcessorHost host, Lease lease) protected AsyncLock ProcessingAsyncLock { get; } - internal void SetLease(Lease newLease) + internal void SetLeaseToken(string newToken) { - this.PartitionContext.Lease = newLease; + this.PartitionContext.Lease.Token = newToken; } public async Task OpenAsync() @@ -42,7 +42,12 @@ public async Task OpenAsync() this.cancellationTokenSource = new CancellationTokenSource(); - this.PartitionContext = new PartitionContext(this.Host, this.Lease.PartitionId, this.Host.EventHubPath, this.Host.ConsumerGroupName, this.cancellationTokenSource.Token); + this.PartitionContext = new PartitionContext( + this.Host, + this.Lease.PartitionId, + this.Host.EventHubPath, + this.Host.ConsumerGroupName, + this.cancellationTokenSource.Token); this.PartitionContext.Lease = this.Lease; if (this.PumpStatus == PartitionPumpStatus.Opening) @@ -119,12 +124,15 @@ public async Task CloseAsync(CloseReason reason) if (reason != CloseReason.LeaseLost) { // Since this pump is dead, release the lease. - // Ignore LeaseLostException try { await this.Host.LeaseManager.ReleaseLeaseAsync(this.PartitionContext.Lease).ConfigureAwait(false); } - catch (LeaseLostException) { } + catch (Exception e) + { + // Log and ignore any failure since expired lease will be picked by another host. + this.Host.EventProcessorOptions.NotifyOfException(this.Host.HostName, this.PartitionContext.PartitionId, e, EventProcessorHostActionStrings.ReleasingLease); + } } this.PumpStatus = PartitionPumpStatus.Closed; @@ -160,10 +168,7 @@ protected async Task ProcessEventsAsync(IEnumerable events) this.PartitionContext.SetOffsetAndSequenceNumber(last); if (this.Host.EventProcessorOptions.EnableReceiverRuntimeMetric) { - this.PartitionContext.RuntimeInformation.LastSequenceNumber = last.LastSequenceNumber; - this.PartitionContext.RuntimeInformation.LastEnqueuedOffset = last.LastEnqueuedOffset; - this.PartitionContext.RuntimeInformation.LastEnqueuedTimeUtc = last.LastEnqueuedTime; - this.PartitionContext.RuntimeInformation.RetrievalTime = last.RetrievalTime; + this.PartitionContext.RuntimeInformation.Update(last); } } diff --git a/src/Microsoft.Azure.EventHubs.Processor/ProcessorEventSource.cs b/src/Microsoft.Azure.EventHubs.Processor/ProcessorEventSource.cs index 01969a3..c3b5504 100644 --- a/src/Microsoft.Azure.EventHubs.Processor/ProcessorEventSource.cs +++ b/src/Microsoft.Azure.EventHubs.Processor/ProcessorEventSource.cs @@ -3,6 +3,7 @@ namespace Microsoft.Azure.EventHubs { + using System; using System.Diagnostics.Tracing; /// diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Checkpoint.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Checkpoint.cs new file mode 100644 index 0000000..15456c3 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Checkpoint.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using System; + using System.Collections.Generic; + + /// + /// A persistable representation of what events in the stream have been processed. + /// Version 1 checkpoint is just a high-water mark, containing an offset and sequence number. All events at or lower than the given position + /// have been processed. Any events higher than the given position are unprocessed. + /// + public class Checkpoint + { + /// + /// Create an uninitialized checkpoint of the given version. + /// + /// + internal Checkpoint(int version) + { + this.Version = version; + this.Valid = false; + } + + /// + /// Create an initialized version 1 checkpoint. + /// + /// Offset of highest-processed position. + /// Sequence number of highest-processed position. + public Checkpoint(string offset, long sequenceNumber) + { + this.Version = 1; + this.Offset = offset; + this.SequenceNumber = sequenceNumber; + this.Valid = true; + } + + #region AllVersions + // + // Methods and properties valid for all versions. + // + + /// + /// Version of this checkpoint. + /// + public int Version { get; protected set; } + + /// + /// True if this checkpoint contains a valid position. + /// + public bool Valid { get; protected set; } + + /// + /// Serialize this instance to a persistable representation as a name-value dictionary. + /// + /// Serialized dictionary representation. + public Dictionary ToDictionary() + { + Dictionary converted = new Dictionary(); + + converted.Add(Constants.CheckpointPropertyVersion, this.Version); + converted.Add(Constants.CheckpointPropertyValid, this.Valid); + + switch (this.Version) + { + case 1: + converted.Add(Constants.CheckpointPropertyOffsetV1, this.Offset); + converted.Add(Constants.CheckpointPropertySequenceNumberV1, this.SequenceNumber); + break; + + default: + throw new NotImplementedException(); + } + + return converted; + } + + /// + /// Deserialize from a name-value dictionary. + /// + /// Serialized representation. + /// Deserialized instance. + static public Checkpoint CreateFromDictionary(Dictionary dictionary) + { + int version = (int)dictionary[Constants.CheckpointPropertyVersion]; + bool valid = (bool)dictionary[Constants.CheckpointPropertyValid]; + + Checkpoint result = new Checkpoint(version); + + if (valid) + { + result.Valid = true; + + switch (result.Version) + { + case 1: + result.Offset = (string)dictionary[Constants.CheckpointPropertyOffsetV1]; + result.SequenceNumber = (long)dictionary[Constants.CheckpointPropertySequenceNumberV1]; + break; + + default: + throw new NotImplementedException($"Unrecognized checkpoint version {result.Version}"); + } + } + + return result; + } + #endregion AllVersions + + #region Version1 + // + // Methods and properties for Version==1 + // + + /// + /// Initialize an uninitialized instance as a version 1 checkpoint. + /// + /// Offset of highest-processed position. + /// Sequence number of highest-processed position. + public void InitializeV1(string offset, long sequenceNumber) + { + this.Version = 1; + + if (string.IsNullOrEmpty(offset)) + { + throw new ArgumentException("offset must not be null or empty"); + } + if (sequenceNumber < 0) + { + throw new ArgumentException("sequenceNumber must be >= 0"); + } + + this.Offset = offset; + this.SequenceNumber = sequenceNumber; + + this.Valid = true; + } + + /// + /// Offset of highest-processed position. Immutable after construction or initialization. + /// + public string Offset { get; private set; } + + /// + /// Sequence number of highest-processed position. Immutable after construction or initialization. + /// + public long SequenceNumber { get; private set; } + #endregion Version1 + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/CloseReason.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/CloseReason.cs new file mode 100644 index 0000000..b0755f7 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/CloseReason.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + /// + /// Why the event processor is being shut down. + /// + public enum CloseReason + { + /// + /// It was cancelled by Service Fabric. + /// + Cancelled, + + /// + /// There was an event hubs failure. + /// + Failure + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Constants.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Constants.cs new file mode 100644 index 0000000..2c80296 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Constants.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using System; + + class Constants + { + internal static readonly int RetryCount = 5; + + internal static readonly int FixedReceiverEpoch = 0; + + internal static readonly TimeSpan MetricReportingInterval = TimeSpan.FromMinutes(1.0); + internal static readonly string DefaultUserLoadMetricName = "CountOfPartitions"; + + internal static readonly TimeSpan ReliableDictionaryTimeout = TimeSpan.FromSeconds(10.0); // arbitrary + internal static readonly string CheckpointDictionaryName = "EventProcessorCheckpointDictionary"; + internal static readonly string CheckpointPropertyVersion = "version"; + internal static readonly string CheckpointPropertyValid = "valid"; + internal static readonly string CheckpointPropertyOffsetV1 = "offsetV1"; + internal static readonly string CheckpointPropertySequenceNumberV1 = "sequenceNumberV1"; + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventHubMocks.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventHubMocks.cs new file mode 100644 index 0000000..ed164a6 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventHubMocks.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Mocks for the underlying event hub client. Using these instead of the regular wrappers allows unit testing without an event hub. + /// By default, EventProcessorService.EventHubClientFactory is a EventHubWrappers.EventHubClientFactory. + /// To use the mocks, change it to a EventHubMocks.EventHubClientFactoryMock. + /// + public class EventHubMocks + { + /// + /// Mock of an Event Hub partition receiver. + /// + public class PartitionReceiverMock : EventHubWrappers.IPartitionReceiver + { + private readonly string partitionId; + private long sequenceNumber; + private IPartitionReceiveHandler outerHandler; + private bool invokeWhenNoEvents; + private readonly CancellationToken token; + + /// + /// Construct the partition receiver mock. + /// + /// + /// + /// + public PartitionReceiverMock(string partitionId, long sequenceNumber, CancellationToken token) + { + this.partitionId = partitionId; + this.sequenceNumber = sequenceNumber; + this.token = token; + } + + /// + /// Receive mock events. + /// + /// + /// + /// + public Task> ReceiveAsync(int maxEventCount, TimeSpan waitTime) + { + List events = new List(); + for (int i = 0; i < maxEventCount; i++) + { + this.sequenceNumber++; + byte[] body = new byte[] { 0x4D, 0x4F, 0x43, 0x4B, 0x42, 0x4F, 0x44, 0x59 }; // M O C K B O D Y + EventData e = new EventData(body); + // TODO -- need a way to set the system properties of the EventData + //e.ForceSystemProperties(new EventData.SystemPropertiesCollection(this.sequenceNumber, DateTime.UtcNow, (this.sequenceNumber * 100).ToString(), "")); + e.Properties.Add("userkey", "uservalue"); + events.Add(e); + } + Thread.Sleep(5000); + EventProcessorEventSource.Current.Message($"MOCK ReceiveAsync returning {maxEventCount} events for partition {this.partitionId} ending at {this.sequenceNumber}"); + return Task.FromResult>(events); + } + + /// + /// Set a mock receive handler. + /// + /// + /// + public void SetReceiveHandler(IPartitionReceiveHandler receiveHandler, bool invokeWhenNoEvents = false) + { + EventProcessorEventSource.Current.Message("MOCK IPartitionReceiver.SetReceiveHandler"); + this.outerHandler = receiveHandler; + this.invokeWhenNoEvents = invokeWhenNoEvents; // TODO mock does not emulate receive timeouts + if (this.outerHandler != null) + { + Task.Run(() => GenerateMessages()); + } + } + + /// + /// Close the mock receiver. + /// + /// + public Task CloseAsync() + { + EventProcessorEventSource.Current.Message("MOCK IPartitionReceiver.CloseAsync"); + return Task.CompletedTask; + } + + private async void GenerateMessages() + { + while ((!this.token.IsCancellationRequested) && (this.outerHandler != null)) + { + EventProcessorEventSource.Current.Message("MOCK Generating messages and sending to handler"); + IEnumerable events = ReceiveAsync(10, TimeSpan.FromSeconds(10.0)).Result; // TODO get count from somewhere real + IPartitionReceiveHandler capturedHandler = this.outerHandler; + if (capturedHandler != null) + { + await capturedHandler.ProcessEventsAsync(events); + } + } + EventProcessorEventSource.Current.Message("MOCK Message generation ending"); + } + } + + /// + /// Mock of EventHubClient class. + /// + public class EventHubClientMock : EventHubWrappers.IEventHubClient + { + private readonly int partitionCount; + private readonly EventHubsConnectionStringBuilder csb; + private CancellationToken token = new CancellationToken(); + + /// + /// Construct the mock. + /// + /// + /// + public EventHubClientMock(int partitionCount, EventHubsConnectionStringBuilder csb) + { + this.partitionCount = partitionCount; + this.csb = csb; + } + + internal void SetCancellationToken(CancellationToken t) + { + this.token = t; + } + + /// + /// Get runtime info of the fake event hub. + /// + /// + public Task GetRuntimeInformationAsync() + { + EventHubRuntimeInformation ehri = new EventHubRuntimeInformation(); + ehri.PartitionCount = this.partitionCount; + ehri.PartitionIds = new string[this.partitionCount]; + for (int i = 0; i < this.partitionCount; i++) + { + ehri.PartitionIds[i] = i.ToString(); + } + ehri.Path = csb.EntityPath; + EventProcessorEventSource.Current.Message($"MOCK GetRuntimeInformationAsync for {ehri.Path}"); + return Task.FromResult(ehri); + } + + /// + /// Create a mock receiver on the fake event hub. + /// + /// + /// + /// + /// + /// + /// + /// + public EventHubWrappers.IPartitionReceiver CreateEpochReceiver(string consumerGroupName, string partitionId, EventPosition eventPosition, string offset, long epoch, ReceiverOptions receiverOptions) + { + EventProcessorEventSource.Current.Message($"MOCK CreateEpochReceiver(CG {consumerGroupName}, part {partitionId}, offset {offset} epoch {epoch})"); + // TODO implement epoch semantics + long startSeq = (offset != null) ? (long.Parse(offset) / 100L) : 0L; + return new PartitionReceiverMock(partitionId, startSeq, this.token); + } + + /// + /// Close the mock EventHubClient. + /// + /// + public Task CloseAsync() + { + EventProcessorEventSource.Current.Message("MOCK IEventHubClient.CloseAsync"); + return Task.CompletedTask; + } + } + + /// + /// An EventHubClient factory which dispenses mocks. + /// + public class EventHubClientFactoryMock : EventHubWrappers.IEventHubClientFactory + { + private readonly int partitionCount; + + /// + /// Construct the mock factory. + /// + /// + public EventHubClientFactoryMock(int partitionCount) + { + this.partitionCount = partitionCount; + } + + /// + /// Dispense a mock instance operating on a fake event hub with name taken from the connection string. + /// + /// + /// + public EventHubWrappers.IEventHubClient CreateFromConnectionString(string connectionString) + { + throw new NotImplementedException("Need a change to EventData before mocks can be supported"); + //EventProcessorEventSource.Current.Message($"MOCK Creating IEventHubClient {connectionString} with {this.partitionCount} partitions"); + //return new EventHubClientMock(this.partitionCount, new EventHubsConnectionStringBuilder(connectionString)); + } + } + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventHubWrappers.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventHubWrappers.cs new file mode 100644 index 0000000..d67ad29 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventHubWrappers.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + /// + /// Wrappers for the underlying Event Hub client which allow mocking. + /// The interfaces include only the client functionality used by the Service Fabric Processor. + /// + public class EventHubWrappers + { + /// + /// Interface for a partition receiver. + /// + public interface IPartitionReceiver + { + /// + /// + /// + /// + /// + Task> ReceiveAsync(int maxEventCount, TimeSpan waitTime); + + /// + /// + /// + /// + void SetReceiveHandler(IPartitionReceiveHandler receiveHandler, bool invokeWhenNoEvents = false); + + /// + /// + /// + Task CloseAsync(); + } + + /// + /// Interface representing EventHubClient + /// + public interface IEventHubClient + { + /// + /// + /// + Task GetRuntimeInformationAsync(); + + /// + /// + /// + /// + /// + /// Only used by mocks + /// + /// + /// + IPartitionReceiver CreateEpochReceiver(string consumerGroupName, string partitionId, EventPosition eventPosition, string offset, long epoch, ReceiverOptions receiverOptions); + + /// + /// + /// + Task CloseAsync(); + } + + /// + /// Interface for an EventHubClient factory so that we can have factories which dispense different implementations of IEventHubClient. + /// + public interface IEventHubClientFactory + { + /// + /// + /// + /// + IEventHubClient CreateFromConnectionString(string connectionString); + } + + internal class PartitionReceiverWrapper : IPartitionReceiver + { + private readonly PartitionReceiver inner; + + internal PartitionReceiverWrapper(PartitionReceiver receiver) + { + this.inner = receiver; + this.MaxBatchSize = 10; // TODO get this from somewhere real + } + + public Task> ReceiveAsync(int maxEventCount, TimeSpan waitTime) + { + return this.inner.ReceiveAsync(maxEventCount, waitTime); + } + + public void SetReceiveHandler(IPartitionReceiveHandler receiveHandler, bool invokeWhenNoEvents = false) + { + this.inner.SetReceiveHandler(receiveHandler, invokeWhenNoEvents); + } + + public Task CloseAsync() + { + return this.inner.CloseAsync(); + } + + public int MaxBatchSize { get; set; } + } + + internal class EventHubClientWrapper : IEventHubClient + { + private readonly EventHubClient inner; + + internal EventHubClientWrapper(EventHubClient ehc) + { + this.inner = ehc; + } + + public Task GetRuntimeInformationAsync() + { + return this.inner.GetRuntimeInformationAsync(); + } + + public IPartitionReceiver CreateEpochReceiver(string consumerGroupName, string partitionId, EventPosition eventPosition, string offset, long epoch, ReceiverOptions receiverOptions) + { + return new PartitionReceiverWrapper(this.inner.CreateEpochReceiver(consumerGroupName, partitionId, eventPosition, epoch, receiverOptions)); + } + + public Task CloseAsync() + { + return this.inner.CloseAsync(); + } + } + + internal class EventHubClientFactory : IEventHubClientFactory + { + public IEventHubClient CreateFromConnectionString(string connectionString) + { + return new EventHubClientWrapper(EventHubClient.CreateFromConnectionString(connectionString)); + } + } + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorConfigurationException.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorConfigurationException.cs new file mode 100644 index 0000000..7505b54 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorConfigurationException.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using System; + + /// + /// Exception thrown when the configuration of the service has a problem. + /// + public class EventProcessorConfigurationException : Exception + { + /// + /// Construct the exception. + /// + /// + public EventProcessorConfigurationException(string message) : base(message) + { + } + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorEventSource.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorEventSource.cs new file mode 100644 index 0000000..c29b021 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorEventSource.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using System; + using System.Diagnostics.Tracing; + using System.Fabric; + + [EventSource(Name = "Microsoft-Azure-EventHubs-ServiceFabricProcessor")] + internal sealed class EventProcessorEventSource : EventSource + { + public static readonly EventProcessorEventSource Current = new EventProcessorEventSource(); + + // Instance constructor is private to enforce singleton semantics + private EventProcessorEventSource() : base() { } + + #region Keywords + // Event keywords can be used to categorize events. + // Each keyword is a bit flag. A single event can be associated with multiple keywords (via EventAttribute.Keywords property). + // Keywords must be defined as a public class named 'Keywords' inside EventSource that uses them. + public static class Keywords + { + public const EventKeywords Requests = (EventKeywords)0x1L; + public const EventKeywords ServiceInitialization = (EventKeywords)0x2L; + } + #endregion + + #region Events + // Define an instance method for each event you want to record and apply an [Event] attribute to it. + // The method name is the name of the event. + // Pass any parameters you want to record with the event (only primitive integer types, DateTime, Guid & string are allowed). + // Each event method implementation should check whether the event source is enabled, and if it is, call WriteEvent() method to raise the event. + // The number and types of arguments passed to every event method must exactly match what is passed to WriteEvent(). + // Put [NonEvent] attribute on all methods that do not define an event. + // For more information see https://msdn.microsoft.com/en-us/library/system.diagnostics.tracing.eventsource.aspx + + [NonEvent] + public void Message(string message, params object[] args) + { + if (this.IsEnabled()) + { + string finalMessage = string.Format(message, args); + Message(finalMessage); + } + } + + private const int MessageEventId = 1; + [Event(MessageEventId, Level = EventLevel.Informational, Message = "{0}")] + public void Message(string message) + { + if (this.IsEnabled()) + { + WriteEvent(MessageEventId, message); + } + } + + [NonEvent] + public void ServiceMessage(StatefulServiceContext serviceContext, string message, params object[] args) + { + if (this.IsEnabled()) + { + string finalMessage = string.Format(message, args); + ServiceMessage( + serviceContext.ServiceName.ToString(), + serviceContext.ServiceTypeName, + serviceContext.ReplicaId, + serviceContext.PartitionId, + serviceContext.CodePackageActivationContext.ApplicationName, + serviceContext.CodePackageActivationContext.ApplicationTypeName, + serviceContext.NodeContext.NodeName, + finalMessage); + } + } + + private const int ServiceMessageEventId = 2; + [Event(ServiceMessageEventId, Level = EventLevel.Informational, Message = "{7}")] + private + void ServiceMessage( + string serviceName, + string serviceTypeName, + long replicaOrInstanceId, + Guid partitionId, + string applicationName, + string applicationTypeName, + string nodeName, + string message) + { + WriteEvent(ServiceMessageEventId, serviceName, serviceTypeName, replicaOrInstanceId, partitionId, applicationName, applicationTypeName, nodeName, message); + } + + private const int ServiceTypeRegisteredEventId = 3; + [Event(ServiceTypeRegisteredEventId, Level = EventLevel.Informational, Message = "Service host process {0} registered service type {1}", Keywords = Keywords.ServiceInitialization)] + public void ServiceTypeRegistered(int hostProcessId, string serviceType) + { + WriteEvent(ServiceTypeRegisteredEventId, hostProcessId, serviceType); + } + + private const int ServiceHostInitializationFailedEventId = 4; + [Event(ServiceHostInitializationFailedEventId, Level = EventLevel.Error, Message = "Service host initialization failed", Keywords = Keywords.ServiceInitialization)] + public void ServiceHostInitializationFailed(string exception) + { + WriteEvent(ServiceHostInitializationFailedEventId, exception); + } + + // A pair of events sharing the same name prefix with a "Start"/"Stop" suffix implicitly marks boundaries of an event tracing activity. + // These activities can be automatically picked up by debugging and profiling tools, which can compute their execution time, child activities, + // and other statistics. + private const int ServiceRequestStartEventId = 5; + [Event(ServiceRequestStartEventId, Level = EventLevel.Informational, Message = "Service request '{0}' started", Keywords = Keywords.Requests)] + public void ServiceRequestStart(string requestTypeName) + { + WriteEvent(ServiceRequestStartEventId, requestTypeName); + } + + private const int ServiceRequestStopEventId = 6; + [Event(ServiceRequestStopEventId, Level = EventLevel.Informational, Message = "Service request '{0}' finished", Keywords = Keywords.Requests)] + public void ServiceRequestStop(string requestTypeName, string exception = "") + { + WriteEvent(ServiceRequestStopEventId, requestTypeName, exception); + } + #endregion + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorOptions.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorOptions.cs new file mode 100644 index 0000000..17d54df --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/EventProcessorOptions.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using System; + + /// + /// Type used for OnShutdown property. + /// + /// + public delegate void ShutdownNotification(Exception e); + + /// + /// Options that govern the functioning of the processor. + /// + public class EventProcessorOptions + { + /// + /// Construct with default options. + /// + public EventProcessorOptions() + { + this.MaxBatchSize = 10; + this.PrefetchCount = 300; + this.ReceiveTimeout = TimeSpan.FromMinutes(1); + this.EnableReceiverRuntimeMetric = false; + this.InvokeProcessorAfterReceiveTimeout = false; + this.InitialPositionProvider = partitionId => EventPosition.FromStart(); + this.ClientReceiverOptions = null; + this.OnShutdown = null; + } + + /// + /// The maximum number of events that will be presented to IEventProcessor.OnEventsAsync in one call. + /// Defaults to 10. + /// + public int MaxBatchSize { get; set; } + + /// + /// The prefetch count for the Event Hubs receiver. + /// Defaults to 300. + /// + public int PrefetchCount { get; set; } + + /// + /// The timeout for the Event Hubs receiver. + /// Defaults to one minute. + /// + public TimeSpan ReceiveTimeout { get; set; } + + /// + /// Gets or sets a value indicating whether the runtime metric of a receiver is enabled (true) or disabled (false). + /// Defaults to false. + /// + public bool EnableReceiverRuntimeMetric { get; set; } + + /// + /// Determines whether IEventProcessor.OnEventsAsync is called when the Event Hubs receiver times out. + /// Set to true to get calls with empty event list. + /// Set to false to not get calls. + /// Defaults to false. + /// + public bool InvokeProcessorAfterReceiveTimeout { get; set; } + + /// + /// If there is no checkpoint, the user can provide a position for the Event Hubs receiver to start at. + /// Defaults to first event available in the stream. + /// + public Func InitialPositionProvider { get; set; } + + /// + /// ReceiverOptions used by the underlying Event Hubs client. + /// Defaults to null. + /// + public ReceiverOptions ClientReceiverOptions { get; set; } + + /// + /// TODO -- is this needed? It's called just before SFP.RunAsync throws out/returns to user code anyway. + /// But user code won't see that until it awaits the Task, so maybe this is useful? + /// + public ShutdownNotification OnShutdown { get; set; } + + internal void NotifyOnShutdown(Exception shutdownException) + { + if (this.OnShutdown != null) + { + this.OnShutdown(shutdownException); + } + } + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ICheckpointMananger.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ICheckpointMananger.cs new file mode 100644 index 0000000..5597d64 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ICheckpointMananger.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using System.Threading; + using System.Threading.Tasks; + + /// + /// Interface for a checkpoint manager which persists Checkpoints. + /// + public interface ICheckpointMananger + { + /// + /// Does the checkpoint store exist? + /// + /// + /// True if it exists, false if not. + Task CheckpointStoreExistsAsync(CancellationToken cancellationToken); + + /// + /// Create the checkpoint store if it doesn't exist. + /// + /// + /// True if it exists or was created OK, false if not. + Task CreateCheckpointStoreIfNotExistsAsync(CancellationToken cancellationToken); + + /// + /// Create an uninitialized checkpoint for the given partition. + /// + /// + /// + /// + Task CreateCheckpointIfNotExistsAsync(string partitionId, CancellationToken cancellationToken); + + /// + /// Get the checkpoint for the given partition. Returns null if there is no checkpoint or if it is uninitialized. + /// + /// + /// + /// + Task GetCheckpointAsync(string partitionId, CancellationToken cancellationToken); + + /// + /// Persist the Checkpoint for the given partition. + /// + /// + /// + /// + /// + Task UpdateCheckpointAsync(string partitionId, Checkpoint checkpoint, CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/IEventProcessor.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/IEventProcessor.cs new file mode 100644 index 0000000..9af4bd9 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/IEventProcessor.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Interface for processing events. + /// + public abstract class IEventProcessor + { + /// + /// Called on startup. + /// + /// + /// + /// + abstract public Task OpenAsync(CancellationToken cancellationToken, PartitionContext context); + + /// + /// Called on shutdown. + /// + /// + /// + /// + abstract public Task CloseAsync(PartitionContext context, CloseReason reason); + + /// + /// Called when events are available. + /// + /// + /// + /// + /// + abstract public Task ProcessEventsAsync(CancellationToken cancellationToken, PartitionContext context, IEnumerable events); + + /// + /// Called when an error occurs. + /// + /// + /// + /// + abstract public Task ProcessErrorAsync(PartitionContext context, Exception error); + + /// + /// Called periodically to get user-supplied load metrics. + /// + /// + /// + /// + virtual public Dictionary GetLoadMetric(CancellationToken cancellationToken, PartitionContext context) + { + // By default all partitions have a metric of named CountOfPartitions with value 1. If Service Fabric is configured to use this metric, + // it will balance primaries across nodes simply by the number of primaries on a node. This can be overridden to return + // more sophisticated metrics like number of events processed or CPU usage. + Dictionary defaultMetric = new Dictionary(); + defaultMetric.Add(Constants.DefaultUserLoadMetricName, 1); + return defaultMetric; + } + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Microsoft.Azure.EventHubs.ServiceFabricProcessor.csproj b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Microsoft.Azure.EventHubs.ServiceFabricProcessor.csproj new file mode 100644 index 0000000..d06dbdc --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/Microsoft.Azure.EventHubs.ServiceFabricProcessor.csproj @@ -0,0 +1,47 @@ + + + + This is the next generation Azure Event Hubs .NET Standard Service Fabric Processor library, which integrates Event Hub event consumption with Service Fabric. For more information about Event Hubs, see https://azure.microsoft.com/en-us/services/event-hubs/ + Microsoft.Azure.EventHubs.ServiceFabricProcessor + 0.5.2-PREVIEW + Microsoft + netstandard2.0 + true + Microsoft.Azure.EventHubs.ServiceFabricProcessor + ../../build/keyfile.snk + true + true + Microsoft.Azure.EventHubs.ServiceFabricProcessor + Azure;Event Hubs;EventHubs;.NET;AMQP;IoT + https://github.com/Azure/azure-event-hubs-dotnet/releases + https://raw.githubusercontent.com/Azure/azure-event-hubs-dotnet/master/event-hubs.png + https://github.com/Azure/azure-event-hubs-dotnet + https://raw.githubusercontent.com/Azure/azure-event-hubs-dotnet/master/LICENSE + true + false + false + false + full + bin\$(Configuration)\$(TargetFramework)\Microsoft.Azure.EventHubs.Processor.xml + 0.5.2 + 0.5.2.0 + 0.5.2.0 + false + © Microsoft Corporation. All rights reserved. + + + + x64 + + + + + + + + + + + + + diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/PartitionContext.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/PartitionContext.cs new file mode 100644 index 0000000..83c2660 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/PartitionContext.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using System.Threading; + using System.Threading.Tasks; + + /// + /// Passed to an event processor instance to describe the environment. + /// + public class PartitionContext + { + readonly private ICheckpointMananger checkpointMananger; + + /// + /// Construct an instance. + /// + /// CancellationToken that the event processor should respect. Same as token passed to IEventProcessor methods. + /// Id of the partition for which the event processor is handling events. + /// Name of the event hub which is the source of events. + /// Name of the consumer group on the event hub. + /// The checkpoint manager instance to use. + public PartitionContext(CancellationToken cancellationToken, string partitionId, string eventHubPath, string consumerGroupName, ICheckpointMananger checkpointMananger) + { + this.CancellationToken = cancellationToken; + this.PartitionId = partitionId; + this.EventHubPath = eventHubPath; + this.ConsumerGroupName = consumerGroupName; + + // TODO: Requires client change to support + this.RuntimeInformation = null; // new ReceiverRuntimeInformation(this.PartitionId); + + this.checkpointMananger = checkpointMananger; + } + + /// + /// The event processor implementation should respect this CancellationToken. It is the same as the token passed + /// in to IEventProcessor methods. It is here primarily for compatibility with Event Processor Host. + /// + public CancellationToken CancellationToken { get; private set; } + + /// + /// Name of the consumer group on the event hub. + /// + public string ConsumerGroupName { get; private set; } + + /// + /// Name of the event hub. + /// + public string EventHubPath { get; private set; } + + /// + /// Id of the partition. + /// + public string PartitionId { get; private set; } + + /// + /// Gets the approximate receiver runtime information for a logical partition of an Event Hub. + /// To enable the setting, refer to + /// + public ReceiverRuntimeInformation RuntimeInformation + { + get; + // internal set; + } + + internal string Offset { get; set; } + + internal long SequenceNumber { get; set; } + + internal void SetOffsetAndSequenceNumber(EventData eventData) + { + this.Offset = eventData.SystemProperties.Offset; + this.SequenceNumber = eventData.SystemProperties.SequenceNumber; + } + + /// + /// Mark the last event of the current batch and all previous events as processed. + /// + /// + public async Task CheckpointAsync() + { + await CheckpointAsync(new Checkpoint(this.Offset, this.SequenceNumber)); + } + + /// + /// Mark the given event and all previous events as processed. + /// + /// Highest-processed event. + /// + public async Task CheckpointAsync(EventData eventData) + { + await CheckpointAsync(new Checkpoint(eventData.SystemProperties.Offset, eventData.SystemProperties.SequenceNumber)); + } + + private async Task CheckpointAsync(Checkpoint checkpoint) + { + await this.checkpointMananger.UpdateCheckpointAsync(this.PartitionId, checkpoint, this.CancellationToken); + } + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ProgrammersGuide.md b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ProgrammersGuide.md new file mode 100644 index 0000000..3ef9fe4 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ProgrammersGuide.md @@ -0,0 +1,271 @@ +# Programmer's Guide to Service Fabric Processor + +## Introduction + +Service Fabric Processor (SFP) allows a user to easily create a Service +Fabric-based stateful service that processes events from an Event Hub. The +service is required to have exactly as many partitions as the target Event Hub +does, because each Service Fabric partition is permanently associated with a +corresponding Event Hub partition. The user's processing logic for events is +contained in their implementation of the IEventProcessor interface. Each +partition of the service has an instance of that implementation, which is called +by SFP to handle events consumed from the corresponding Event Hub partition. +Only the primary Service Fabric replica for each partition consumes and processes +events. The secondary replicas exist to maintain the reliable dictionary that +Service Fabric Processor uses for checkpointing. + +## IEventProcessor + +There are four methods that the user is required to implement, and one more +which the user may override if desired. + +Most of the methods provide a +CancellationToken as an argument, and it is important for long-running +operations in the user's code to honor that token. Service Fabric cancels it +when a primary replica's state is changing, and that is the code's chance to +clean up and shut down gracefully instead of being forcefully terminated! + +All methods provide a PartitionContext as an argument, which contains +various information about the Event Hub and the partition that may be useful +to the user's code. The PartitionContext also has a reference to the +CancellationToken. + +### OpenAsync + +```csharp +Task OpenAsync(CancellationToken cancellationToken, PartitionContext context) +``` + +SFP calls this method when a partition replica becomes primary. This is the +opportunity to initialize resources that the event processing logic will +use, such as a database connection, or perform any other startup needed. + +### ProcessEventsAsync + +```csharp +Task ProcessEventsAsync(CancellationToken cancellationToken, PartitionContext context, IEnumerable events) +``` + +After the Task returned by OpenAsync completes, SFP calls this method repeatedly +as events become available. SFP makes only one call to ProcessEventsAsync at a +time: it waits for the Task returned by the current call to complete before +attempting to consume more events from the Event Hub. By default, this method +is called only when events are available, so if traffic on the associated +Event Hub partition is sparse, it may be some time between calls to this method. + +### CloseAsync + +```csharp +Task CloseAsync(PartitionContext context, CloseReason reason) +``` + +SFP calls this method when a primary replica is being shut down, whether due to +an Event Hub failure or because Service Fabric is changing the replica's state. +No cancellation token is provided because the user's code is expected to already +be shutting down as quickly as possible, and in many cases the token will already +be cancelled. The CloseReason indicates whether the shutdown is due to an Event +Hub failure or Service Fabric cancellation. + +This method will will not be called until the Task returned by the most recent +ProcessEventsAsync call has completed. + +### ProcessErrorAsync + +```csharp +Task ProcessErrorAsync(PartitionContext context, Exception error) +``` + +SFP calls this method when an Event Hubs error has occurred. It is purely +informational. Recovering from the error, if possible, is up to SFP. + +### GetLoadMetric + +```csharp +Dictionary GetLoadMetric(CancellationToken cancellationToken, PartitionContext context) +``` + +Service Fabric offers sophisticated load balancing of +partition replicas between nodes based on user-provided metrics, and this +method allows SFP users to take advantage of that feature. SFP polls this +method periodically and passes the metrics returned to Service Fabric. The +metrics are represented as a dictionary of string-int pairs, where the string +is the metric name and the int is the metric value. SFP provides a default +implementation of this method, which returns a metric named "CountOfPartitions" +that has a constant value of 1. + +It is up to the user to configure Service Fabric to use metrics returned by +this method. Service Fabric ignores any metrics not mentioned in the +configuration, so it is safe to return any metrics that might be interesting +and then decide later which particular ones to use. + +## Integrating With Service Fabric + +SFP requires a stateful Service Fabric service that is configured to have the +same number of partitions as the Event Hub. + +The user's service will have a class derived from the Service Fabric class +StatefulService, which in turn has a RunAsync method that is called on primary +partition replicas. SFP setup and activation occur within that RunAsync method. + +### SFP Options + +SFP provides a class EventProcessorOptions, which allows setting a wide variety +of options that adjust how SFP operates. To use it, create a new instance, which +is initialized with the default settings, then change the options of interest, +and finally pass the instance to the ServiceFabricProcessor constructor. + +```csharp +EventProcessorOptions options = new EventProcessorOptions(); +options.MaxBatchSize = 50; +ServiceFabricProcessor processor = new ServiceFabricProcessor(..., options); +``` + +Available options are: + +* int MaxBatchSize: the _maximum_ number of events that will be passed to +ProcessEventsAsync at one time. Default is 10. The actual number of events for +each call is variable and depends on how many events are available in the Event +Hub partition, how fast they can be transferred, how long the previous call to +ProcessEventsAsync took, and other factors. If your system as a whole is +processing events faster than they are generated, then much of the time +ProcessEventsAsync will be called with only one event. This option only +governs the _maximum_ number. + +* int PrefetchCount: passed to the underlying Event Hubs client, this option +governs how many events can be prefetched. Default is 300. This generally +comes into play only when there is a backlog of events due to slow processing. +If your system as a whole is processing events faster than they are generated, +then the prefetch buffer will be empty, because every event that becomes +available can be immediately passed to ProcessEventsAsync. + +* TimeSpan ReceiveTimeout: passed to the underlying Event Hubs client, this +option governs the timeout duration for the Event Hubs receiver. Default is +60 seconds. + +* bool EnableReceiverRuntimeMetric: TODO -- needs client changes before it +can be supported + +* bool InvokeProcessorAfterReceiveTimeout: if false, ProcessEventsAsync is +called only when at least one event is available. If true, ProcessEventsAsync +is called with an empty event list when a receive timeout occurs. Default is +false. + +* ShutdownNotification OnShutdown: set a delegate which is called just before +the Task returned by ServiceFabricProcessor.RunAsync completes. Depending on +how the code in the service's RunAsync is structured, SFP might shut down due +to an error long before RunAsync awaits the returned Task. This delegate +provides notification of such a shutdown. + +* Func InitialPositionProvider: see "Starting Position +and Checkpointing" below + +### Instantiating ServiceFabricProcessor + +```csharp +IEventProcessor myEventProcessor = new MyEventProcessorClass(...); +ServiceFabricProcessor processor = new ServiceFabricProcessor( + this.Context.ServiceName, + this.Context.PartitionId, + this.StateManager, + this.Partition, + myEventProcessor, + eventHubConnectionString, + eventHubConsumerGroup, + options +); +``` + +* The first four arguments are Service Fabric artifacts that SFP needs to get +information about Service Fabric partitions and to access Service Fabric reliable +dictionaries. They are all available as members of the StatefulService-derived +class. + +* The next argument is an instance of the user's implementation of IEventProcessor. + +* The next two arguments are the connection string of the Event Hub to consume +from, and the consumer group. The consumer group is optional: if null or omitted, +the default consumer group "$Default" is used. + +* The last argument is an optional instance of EventProcessorOptions. If null +or omitted, all options have their default value. + +### Start processing events + +```csharp +Task processing = processor.RunAsync(cancellationToken); +// do other stuff here if desired +await processing; +``` + +Note that ServiceFabricProcessor.RunAsync can be called only once. If the await +throws, you can either allow the exception to propagate out to Service Fabric and +let Service Fabric restart the replica, or you can create a new instance of +ServiceFabricProcessor and call RunAsync on the new instance. + +## Starting Position and Checkpointing + +When ServiceFabricProcessor.RunAsync is called, one of the first things it does +is to create a receiver on the Event Hub partition so it can consume events. +Because Event Hubs do not have a service-side cursor, a receiver must specify +a starting position when it is created. + +One of the features of SFP is checkpointing, which provides a client-side +cursor by persisting the offset of the last event processed successfully. +Checkpointing does not happen automatically, because there are scenarios +which do not need it. To use checkpointing, the user's implementation of +IEventProcessor.ProcessEventsAsync calls the CheckpointAsync methods on +the supplied PartitionContext. + +### Finding the Starting Position + +SFP follows these steps to determine the starting position when creating a +receiver: + +* If there is a checkpoint for the partition, start at the next event after +that position. + +* Else, call EventProcessorOptions.InitialPositionProvider, if present: when +setting up options, the user can provide a function which takes an Event Hub +partition id and returns an EventPosition. + +* Else, start at the oldest available event. + +### Checkpointing + +PartitionContext provides two methods for setting a checkpoint: + +* With no arguments, the checkpoint contains the position of the last event +in the current batch. If once-per-batch is a reasonable checkpoint strategy +for your application, calling PartitionContext.CheckpointAsync just before +returning from IEventProcessor.ProcessEventsAsync is simple and convenient. + +* The other overload of CheckpointAsync takes an EventData as an argument +and sets a checkpoint with the position of the given event. This way, your +application can checkpoint at any interval. + +It is important to await CheckpointAsync, to be sure that the checkpoint +was actually persisted. + +### Special Considerations + +A checkpoint is a representation of a position in the event stream of a +particular Event Hub+consumer group+partition combination. All events up to and +including the checkpointed position are assumed to be processed, and any events +after the position are assumed to be unprocessed. The intention is that a +newly-created receiver will pick up where the previous receiver left off, for +example when Service Fabric moves the primary replica of a partition from one +node to another for load balancing. + +For performance reasons, it is not always desirable to checkpoint after processing +each event. Checkpointing at larger intervals (for example, every ten events, or +every ten seconds, etc.) can improve performance, but also means that if the +receiver is recreated, the new receiver may consume events that have already +been processed. It is up to the application owner to evaluate the tradeoff between +performance and reprocessing events. + +Even if the application checkpoints after every event, that still +does not completely prevent the possibility of reprocessing an already-consumed +event, because there is always a time window between when an event is processed +and when the checkpoint is fully persisted, during which a node could fail. As a +best practice, an application must be able to cope with event reprocessing in some +way that is reasonable for the impact. diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ReliableDictionaryCheckpointMananger.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ReliableDictionaryCheckpointMananger.cs new file mode 100644 index 0000000..57e91c4 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ReliableDictionaryCheckpointMananger.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using Microsoft.ServiceFabric.Data; + using Microsoft.ServiceFabric.Data.Collections; + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class ReliableDictionaryCheckpointMananger : ICheckpointMananger + { + private IReliableStateManager reliableStateManager = null; + private IReliableDictionary> store = null; + + internal ReliableDictionaryCheckpointMananger(IReliableStateManager rsm) + { + this.reliableStateManager = rsm; + } + + public async Task CheckpointStoreExistsAsync(CancellationToken cancellationToken) + { + ConditionalValue> tryStore = await + this.reliableStateManager.TryGetAsync>(Constants.CheckpointDictionaryName); + EventProcessorEventSource.Current.Message($"CheckpointStoreExistsAsync = {tryStore.HasValue}"); + return tryStore.HasValue; + } + + public async Task CreateCheckpointStoreIfNotExistsAsync(CancellationToken cancellationToken) + { + // Create or get access to the dictionary. + this.store = await reliableStateManager.GetOrAddAsync>>(Constants.CheckpointDictionaryName); + EventProcessorEventSource.Current.Message("CreateCheckpointStoreIfNotExistsAsync OK"); + return true; + } + + public async Task CreateCheckpointIfNotExistsAsync(string partitionId, CancellationToken cancellationToken) + { + Checkpoint existingCheckpoint = await GetWithRetry(partitionId, cancellationToken); + + if (existingCheckpoint == null) + { + existingCheckpoint = new Checkpoint(1); + await PutWithRetry(partitionId, existingCheckpoint, cancellationToken); + } + EventProcessorEventSource.Current.Message("CreateCheckpointIfNotExists OK"); + + return existingCheckpoint; + } + + public async Task GetCheckpointAsync(string partitionId, CancellationToken cancellationToken) + { + return await GetWithRetry(partitionId, cancellationToken); + } + + public async Task UpdateCheckpointAsync(string partitionId, Checkpoint checkpoint, CancellationToken cancellationToken) + { + await PutWithRetry(partitionId, checkpoint, cancellationToken); + } + + // Throws on error or if cancelled. + // Returns null if there is no entry for the given partition. + private async Task GetWithRetry(string partitionId, CancellationToken cancellationToken) + { + EventProcessorEventSource.Current.Message($"Getting checkpoint for {partitionId}"); + + Checkpoint result = null; + Exception lastException = null; + for (int i = 0; i < Constants.RetryCount; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + lastException = null; + + try + { + using (ITransaction tx = this.reliableStateManager.CreateTransaction()) + { + ConditionalValue> rawCheckpoint = await + this.store.TryGetValueAsync(tx, partitionId, Constants.ReliableDictionaryTimeout, cancellationToken); + + await tx.CommitAsync(); + + // Success! Save the result, if any, and break out of the retry loop. + if (rawCheckpoint.HasValue) + { + result = Checkpoint.CreateFromDictionary(rawCheckpoint.Value); + } + else + { + result = null; + } + break; + } + } + catch (TimeoutException e) + { + lastException = e; + } + } + + if (lastException != null) + { + // Ran out of retries, throw. + throw new Exception("Ran out of retries creating checkpoint", lastException); + } + + if (result != null) + { + EventProcessorEventSource.Current.Message($"Got checkpoint for {partitionId}: {result.Offset}//{result.SequenceNumber}"); + } + else + { + EventProcessorEventSource.Current.Message($"No checkpoint found for {partitionId}: returning null"); + } + + return result; + } + + private async Task PutWithRetry(string partitionId, Checkpoint checkpoint, CancellationToken cancellationToken) + { + EventProcessorEventSource.Current.Message($"Setting checkpoint for {partitionId}: {checkpoint.Offset}//{checkpoint.SequenceNumber}"); + + Exception lastException = null; + for (int i = 0; i < Constants.RetryCount; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + lastException = null; + + Dictionary putThis = checkpoint.ToDictionary(); + + try + { + using (ITransaction tx = this.reliableStateManager.CreateTransaction()) + { + await this.store.SetAsync(tx, partitionId, putThis, Constants.ReliableDictionaryTimeout, cancellationToken); + await tx.CommitAsync(); + + // Success! Break out of the retry loop. + break; + } + } + catch (TimeoutException e) + { + lastException = e; + } + } + + if (lastException != null) + { + // Ran out of retries, throw. + throw new Exception("Ran out of retries creating checkpoint", lastException); + } + + EventProcessorEventSource.Current.Message($"Set checkpoint for {partitionId} OK"); + } + } +} diff --git a/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ServiceFabricProcessor.cs b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ServiceFabricProcessor.cs new file mode 100644 index 0000000..dd87c11 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs.ServiceFabricProcessor/ServiceFabricProcessor.cs @@ -0,0 +1,464 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information.using System; + +namespace Microsoft.Azure.EventHubs.ServiceFabricProcessor +{ + using System; + using System.Collections.Generic; + using System.Fabric; + using System.Fabric.Description; + using System.Fabric.Query; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.ServiceFabric.Data; + + /// + /// Base class that implements event processor functionality. + /// + public class ServiceFabricProcessor : IPartitionReceiveHandler + { + // Service Fabric objects initialized in constructor + private readonly IReliableStateManager serviceStateManager; + private readonly Uri serviceFabricServiceName; + private readonly Guid serviceFabricPartitionId; + private readonly IStatefulServicePartition servicePartition; + + // ServiceFabricProcessor settings initialized in constructor + private readonly IEventProcessor userEventProcessor; + private readonly EventProcessorOptions options; + private readonly ICheckpointMananger checkpointManager; + + // Initialized during RunAsync startup + private int fabricPartitionOrdinal = -1; + private int servicePartitionCount = -1; + private string hubPartitionId; + private PartitionContext partitionContext; + private string initialOffset; + private CancellationTokenSource internalCanceller; + private Exception internalFatalException; + private CancellationToken linkedCancellationToken; + private EventHubsConnectionStringBuilder ehConnectionString; + private string consumerGroupName; + + // Value managed by RunAsync + private int running = 0; + + + /// + /// Constructor. Arguments break down into three groups: (1) Service Fabric objects so this library can access + /// Service Fabric facilities, (2) Event Hub-related arguments which indicate what event hub to receive from and + /// how to process the events, and (3) advanced, which right now consists only of the ability to replace the default + /// reliable dictionary-based checkpoint manager with a user-provided implementation. + /// + /// Service Fabric Uri found in StatefulServiceContext + /// Service Fabric partition id found in StatefulServiceContext + /// Service Fabric-provided state manager, provides access to reliable dictionaries + /// Service Fabric-provided partition information + /// User's event processor implementation + /// Connection string for user's event hub + /// Name of event hub consumer group to receive from + /// Optional: Options structure for ServiceFabricProcessor library + /// Very advanced/optional: user-provided checkpoint manager implementation + public ServiceFabricProcessor(Uri serviceFabricServiceName, Guid serviceFabricPartitionId, IReliableStateManager stateManager, IStatefulServicePartition partition, IEventProcessor userEventProcessor, + string eventHubConnectionString, string eventHubConsumerGroup, + EventProcessorOptions options = null, ICheckpointMananger checkpointManager = null) + { + if (serviceFabricServiceName == null) + { + throw new ArgumentNullException("serviceFabricServiceName is null"); + } + if (serviceFabricPartitionId == null) + { + throw new ArgumentNullException("serviceFabricPartitionId is null"); + } + if (stateManager == null) + { + throw new ArgumentNullException("stateManager is null"); + } + if (partition == null) + { + throw new ArgumentNullException("partition is null"); + } + if (userEventProcessor == null) + { + throw new ArgumentNullException("userEventProcessor is null"); + } + if (string.IsNullOrEmpty(eventHubConnectionString)) + { + throw new ArgumentException("eventHubConnectionString is null or empty"); + } + if (string.IsNullOrEmpty(eventHubConsumerGroup)) + { + throw new ArgumentException("eventHubConsumerGroup is null or empty"); + } + + this.serviceFabricServiceName = serviceFabricServiceName; + this.serviceFabricPartitionId = serviceFabricPartitionId; + this.serviceStateManager = stateManager; + this.servicePartition = partition; + + this.userEventProcessor = userEventProcessor; + + this.ehConnectionString = new EventHubsConnectionStringBuilder(eventHubConnectionString); + this.consumerGroupName = eventHubConsumerGroup; + + this.options = options ?? new EventProcessorOptions(); + this.checkpointManager = checkpointManager ?? new ReliableDictionaryCheckpointMananger(this.serviceStateManager); + + this.EventHubClientFactory = new EventHubWrappers.EventHubClientFactory(); + this.TestMode = false; + } + + /// + /// For testing purposes. Do not change after calling RunAsync. + /// + public EventHubWrappers.IEventHubClientFactory EventHubClientFactory { get; set; } + + /// + /// For testing purposes. Do not change after calling RunAsync. + /// + public bool TestMode { get; set; } + + /// + /// Starts processing of events. + /// + /// Cancellation token provided by Service Fabric, assumed to indicate instance shutdown when cancelled. + /// Task that completes when event processing shuts down. + public async Task RunAsync(CancellationToken fabricCancellationToken) + { + if (Interlocked.Exchange(ref this.running, 1) == 1) + { + EventProcessorEventSource.Current.Message("Already running"); + throw new InvalidOperationException("EventProcessorService.RunAsync has already been called."); + } + + this.internalCanceller = new CancellationTokenSource(); + this.internalFatalException = null; + + try + { + using (CancellationTokenSource linkedCanceller = CancellationTokenSource.CreateLinkedTokenSource(fabricCancellationToken, this.internalCanceller.Token)) + { + this.linkedCancellationToken = linkedCanceller.Token; + + await InnerRunAsync(); + + this.options.NotifyOnShutdown(null); + } + } + catch (Exception e) + { + // If InnerRunAsync throws, that is intended to be a fatal exception for this instance. + // Catch it here just long enough to log and notify, then rethrow. + + EventProcessorEventSource.Current.Message("THROWING OUT: {0}", e); + if (e.InnerException != null) + { + EventProcessorEventSource.Current.Message("THROWING OUT INNER: {0}", e.InnerException); + } + this.options.NotifyOnShutdown(e); + throw e; + } + } + + private async Task InnerRunAsync() + { + EventHubWrappers.IEventHubClient ehclient = null; + EventHubWrappers.IPartitionReceiver receiver = null; + + try + { + // + // Get Service Fabric partition information. + // + await GetServicePartitionId(this.linkedCancellationToken); + + // + // Create EventHubClient and check partition count. + // + Exception lastException = null; + EventProcessorEventSource.Current.Message("Creating event hub client"); + lastException = RetryWrapper(() => { ehclient = this.EventHubClientFactory.CreateFromConnectionString(this.ehConnectionString.ToString()); }); + if (ehclient == null) + { + EventProcessorEventSource.Current.Message("Out of retries event hub client"); + throw new Exception("Out of retries creating EventHubClient", lastException); + } + EventProcessorEventSource.Current.Message("Event hub client OK"); + EventProcessorEventSource.Current.Message("Getting event hub info"); + EventHubRuntimeInformation ehInfo = null; + // Lambda MUST be synchronous to work with RetryWrapper! + lastException = RetryWrapper(() => { ehInfo = ehclient.GetRuntimeInformationAsync().Result; }); + if (ehInfo == null) + { + EventProcessorEventSource.Current.Message("Out of retries getting event hub info"); + throw new Exception("Out of retries getting event hub runtime info", lastException); + } + if (this.TestMode) + { + if (this.servicePartitionCount > ehInfo.PartitionCount) + { + EventProcessorEventSource.Current.Message("TestMode requires event hub partition count larger than service partitinon count"); + throw new EventProcessorConfigurationException("TestMode requires event hub partition count larger than service partitinon count"); + } + else if (this.servicePartitionCount < ehInfo.PartitionCount) + { + EventProcessorEventSource.Current.Message("TestMode: receiving from subset of event hub"); + } + } + else if (ehInfo.PartitionCount != this.servicePartitionCount) + { + EventProcessorEventSource.Current.Message($"Service partition count {this.servicePartitionCount} does not match event hub partition count {ehInfo.PartitionCount}"); + throw new EventProcessorConfigurationException($"Service partition count {this.servicePartitionCount} does not match event hub partition count {ehInfo.PartitionCount}"); + } + this.hubPartitionId = ehInfo.PartitionIds[this.fabricPartitionOrdinal]; + + // + // Generate a PartitionContext now that the required info is available. + // + this.partitionContext = new PartitionContext(this.linkedCancellationToken, this.hubPartitionId, this.ehConnectionString.EntityPath, this.consumerGroupName, this.checkpointManager); + + // + // Start up checkpoint manager. + // + await CheckpointStartup(this.linkedCancellationToken); + + // + // If there was a checkpoint, the offset is in this.initialOffset, so convert it to an EventPosition. + // If no checkpoint, get starting point from user-supplied provider. + // + EventPosition initialPosition = null; + if (this.initialOffset != null) + { + EventProcessorEventSource.Current.Message($"Initial position from checkpoint, offset {this.initialOffset}"); + initialPosition = EventPosition.FromOffset(this.initialOffset); + } + else + { + initialPosition = this.options.InitialPositionProvider(this.hubPartitionId); + EventProcessorEventSource.Current.Message("Initial position from provider"); + } + + // + // Create receiver. + // + EventProcessorEventSource.Current.Message("Creating receiver"); + lastException = RetryWrapper(() => { receiver = ehclient.CreateEpochReceiver(this.consumerGroupName, this.hubPartitionId, initialPosition, this.initialOffset, + Constants.FixedReceiverEpoch, this.options.ClientReceiverOptions); }); + if (receiver == null) + { + EventProcessorEventSource.Current.Message("Out of retries creating receiver"); + throw new Exception("Out of retries creating event hub receiver", lastException); + } + + // + // Call Open on user's event processor instance. + // If user's Open code fails, treat that as a fatal exception and let it throw out. + // + EventProcessorEventSource.Current.Message("Creating event processor"); + await this.userEventProcessor.OpenAsync(this.linkedCancellationToken, this.partitionContext); + EventProcessorEventSource.Current.Message("Event processor created and opened OK"); + + // + // Start metrics reporting. This runs as a separate background thread. + // + Thread t = new Thread(this.MetricsHandler); + t.Start(); + + // + // Receive pump. + // + EventProcessorEventSource.Current.Message("RunAsync setting handler and waiting"); + this.MaxBatchSize = this.options.MaxBatchSize; + receiver.SetReceiveHandler(this, this.options.InvokeProcessorAfterReceiveTimeout); + this.linkedCancellationToken.WaitHandle.WaitOne(); + + EventProcessorEventSource.Current.Message("RunAsync continuing, cleanup"); + } + finally + { + if (this.partitionContext != null) + { + await this.userEventProcessor.CloseAsync(this.partitionContext, this.linkedCancellationToken.IsCancellationRequested ? CloseReason.Cancelled : CloseReason.Failure); + } + if (receiver != null) + { + receiver.SetReceiveHandler(null); + await receiver.CloseAsync(); + } + if (ehclient != null) + { + await ehclient.CloseAsync(); + } + if (this.internalFatalException != null) + { + throw this.internalFatalException; + } + } + } + + private EventHubsException RetryWrapper(Action action) + { + EventHubsException lastException = null; + + for (int i = 0; i < Constants.RetryCount; i++) + { + this.linkedCancellationToken.ThrowIfCancellationRequested(); + try + { + action.Invoke(); + break; + } + catch (EventHubsException e) + { + if (!e.IsTransient) + { + throw e; + } + lastException = e; + } + } + + return lastException; + } + + /// + /// From IPartitionReceiveHandler + /// + public int MaxBatchSize { get; set; } + + async Task IPartitionReceiveHandler.ProcessEventsAsync(IEnumerable events) + { + IEnumerable effectiveEvents = events ?? new List(); // convert to empty list if events is null + + if (events != null) + { + // Save position of last event if we got a real list of events + IEnumerator scanner = effectiveEvents.GetEnumerator(); + EventData last = null; + while (scanner.MoveNext()) + { + last = scanner.Current; + } + if (last != null) + { + this.partitionContext.SetOffsetAndSequenceNumber(last); + if (this.options.EnableReceiverRuntimeMetric) + { + // TODO: requires client change to support + // this.partitionContext.RuntimeInformation.Update(last); + } + } + } + + await this.userEventProcessor.ProcessEventsAsync(this.linkedCancellationToken, this.partitionContext, effectiveEvents); + + foreach (EventData ev in effectiveEvents) + { + ev.Dispose(); + } + } + + Task IPartitionReceiveHandler.ProcessErrorAsync(Exception error) + { + EventProcessorEventSource.Current.Message($"RECEIVE EXCEPTION on {this.hubPartitionId}: {error}"); + this.userEventProcessor.ProcessErrorAsync(this.partitionContext, error); + if (error is EventHubsException) + { + if (!(error as EventHubsException).IsTransient) + { + this.internalFatalException = error; + this.internalCanceller.Cancel(); + } + // else don't cancel on transient errors + } + else + { + // All other exceptions are assumed fatal. + this.internalFatalException = error; + this.internalCanceller.Cancel(); + } + return Task.CompletedTask; + } + + private async Task CheckpointStartup(CancellationToken cancellationToken) + { + // Set up store and get checkpoint, if any. + await this.checkpointManager.CreateCheckpointStoreIfNotExistsAsync(cancellationToken); + Checkpoint checkpoint = await this.checkpointManager.CreateCheckpointIfNotExistsAsync(this.hubPartitionId, cancellationToken); + if (!checkpoint.Valid) + { + // Not actually any existing checkpoint. + this.initialOffset = null; + EventProcessorEventSource.Current.Message("No checkpoint"); + } + else if (checkpoint.Version == 1) + { + this.initialOffset = checkpoint.Offset; + EventProcessorEventSource.Current.Message($"Checkpoint provides initial offset {this.initialOffset}"); + } + else + { + // It's actually a later-version checkpoint but we don't know the details. + // Access it via the V1 interface and hope it does something sensible. + this.initialOffset = checkpoint.Offset; + EventProcessorEventSource.Current.Message($"Unexpected checkpoint version {checkpoint.Version}, provided initial offset {this.initialOffset}"); + } + } + + private async Task GetServicePartitionId(CancellationToken cancellationToken) + { + if (this.fabricPartitionOrdinal == -1) + { + using (FabricClient fabricClient = new FabricClient()) + { + ServicePartitionList partitionList = + await fabricClient.QueryManager.GetPartitionListAsync(this.serviceFabricServiceName); + + // Set the number of partitions + this.servicePartitionCount = partitionList.Count; + + // Which partition is this one? + for (int a = 0; a < partitionList.Count; a++) + { + if (partitionList[a].PartitionInformation.Id == this.serviceFabricPartitionId) + { + this.fabricPartitionOrdinal = a; + break; + } + } + + EventProcessorEventSource.Current.Message($"Total partitions {this.servicePartitionCount}"); + } + } + } + + private void MetricsHandler() + { + EventProcessorEventSource.Current.Message("METRIC reporter starting"); + + while (!this.linkedCancellationToken.IsCancellationRequested) + { + Dictionary userMetrics = this.userEventProcessor.GetLoadMetric(this.linkedCancellationToken, this.partitionContext); + + try + { + List reportableMetrics = new List(); + foreach (KeyValuePair metric in userMetrics) + { + EventProcessorEventSource.Current.Message($"METRIC {metric.Key} for partition {this.partitionContext.PartitionId} is {metric.Value}"); + reportableMetrics.Add(new LoadMetric(metric.Key, metric.Value)); + } + this.servicePartition.ReportLoad(reportableMetrics); + Task.Delay(Constants.MetricReportingInterval, this.linkedCancellationToken).Wait(); // throws on cancel + } + catch (Exception e) + { + EventProcessorEventSource.Current.Message($"METRIC partition {this.partitionContext.PartitionId} exception {e}"); + } + } + + EventProcessorEventSource.Current.Message("METRIC reporter exiting"); + } + } +} diff --git a/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLink.cs b/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLink.cs index 0507732..3add249 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLink.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLink.cs @@ -16,20 +16,8 @@ public ActiveClientLink(AmqpLink link, string audience, string endpointUri, stri this.link = link; } - public AmqpLink Link - { - get - { - return this.link; - } - } + public AmqpLink Link => this.link; - public override AmqpConnection Connection - { - get - { - return this.link.Session.Connection; - } - } + public override AmqpConnection Connection => this.link.Session.Connection; } } \ No newline at end of file diff --git a/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLinkManager.cs b/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLinkManager.cs index fd871d9..face41a 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLinkManager.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLinkManager.cs @@ -21,7 +21,7 @@ sealed class ActiveClientLinkManager public ActiveClientLinkManager(AmqpEventHubClient eventHubClient) { this.eventHubClient = eventHubClient; - this.validityTimer = new Timer(s => OnLinkExpiration(s), this, Timeout.Infinite, Timeout.Infinite); + this.validityTimer = new Timer(OnLinkExpiration, this, Timeout.Infinite, Timeout.Infinite); this.syncRoot = new object(); } @@ -30,7 +30,7 @@ public void SetActiveLink(ActiveClientLinkObject activeClientLink) lock (this.syncRoot) { this.activeClientLink = activeClientLink; - this.activeClientLink.LinkObject.Closed += new EventHandler(this.OnLinkClosed); + this.activeClientLink.LinkObject.Closed += this.OnLinkClosed; if (this.activeClientLink.LinkObject.State == AmqpObjectState.Opened && this.activeClientLink.IsClientToken) { @@ -68,7 +68,6 @@ static async void OnLinkExpiration(object state) ActiveClientLinkManager.SendTokenTimeout).ConfigureAwait(false); //DNX_TODO: MessagingClientEtwProvider.Provider.EventWriteAmqpManageLink("After SendToken", thisPtr.activeClientLink.LinkObject, validTo.ToString(CultureInfo.InvariantCulture)); - lock (thisPtr.syncRoot) { thisPtr.activeClientLink.AuthorizationValidToUtc = validTo; diff --git a/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLinkObject.cs b/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLinkObject.cs index 2c7831f..e6e4a7b 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLinkObject.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientLinkObject.cs @@ -15,7 +15,13 @@ abstract class ActiveClientLinkObject readonly AmqpObject amqpLinkObject; DateTime authorizationValidToUtc; - public ActiveClientLinkObject(AmqpObject amqpLinkObject, string audience, string endpointUri, string[] requiredClaims, bool isClientToken, DateTime authorizationValidToUtc) + protected ActiveClientLinkObject( + AmqpObject amqpLinkObject, + string audience, + string endpointUri, + string[] requiredClaims, + bool isClientToken, + DateTime authorizationValidToUtc) { this.amqpLinkObject = amqpLinkObject; this.audience = audience; @@ -25,43 +31,22 @@ public ActiveClientLinkObject(AmqpObject amqpLinkObject, string audience, string this.authorizationValidToUtc = authorizationValidToUtc; } - public bool IsClientToken - { - get { return this.isClientToken; } - } + public bool IsClientToken => this.isClientToken; - public string Audience - { - get { return this.audience; } - } + public string Audience => this.audience; - public string EndpointUri - { - get { return this.endpointUri; } - } + public string EndpointUri => this.endpointUri; - public string[] RequiredClaims - { - get { return (string[])this.requiredClaims.Clone(); } - } + public string[] RequiredClaims => (string[])this.requiredClaims.Clone(); public DateTime AuthorizationValidToUtc { - get { return this.authorizationValidToUtc; } - set { this.authorizationValidToUtc = value; } + get => this.authorizationValidToUtc; + set => this.authorizationValidToUtc = value; } - public AmqpObject LinkObject - { - get - { - return this.amqpLinkObject; - } - } + public AmqpObject LinkObject => this.amqpLinkObject; - public abstract AmqpConnection Connection - { - get; - } + public abstract AmqpConnection Connection { get; } } } diff --git a/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientRequestResponseLink.cs b/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientRequestResponseLink.cs index 846489f..687010c 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientRequestResponseLink.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/ActiveClientRequestResponseLink.cs @@ -16,20 +16,8 @@ public ActiveClientRequestResponseLink(RequestResponseAmqpLink link, string audi this.link = link; } - public RequestResponseAmqpLink Link - { - get - { - return this.link; - } - } + public RequestResponseAmqpLink Link => this.link; - public override AmqpConnection Connection - { - get - { - return this.link.Session.Connection; - } - } + public override AmqpConnection Connection => this.link.Session.Connection; } } \ No newline at end of file diff --git a/src/Microsoft.Azure.EventHubs/Amqp/AmqpClientConstants.cs b/src/Microsoft.Azure.EventHubs/Amqp/AmqpClientConstants.cs index e5c16ab..d7be6da 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/AmqpClientConstants.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/AmqpClientConstants.cs @@ -72,6 +72,7 @@ class AmqpClientConstants public const string ManagementPartitionLastEnqueuedOffset = "last_enqueued_offset"; public const string ManagementPartitionLastEnqueuedTimeUtc = "last_enqueued_time_utc"; public const string ManagementPartitionRuntimeInfoRetrievalTimeUtc = "runtime_info_retrieval_time_utc"; + public const string ManagementPartitionRuntimeInfoPartitionIsEmpty = "is_partition_empty"; // Response codes public const string ResponseStatusCode = "status-code"; diff --git a/src/Microsoft.Azure.EventHubs/Amqp/AmqpEventDataSender.cs b/src/Microsoft.Azure.EventHubs/Amqp/AmqpEventDataSender.cs index cfa9720..948e2ee 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/AmqpEventDataSender.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/AmqpEventDataSender.cs @@ -18,14 +18,9 @@ class AmqpEventDataSender : EventDataSender internal AmqpEventDataSender(AmqpEventHubClient eventHubClient, string partitionId) : base(eventHubClient, partitionId) { - if (!string.IsNullOrEmpty(partitionId)) - { - this.Path = $"{eventHubClient.EventHubName}/Partitions/{partitionId}"; - } - else - { - this.Path = eventHubClient.EventHubName; - } + this.Path = !string.IsNullOrEmpty(partitionId) + ? $"{eventHubClient.EventHubName}/Partitions/{partitionId}" + : eventHubClient.EventHubName; this.SendLinkManager = new FaultTolerantAmqpObject(this.CreateLinkAsync, this.CloseSession); this.clientLinkManager = new ActiveClientLinkManager((AmqpEventHubClient)this.EventHubClient); @@ -47,7 +42,7 @@ protected override async Task OnSendAsync(IEnumerable eventDatas, str bool shouldRetry; int retryCount = 0; - var timeoutHelper = new TimeoutHelper(this.EventHubClient.ConnectionStringBuilder.OperationTimeout, true); + var timeoutHelper = new TimeoutHelper(this.EventHubClient.ConnectionStringBuilder.OperationTimeout, startTimeout: true); do { @@ -71,7 +66,11 @@ protected override async Task OnSendAsync(IEnumerable eventDatas, str } } - Outcome outcome = await amqpLink.SendMessageAsync(amqpMessage, this.GetNextDeliveryTag(), AmqpConstants.NullBinary, timeoutHelper.RemainingTime()).ConfigureAwait(false); + Outcome outcome = await amqpLink.SendMessageAsync( + amqpMessage, + this.GetNextDeliveryTag(), + AmqpConstants.NullBinary, + timeoutHelper.RemainingTime()).ConfigureAwait(false); if (outcome.DescriptorCode != Accepted.Code) { Rejected rejected = (Rejected)outcome; @@ -121,7 +120,13 @@ async Task CreateLinkAsync(TimeSpan timeout) Uri address = new Uri(amqpEventHubClient.ConnectionStringBuilder.Endpoint, this.Path); string audience = address.AbsoluteUri; string resource = address.AbsoluteUri; - var expiresAt = await cbsLink.SendTokenAsync(cbsTokenProvider, address, audience, resource, new[] { ClaimConstants.Send }, timeoutHelper.RemainingTime()).ConfigureAwait(false); + var expiresAt = await cbsLink.SendTokenAsync( + cbsTokenProvider, + address, + audience, + resource, + new[] { ClaimConstants.Send }, + timeoutHelper.RemainingTime()).ConfigureAwait(false); AmqpSession session = null; try @@ -155,9 +160,7 @@ async Task CreateLinkAsync(TimeSpan timeout) expiresAt); this.MaxMessageSize = (long)activeClientLink.Link.Settings.MaxMessageSize(); - this.clientLinkManager.SetActiveLink(activeClientLink); - return link; } catch diff --git a/src/Microsoft.Azure.EventHubs/Amqp/AmqpEventHubClient.cs b/src/Microsoft.Azure.EventHubs/Amqp/AmqpEventHubClient.cs index fd8a596..551b03b 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/AmqpEventHubClient.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/AmqpEventHubClient.cs @@ -14,42 +14,37 @@ namespace Microsoft.Azure.EventHubs.Amqp sealed class AmqpEventHubClient : EventHubClient { const string CbsSaslMechanismName = "MSSBCBS"; - AmqpServiceClient managementServiceClient; // serviceClient that handles management calls + readonly Lazy managementServiceClient; // serviceClient that handles management calls public AmqpEventHubClient(EventHubsConnectionStringBuilder csb) - : base(csb) + : this(csb, + !string.IsNullOrWhiteSpace(csb.SharedAccessSignature) + ? TokenProvider.CreateSharedAccessSignatureTokenProvider(csb.SharedAccessSignature) + : TokenProvider.CreateSharedAccessSignatureTokenProvider(csb.SasKeyName, csb.SasKey)) { - this.ContainerId = Guid.NewGuid().ToString("N"); - this.AmqpVersion = new Version(1, 0, 0, 0); - this.MaxFrameSize = AmqpConstants.DefaultMaxFrameSize; - - if (!string.IsNullOrWhiteSpace(csb.SharedAccessSignature)) - { - this.InternalTokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(csb.SharedAccessSignature); - } - else - { - this.InternalTokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(csb.SasKeyName, csb.SasKey); - } - - this.CbsTokenProvider = new TokenProviderAdapter(this); - this.ConnectionManager = new FaultTolerantAmqpObject(this.CreateConnectionAsync, this.CloseConnection); } public AmqpEventHubClient( - Uri endpointAddress, - string entityPath, - ITokenProvider tokenProvider, + Uri endpointAddress, + string entityPath, + ITokenProvider tokenProvider, TimeSpan operationTimeout, EventHubs.TransportType transportType) - : base(new EventHubsConnectionStringBuilder(endpointAddress, entityPath, operationTimeout, transportType)) + : this(new EventHubsConnectionStringBuilder(endpointAddress, entityPath, operationTimeout, transportType), tokenProvider) + { + } + + private AmqpEventHubClient(EventHubsConnectionStringBuilder csb, ITokenProvider tokenProvider) + : base(csb) { this.ContainerId = Guid.NewGuid().ToString("N"); this.AmqpVersion = new Version(1, 0, 0, 0); this.MaxFrameSize = AmqpConstants.DefaultMaxFrameSize; this.InternalTokenProvider = tokenProvider; + this.CbsTokenProvider = new TokenProviderAdapter(this); - this.ConnectionManager = new FaultTolerantAmqpObject(this.CreateConnectionAsync, this.CloseConnection); + this.ConnectionManager = new FaultTolerantAmqpObject(this.CreateConnectionAsync, CloseConnection); + this.managementServiceClient = new Lazy(this.CreateAmpqServiceClient); } internal ICbsTokenProvider CbsTokenProvider { get; } @@ -66,7 +61,18 @@ public AmqpEventHubClient( internal override EventDataSender OnCreateEventSender(string partitionId) { - return new AmqpEventDataSender(this, partitionId); + var sender = new AmqpEventDataSender(this, partitionId); + + if (this.RegisteredPlugins.Count >= 1) + { + // register all the plugins + foreach (var plugin in this.RegisteredPlugins) + { + sender.RegisterPlugin(plugin.Value); + } + } + + return sender; } protected override PartitionReceiver OnCreateReceiver( @@ -82,51 +88,25 @@ protected override Task OnCloseAsync() return this.ConnectionManager.CloseAsync(); } - protected override async Task OnGetRuntimeInformationAsync() - { - var serviceClient = this.GetManagementServiceClient(); - var eventHubRuntimeInformation = await serviceClient.GetRuntimeInformationAsync().ConfigureAwait(false); - - return eventHubRuntimeInformation; - } - - protected override async Task OnGetPartitionRuntimeInformationAsync(string partitionId) + protected override Task OnGetRuntimeInformationAsync() { - var serviceClient = this.GetManagementServiceClient(); - var eventHubPartitionRuntimeInformation = await serviceClient. - GetPartitionRuntimeInformationAsync(partitionId).ConfigureAwait(false); - - return eventHubPartitionRuntimeInformation; + return this.managementServiceClient.Value.GetRuntimeInformationAsync(); } - internal AmqpServiceClient GetManagementServiceClient() + protected override Task OnGetPartitionRuntimeInformationAsync(string partitionId) { - if (this.managementServiceClient == null) - { - lock (ThisLock) - { - if (this.managementServiceClient == null) - { - this.managementServiceClient = new AmqpServiceClient(this, AmqpClientConstants.ManagementAddress); - } - - Fx.Assert(string.Equals(this.managementServiceClient.Address, AmqpClientConstants.ManagementAddress, StringComparison.OrdinalIgnoreCase), - "The address should match the address of managementServiceClient"); - } - } - - return this.managementServiceClient; + return this.managementServiceClient.Value.GetPartitionRuntimeInformationAsync(partitionId); } - internal static AmqpSettings CreateAmqpSettings( - Version amqpVersion, - bool useSslStreamSecurity, - bool hasTokenProvider, - string sslHostName = null, - bool useWebSockets = false, - bool sslStreamUpgrade = false, - NetworkCredential networkCredential = null, - bool forceTokenProvider = true) + static AmqpSettings CreateAmqpSettings( + Version amqpVersion, + bool useSslStreamSecurity, + bool hasTokenProvider, + string sslHostName = null, + bool useWebSockets = false, + bool sslStreamUpgrade = false, + NetworkCredential networkCredential = null, + bool forceTokenProvider = true) { var settings = new AmqpSettings(); if (useSslStreamSecurity && !useWebSockets && sslStreamUpgrade) @@ -200,13 +180,13 @@ static TransportSettings CreateWebSocketsTransportSettings(string hostName, IWeb Scheme = AmqpClientConstants.UriSchemeWss, Port = -1 // Port will be assigned on transport listener. }; - var ts = new WebSocketTransportSettings() + var ts = new WebSocketTransportSettings { Uri = uriBuilder.Uri }; // Proxy Uri provided? - if(webProxy != null) + if (webProxy != null) { ts.Proxy = webProxy; } @@ -231,7 +211,7 @@ async Task CreateConnectionAsync(TimeSpan timeout) { string hostName = this.ConnectionStringBuilder.Endpoint.Host; int port = this.ConnectionStringBuilder.Endpoint.Port; - bool useWebSockets = this.ConnectionStringBuilder.TransportType == Microsoft.Azure.EventHubs.TransportType.AmqpWebSockets; + bool useWebSockets = this.ConnectionStringBuilder.TransportType == EventHubs.TransportType.AmqpWebSockets; var timeoutHelper = new TimeoutHelper(timeout); var amqpSettings = CreateAmqpSettings( @@ -240,15 +220,9 @@ async Task CreateConnectionAsync(TimeSpan timeout) hasTokenProvider: true, useWebSockets: useWebSockets); - TransportSettings tpSettings = null; - if (useWebSockets) - { - tpSettings = CreateWebSocketsTransportSettings(hostName, this.WebProxy); - } - else - { - tpSettings = CreateTcpTlsTransportSettings(hostName, port); - } + TransportSettings tpSettings = useWebSockets + ? CreateWebSocketsTransportSettings(hostName, this.WebProxy) + : CreateTcpTlsTransportSettings(hostName, port); var initiator = new AmqpTransportInitiator(amqpSettings, tpSettings); var transport = await initiator.ConnectTaskAsync(timeoutHelper.RemainingTime()).ConfigureAwait(false); @@ -267,11 +241,19 @@ async Task CreateConnectionAsync(TimeSpan timeout) return connection; } - void CloseConnection(AmqpConnection connection) + static void CloseConnection(AmqpConnection connection) { connection.SafeClose(); } + AmqpServiceClient CreateAmpqServiceClient() + { + var client = new AmqpServiceClient(this, AmqpClientConstants.ManagementAddress); + Fx.Assert(string.Equals(client.Address, AmqpClientConstants.ManagementAddress, StringComparison.OrdinalIgnoreCase), + "The address should match the address of managementServiceClient"); + return client; + } + /// /// Provides an adapter from TokenProvider to ICbsTokenProvider for AMQP CBS usage. /// diff --git a/src/Microsoft.Azure.EventHubs/Amqp/AmqpExceptionHelper.cs b/src/Microsoft.Azure.EventHubs/Amqp/AmqpExceptionHelper.cs index 002aa08..d30f7da 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/AmqpExceptionHelper.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/AmqpExceptionHelper.cs @@ -6,7 +6,6 @@ namespace Microsoft.Azure.EventHubs.Amqp using System; using System.Collections.Generic; using System.Text.RegularExpressions; - using Microsoft.Azure.Amqp; using Microsoft.Azure.Amqp.Encoding; using Microsoft.Azure.Amqp.Framing; @@ -44,85 +43,85 @@ public static AmqpSymbol GetResponseErrorCondition(AmqpMessage response, AmqpRes { return (AmqpSymbol)condition; } - else + + // Most of the time we should have an error condition + foreach (var kvp in conditionToStatusMap) { - // Most of the time we should have an error condition - foreach (var kvp in conditionToStatusMap) + if (kvp.Value == statusCode) { - if (kvp.Value == statusCode) - { - return kvp.Key; - } + return kvp.Key; } - - return AmqpErrorCode.InternalError; } + + return AmqpErrorCode.InternalError; } public static Exception ToMessagingContract(Error error, bool connectionError = false) { - if (error == null) - { - return new EventHubsException(true, "Unknown error."); - } - - return ToMessagingContract(error.Condition.Value, error.Description, connectionError); + return error == null ? + new EventHubsException(true, "Unknown error.") + : ToMessagingContract(error.Condition.Value, error.Description, connectionError); } public static Exception ToMessagingContract(string condition, string message, bool connectionError = false) { if (string.Equals(condition, AmqpClientConstants.TimeoutError.Value)) { - return new TimeoutException(message); + return new EventHubsTimeoutException(message); } - else if (string.Equals(condition, AmqpErrorCode.NotFound.Value)) + + if (string.Equals(condition, AmqpErrorCode.NotFound.Value)) { if (message.ToLower().Contains("status-code: 404") || Regex.IsMatch(message, "The messaging entity .* could not be found")) { return new MessagingEntityNotFoundException(message); } - else - { - return new EventHubsCommunicationException(message); - } + + return new EventHubsCommunicationException(message); } - else if (string.Equals(condition, AmqpErrorCode.NotImplemented.Value)) + + if (string.Equals(condition, AmqpErrorCode.NotImplemented.Value)) { return new NotSupportedException(message); } - else if (string.Equals(condition, AmqpErrorCode.NotAllowed.Value)) + + if (string.Equals(condition, AmqpErrorCode.NotAllowed.Value)) { return new InvalidOperationException(message); } - else if (string.Equals(condition, AmqpErrorCode.UnauthorizedAccess.Value)) + + if (string.Equals(condition, AmqpErrorCode.UnauthorizedAccess.Value)) { return new UnauthorizedAccessException(message); } - else if (string.Equals(condition, AmqpClientConstants.ServerBusyError.Value)) + + if (string.Equals(condition, AmqpClientConstants.ServerBusyError.Value)) { return new ServerBusyException(message); } - else if (string.Equals(condition, AmqpClientConstants.ArgumentError.Value)) + + if (string.Equals(condition, AmqpClientConstants.ArgumentError.Value)) { return new ArgumentException(message); } - else if (string.Equals(condition, AmqpClientConstants.ArgumentOutOfRangeError.Value)) + + if (string.Equals(condition, AmqpClientConstants.ArgumentOutOfRangeError.Value)) { return new ArgumentOutOfRangeException(message); } - else if (string.Equals(condition, AmqpErrorCode.Stolen.Value)) + + if (string.Equals(condition, AmqpErrorCode.Stolen.Value)) { return new ReceiverDisconnectedException(message); } - else if (string.Equals(condition, AmqpErrorCode.ResourceLimitExceeded.Value)) + + if (string.Equals(condition, AmqpErrorCode.ResourceLimitExceeded.Value)) { return new QuotaExceededException(message); } - else - { - return new EventHubsException(true, message); - } + + return new EventHubsException(true, message); } } } diff --git a/src/Microsoft.Azure.EventHubs/Amqp/AmqpMessageConverter.cs b/src/Microsoft.Azure.EventHubs/Amqp/AmqpMessageConverter.cs index 176ee33..ab796c0 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/AmqpMessageConverter.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/AmqpMessageConverter.cs @@ -12,6 +12,7 @@ namespace Microsoft.Azure.EventHubs.Amqp using Microsoft.Azure.Amqp; using Microsoft.Azure.Amqp.Encoding; using Microsoft.Azure.Amqp.Framing; + using Microsoft.Azure.EventHubs.Primitives; static class AmqpMessageConverter { @@ -27,11 +28,7 @@ static class AmqpMessageConverter public static EventData AmqpMessageToEventData(AmqpMessage amqpMessage) { - if (amqpMessage == null) - { - throw Fx.Exception.ArgumentNull("amqpMessage"); - } - + Guard.ArgumentNotNull(nameof(amqpMessage), amqpMessage); EventData eventData = new EventData(StreamToBytes(amqpMessage.BodyStream)); UpdateEventDataHeaderAndProperties(amqpMessage, eventData); return eventData; @@ -39,10 +36,7 @@ public static EventData AmqpMessageToEventData(AmqpMessage amqpMessage) public static AmqpMessage EventDatasToAmqpMessage(IEnumerable eventDatas, string partitionKey) { - if (eventDatas == null) - { - throw new ArgumentNullException(nameof(eventDatas)); - } + Guard.ArgumentNotNull(nameof(eventDatas), eventDatas); AmqpMessage returnMessage = null; var dataCount = eventDatas.Count(); @@ -107,7 +101,7 @@ public static AmqpMessage EventDatasToAmqpMessage(IEnumerable eventDa public static AmqpMessage EventDataToAmqpMessage(EventData eventData) { - AmqpMessage amqpMessage = AmqpMessage.Create(new Data { Value = eventData.Body }); + AmqpMessage amqpMessage = AmqpMessage.Create(new Data { Value = eventData.Body }); UpdateAmqpMessageHeadersAndProperties(amqpMessage, null, eventData, true); return amqpMessage; @@ -174,25 +168,25 @@ static void UpdateEventDataHeaderAndProperties(AmqpMessage amqpMessage, EventDat if ((sections & SectionFlag.DeliveryAnnotations) != 0) { long lastSequenceNumber; - if (amqpMessage.DeliveryAnnotations.Map.TryGetValue(AmqpClientConstants.ManagementPartitionLastEnqueuedSequenceNumber, out lastSequenceNumber)) + if (amqpMessage.DeliveryAnnotations.Map.TryGetValue(AmqpClientConstants.ManagementPartitionLastEnqueuedSequenceNumber, out lastSequenceNumber)) { data.LastSequenceNumber = lastSequenceNumber; } string lastEnqueuedOffset; - if (amqpMessage.DeliveryAnnotations.Map.TryGetValue(AmqpClientConstants.ManagementPartitionLastEnqueuedOffset, out lastEnqueuedOffset)) + if (amqpMessage.DeliveryAnnotations.Map.TryGetValue(AmqpClientConstants.ManagementPartitionLastEnqueuedOffset, out lastEnqueuedOffset)) { data.LastEnqueuedOffset = lastEnqueuedOffset; } DateTime lastEnqueuedTime; - if (amqpMessage.DeliveryAnnotations.Map.TryGetValue(AmqpClientConstants.ManagementPartitionLastEnqueuedTimeUtc, out lastEnqueuedTime)) + if (amqpMessage.DeliveryAnnotations.Map.TryGetValue(AmqpClientConstants.ManagementPartitionLastEnqueuedTimeUtc, out lastEnqueuedTime)) { data.LastEnqueuedTime = lastEnqueuedTime; } DateTime retrievalTime; - if (amqpMessage.DeliveryAnnotations.Map.TryGetValue(AmqpClientConstants.ManagementPartitionRuntimeInfoRetrievalTimeUtc, out retrievalTime)) + if (amqpMessage.DeliveryAnnotations.Map.TryGetValue(AmqpClientConstants.ManagementPartitionRuntimeInfoRetrievalTimeUtc, out retrievalTime)) { data.RetrievalTime = retrievalTime; } diff --git a/src/Microsoft.Azure.EventHubs/Amqp/AmqpPartitionReceiver.cs b/src/Microsoft.Azure.EventHubs/Amqp/AmqpPartitionReceiver.cs index 7ed5380..bade778 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/AmqpPartitionReceiver.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/AmqpPartitionReceiver.cs @@ -5,7 +5,6 @@ namespace Microsoft.Azure.EventHubs.Amqp { using System; using System.Collections.Generic; - using System.Globalization; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Amqp; @@ -40,12 +39,12 @@ public AmqpPartitionReceiver( FaultTolerantAmqpObject ReceiveLinkManager { get; } - protected async override Task OnCloseAsync() + protected override async Task OnCloseAsync() { // Close any ReceiveHandler (this is safe if there is none) and the ReceiveLinkManager in parallel. - await this.ReceiveHandlerClose(); + await this.ReceiveHandlerClose().ConfigureAwait(false); this.clientLinkManager.Close(); - await this.ReceiveLinkManager.CloseAsync(); + await this.ReceiveLinkManager.CloseAsync().ConfigureAwait(false); } protected override async Task> OnReceiveAsync(int maxMessageCount, TimeSpan waitTime) @@ -112,9 +111,9 @@ await this.ReceiveLinkManager.GetOrCreateAsync( } else { - // Handle System.TimeoutException explicitly. - // We don't really want to to throw TimeoutException on this call. - if (ex is TimeoutException) + // Handle EventHubsTimeoutException explicitly. + // We don't really want to to throw EventHubsTimeoutException on this call. + if (ex is EventHubsTimeoutException) { break; } @@ -139,12 +138,8 @@ protected override void OnSetReceiveHandler(IPartitionReceiveHandler newReceiveH // Notify existing handler first (but don't wait). Task.Run(() => this.receiveHandler.ProcessErrorAsync(new OperationCanceledException("New handler has registered for this receiver."))) - .ContinueWith(t => - t.Exception.Handle(ex => - { - // We omit any failures from ProcessErrorAsync - return true; - }), TaskContinuationOptions.OnlyOnFaulted); + // We omit any failures from ProcessErrorAsync + .ContinueWith(t => t.Exception.Handle(ex => true), TaskContinuationOptions.OnlyOnFaulted); } this.receiveHandler = newReceiveHandler; @@ -206,10 +201,12 @@ async Task CreateLinkAsync(TimeSpan timeout) } // Create our Link - var linkSettings = new AmqpLinkSettings(); - linkSettings.Role = true; - linkSettings.TotalLinkCredit = (uint)this.PrefetchCount; - linkSettings.AutoSendFlow = this.PrefetchCount > 0; + var linkSettings = new AmqpLinkSettings + { + Role = true, + TotalLinkCredit = (uint)this.PrefetchCount, + AutoSendFlow = this.PrefetchCount > 0 + }; linkSettings.AddProperty(AmqpClientConstants.EntityTypeName, (int)MessagingEntityType.ConsumerGroup); linkSettings.Source = new Source { Address = address.AbsolutePath, FilterSet = filterMap }; linkSettings.Target = new Target { Address = this.ClientId }; @@ -219,9 +216,9 @@ async Task CreateLinkAsync(TimeSpan timeout) if (this.ReceiverRuntimeMetricEnabled) { linkSettings.DesiredCapabilities = new Multiple(new List - { - AmqpClientConstants.EnableReceiverRuntimeMetricName - }); + { + AmqpClientConstants.EnableReceiverRuntimeMetricName + }); } if (this.Epoch.HasValue) @@ -270,8 +267,7 @@ IList CreateFilters() if (this.EventPosition != null) { - filterMap = new List(); - filterMap.Add(new AmqpSelectorFilter(this.EventPosition.GetExpression())); + filterMap = new List { new AmqpSelectorFilter(this.EventPosition.GetExpression()) }; } return filterMap; @@ -305,11 +301,22 @@ async Task ReceivePumpAsync(CancellationToken cancellationToken, bool invokeWhen } catch (Exception e) { - EventHubsEventSource.Log.ReceiveHandlerExitingWithError(this.ClientId, this.PartitionId, e.Message); - await this.ReceiveHandlerProcessErrorAsync(e).ConfigureAwait(false); + // Omit any failures at exception handling. Pump should continue until cancellation is triggered. + try + { + EventHubsEventSource.Log.ReceiveHandlerExitingWithError(this.ClientId, this.PartitionId, e.Message); + await this.ReceiveHandlerProcessErrorAsync(e).ConfigureAwait(false); - // Avoid tight loop if Receieve call keeps faling. - await Task.Delay(100).ConfigureAwait(false); + // Avoid tight loop if Receieve call keeps faling. + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + catch { } + + // ReceiverDisconnectedException is a special case where we know we cannot recover the pump. + if (e is ReceiverDisconnectedException) + { + break; + } continue; } diff --git a/src/Microsoft.Azure.EventHubs/Amqp/AmqpSelectorFilter.cs b/src/Microsoft.Azure.EventHubs/Amqp/AmqpSelectorFilter.cs index 92b0afd..04ac81f 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/AmqpSelectorFilter.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/AmqpSelectorFilter.cs @@ -17,9 +17,6 @@ public AmqpSelectorFilter(string sqlExpression) this.Value = sqlExpression; } - public string SqlExpression - { - get { return (string)this.Value; } - } + public string SqlExpression => this.Value?.ToString(); } } diff --git a/src/Microsoft.Azure.EventHubs/Amqp/AmqpServiceClient.cs b/src/Microsoft.Azure.EventHubs/Amqp/AmqpServiceClient.cs index 6fea17d..adbe010 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/AmqpServiceClient.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/AmqpServiceClient.cs @@ -4,11 +4,6 @@ namespace Microsoft.Azure.EventHubs.Amqp.Management { using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.Azure.Amqp; using Microsoft.Azure.Amqp.Encoding; @@ -20,16 +15,16 @@ class AmqpServiceClient : ClientEntity readonly AmqpEventHubClient eventHubClient; readonly FaultTolerantAmqpObject link; + readonly AsyncLock tokenLock = new AsyncLock(); SecurityToken token; - AsyncLock tokenLock = new AsyncLock(); public AmqpServiceClient(AmqpEventHubClient eventHubClient, string address) : base("AmqpServiceClient-" + StringUtility.GetRandomString()) { this.eventHubClient = eventHubClient; this.Address = address; - this.link = new FaultTolerantAmqpObject(t => this.OpenLinkAsync(t), rrlink => rrlink.CloseAsync(TimeSpan.FromSeconds(10))); + this.link = new FaultTolerantAmqpObject(this.OpenLinkAsync, rrlink => rrlink.CloseAsync(TimeSpan.FromSeconds(10))); } AmqpMessage CreateGetRuntimeInformationRequest() @@ -127,13 +122,14 @@ public async Task GetPartitionRuntimeInform return new EventHubPartitionRuntimeInformation() { - Type = (string)infoMap[new MapKey("type")], - Path = (string)infoMap[new MapKey("name")], - PartitionId = (string)infoMap[new MapKey("partition")], - BeginSequenceNumber = (long)infoMap[new MapKey("begin_sequence_number")], - LastEnqueuedSequenceNumber = (long)infoMap[new MapKey("last_enqueued_sequence_number")], - LastEnqueuedOffset = (string)infoMap[new MapKey("last_enqueued_offset")], - LastEnqueuedTimeUtc = (DateTime)infoMap[new MapKey("last_enqueued_time_utc")] + Type = (string)infoMap[new MapKey(AmqpClientConstants.EntityTypeName)], + Path = (string)infoMap[new MapKey(AmqpClientConstants.EntityNameKey)], + PartitionId = (string)infoMap[new MapKey(AmqpClientConstants.PartitionNameKey)], + BeginSequenceNumber = (long)infoMap[new MapKey(AmqpClientConstants.ManagementPartitionBeginSequenceNumber)], + LastEnqueuedSequenceNumber = (long)infoMap[new MapKey(AmqpClientConstants.ManagementPartitionLastEnqueuedSequenceNumber)], + LastEnqueuedOffset = (string)infoMap[new MapKey(AmqpClientConstants.ManagementPartitionLastEnqueuedOffset)], + LastEnqueuedTimeUtc = (DateTime)infoMap[new MapKey(AmqpClientConstants.ManagementPartitionLastEnqueuedTimeUtc)], + IsEmpty = (bool)infoMap[new MapKey(AmqpClientConstants.ManagementPartitionRuntimeInfoPartitionIsEmpty)] }; } @@ -227,7 +223,6 @@ async Task OpenRequestResponseLinkAsync( { // Aborting the session will cleanup the link as well. session?.Abort(); - throw; } } diff --git a/src/Microsoft.Azure.EventHubs/Amqp/SerializationUtilities.cs b/src/Microsoft.Azure.EventHubs/Amqp/SerializationUtilities.cs index bbd66ef..43cada3 100644 --- a/src/Microsoft.Azure.EventHubs/Amqp/SerializationUtilities.cs +++ b/src/Microsoft.Azure.EventHubs/Amqp/SerializationUtilities.cs @@ -15,12 +15,12 @@ enum PropertyValueType Byte, SByte, Char, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double, Decimal, // Numeric types Boolean, Guid, String, Uri, DateTime, DateTimeOffset, TimeSpan, Stream, - Unknown, + Unknown } class SerializationUtilities { - readonly static Dictionary typeToIntMap = new Dictionary + static readonly Dictionary typeToIntMap = new Dictionary { { typeof(byte), PropertyValueType.Byte }, { typeof(sbyte), PropertyValueType.SByte }, diff --git a/src/Microsoft.Azure.EventHubs/Core/EventHubsPlugin.cs b/src/Microsoft.Azure.EventHubs/Core/EventHubsPlugin.cs new file mode 100644 index 0000000..ab2b793 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs/Core/EventHubsPlugin.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.EventHubs.Core +{ + using System.Threading.Tasks; + + /// + /// This class provides methods that can be overridden to manipulate messages for custom plugin functionality. + /// + public abstract class EventHubsPlugin + { + /// + /// Gets the name of the . + /// + /// This name is used to identify the plugin, and prevent a plugin from being registered multiple times. + public abstract string Name { get; } + + /// + /// Determines whether or an exception in the plugin should prevent a send or receive operation. + /// + public virtual bool ShouldContinueOnException => false; + + /// + /// This operation is called before an event is sent. + /// + /// The to be modified by the plugin + /// The modified event + public virtual Task BeforeEventSend(EventData eventData) + { + return Task.FromResult(eventData); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Azure.EventHubs/EventData.cs b/src/Microsoft.Azure.EventHubs/EventData.cs index f77e515..97d5c89 100644 --- a/src/Microsoft.Azure.EventHubs/EventData.cs +++ b/src/Microsoft.Azure.EventHubs/EventData.cs @@ -58,18 +58,12 @@ public EventData(ArraySegment arraySegment) /// Get the actual Payload/Data wrapped by EventData. /// This is intended to be used after receiving EventData using . /// - public ArraySegment Body - { - get; - } + public ArraySegment Body { get; } /// /// Application property bag /// - public IDictionary Properties - { - get; internal set; - } + public IDictionary Properties { get; internal set; } /// /// SystemProperties that are populated by EventHubService. @@ -77,7 +71,7 @@ public IDictionary Properties /// public SystemPropertiesCollection SystemProperties { - get; internal set; + get; set; } internal AmqpMessage AmqpMessage { get; set; } @@ -104,10 +98,7 @@ void Dispose(bool disposing) { if (disposing) { - if (this.AmqpMessage != null) - { - this.AmqpMessage.Dispose(); - } + AmqpMessage?.Dispose(); } disposed = true; @@ -123,6 +114,21 @@ internal SystemPropertiesCollection() { } + /// + /// Construct and initialize a new instance. + /// + /// + /// + /// + /// + public SystemPropertiesCollection(long sequenceNumber, DateTime enqueuedTimeUtc, string offset, string partitionKey) + { + this[ClientConstants.SequenceNumberName] = sequenceNumber; + this[ClientConstants.EnqueuedTimeUtcName] = enqueuedTimeUtc; + this[ClientConstants.OffsetName] = offset; + this[ClientConstants.PartitionKeyName] = partitionKey; + } + /// Gets the logical sequence number of the event within the partition stream of the Event Hub. public long SequenceNumber { @@ -133,10 +139,8 @@ public long SequenceNumber { return (long)value; } - else - { - throw new ArgumentException(Resources.MissingSystemProperty.FormatForUser(ClientConstants.SequenceNumberName)); - } + + throw new ArgumentException(Resources.MissingSystemProperty.FormatForUser(ClientConstants.SequenceNumberName)); } } @@ -151,10 +155,8 @@ public DateTime EnqueuedTimeUtc { return (DateTime)value; } - else - { - throw new ArgumentException(Resources.MissingSystemProperty.FormatForUser(ClientConstants.EnqueuedTimeUtcName)); - } + + throw new ArgumentException(Resources.MissingSystemProperty.FormatForUser(ClientConstants.EnqueuedTimeUtcName)); } } @@ -170,10 +172,8 @@ public string Offset { return (string)value; } - else - { - throw new ArgumentException(Resources.MissingSystemProperty.FormatForUser(ClientConstants.OffsetName)); - } + + throw new ArgumentException(Resources.MissingSystemProperty.FormatForUser(ClientConstants.OffsetName)); } } @@ -187,12 +187,11 @@ public string PartitionKey { return (string)value; } - else - { - return null; - } + + return null; } } } } } + diff --git a/src/Microsoft.Azure.EventHubs/EventDataBatch.cs b/src/Microsoft.Azure.EventHubs/EventDataBatch.cs index 8d60bbd..6971df9 100644 --- a/src/Microsoft.Azure.EventHubs/EventDataBatch.cs +++ b/src/Microsoft.Azure.EventHubs/EventDataBatch.cs @@ -7,6 +7,7 @@ namespace Microsoft.Azure.EventHubs using System.Collections.Generic; using Microsoft.Azure.Amqp; using Microsoft.Azure.EventHubs.Amqp; + using Microsoft.Azure.EventHubs.Primitives; /// A helper class for creating an IEnumerable<> taking into account the max size limit, so that the IEnumerable<> can be passed to the Send or SendAsync method of an to send the objects as a batch. public class EventDataBatch : IDisposable @@ -60,10 +61,7 @@ public int Count /// public bool TryAdd(EventData eventData) { - if (eventData == null) - { - throw new ArgumentNullException(nameof(eventData)); - } + Guard.ArgumentNotNull(nameof(eventData), eventData); this.ThrowIfDisposed(); long size = GetEventSizeForBatch(eventData); @@ -78,10 +76,7 @@ public bool TryAdd(EventData eventData) return true; } - internal string PartitionKey - { - get; set; - } + internal string PartitionKey { get; set; } long GetEventSizeForBatch(EventData eventData) { @@ -91,15 +86,10 @@ long GetEventSizeForBatch(EventData eventData) eventData.AmqpMessage = amqpMessage; // Calculate overhead depending on the message size. - if (eventData.AmqpMessage.SerializedMessageSize < 256) - { - // Overhead is smaller for messages smaller than 256 bytes. - return eventData.AmqpMessage.SerializedMessageSize + 5; - } - else - { - return eventData.AmqpMessage.SerializedMessageSize + 8; - } + // Overhead is smaller for messages smaller than 256 bytes. + long overhead = eventData.AmqpMessage.SerializedMessageSize < 256 ? 5 : 8; + + return eventData.AmqpMessage.SerializedMessageSize + overhead; } /// @@ -135,10 +125,9 @@ void ThrowIfDisposed() } } - /// Converts the batch to an IEnumerable of EventData objects that can be accepted by the - /// SendBatchAsync method. - /// Returns an IEnumerable of EventData objects. - internal IEnumerable ToEnumerable() + /// Returns the enumerator of EventData objects in the batch. + /// IEnumerable of EventData objects. + public IEnumerable ToEnumerable() { this.ThrowIfDisposed(); return this.eventDataList; diff --git a/src/Microsoft.Azure.EventHubs/EventDataDiagnosticExtensions.cs b/src/Microsoft.Azure.EventHubs/EventDataDiagnosticExtensions.cs index cb9aa08..7582d47 100644 --- a/src/Microsoft.Azure.EventHubs/EventDataDiagnosticExtensions.cs +++ b/src/Microsoft.Azure.EventHubs/EventDataDiagnosticExtensions.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.EventHubs using System; using System.Collections.Generic; using System.Diagnostics; + using Microsoft.Azure.EventHubs.Primitives; /// /// Diagnostic extension methods for . @@ -66,10 +67,7 @@ public static class EventDataDiagnosticExtensions public static Activity ExtractActivity(this EventData eventData, string activityName = null) { - if (eventData == null) - { - throw new ArgumentNullException(nameof(eventData)); - } + Guard.ArgumentNotNull(nameof(eventData), eventData); if (activityName == null) { diff --git a/src/Microsoft.Azure.EventHubs/EventDataSender.cs b/src/Microsoft.Azure.EventHubs/EventDataSender.cs index 508067a..449a3e4 100644 --- a/src/Microsoft.Azure.EventHubs/EventDataSender.cs +++ b/src/Microsoft.Azure.EventHubs/EventDataSender.cs @@ -3,6 +3,7 @@ namespace Microsoft.Azure.EventHubs { + using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -21,9 +22,12 @@ protected EventDataSender(EventHubClient eventHubClient, string partitionId) protected string PartitionId { get; } - public Task SendAsync(IEnumerable eventDatas, string partitionKey) + public async Task SendAsync(IEnumerable eventDatas, string partitionKey) { - return this.OnSendAsync(eventDatas, partitionKey); + var processedEvents = await this.ProcessEvents(eventDatas).ConfigureAwait(false); + + await this.OnSendAsync(processedEvents, partitionKey) + .ConfigureAwait(false); } protected abstract Task OnSendAsync(IEnumerable eventDatas, string partitionKey); @@ -40,10 +44,51 @@ internal static int ValidateEvents(IEnumerable eventDatas) return count; } - internal long MaxMessageSize + async Task ProcessEvent(EventData eventData) { - get; - set; + if (this.RegisteredPlugins == null || this.RegisteredPlugins.Count == 0) + return eventData; + + var processedEvent = eventData; + foreach (var plugin in this.RegisteredPlugins.Values) + { + try + { + EventHubsEventSource.Log.PluginCallStarted(plugin.Name, ClientId); + processedEvent = await plugin.BeforeEventSend(processedEvent).ConfigureAwait(false); + EventHubsEventSource.Log.PluginCallCompleted(plugin.Name, ClientId); + } + catch (Exception ex) + { + EventHubsEventSource.Log.PluginCallFailed(plugin.Name, ClientId, ex); + + if (!plugin.ShouldContinueOnException) + { + throw; + } + } + } + return processedEvent; } + + async Task> ProcessEvents(IEnumerable eventDatas) + { + if (this.RegisteredPlugins.Count < 1) + { + return eventDatas; + } + + var processedEventList = new List(); + foreach (var eventData in eventDatas) + { + var processedMessage = await this.ProcessEvent(eventData) + .ConfigureAwait(false); + processedEventList.Add(processedMessage); + } + + return processedEventList; + } + + internal long MaxMessageSize { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.Azure.EventHubs/EventHubClient.cs b/src/Microsoft.Azure.EventHubs/EventHubClient.cs index 782697b..85c79bf 100644 --- a/src/Microsoft.Azure.EventHubs/EventHubClient.cs +++ b/src/Microsoft.Azure.EventHubs/EventHubClient.cs @@ -9,6 +9,7 @@ namespace Microsoft.Azure.EventHubs using System.Net; using System.Threading.Tasks; using Microsoft.Azure.EventHubs.Amqp; + using Microsoft.Azure.EventHubs.Primitives; using Microsoft.IdentityModel.Clients.ActiveDirectory; /// @@ -17,12 +18,14 @@ namespace Microsoft.Azure.EventHubs /// public abstract class EventHubClient : ClientEntity { - EventDataSender innerSender; + readonly Lazy innerSender; bool closeCalled = false; internal EventHubClient(EventHubsConnectionStringBuilder csb) : base($"{nameof(EventHubClient)}{ClientEntity.GetNextId()}({csb.EntityPath})") { + this.innerSender = new Lazy(() => this.CreateEventSender()); + this.ConnectionStringBuilder = csb; this.EventHubName = csb.EntityPath; this.RetryPolicy = RetryPolicy.Default; @@ -35,27 +38,7 @@ internal EventHubClient(EventHubsConnectionStringBuilder csb) internal EventHubsConnectionStringBuilder ConnectionStringBuilder { get; } - /// - protected object ThisLock { get; } = new object(); - - EventDataSender InnerSender - { - get - { - if (this.innerSender == null) - { - lock (this.ThisLock) - { - if (this.innerSender == null) - { - this.innerSender = this.CreateEventSender(); - } - } - } - - return this.innerSender; - } - } + EventDataSender InnerSender => this.innerSender.Value; /// /// Creates a new instance of the Event Hubs client using the specified connection string. You can populate the EntityPath property with the name of the Event Hub. @@ -64,10 +47,7 @@ EventDataSender InnerSender /// public static EventHubClient CreateFromConnectionString(string connectionString) { - if (string.IsNullOrWhiteSpace(connectionString)) - { - throw Fx.Exception.ArgumentNullOrWhiteSpace(nameof(connectionString)); - } + Guard.ArgumentNotNullOrWhiteSpace(nameof(connectionString), connectionString); var csb = new EventHubsConnectionStringBuilder(connectionString); return Create(csb); @@ -83,38 +63,28 @@ public static EventHubClient CreateFromConnectionString(string connectionString) /// Transport type on connection. /// public static EventHubClient Create( - Uri endpointAddress, - string entityPath, - ITokenProvider tokenProvider, - TimeSpan? operationTimeout = null, + Uri endpointAddress, + string entityPath, + ITokenProvider tokenProvider, + TimeSpan? operationTimeout = null, TransportType transportType = TransportType.Amqp) { - if (endpointAddress == null) - { - throw Fx.Exception.ArgumentNull(nameof(endpointAddress)); - } - - if (string.IsNullOrWhiteSpace(entityPath)) - { - throw Fx.Exception.ArgumentNullOrWhiteSpace(nameof(entityPath)); - } - - if (tokenProvider == null) - { - throw Fx.Exception.ArgumentNull(nameof(tokenProvider)); - } + Guard.ArgumentNotNull(nameof(endpointAddress), endpointAddress); + Guard.ArgumentNotNull(nameof(tokenProvider), tokenProvider); + Guard.ArgumentNotNullOrWhiteSpace(nameof(entityPath), entityPath); EventHubsEventSource.Log.EventHubClientCreateStart(endpointAddress.Host, entityPath); EventHubClient eventHubClient = new AmqpEventHubClient( endpointAddress, entityPath, tokenProvider, - operationTimeout?? ClientConstants.DefaultOperationTimeout, + operationTimeout ?? ClientConstants.DefaultOperationTimeout, transportType); EventHubsEventSource.Log.EventHubClientCreateStop(eventHubClient.ClientId); return eventHubClient; } +#if !UAP10_0 && !IOS /// /// Creates a new instance of the Event Hubs client using the specified endpoint, entity path, AAD authentication context. /// @@ -126,20 +96,21 @@ public static EventHubClient Create( /// Transport type on connection. /// public static EventHubClient Create( - Uri endpointAddress, - string entityPath, + Uri endpointAddress, + string entityPath, AuthenticationContext authContext, ClientCredential clientCredential, TimeSpan? operationTimeout = null, TransportType transportType = TransportType.Amqp) { return Create( - endpointAddress, - entityPath, + endpointAddress, + entityPath, TokenProvider.CreateAadTokenProvider(authContext, clientCredential), operationTimeout, transportType); } +#endif /// /// Creates a new instance of the Event Hubs client using the specified endpoint, entity path, AAD authentication context. @@ -173,7 +144,7 @@ public static EventHubClient Create( transportType); } -#if !UAP10_0 +#if !UAP10_0 && !IOS /// /// Creates a new instance of the Event Hubs client using the specified endpoint, entity path, AAD authentication context. /// @@ -223,12 +194,15 @@ public static EventHubClient CreateWithManagedServiceIdentity( transportType); } - static EventHubClient Create(EventHubsConnectionStringBuilder csb) + /// + /// Creates a new instance of the Event Hubs client using the specified connection string builder. + /// + /// + /// + public static EventHubClient Create(EventHubsConnectionStringBuilder csb) { - if (string.IsNullOrWhiteSpace(csb.EntityPath)) - { - throw Fx.Exception.ArgumentNullOrWhiteSpace(nameof(csb.EntityPath)); - } + Guard.ArgumentNotNull(nameof(csb), csb); + Guard.ArgumentNotNullOrWhiteSpace(nameof(csb.EntityPath), csb.EntityPath); EventHubsEventSource.Log.EventHubClientCreateStart(csb.Endpoint.Host, csb.EntityPath); EventHubClient eventHubClient = new AmqpEventHubClient(csb); @@ -275,11 +249,7 @@ public sealed override async Task CloseAsync() /// public Task SendAsync(EventData eventData) { - if (eventData == null) - { - throw Fx.Exception.ArgumentNull(nameof(eventData)); - } - + Guard.ArgumentNotNull(nameof(eventData), eventData); return this.SendAsync(new[] { eventData }, null); } @@ -347,10 +317,8 @@ public Task SendAsync(IEnumerable eventDatas) /// public Task SendAsync(EventData eventData, string partitionKey) { - if (eventData == null || string.IsNullOrEmpty(partitionKey)) - { - throw Fx.Exception.ArgumentNull(eventData == null ? nameof(eventData) : nameof(partitionKey)); - } + Guard.ArgumentNotNull(nameof(eventData), eventData); + Guard.ArgumentNotNullOrWhiteSpace(nameof(partitionKey), partitionKey); return this.SendAsync(new[] { eventData }, partitionKey); } @@ -426,11 +394,7 @@ public async Task SendAsync(EventDataBatch eventDataBatch) /// public PartitionSender CreatePartitionSender(string partitionId) { - if (string.IsNullOrWhiteSpace(partitionId)) - { - throw Fx.Exception.ArgumentNullOrWhiteSpace(nameof(partitionId)); - } - + Guard.ArgumentNotNullOrWhiteSpace(nameof(partitionId), partitionId); return new PartitionSender(this, partitionId); } @@ -446,11 +410,7 @@ public PartitionSender CreatePartitionSender(string partitionId) /// public PartitionReceiver CreateReceiver(string consumerGroupName, string partitionId, EventPosition eventPosition, ReceiverOptions receiverOptions = null) { - if (eventPosition == null) - { - throw Fx.Exception.ArgumentNull(nameof(eventPosition)); - } - + Guard.ArgumentNotNull(nameof(eventPosition), eventPosition); return this.OnCreateReceiver(consumerGroupName, partitionId, eventPosition, null, receiverOptions); } @@ -471,11 +431,7 @@ public PartitionReceiver CreateReceiver(string consumerGroupName, string partiti /// public PartitionReceiver CreateEpochReceiver(string consumerGroupName, string partitionId, EventPosition eventPosition, long epoch, ReceiverOptions receiverOptions = null) { - if (eventPosition == null) - { - throw Fx.Exception.ArgumentNull(nameof(eventPosition)); - } - + Guard.ArgumentNotNull(nameof(eventPosition), eventPosition); return this.OnCreateReceiver(consumerGroupName, partitionId, eventPosition, epoch, receiverOptions); } @@ -506,11 +462,7 @@ public async Task GetRuntimeInformationAsync() /// Returns . public async Task GetPartitionRuntimeInformationAsync(string partitionId) { - if (string.IsNullOrWhiteSpace(partitionId)) - { - throw Fx.Exception.ArgumentNullOrWhiteSpace(nameof(partitionId)); - } - + Guard.ArgumentNotNullOrWhiteSpace(nameof(partitionId), partitionId); EventHubsEventSource.Log.GetEventHubPartitionRuntimeInformationStart(this.ClientId, partitionId); try @@ -534,7 +486,7 @@ public EventDataBatch CreateBatch() { return this.CreateBatch(new BatchOptions()); } - + /// Creates a batch where event data objects can be added for later SendAsync call. /// to define partition key and max message size. /// Returns . @@ -546,24 +498,16 @@ public EventDataBatch CreateBatch(BatchOptions options) /// Gets or sets a value indicating whether the runtime metric of a receiver is enabled. /// true if a client wants to access using . - public bool EnableReceiverRuntimeMetric - { - get; - set; - } + public bool EnableReceiverRuntimeMetric { get; set; } /// /// Gets or sets the web proxy. /// A proxy is applicable only when transport type is set to AmqpWebSockets. /// If not set, systemwide proxy settings will be honored. /// - public IWebProxy WebProxy - { - get; - set; - } + public IWebProxy WebProxy { get; set; } - internal bool CloseCalled { get => this.closeCalled; } + internal bool CloseCalled => this.closeCalled; internal EventDataSender CreateEventSender(string partitionId = null) { @@ -599,10 +543,9 @@ internal EventDataSender CreateEventSender(string partitionId = null) /// protected override void OnRetryPolicyUpdate() { - // Propagate retry policy updates to inner sender if there is any. - if (this.innerSender != null) + if (this.innerSender.IsValueCreated) { - this.innerSender.RetryPolicy = this.RetryPolicy.Clone(); + this.innerSender.Value.RetryPolicy = this.RetryPolicy.Clone(); } } } diff --git a/src/Microsoft.Azure.EventHubs/EventHubPartitionRuntimeInformation.cs b/src/Microsoft.Azure.EventHubs/EventHubPartitionRuntimeInformation.cs index b7394a6..d362b70 100644 --- a/src/Microsoft.Azure.EventHubs/EventHubPartitionRuntimeInformation.cs +++ b/src/Microsoft.Azure.EventHubs/EventHubPartitionRuntimeInformation.cs @@ -35,5 +35,8 @@ public class EventHubPartitionRuntimeInformation /// Gets the enqueued UTC time of the last event. /// The enqueued time of the last event. public DateTime LastEnqueuedTimeUtc { get; set; } + + /// Gets whether partition is empty or not. + public bool IsEmpty { get; set; } } } diff --git a/src/Microsoft.Azure.EventHubs/EventHubsDiagnosticSource.cs b/src/Microsoft.Azure.EventHubs/EventHubsDiagnosticSource.cs index a832de3..146f409 100644 --- a/src/Microsoft.Azure.EventHubs/EventHubsDiagnosticSource.cs +++ b/src/Microsoft.Azure.EventHubs/EventHubsDiagnosticSource.cs @@ -61,7 +61,7 @@ internal static Activity StartSendActivity(string clientId, EventHubsConnectionS DiagnosticListener.StartActivity(activity, new { - Endpoint = csb.Endpoint, + csb.Endpoint, Entity = csb.EntityPath, PartitionKey = partitionKey, EventDatas = eventDatas @@ -87,7 +87,7 @@ internal static void FailSendActivity(Activity activity, EventHubsConnectionStri DiagnosticListener.Write(SendActivityExceptionName, new { - Endpoint = csb.Endpoint, + csb.Endpoint, Entity = csb.EntityPath, PartitionKey = partitionKey, EventDatas = eventDatas, @@ -105,19 +105,19 @@ internal static void StopSendActivity(Activity activity, EventHubsConnectionStri DiagnosticListener.StopActivity(activity, new { - Endpoint = csb.Endpoint, + csb.Endpoint, Entity = csb.EntityPath, PartitionKey = partitionKey, EventDatas = eventDatas, - Status = sendTask?.Status + sendTask?.Status }); } internal static Activity StartReceiveActivity( - string clientId, - EventHubsConnectionStringBuilder csb, - string partitionKey, - string consumerGroup, + string clientId, + EventHubsConnectionStringBuilder csb, + string partitionKey, + string consumerGroup, EventPosition eventPosition) { // skip if diagnostic source not enabled @@ -168,7 +168,6 @@ internal static Activity StartReceiveActivity( internal static void FailReceiveActivity(Activity activity, EventHubsConnectionStringBuilder csb, string partitionKey, string consumerGroup, Exception ex) { // TODO consider enriching activity with data from exception - if (!DiagnosticListener.IsEnabled() || !DiagnosticListener.IsEnabled(ReceiveActivityExceptionName)) { return; @@ -177,7 +176,7 @@ internal static void FailReceiveActivity(Activity activity, EventHubsConnectionS DiagnosticListener.Write(ReceiveActivityExceptionName, new { - Endpoint = csb.Endpoint, + csb.Endpoint, Entity = csb.EntityPath, PartitionKey = partitionKey, ConsumerGroup = consumerGroup, @@ -198,12 +197,12 @@ internal static void StopReceiveActivity(Activity activity, EventHubsConnectionS DiagnosticListener.StopActivity(activity, new { - Endpoint = csb.Endpoint, + csb.Endpoint, Entity = csb.EntityPath, PartitionKey = partitionKey, ConsumerGroup = consumerGroup, EventDatas = events, - Status = receiveTask?.Status + receiveTask?.Status }); } @@ -237,11 +236,9 @@ private static void Inject(EventData eventData, string id, string correlationCon internal static string SerializeCorrelationContext(IList> baggage) { - if (baggage.Any()) - { - return string.Join(",", baggage.Select(kvp => kvp.Key + "=" + kvp.Value)); - } - return null; + return baggage.Any() + ? string.Join(",", baggage.Select(kvp => kvp.Key + "=" + kvp.Value)) + : null; } private static void SetRelatedOperations(Activity activity, IEnumerable eventDatas) diff --git a/src/Microsoft.Azure.EventHubs/EventHubsEventSource.cs b/src/Microsoft.Azure.EventHubs/EventHubsEventSource.cs index 29a3010..5cf3fee 100644 --- a/src/Microsoft.Azure.EventHubs/EventHubsEventSource.cs +++ b/src/Microsoft.Azure.EventHubs/EventHubsEventSource.cs @@ -3,6 +3,7 @@ namespace Microsoft.Azure.EventHubs { + using System; using System.Diagnostics.Tracing; /// @@ -209,6 +210,36 @@ public void ReceiveHandlerExitingWithError(string clientId, string partitionId, } } + // + // 100-120 reserved for Plugins traces + // + [Event(100, Level = EventLevel.Verbose, Message = "User plugin {0} called on client {1}")] + public void PluginCallStarted(string pluginName, string clientId) + { + if (this.IsEnabled()) + { + this.WriteEvent(100, pluginName, clientId); + } + } + + [Event(101, Level = EventLevel.Verbose, Message = "User plugin {0} completed on client {1}")] + public void PluginCallCompleted(string pluginName, string clientId) + { + if (this.IsEnabled()) + { + this.WriteEvent(101, pluginName, clientId); + } + } + + [Event(102, Level = EventLevel.Error, Message = "Exception during {0} plugin execution. clientId: {1}, Exception {2}")] + public void PluginCallFailed(string pluginName, string clientId, Exception exception) + { + if (this.IsEnabled()) + { + this.WriteEvent(102, pluginName, clientId, exception); + } + } + // TODO: Add Keywords if desired. //public class Keywords // This is a bitvector //{ diff --git a/src/Microsoft.Azure.EventHubs/EventPosition.cs b/src/Microsoft.Azure.EventHubs/EventPosition.cs index eea8f2e..2fa6b76 100644 --- a/src/Microsoft.Azure.EventHubs/EventPosition.cs +++ b/src/Microsoft.Azure.EventHubs/EventPosition.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.EventHubs using System; using Microsoft.Azure.Amqp; using Microsoft.Azure.EventHubs.Amqp; + using Microsoft.Azure.EventHubs.Primitives; /// /// Represents options can be set during the creation of a event hub receiver. @@ -39,7 +40,7 @@ public static EventPosition FromEnd() { return EventPosition.FromOffset(EndOfStream); } - + /// /// Creates a position at the given offset. /// @@ -48,12 +49,9 @@ public static EventPosition FromEnd() /// An object. public static EventPosition FromOffset(string offset, bool inclusive = false) { - if (string.IsNullOrEmpty(offset)) - { - throw new ArgumentNullException(nameof(offset)); - } - - return new EventPosition() { Offset = offset, IsInclusive = inclusive }; + Guard.ArgumentNotNullOrWhiteSpace(nameof(offset), offset); + + return new EventPosition { Offset = offset, IsInclusive = inclusive }; } /// @@ -64,7 +62,7 @@ public static EventPosition FromOffset(string offset, bool inclusive = false) /// An object. public static EventPosition FromSequenceNumber(long sequenceNumber, bool inclusive = false) { - return new EventPosition() { SequenceNumber = sequenceNumber, IsInclusive = inclusive }; + return new EventPosition { SequenceNumber = sequenceNumber, IsInclusive = inclusive }; } /// @@ -74,45 +72,32 @@ public static EventPosition FromSequenceNumber(long sequenceNumber, bool inclusi /// An object. public static EventPosition FromEnqueuedTime(DateTime enqueuedTimeUtc) { - return new EventPosition() { EnqueuedTimeUtc = enqueuedTimeUtc }; + return new EventPosition { EnqueuedTimeUtc = enqueuedTimeUtc }; } /// /// Gets the offset of the event at the position. It can be null if the position is just created /// from a sequence number or an enqueued time. /// - internal string Offset - { - get; set; - } + internal string Offset { get; set; } /// /// Indicates if the current event at the specified offset is included or not. /// It is only applicable if offset is set. /// - internal bool IsInclusive - { - get; set; - } + internal bool IsInclusive { get; set; } /// /// Gets the enqueued time of the event at the position. It can be null if the position is just created /// from an offset or a sequence number. /// - internal DateTime? EnqueuedTimeUtc - { - get; set; - } + internal DateTime? EnqueuedTimeUtc { get; set; } /// /// Gets the sequence number of the event at the position. It can be null if the position is just created /// from an offset or an enqueued time. /// - public long? SequenceNumber - { - get; - internal set; - } + public long? SequenceNumber { get; internal set; } internal string GetExpression() { @@ -140,9 +125,8 @@ internal string GetExpression() throw new ArgumentException("No starting position was set"); } - // This is equivalent to Microsoft.Azure.Amqp's internal API TimeStampEncoding.GetMilliseconds - long TimeStampEncodingGetMilliseconds(DateTime value) + static long TimeStampEncodingGetMilliseconds(DateTime value) { DateTime utcValue = value.ToUniversalTime(); double milliseconds = (utcValue - AmqpConstants.StartOfEpoch).TotalMilliseconds; diff --git a/src/Microsoft.Azure.EventHubs/Microsoft.Azure.EventHubs.csproj b/src/Microsoft.Azure.EventHubs/Microsoft.Azure.EventHubs.csproj index b3d0ecf..0717eaf 100644 --- a/src/Microsoft.Azure.EventHubs/Microsoft.Azure.EventHubs.csproj +++ b/src/Microsoft.Azure.EventHubs/Microsoft.Azure.EventHubs.csproj @@ -3,9 +3,9 @@ This is the next generation Azure Event Hubs .NET Standard client library. For more information about Event Hubs, see https://azure.microsoft.com/en-us/services/event-hubs/ Microsoft.Azure.EventHubs - 2.2.1 + 3.0.0 Microsoft - net461;netstandard2.0;uap10.0 + net461;netstandard2.0;uap10.0;Xamarin.iOS10 true Microsoft.Azure.EventHubs ../../build/keyfile.snk @@ -24,9 +24,9 @@ false full bin\$(Configuration)\$(TargetFramework)\Microsoft.Azure.EventHubs.xml - 2.2.1 - 2.2.1.0 - 2.2.1.0 + 3.0.0 + 3.0.0.0 + 3.0.0.0 false © Microsoft Corporation. All rights reserved. @@ -49,11 +49,15 @@ $(DefineConstants);NETSTANDARD2_0 + + $(DefineConstants);IOS + + - + @@ -67,17 +71,21 @@ + + + + + - + - + - - + True True @@ -92,4 +100,6 @@ + + diff --git a/src/Microsoft.Azure.EventHubs/PartitionReceiver.cs b/src/Microsoft.Azure.EventHubs/PartitionReceiver.cs index 73b5c39..8e217b4 100644 --- a/src/Microsoft.Azure.EventHubs/PartitionReceiver.cs +++ b/src/Microsoft.Azure.EventHubs/PartitionReceiver.cs @@ -57,9 +57,13 @@ protected internal PartitionReceiver( this.prefetchCount = DefaultPrefetchCount; this.Epoch = epoch; this.RuntimeInfo = new ReceiverRuntimeInformation(partitionId); - this.ReceiverRuntimeMetricEnabled = receiverOptions == null ? this.EventHubClient.EnableReceiverRuntimeMetric + this.ReceiverRuntimeMetricEnabled = receiverOptions == null + ? this.EventHubClient.EnableReceiverRuntimeMetric : receiverOptions.EnableReceiverRuntimeMetric; - this.Identifier = receiverOptions != null ? receiverOptions.Identifier : null; + + this.Identifier = receiverOptions != null + ? receiverOptions.Identifier + : null; this.RetryPolicy = eventHubClient.RetryPolicy.Clone(); EventHubsEventSource.Log.ClientCreated(this.ClientId, this.FormatTraceDetails()); @@ -87,10 +91,7 @@ protected internal PartitionReceiver( /// The upper limit of events this receiver will actively receive regardless of whether a receive operation is pending. public int PrefetchCount { - get - { - return this.prefetchCount; - } + get => this.prefetchCount; set { @@ -118,11 +119,7 @@ public int PrefetchCount /// Gets the identifier of a receiver which was set during the creation of the receiver. /// A string representing the identifier of a receiver. It will return null if the identifier is not set. - public string Identifier - { - get; - private set; - } + public string Identifier { get; private set; } /// /// Receive a batch of 's from an EventHub partition @@ -190,10 +187,7 @@ public async Task> ReceiveAsync(int maxMessageCount, Time // Update receiver runtime metrics? if (this.ReceiverRuntimeMetricEnabled) { - this.RuntimeInfo.LastSequenceNumber = lastEvent.LastSequenceNumber; - this.RuntimeInfo.LastEnqueuedOffset = lastEvent.LastEnqueuedOffset; - this.RuntimeInfo.LastEnqueuedTimeUtc = lastEvent.LastEnqueuedTime; - this.RuntimeInfo.RetrievalTime = lastEvent.RetrievalTime; + this.RuntimeInfo.Update(lastEvent); } } @@ -256,18 +250,10 @@ public sealed override Task CloseAsync() /// Gets the approximate receiver runtime information for a logical partition of an Event Hub. /// To enable the setting, refer to and /// - public ReceiverRuntimeInformation RuntimeInfo - { - get; - private set; - } + public ReceiverRuntimeInformation RuntimeInfo { get; private set; } /// Gets a value indicating whether the runtime metric of a receiver is enabled. - public bool ReceiverRuntimeMetricEnabled - { - get; - private set; - } + public bool ReceiverRuntimeMetricEnabled { get; private set; } /// /// diff --git a/src/Microsoft.Azure.EventHubs/PartitionSender.cs b/src/Microsoft.Azure.EventHubs/PartitionSender.cs index 0192b7b..ddc8944 100644 --- a/src/Microsoft.Azure.EventHubs/PartitionSender.cs +++ b/src/Microsoft.Azure.EventHubs/PartitionSender.cs @@ -7,6 +7,7 @@ namespace Microsoft.Azure.EventHubs using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; + using Microsoft.Azure.EventHubs.Primitives; /// /// This sender class is a logical representation of sending events to a specific EventHub partition. Do not use this class @@ -46,7 +47,7 @@ public EventDataBatch CreateBatch() { return this.CreateBatch(new BatchOptions()); } - + /// Creates a batch where event data objects can be added for later SendAsync call. /// to define partition key and max message size. /// Returns . @@ -77,11 +78,7 @@ public EventDataBatch CreateBatch(BatchOptions options) /// Event Hubs service encountered problems during the operation. public Task SendAsync(EventData eventData) { - if (eventData == null) - { - throw Fx.Exception.ArgumentNull(nameof(eventData)); - } - + Guard.ArgumentNotNull(nameof(eventData), eventData); return this.SendAsync(new[] { eventData }); } @@ -126,10 +123,7 @@ public Task SendAsync(EventData eventData) /// Event Hubs service encountered problems during the operation. public async Task SendAsync(IEnumerable eventDatas) { - if (eventDatas == null) - { - throw Fx.Exception.ArgumentNull(nameof(eventDatas)); - } + Guard.ArgumentNotNull(nameof(eventDatas), eventDatas); if (eventDatas is EventDataBatch && !string.IsNullOrEmpty(((EventDataBatch)eventDatas).PartitionKey)) { @@ -178,11 +172,11 @@ public async Task SendAsync(EventDataBatch eventDataBatch) await this.SendAsync(eventDataBatch.ToEnumerable()); } - + /// - /// Closes and releases resources for the . - /// - /// An asynchronous operation + /// Closes and releases resources for the . + /// + /// An asynchronous operation public override async Task CloseAsync() { EventHubsEventSource.Log.ClientCloseStart(this.ClientId); diff --git a/src/Microsoft.Azure.EventHubs/Primitives/AsyncLock.cs b/src/Microsoft.Azure.EventHubs/Primitives/AsyncLock.cs index 214107b..235a9ea 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/AsyncLock.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/AsyncLock.cs @@ -14,16 +14,15 @@ namespace Microsoft.Azure.EventHubs /// public class AsyncLock : IDisposable { - readonly SemaphoreSlim asyncSemaphore; + readonly SemaphoreSlim asyncSemaphore = new SemaphoreSlim(1); readonly Task lockRelease; - bool disposed = false; + bool disposed; /// /// Returns a new AsyncLock. /// public AsyncLock() { - asyncSemaphore = new SemaphoreSlim(1); lockRelease = Task.FromResult(new LockRelease(this)); } diff --git a/src/Microsoft.Azure.EventHubs/Primitives/AzureActiveDirectoryTokenProvider.cs b/src/Microsoft.Azure.EventHubs/Primitives/AzureActiveDirectoryTokenProvider.cs index 6877cc6..5fd5d8d 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/AzureActiveDirectoryTokenProvider.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/AzureActiveDirectoryTokenProvider.cs @@ -13,8 +13,9 @@ namespace Microsoft.Azure.EventHubs public class AzureActiveDirectoryTokenProvider : TokenProvider { readonly AuthenticationContext authContext; + +#if !UAP10_0 && !IOS readonly ClientCredential clientCredential; -#if !UAP10_0 readonly ClientAssertionCertificate clientAssertionCertificate; #endif readonly string clientId; @@ -32,6 +33,7 @@ enum AuthType readonly AuthType authType; +#if !UAP10_0 && !IOS internal AzureActiveDirectoryTokenProvider(AuthenticationContext authContext, ClientCredential credential) { this.clientCredential = credential; @@ -40,7 +42,6 @@ internal AzureActiveDirectoryTokenProvider(AuthenticationContext authContext, Cl this.clientId = clientCredential.ClientId; } -#if !UAP10_0 internal AzureActiveDirectoryTokenProvider(AuthenticationContext authContext, ClientAssertionCertificate clientAssertionCertificate) { this.clientAssertionCertificate = clientAssertionCertificate; @@ -72,16 +73,15 @@ public override async Task GetTokenAsync(string appliesTo, TimeSp switch (this.authType) { +#if !UAP10_0 && !IOS case AuthType.ClientCredential: authResult = await this.authContext.AcquireTokenAsync(ClientConstants.AadEventHubsAudience, this.clientCredential); break; -#if !UAP10_0 case AuthType.ClientAssertionCertificate: authResult = await this.authContext.AcquireTokenAsync(ClientConstants.AadEventHubsAudience, this.clientAssertionCertificate); break; #endif - case AuthType.InteractiveUserLogin: authResult = await this.authContext.AcquireTokenAsync(ClientConstants.AadEventHubsAudience, this.clientId, this.redirectUri, this.platformParameters, this.userIdentifier); break; diff --git a/src/Microsoft.Azure.EventHubs/Primitives/ClientEntity.cs b/src/Microsoft.Azure.EventHubs/Primitives/ClientEntity.cs index 9c5b560..5ade18d 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/ClientEntity.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/ClientEntity.cs @@ -1,11 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. + namespace Microsoft.Azure.EventHubs { using System; + using System.Collections.Concurrent; + using System.Linq; using System.Threading; using System.Threading.Tasks; + using Microsoft.Azure.EventHubs.Core; /// /// Contract for all client entities with Open-Close/Abort state m/c @@ -14,7 +18,6 @@ namespace Microsoft.Azure.EventHubs public abstract class ClientEntity { static int nextId; - RetryPolicy retryPolicy; /// @@ -33,14 +36,17 @@ public string ClientId } /// - /// Gets the for the ClientEntity. + /// Gets a list of currently registered plugins for this Client. + /// + public virtual ConcurrentDictionary RegisteredPlugins { get; } + = new ConcurrentDictionary(); + + /// + /// Gets the for the ClientEntity. /// public RetryPolicy RetryPolicy { - get - { - return this.retryPolicy; - } + get => this.retryPolicy; set { @@ -55,6 +61,44 @@ public RetryPolicy RetryPolicy /// The asynchronous operation public abstract Task CloseAsync(); + /// + /// Registers a to be used with this client. + /// + public virtual void RegisterPlugin(EventHubsPlugin eventHubsPlugin) + { + if (eventHubsPlugin == null) + { + throw new ArgumentNullException(nameof(eventHubsPlugin), Resources.ArgumentNullOrWhiteSpace.FormatForUser(nameof(eventHubsPlugin))); + } + if (this.RegisteredPlugins.Any(p => p.Value.Name == eventHubsPlugin.Name)) + { + throw new ArgumentException(eventHubsPlugin.Name, Resources.PluginAlreadyRegistered.FormatForUser(eventHubsPlugin.Name)); + } + if (!this.RegisteredPlugins.TryAdd(eventHubsPlugin.Name, eventHubsPlugin)) + { + throw new ArgumentException(eventHubsPlugin.Name, Resources.PluginRegistrationFailed.FormatForUser(eventHubsPlugin.Name)); + } + } + + /// + /// Unregisters a . + /// + /// The of the plugin to be unregistered. + public virtual void UnregisterPlugin(string pluginName) + { + if (this.RegisteredPlugins == null) + { + return; + } + + if (string.IsNullOrWhiteSpace(pluginName)) + { + throw new ArgumentNullException(nameof(pluginName), Resources.ArgumentNullOrWhiteSpace.FormatForUser(nameof(pluginName))); + } + + this.RegisteredPlugins.TryRemove(pluginName, out EventHubsPlugin plugin); + } + /// /// Closes the ClientEntity. /// diff --git a/src/Microsoft.Azure.EventHubs/Primitives/ClientInfo.cs b/src/Microsoft.Azure.EventHubs/Primitives/ClientInfo.cs index a1291ca..3a0165b 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/ClientInfo.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/ClientInfo.cs @@ -29,6 +29,8 @@ static ClientInfo() platform = "UAP"; #elif NET461 platform = Environment.OSVersion.VersionString; +#elif IOS + platform = "IOS"; #endif } catch { } diff --git a/src/Microsoft.Azure.EventHubs/Primitives/EventHubsConnectionStringBuilder.cs b/src/Microsoft.Azure.EventHubs/Primitives/EventHubsConnectionStringBuilder.cs index af16d7f..2f66fd1 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/EventHubsConnectionStringBuilder.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/EventHubsConnectionStringBuilder.cs @@ -5,6 +5,7 @@ namespace Microsoft.Azure.EventHubs { using System; using System.Text; + using Microsoft.Azure.EventHubs.Primitives; /// /// Supported transport types @@ -90,10 +91,8 @@ public EventHubsConnectionStringBuilder( TimeSpan operationTimeout) : this(endpointAddress, entityPath, operationTimeout) { - if (string.IsNullOrWhiteSpace(sharedAccessKeyName) || string.IsNullOrWhiteSpace(sharedAccessKey)) - { - throw Fx.Exception.ArgumentNullOrWhiteSpace(string.IsNullOrWhiteSpace(sharedAccessKeyName) ? nameof(sharedAccessKeyName) : nameof(sharedAccessKey)); - } + Guard.ArgumentNotNullOrWhiteSpace(nameof(sharedAccessKey), sharedAccessKey); + Guard.ArgumentNotNullOrWhiteSpace(nameof(sharedAccessKeyName), sharedAccessKeyName); this.SasKey = sharedAccessKey; this.SasKeyName = sharedAccessKeyName; @@ -113,10 +112,7 @@ public EventHubsConnectionStringBuilder( TimeSpan operationTimeout) : this(endpointAddress, entityPath, operationTimeout) { - if (string.IsNullOrWhiteSpace(sharedAccessSignature)) - { - throw Fx.Exception.ArgumentNullOrWhiteSpace(nameof(sharedAccessSignature)); - } + Guard.ArgumentNotNullOrWhiteSpace(nameof(sharedAccessSignature), sharedAccessSignature); this.SharedAccessSignature = sharedAccessSignature; } @@ -128,10 +124,7 @@ public EventHubsConnectionStringBuilder( /// Event Hubs ConnectionString public EventHubsConnectionStringBuilder(string connectionString) { - if (string.IsNullOrWhiteSpace(connectionString)) - { - throw Fx.Exception.ArgumentNullOrWhiteSpace(nameof(connectionString)); - } + Guard.ArgumentNotNullOrWhiteSpace(nameof(connectionString), connectionString); // Assign default values. this.OperationTimeout = ClientConstants.DefaultOperationTimeout; @@ -147,14 +140,8 @@ internal EventHubsConnectionStringBuilder( TimeSpan operationTimeout, TransportType transportType = TransportType.Amqp) { - if (endpointAddress == null) - { - throw Fx.Exception.ArgumentNull(nameof(endpointAddress)); - } - if (string.IsNullOrWhiteSpace(entityPath)) - { - throw Fx.Exception.ArgumentNullOrWhiteSpace(nameof(entityPath)); - } + Guard.ArgumentNotNull(nameof(endpointAddress), endpointAddress); + Guard.ArgumentNotNullOrWhiteSpace(nameof(entityPath), entityPath); // Replace the scheme. We cannot really make sure that user passed an amps:// scheme to us. var uriBuilder = new UriBuilder(endpointAddress.AbsoluteUri) diff --git a/src/Microsoft.Azure.EventHubs/Primitives/EventHubsTimeoutException.cs b/src/Microsoft.Azure.EventHubs/Primitives/EventHubsTimeoutException.cs new file mode 100644 index 0000000..efa4ddb --- /dev/null +++ b/src/Microsoft.Azure.EventHubs/Primitives/EventHubsTimeoutException.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.EventHubs +{ + using System; + + /// + /// The exception that is thrown when a time out is encountered. Callers retry the operation. + /// + public class EventHubsTimeoutException : EventHubsException + { + internal EventHubsTimeoutException(string message) : this(message, null) + { + } + + /// + /// + /// + /// + /// + internal EventHubsTimeoutException(string message, Exception innerException) + : base(true, message, innerException) + { + } + } +} diff --git a/src/Microsoft.Azure.EventHubs/Primitives/ExceptionUtility.cs b/src/Microsoft.Azure.EventHubs/Primitives/ExceptionUtility.cs index 785908c..5e592c7 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/ExceptionUtility.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/ExceptionUtility.cs @@ -4,13 +4,10 @@ namespace Microsoft.Azure.EventHubs { using System; + using System.Runtime.ExceptionServices; class ExceptionUtility { - internal ExceptionUtility() - { - } - public ArgumentException Argument(string paramName, string message) { return new ArgumentException(message, paramName); @@ -41,5 +38,18 @@ public Exception AsError(Exception exception) EventHubsEventSource.Log.ThrowingExceptionError(exception.ToString()); return exception; } + + /// + /// Attempts to prepare the exception for re-throwing by preserving the stack trace. The returned exception should be immediately thrown. + /// + /// The exception. May not be null. + /// The that was passed into this method. + public static Exception PrepareForRethrow(Exception exception) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + // The code cannot ever get here. We just return a value to work around a badly-designed API (ExceptionDispatchInfo.Throw): + // https://connect.microsoft.com/VisualStudio/feedback/details/689516/exceptiondispatchinfo-api-modifications (http://www.webcitation.org/6XQ7RoJmO) + return exception; + } } } diff --git a/src/Microsoft.Azure.EventHubs/Primitives/Fx.cs b/src/Microsoft.Azure.EventHubs/Primitives/Fx.cs index 4432ebe..08fb61d 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/Fx.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/Fx.cs @@ -3,24 +3,14 @@ namespace Microsoft.Azure.EventHubs { + using System; using System.Diagnostics; static class Fx { - static ExceptionUtility exceptionUtility; + private static readonly Lazy exceptionUtility = new Lazy(() => new ExceptionUtility()); - public static ExceptionUtility Exception - { - get - { - if (exceptionUtility == null) - { - exceptionUtility = new ExceptionUtility(); - } - - return exceptionUtility; - } - } + public static ExceptionUtility Exception => exceptionUtility.Value; [Conditional("DEBUG")] public static void Assert(bool condition, string message) diff --git a/src/Microsoft.Azure.EventHubs/Primitives/Guard.cs b/src/Microsoft.Azure.EventHubs/Primitives/Guard.cs new file mode 100644 index 0000000..134d1c4 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs/Primitives/Guard.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.EventHubs.Primitives +{ + static class Guard + { + internal static void ArgumentNotNull(string argumentName, object value) + { + if (value == null) + { + throw Fx.Exception.ArgumentNull(argumentName); + } + } + + internal static void ArgumentNotNullOrWhiteSpace(string argumentName, string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw Fx.Exception.ArgumentNull(argumentName); + } + } + } +} diff --git a/src/Microsoft.Azure.EventHubs/Primitives/ITokenProvider.cs b/src/Microsoft.Azure.EventHubs/Primitives/ITokenProvider.cs index f1c4fd9..3b0bd7c 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/ITokenProvider.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/ITokenProvider.cs @@ -4,9 +4,6 @@ namespace Microsoft.Azure.EventHubs { using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; using System.Threading.Tasks; /// diff --git a/src/Microsoft.Azure.EventHubs/Primitives/JsonSecurityToken.cs b/src/Microsoft.Azure.EventHubs/Primitives/JsonSecurityToken.cs index 2f3e32b..bd866a8 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/JsonSecurityToken.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/JsonSecurityToken.cs @@ -4,7 +4,6 @@ namespace Microsoft.Azure.EventHubs { using System; - using System.Collections.ObjectModel; using System.IdentityModel.Tokens; #if !NET461 using System.IdentityModel.Tokens.Jwt; diff --git a/src/Microsoft.Azure.EventHubs/Primitives/ManagedServiceIdentityTokenProvider.cs b/src/Microsoft.Azure.EventHubs/Primitives/ManagedServiceIdentityTokenProvider.cs index ff3618b..e1124b8 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/ManagedServiceIdentityTokenProvider.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/ManagedServiceIdentityTokenProvider.cs @@ -12,7 +12,7 @@ namespace Microsoft.Azure.EventHubs /// public class ManagedServiceIdentityTokenProvider : TokenProvider { - static AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider(); + static readonly AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider(); /// /// Gets a for the given audience and duration. @@ -20,7 +20,7 @@ public class ManagedServiceIdentityTokenProvider : TokenProvider /// The URI which the access token applies to /// The time span that specifies the timeout value for the message that gets the security token /// - public async override Task GetTokenAsync(string appliesTo, TimeSpan timeout) + public override async Task GetTokenAsync(string appliesTo, TimeSpan timeout) { string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync(ClientConstants.AadEventHubsAudience); return new JsonSecurityToken(accessToken, appliesTo); diff --git a/src/Microsoft.Azure.EventHubs/Primitives/RetryPolicy.cs b/src/Microsoft.Azure.EventHubs/Primitives/RetryPolicy.cs index cd82a4d..ff69a7f 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/RetryPolicy.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/RetryPolicy.cs @@ -4,9 +4,9 @@ namespace Microsoft.Azure.EventHubs { using System; - using System.Collections.Concurrent; using System.Net.Sockets; using System.Threading.Tasks; + using Microsoft.Azure.EventHubs.Primitives; /// /// Represents an abstraction for retrying messaging operations. Users should not @@ -19,13 +19,7 @@ public abstract class RetryPolicy static readonly TimeSpan DefaultRetryMinBackoff = TimeSpan.Zero; static readonly TimeSpan DefaultRetryMaxBackoff = TimeSpan.FromSeconds(30); - object serverBusySync; - - /// - protected RetryPolicy() - { - this.serverBusySync = new Object(); - } + readonly object serverBusySync = new object(); /// /// Determines whether or not the exception can be retried. @@ -34,69 +28,43 @@ protected RetryPolicy() /// A bool indicating whether or not the operation can be retried. public static bool IsRetryableException(Exception exception) { - if (exception == null) - { - throw new ArgumentNullException("exception"); - } + Guard.ArgumentNotNull(nameof(exception), exception); if (exception is EventHubsException) { return ((EventHubsException)exception).IsTransient; } - else if (exception is TaskCanceledException) - { - if (exception.InnerException != null) - { - return IsRetryableException(exception.InnerException); - } - return true; + if (exception is TaskCanceledException) + { + return exception.InnerException == null || IsRetryableException(exception.InnerException); } // Flatten AggregateException - else if (exception is AggregateException) + if (exception is AggregateException) { var fltAggException = (exception as AggregateException).Flatten(); - if (fltAggException.InnerException != null) - { - return IsRetryableException(fltAggException.InnerException); - } - - return false; + return fltAggException.InnerException != null && IsRetryableException(fltAggException.InnerException); } // Other retryable exceptions here. - else if (exception is OperationCanceledException || - exception is SocketException) + if (exception is OperationCanceledException || exception is SocketException) { return true; } - return false; } /// /// Returns the default retry policy, . /// - public static RetryPolicy Default - { - get - { - return new RetryExponential(DefaultRetryMinBackoff, DefaultRetryMaxBackoff, DefaultRetryMaxCount); - } - } + public static RetryPolicy Default => new RetryExponential(DefaultRetryMinBackoff, DefaultRetryMaxBackoff, DefaultRetryMaxCount); /// /// Returns the default retry policy, . /// - public static RetryPolicy NoRetry - { - get - { - return new RetryExponential(TimeSpan.Zero, TimeSpan.Zero, 0); - } - } + public static RetryPolicy NoRetry => new RetryExponential(TimeSpan.Zero, TimeSpan.Zero, 0); /// /// @@ -116,7 +84,7 @@ public static RetryPolicy NoRetry public TimeSpan? GetNextRetryInterval(Exception lastException, TimeSpan remainingTime, int retryCount) { int baseWaitTime = 0; - lock(this.serverBusySync) + lock (this.serverBusySync) { if (lastException != null && (lastException is ServerBusyException || (lastException.InnerException != null && lastException.InnerException is ServerBusyException))) @@ -128,7 +96,7 @@ public static RetryPolicy NoRetry var retryAfter = this.OnGetNextRetryInterval(lastException, remainingTime, baseWaitTime, retryCount); // Don't retry if remaining time isn't enough. - if (retryAfter == null || + if (retryAfter == null || remainingTime.TotalSeconds < Math.Max(retryAfter.Value.TotalSeconds, ClientConstants.TimerToleranceInSeconds)) { return null; diff --git a/src/Microsoft.Azure.EventHubs/Primitives/SecurityToken.cs b/src/Microsoft.Azure.EventHubs/Primitives/SecurityToken.cs index fbc537a..a12ad02 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/SecurityToken.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/SecurityToken.cs @@ -13,22 +13,22 @@ public class SecurityToken /// /// Token literal /// - string token; + readonly string token; /// /// Expiry date-time /// - DateTime expiresAtUtc; + readonly DateTime expiresAtUtc; /// /// Token audience /// - string audience; + readonly string audience; /// /// Token type /// - string tokenType; + readonly string tokenType; /// /// Creates a new instance of the class. diff --git a/src/Microsoft.Azure.EventHubs/Primitives/SharedAccessSignatureToken.cs b/src/Microsoft.Azure.EventHubs/Primitives/SharedAccessSignatureToken.cs index 5e28f68..15f3d9d 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/SharedAccessSignatureToken.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/SharedAccessSignatureToken.cs @@ -7,6 +7,7 @@ namespace Microsoft.Azure.EventHubs using System.Collections.Generic; using System.Globalization; using System.Net; + using Microsoft.Azure.EventHubs.Primitives; /// /// A SecurityToken that wraps a Shared Access Signature @@ -38,33 +39,26 @@ public SharedAccessSignatureToken(string tokenString) internal static void Validate(string sharedAccessSignature) { - if (string.IsNullOrEmpty(sharedAccessSignature)) - { - throw new ArgumentNullException(nameof(sharedAccessSignature)); - } + Guard.ArgumentNotNullOrWhiteSpace(nameof(sharedAccessSignature), sharedAccessSignature); IDictionary parsedFields = ExtractFieldValues(sharedAccessSignature); - string signature; - if (!parsedFields.TryGetValue(Signature, out signature)) + if (!parsedFields.TryGetValue(Signature, out _)) { throw new ArgumentNullException(Signature); } - string expiry; - if (!parsedFields.TryGetValue(SignedExpiry, out expiry)) + if (!parsedFields.TryGetValue(SignedExpiry, out _)) { throw new ArgumentNullException(SignedExpiry); } - string keyName; - if (!parsedFields.TryGetValue(SignedKeyName, out keyName)) + if (!parsedFields.TryGetValue(SignedKeyName, out _)) { throw new ArgumentNullException(SignedKeyName); } - string encodedAudience; - if (!parsedFields.TryGetValue(SignedResource, out encodedAudience)) + if (!parsedFields.TryGetValue(SignedResource, out _)) { throw new ArgumentNullException(SignedResource); } diff --git a/src/Microsoft.Azure.EventHubs/Primitives/SharedAccessSignatureTokenProvider.cs b/src/Microsoft.Azure.EventHubs/Primitives/SharedAccessSignatureTokenProvider.cs index 58730a9..494c12a 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/SharedAccessSignatureTokenProvider.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/SharedAccessSignatureTokenProvider.cs @@ -11,14 +11,13 @@ namespace Microsoft.Azure.EventHubs using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; + using Microsoft.Azure.EventHubs.Primitives; /// /// The SharedAccessSignatureTokenProvider generates tokens using a shared access key or existing signature. /// public class SharedAccessSignatureTokenProvider : TokenProvider { - const TokenScope DefaultTokenScope = TokenScope.Entity; - internal static readonly TimeSpan DefaultTokenTimeout = TimeSpan.FromMinutes(60); /// @@ -57,10 +56,7 @@ internal SharedAccessSignatureTokenProvider(string keyName, string sharedAccessK /// protected SharedAccessSignatureTokenProvider(string keyName, string sharedAccessKey, Func customKeyEncoder, TimeSpan tokenTimeToLive, TokenScope tokenScope) { - if (string.IsNullOrEmpty(keyName)) - { - throw new ArgumentNullException(nameof(keyName)); - } + Guard.ArgumentNotNullOrWhiteSpace(nameof(keyName), keyName); if (keyName.Length > SharedAccessSignatureToken.MaxKeyNameLength) { @@ -69,10 +65,7 @@ protected SharedAccessSignatureTokenProvider(string keyName, string sharedAccess Resources.ArgumentStringTooBig.FormatForUser(nameof(keyName), SharedAccessSignatureToken.MaxKeyNameLength)); } - if (string.IsNullOrEmpty(sharedAccessKey)) - { - throw new ArgumentNullException(nameof(sharedAccessKey)); - } + Guard.ArgumentNotNullOrWhiteSpace(nameof(sharedAccessKey), sharedAccessKey); if (sharedAccessKey.Length > SharedAccessSignatureToken.MaxKeyLength) { diff --git a/src/Microsoft.Azure.EventHubs/Primitives/TaskExtensions.cs b/src/Microsoft.Azure.EventHubs/Primitives/TaskExtensions.cs new file mode 100644 index 0000000..70a5298 --- /dev/null +++ b/src/Microsoft.Azure.EventHubs/Primitives/TaskExtensions.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.EventHubs.Primitives +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Provides synchronous extension methods for tasks. + /// + internal static class TaskExtensions + { + /// + /// Waits for the task to complete, unwrapping any exceptions. + /// + /// The task. May not be null. + public static void WaitAndUnwrapException(this Task task) + { + Guard.ArgumentNotNull(nameof(task), task); + task.ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + /// Waits for the task to complete, unwrapping any exceptions. + /// + /// The task. May not be null. + /// A cancellation token to observe while waiting for the task to complete. + /// The was cancelled before the completed, or the raised an . + public static void WaitAndUnwrapException(this Task task, CancellationToken cancellationToken) + { + Guard.ArgumentNotNull(nameof(task), task); + + try + { + task.Wait(cancellationToken); + } + catch (AggregateException ex) + { + throw ExceptionUtility.PrepareForRethrow(ex.InnerException); + } + } + + /// + /// Waits for the task to complete, unwrapping any exceptions. + /// + /// The type of the result of the task. + /// The task. May not be null. + /// The result of the task. + public static TResult WaitAndUnwrapException(this Task task) + { + Guard.ArgumentNotNull(nameof(task), task); + return task.ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + /// Waits for the task to complete, unwrapping any exceptions. + /// + /// The type of the result of the task. + /// The task. May not be null. + /// A cancellation token to observe while waiting for the task to complete. + /// The result of the task. + /// The was cancelled before the completed, or the raised an . + public static TResult WaitAndUnwrapException(this Task task, CancellationToken cancellationToken) + { + Guard.ArgumentNotNull(nameof(task), task); + + try + { + task.Wait(cancellationToken); + return task.Result; + } + catch (AggregateException ex) + { + throw ExceptionUtility.PrepareForRethrow(ex.InnerException); + } + } + + /// + /// Waits for the task to complete, but does not raise task exceptions. The task exception (if any) is unobserved. + /// + /// The task. May not be null. + public static void WaitWithoutException(this Task task) + { + Guard.ArgumentNotNull(nameof(task), task); + + try + { + task.Wait(); + } + catch (AggregateException) + { + } + } + + /// + /// Waits for the task to complete, but does not raise task exceptions. The task exception (if any) is unobserved. + /// + /// The task. May not be null. + /// A cancellation token to observe while waiting for the task to complete. + /// The was cancelled before the completed. + public static void WaitWithoutException(this Task task, CancellationToken cancellationToken) + { + Guard.ArgumentNotNull(nameof(task), task); + + try + { + task.Wait(cancellationToken); + } + catch (AggregateException) + { + cancellationToken.ThrowIfCancellationRequested(); + } + } + } +} diff --git a/src/Microsoft.Azure.EventHubs/Primitives/Ticks.cs b/src/Microsoft.Azure.EventHubs/Primitives/Ticks.cs index 83d9fe5..fe9b304 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/Ticks.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/Ticks.cs @@ -18,7 +18,7 @@ public static long Now public static long FromMilliseconds(int milliseconds) { - return checked((long)milliseconds * TimeSpan.TicksPerMillisecond); + return checked(milliseconds * TimeSpan.TicksPerMillisecond); } public static int ToMilliseconds(long ticks) diff --git a/src/Microsoft.Azure.EventHubs/Primitives/TimeoutHelper.cs b/src/Microsoft.Azure.EventHubs/Primitives/TimeoutHelper.cs index 4432ab6..650180c 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/TimeoutHelper.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/TimeoutHelper.cs @@ -12,8 +12,8 @@ struct TimeoutHelper { DateTime deadline; bool deadlineSet; - TimeSpan originalTimeout; - public static readonly TimeSpan MaxWait = TimeSpan.FromMilliseconds(Int32.MaxValue); + readonly TimeSpan originalTimeout; + public static readonly TimeSpan MaxWait = TimeSpan.FromMilliseconds(int.MaxValue); public TimeoutHelper(TimeSpan timeout) : this(timeout, false) @@ -34,10 +34,7 @@ public TimeoutHelper(TimeSpan timeout, bool startTimeout) } } - public TimeSpan OriginalTimeout - { - get { return this.originalTimeout; } - } + public TimeSpan OriginalTimeout => this.originalTimeout; public static bool IsTooLarge(TimeSpan timeout) { @@ -46,14 +43,9 @@ public static bool IsTooLarge(TimeSpan timeout) public static TimeSpan FromMilliseconds(int milliseconds) { - if (milliseconds == Timeout.Infinite) - { - return TimeSpan.MaxValue; - } - else - { - return TimeSpan.FromMilliseconds(milliseconds); - } + return milliseconds == Timeout.Infinite + ? TimeSpan.MaxValue + : TimeSpan.FromMilliseconds(milliseconds); } public static int ToMilliseconds(TimeSpan timeout) @@ -62,39 +54,22 @@ public static int ToMilliseconds(TimeSpan timeout) { return Timeout.Infinite; } - else - { - long ticks = Ticks.FromTimeSpan(timeout); - if (ticks / TimeSpan.TicksPerMillisecond > int.MaxValue) - { - return int.MaxValue; - } - return Ticks.ToMilliseconds(ticks); - } + + long ticks = Ticks.FromTimeSpan(timeout); + + return ticks / TimeSpan.TicksPerMillisecond > int.MaxValue + ? int.MaxValue + : Ticks.ToMilliseconds(ticks); } public static TimeSpan Min(TimeSpan val1, TimeSpan val2) { - if (val1 > val2) - { - return val2; - } - else - { - return val1; - } + return val1 > val2 ? val2 : val1; } public static DateTime Min(DateTime val1, DateTime val2) { - if (val1 > val2) - { - return val2; - } - else - { - return val1; - } + return val1 > val2 ? val2 : val1; } public static TimeSpan Add(TimeSpan timeout1, TimeSpan timeout2) @@ -122,12 +97,9 @@ public static DateTime Subtract(DateTime time, TimeSpan timeout) public static TimeSpan Divide(TimeSpan timeout, int factor) { - if (timeout == TimeSpan.MaxValue) - { - return TimeSpan.MaxValue; - } - - return Ticks.ToTimeSpan((Ticks.FromTimeSpan(timeout) / factor) + 1); + return timeout == TimeSpan.MaxValue + ? TimeSpan.MaxValue + : Ticks.ToTimeSpan((Ticks.FromTimeSpan(timeout) / factor) + 1); } public TimeSpan RemainingTime() @@ -137,22 +109,14 @@ public TimeSpan RemainingTime() this.SetDeadline(); return this.originalTimeout; } - else if (this.deadline == DateTime.MaxValue) + + if (this.deadline == DateTime.MaxValue) { return TimeSpan.MaxValue; } - else - { - TimeSpan remaining = this.deadline - DateTime.UtcNow; - if (remaining <= TimeSpan.Zero) - { - return TimeSpan.Zero; - } - else - { - return remaining; - } - } + + TimeSpan remaining = this.deadline - DateTime.UtcNow; + return remaining <= TimeSpan.Zero ? TimeSpan.Zero : remaining; } public TimeSpan ElapsedTime() @@ -201,10 +165,8 @@ public static bool WaitOne(WaitHandle waitHandle, TimeSpan timeout) waitHandle.WaitOne(); return true; } - else - { - return waitHandle.WaitOne(timeout); - } + + return waitHandle.WaitOne(timeout); } } } \ No newline at end of file diff --git a/src/Microsoft.Azure.EventHubs/Primitives/TokenProvider.cs b/src/Microsoft.Azure.EventHubs/Primitives/TokenProvider.cs index 145049e..c208666 100644 --- a/src/Microsoft.Azure.EventHubs/Primitives/TokenProvider.cs +++ b/src/Microsoft.Azure.EventHubs/Primitives/TokenProvider.cs @@ -5,6 +5,7 @@ namespace Microsoft.Azure.EventHubs { using System; using System.Threading.Tasks; + using Microsoft.Azure.EventHubs.Primitives; using Microsoft.IdentityModel.Clients.ActiveDirectory; /// @@ -75,24 +76,20 @@ public static TokenProvider CreateSharedAccessSignatureTokenProvider(string keyN return new SharedAccessSignatureTokenProvider(keyName, sharedAccessKey, tokenTimeToLive, tokenScope); } + +#if !UAP10_0 && !IOS /// Creates an Azure Active Directory token provider. /// AuthenticationContext for AAD. /// The app credential. /// The for returning Json web token. public static TokenProvider CreateAadTokenProvider(AuthenticationContext authContext, ClientCredential clientCredential) { - if (authContext == null) - { - throw new ArgumentNullException(nameof(authContext)); - } - - if (clientCredential == null) - { - throw new ArgumentNullException(nameof(clientCredential)); - } + Guard.ArgumentNotNull(nameof(authContext), authContext); + Guard.ArgumentNotNull(nameof(clientCredential), clientCredential); return new AzureActiveDirectoryTokenProvider(authContext, clientCredential); } +#endif /// Creates an Azure Active Directory token provider. /// AuthenticationContext for AAD. @@ -108,45 +105,23 @@ public static TokenProvider CreateAadTokenProvider( IPlatformParameters platformParameters, UserIdentifier userIdentifier = null) { - if (authContext == null) - { - throw new ArgumentNullException(nameof(authContext)); - } - - if (string.IsNullOrEmpty(clientId)) - { - throw new ArgumentNullException(nameof(clientId)); - } - - if (redirectUri == null) - { - throw new ArgumentNullException(nameof(redirectUri)); - } - - if (platformParameters == null) - { - throw new ArgumentNullException(nameof(platformParameters)); - } + Guard.ArgumentNotNull(nameof(authContext), authContext); + Guard.ArgumentNotNullOrWhiteSpace(nameof(clientId), clientId); + Guard.ArgumentNotNull(nameof(redirectUri), redirectUri); + Guard.ArgumentNotNull(nameof(platformParameters), platformParameters); return new AzureActiveDirectoryTokenProvider(authContext, clientId, redirectUri, platformParameters, userIdentifier); } -#if !UAP10_0 +#if !UAP10_0 && !IOS /// Creates an Azure Active Directory token provider. /// AuthenticationContext for AAD. /// The client assertion certificate credential. /// The for returning Json web token. public static TokenProvider CreateAadTokenProvider(AuthenticationContext authContext, ClientAssertionCertificate clientAssertionCertificate) { - if (authContext == null) - { - throw new ArgumentNullException(nameof(authContext)); - } - - if (clientAssertionCertificate == null) - { - throw new ArgumentNullException(nameof(clientAssertionCertificate)); - } + Guard.ArgumentNotNull(nameof(authContext), authContext); + Guard.ArgumentNotNull(nameof(clientAssertionCertificate), clientAssertionCertificate); return new AzureActiveDirectoryTokenProvider(authContext, clientAssertionCertificate); } diff --git a/src/Microsoft.Azure.EventHubs/Properties/AssemblyInfo.cs b/src/Microsoft.Azure.EventHubs/Properties/AssemblyInfo.cs index 5aaee42..568ea55 100644 --- a/src/Microsoft.Azure.EventHubs/Properties/AssemblyInfo.cs +++ b/src/Microsoft.Azure.EventHubs/Properties/AssemblyInfo.cs @@ -33,4 +33,4 @@ "dd8a96737e5385b31414369dc3e42f371172127252856a0650793e1f5673a16d5d78e2ac852a10" + "4bc51e6f018dca44fdd26a219c27cb2b263956a80620223c8e9c2f8913c3c903e1e453e9e4e840" + "98afdad5f4badb8c1ebe0a7b0a4b57a08454646a65886afe3e290a791ff3260099ce0edf0bdbcc" + -"afadfeb6")] \ No newline at end of file +"afadfeb6")] diff --git a/src/Microsoft.Azure.EventHubs/ReceiverOptions.cs b/src/Microsoft.Azure.EventHubs/ReceiverOptions.cs index 7d58870..d66836f 100644 --- a/src/Microsoft.Azure.EventHubs/ReceiverOptions.cs +++ b/src/Microsoft.Azure.EventHubs/ReceiverOptions.cs @@ -15,17 +15,14 @@ public class ReceiverOptions /// Thrown if the length of the value is greater than the maximum length of 64. public string Identifier { - get - { - return this.identifier; - } + get => this.identifier; set { ReceiverOptions.ValidateReceiverIdentifier(value); this.identifier = value; } } - + /// Gets or sets a value indicating whether the runtime metric of a receiver is enabled. /// true if a client wants to access using . public bool EnableReceiverRuntimeMetric diff --git a/src/Microsoft.Azure.EventHubs/ReceiverRuntimeInfo.cs b/src/Microsoft.Azure.EventHubs/ReceiverRuntimeInfo.cs index 287c80f..df11a2f 100644 --- a/src/Microsoft.Azure.EventHubs/ReceiverRuntimeInfo.cs +++ b/src/Microsoft.Azure.EventHubs/ReceiverRuntimeInfo.cs @@ -8,49 +8,45 @@ namespace Microsoft.Azure.EventHubs /// Represents the approximate receiver runtime information for a logical partition of an Event Hub. public class ReceiverRuntimeInformation { - internal ReceiverRuntimeInformation(string partitionId) + /// + /// Construct a new instance for the given partition. + /// + /// + public ReceiverRuntimeInformation(string partitionId) { this.PartitionId = partitionId; } /// Gets the partition ID for a logical partition of an Event Hub. /// The partition identifier. - public string PartitionId - { - get; - internal set; - } + public string PartitionId { get; internal set; } /// Gets the last sequence number of the event within the partition stream of the Event Hub. /// The logical sequence number of the event. - public long LastSequenceNumber - { - get; - internal set; - } + public long LastSequenceNumber { get; internal set; } /// Gets the enqueued UTC time of the last event. /// The enqueued time of the last event. - public DateTime LastEnqueuedTimeUtc - { - get; - internal set; - } + public DateTime LastEnqueuedTimeUtc { get; internal set; } /// Gets the offset of the last enqueued event. /// The offset of the last enqueued event. - public string LastEnqueuedOffset - { - get; - internal set; - } + public string LastEnqueuedOffset { get; internal set; } /// Gets the time of when the runtime info was retrieved. /// The enqueued time of the last event. - public DateTime RetrievalTime + public DateTime RetrievalTime { get; internal set; } + + /// + /// Update the properties of this instance with the values from the given event. + /// + /// + public void Update(EventData updateFrom) { - get; - internal set; + this.LastSequenceNumber = updateFrom.LastSequenceNumber; + this.LastEnqueuedOffset = updateFrom.LastEnqueuedOffset; + this.LastEnqueuedTimeUtc = updateFrom.LastEnqueuedTime; + this.RetrievalTime = updateFrom.RetrievalTime; } } } diff --git a/src/Microsoft.Azure.EventHubs/Resources.Designer.cs b/src/Microsoft.Azure.EventHubs/Resources.Designer.cs index 4f67b7b..46b4a4c 100644 --- a/src/Microsoft.Azure.EventHubs/Resources.Designer.cs +++ b/src/Microsoft.Azure.EventHubs/Resources.Designer.cs @@ -12,6 +12,7 @@ namespace Microsoft.Azure.EventHubs { using System; using System.Reflection; + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -150,6 +151,24 @@ internal static string PartitionSenderInvalidWithPartitionKeyOnBatch { } } + /// + /// Looks up a localized string similar to The {0} plugin has already been registered.. + /// + internal static string PluginAlreadyRegistered { + get { + return ResourceManager.GetString("PluginAlreadyRegistered", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There was an error trying to register the {0} plugin.. + /// + internal static string PluginRegistrationFailed { + get { + return ResourceManager.GetString("PluginRegistrationFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to The 'identifier' parameter exceeds the maximum allowed size of {0} characters.. /// diff --git a/src/Microsoft.Azure.EventHubs/Resources.resx b/src/Microsoft.Azure.EventHubs/Resources.resx index 49e15e2..2c059ac 100644 --- a/src/Microsoft.Azure.EventHubs/Resources.resx +++ b/src/Microsoft.Azure.EventHubs/Resources.resx @@ -168,4 +168,10 @@ The value supplied must be between {0} and {1}. + + The {0} plugin has already been registered. + + + There was an error trying to register the {0} plugin. + \ No newline at end of file diff --git a/test/Microsoft.Azure.EventHubs.Tests/Amqp/AmqpMEssageCoverterTests.cs b/test/Microsoft.Azure.EventHubs.Tests/Amqp/AmqpMEssageCoverterTests.cs index 3006922..3c66c20 100644 --- a/test/Microsoft.Azure.EventHubs.Tests/Amqp/AmqpMEssageCoverterTests.cs +++ b/test/Microsoft.Azure.EventHubs.Tests/Amqp/AmqpMEssageCoverterTests.cs @@ -21,16 +21,14 @@ public class AmqpMessageCoverterTests void UpdateEventDataHeaderAndPropertiesReceiveCorrelationIdAndCopyItsValueToEventData() { // Arrange - // the following simulates a message's round trip from client to broker to client var message = AmqpMessage.Create(new MemoryStream(new byte[12]), true); AddSection(message, SectionFlag.Properties); // serialize - send the message on client side ArraySegment[] buffers = ReadMessagePayLoad(message, 71); - EventData eventData; - + // Act - eventData = AmqpMessageConverter.AmqpMessageToEventData(message); + var eventData = AmqpMessageConverter.AmqpMessageToEventData(message); // Assert Assert.NotNull(eventData); diff --git a/test/Microsoft.Azure.EventHubs.Tests/Client/ClientTestBase.cs b/test/Microsoft.Azure.EventHubs.Tests/Client/ClientTestBase.cs index 61103b2..10663ef 100644 --- a/test/Microsoft.Azure.EventHubs.Tests/Client/ClientTestBase.cs +++ b/test/Microsoft.Azure.EventHubs.Tests/Client/ClientTestBase.cs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. + namespace Microsoft.Azure.EventHubs.Tests.Client { using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using Microsoft.Azure.EventHubs.Primitives; using Xunit; public class ClientTestBase : IDisposable @@ -20,7 +22,7 @@ public ClientTestBase() this.EventHubClient = EventHubClient.CreateFromConnectionString(TestUtility.EventHubsConnectionString); // Discover partition ids. - var eventHubInfo = this.EventHubClient.GetRuntimeInformationAsync().Result; + var eventHubInfo = this.EventHubClient.GetRuntimeInformationAsync().WaitAndUnwrapException(); this.PartitionIds = eventHubInfo.PartitionIds; TestUtility.Log($"EventHub has {PartitionIds.Length} partitions"); } diff --git a/test/Microsoft.Azure.EventHubs.Tests/Client/DataBatchTests.cs b/test/Microsoft.Azure.EventHubs.Tests/Client/DataBatchTests.cs index 6cb1df2..072e88f 100644 --- a/test/Microsoft.Azure.EventHubs.Tests/Client/DataBatchTests.cs +++ b/test/Microsoft.Azure.EventHubs.Tests/Client/DataBatchTests.cs @@ -137,7 +137,7 @@ protected async Task SendWithEventDataBatch( // We will send a thousand messages where each message is 1K. var totalSent = 0; var rnd = new Random(); - TestUtility.Log($"Starting to send."); + TestUtility.Log("Starting to send."); do { // Send random body size. diff --git a/test/Microsoft.Azure.EventHubs.Tests/Client/NegativeCases.cs b/test/Microsoft.Azure.EventHubs.Tests/Client/NegativeCases.cs index 2559b0a..9b538ed 100644 --- a/test/Microsoft.Azure.EventHubs.Tests/Client/NegativeCases.cs +++ b/test/Microsoft.Azure.EventHubs.Tests/Client/NegativeCases.cs @@ -130,7 +130,7 @@ await Assert.ThrowsAsync(async () => invalidPartitions = new List() { "", " ", null }; foreach (var invalidPartitionId in invalidPartitions) { - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { TestUtility.Log($"Sending to invalid partition {invalidPartitionId}"); sender = this.EventHubClient.CreatePartitionSender(invalidPartitionId); @@ -150,7 +150,7 @@ async Task GetPartitionRuntimeInformationFromInvalidPartition() foreach (var invalidPartitionId in invalidPartitions) { - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { TestUtility.Log($"Getting partition information from invalid partition {invalidPartitionId}"); await this.EventHubClient.GetPartitionRuntimeInformationAsync(invalidPartitionId); @@ -162,7 +162,7 @@ await Assert.ThrowsAsync(async () => invalidPartitions = new List() { "", " ", null }; foreach (var invalidPartitionId in invalidPartitions) { - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { TestUtility.Log($"Getting partition information from invalid partition {invalidPartitionId}"); await this.EventHubClient.GetPartitionRuntimeInformationAsync(invalidPartitionId); @@ -179,7 +179,7 @@ Task CreateClientWithoutEntityPathShouldFail() var csb = new EventHubsConnectionStringBuilder(TestUtility.EventHubsConnectionString); csb.EntityPath = null; - return Assert.ThrowsAsync(() => + return Assert.ThrowsAsync(() => { EventHubClient.CreateFromConnectionString(csb.ToString()); throw new Exception("Entity path wasn't provided in the connection string and this new call was supposed to fail"); @@ -193,7 +193,7 @@ async Task MessageSizeExceededException() try { TestUtility.Log("Sending large event via EventHubClient.SendAsync(EventData)"); - var eventData = new EventData(new byte[300000]); + var eventData = new EventData(new byte[1100000]); await this.EventHubClient.SendAsync(eventData); throw new InvalidOperationException("Send should have failed with " + typeof(MessageSizeExceededException).Name); diff --git a/test/Microsoft.Azure.EventHubs.Tests/Client/PluginTests.cs b/test/Microsoft.Azure.EventHubs.Tests/Client/PluginTests.cs new file mode 100644 index 0000000..a5d5222 --- /dev/null +++ b/test/Microsoft.Azure.EventHubs.Tests/Client/PluginTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.EventHubs.Tests.Client +{ + using System; + using System.Threading.Tasks; + using System.Text; + using Microsoft.Azure.EventHubs.Core; + using Xunit; + + public class PluginTests + { + protected EventHubClient EventHubClient; + + [Fact] + [DisplayTestMethodName] + Task Registering_plugin_multiple_times_should_throw() + { + this.EventHubClient = EventHubClient.CreateFromConnectionString(TestUtility.EventHubsConnectionString); + var firstPlugin = new SamplePlugin(); + var secondPlugin = new SamplePlugin(); + + this.EventHubClient.RegisterPlugin(firstPlugin); + Assert.Throws(() => EventHubClient.RegisterPlugin(secondPlugin)); + return EventHubClient.CloseAsync(); + } + + [Fact] + [DisplayTestMethodName] + Task Unregistering_plugin_should_complete_with_plugin_set() + { + this.EventHubClient = EventHubClient.CreateFromConnectionString(TestUtility.EventHubsConnectionString); + var firstPlugin = new SamplePlugin(); + + this.EventHubClient.RegisterPlugin(firstPlugin); + this.EventHubClient.UnregisterPlugin(firstPlugin.Name); + return this.EventHubClient.CloseAsync(); + } + + [Fact] + [DisplayTestMethodName] + Task Unregistering_plugin_should_complete_without_plugin_set() + { + this.EventHubClient = EventHubClient.CreateFromConnectionString(TestUtility.EventHubsConnectionString); + this.EventHubClient.UnregisterPlugin("Non-existant plugin"); + return this.EventHubClient.CloseAsync(); + } + + [Fact] + [DisplayTestMethodName] + async Task Plugin_without_ShouldContinueOnException_should_throw() + { + this.EventHubClient = EventHubClient.CreateFromConnectionString(TestUtility.EventHubsConnectionString); + try + { + var plugin = new ExceptionPlugin(); + + this.EventHubClient.RegisterPlugin(plugin); + var testEvent = new EventData(Encoding.UTF8.GetBytes("Test message")); + await Assert.ThrowsAsync(() => this.EventHubClient.SendAsync(testEvent)); + } + finally + { + await this.EventHubClient.CloseAsync(); + } + } + + [Fact] + [DisplayTestMethodName] + async Task Plugin_with_ShouldContinueOnException_should_continue() + { + this.EventHubClient = EventHubClient.CreateFromConnectionString(TestUtility.EventHubsConnectionString); + try + { + var plugin = new ShouldCompleteAnywayExceptionPlugin(); + + this.EventHubClient.RegisterPlugin(plugin); + + var testEvent = new EventData(Encoding.UTF8.GetBytes("Test message")); + await this.EventHubClient.SendAsync(testEvent); + } + finally + { + await this.EventHubClient.CloseAsync(); + } + } + } + + internal class SamplePlugin : EventHubsPlugin + { + public override string Name => nameof(SamplePlugin); + + public override Task BeforeEventSend(EventData eventData) + { + eventData.Properties.Add("FirstSendPlugin", true); + return Task.FromResult(eventData); + } + } + + internal class ExceptionPlugin : EventHubsPlugin + { + public override string Name => nameof(ExceptionPlugin); + + public override Task BeforeEventSend(EventData eventData) + { + throw new NotImplementedException(); + } + } + + internal class ShouldCompleteAnywayExceptionPlugin : EventHubsPlugin + { + public override bool ShouldContinueOnException => true; + + public override string Name => nameof(ShouldCompleteAnywayExceptionPlugin); + + public override Task BeforeEventSend(EventData eventData) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Azure.EventHubs.Tests/Client/ReceiverRuntimeMetricsTests.cs b/test/Microsoft.Azure.EventHubs.Tests/Client/ReceiverRuntimeMetricsTests.cs index a999ef0..68cd964 100644 --- a/test/Microsoft.Azure.EventHubs.Tests/Client/ReceiverRuntimeMetricsTests.cs +++ b/test/Microsoft.Azure.EventHubs.Tests/Client/ReceiverRuntimeMetricsTests.cs @@ -131,6 +131,7 @@ async Task ValidateEnabledBehavior(PartitionReceiver partitionReceiver, EventHub var message = messages.Single(); + Assert.False(pInfo.IsEmpty, $"pInfo.IsEmpty == {pInfo.IsEmpty}"); Assert.True(partitionReceiver.RuntimeInfo.LastEnqueuedOffset == pInfo.LastEnqueuedOffset, $"FAILED partitionReceiver.RuntimeInfo.LastEnqueuedOffset == {partitionReceiver.RuntimeInfo.LastEnqueuedOffset}"); Assert.True(partitionReceiver.RuntimeInfo.LastEnqueuedTimeUtc == pInfo.LastEnqueuedTimeUtc, diff --git a/test/Microsoft.Azure.EventHubs.Tests/Client/TimeoutTests.cs b/test/Microsoft.Azure.EventHubs.Tests/Client/TimeoutTests.cs index f893ef1..e7026b3 100644 --- a/test/Microsoft.Azure.EventHubs.Tests/Client/TimeoutTests.cs +++ b/test/Microsoft.Azure.EventHubs.Tests/Client/TimeoutTests.cs @@ -47,8 +47,8 @@ async Task ReceiveTimeout() } /// - /// Small receive timeout should not throw System.TimeoutException. - /// TimeoutException should be returned as NULL to the awaiting client. + /// Small receive timeout should not throw EventHubsTimeoutException. + /// EventHubsTimeoutException should be returned as NULL to the awaiting client. /// /// [Fact] diff --git a/test/Microsoft.Azure.EventHubs.Tests/Processor/ProcessorTestBase.cs b/test/Microsoft.Azure.EventHubs.Tests/Processor/ProcessorTestBase.cs index aaf5ff3..1db6653 100644 --- a/test/Microsoft.Azure.EventHubs.Tests/Processor/ProcessorTestBase.cs +++ b/test/Microsoft.Azure.EventHubs.Tests/Processor/ProcessorTestBase.cs @@ -10,6 +10,7 @@ namespace Microsoft.Azure.EventHubs.Tests.Processor using System.Text; using System.Threading; using System.Threading.Tasks; + using Microsoft.Azure.EventHubs.Primitives; using Microsoft.Azure.EventHubs.Processor; using Microsoft.IdentityModel.Clients.ActiveDirectory; using Microsoft.WindowsAzure.Storage; @@ -30,7 +31,7 @@ public ProcessorTestBase() // Discover partition ids. TestUtility.Log("Discovering partitions on eventhub"); var ehClient = EventHubClient.CreateFromConnectionString(TestUtility.EventHubsConnectionString); - var eventHubInfo = ehClient.GetRuntimeInformationAsync().Result; + var eventHubInfo = ehClient.GetRuntimeInformationAsync().WaitAndUnwrapException(); this.PartitionIds = eventHubInfo.PartitionIds; TestUtility.Log($"EventHub has {PartitionIds.Length} partitions"); } @@ -151,7 +152,9 @@ async Task MultipleProcessorHosts() // Prepare host trackers. var hostReceiveEvents = new ConcurrentDictionary(); + var containerName = Guid.NewGuid().ToString(); var hosts = new List(); + try { for (int hostId = 0; hostId < hostCount; hostId++) @@ -162,11 +165,11 @@ async Task MultipleProcessorHosts() TestUtility.Log("Creating EventProcessorHost"); var eventProcessorHost = new EventProcessorHost( thisHostName, - string.Empty, // Passing empty as entity path here rsince path is already in EH connection string. + string.Empty, // Passing empty as entity path here since path is already in EH connection string. PartitionReceiver.DefaultConsumerGroupName, TestUtility.EventHubsConnectionString, TestUtility.StorageConnectionString, - Guid.NewGuid().ToString()); + containerName); hosts.Add(eventProcessorHost); TestUtility.Log($"Calling RegisterEventProcessorAsync"); var processorOptions = new EventProcessorOptions @@ -672,7 +675,9 @@ async Task HostShouldRecoverAfterReceiverDisconnection() }; }; - await eventProcessorHost.RegisterEventProcessorFactoryAsync(processorFactory); + var epo = EventProcessorOptions.DefaultOptions; + epo.ReceiveTimeout = TimeSpan.FromSeconds(10); + await eventProcessorHost.RegisterEventProcessorFactoryAsync(processorFactory, epo); // Wait 15 seconds then create a new epoch receiver. // This will trigger ReceiverDisconnectedExcetion in the host. @@ -684,7 +689,7 @@ async Task HostShouldRecoverAfterReceiverDisconnection() targetPartition, EventPosition.FromStart(), 2); await externalReceiver.ReceiveAsync(100, TimeSpan.FromSeconds(5)); - // Give another 1 minute for host to recover then do the validatins. + // Give another 1 minute for host to recover then do the validations. await Task.Delay(60000); TestUtility.Log("Verifying that host was able to receive ReceiverDisconnectedException"); @@ -948,6 +953,31 @@ async Task SingleProcessorHostWithAadTokenProvider() await RunGenericScenario(eventProcessorHost, epo); } + [Fact] + [DisplayTestMethodName] + async Task ReRegisterEventProcessor() + { + var eventProcessorHost = new EventProcessorHost( + null, // Entity path will be picked from connection string. + PartitionReceiver.DefaultConsumerGroupName, + TestUtility.EventHubsConnectionString, + TestUtility.StorageConnectionString, + Guid.NewGuid().ToString()); + + // Calling register for the first time should succeed. + TestUtility.Log("Registering EventProcessorHost for the first time."); + await eventProcessorHost.RegisterEventProcessorAsync(); + + // Unregister event processor should succed + TestUtility.Log("Registering EventProcessorHost for the first time."); + await eventProcessorHost.UnregisterEventProcessorAsync(); + + var epo = await GetOptionsAsync(); + + // Run a generic scenario with TestEventProcessor instead + await RunGenericScenario(eventProcessorHost, epo); + } + async Task>> DiscoverEndOfStream() { var ehClient = EventHubClient.CreateFromConnectionString(TestUtility.EventHubsConnectionString); @@ -982,7 +1012,7 @@ async Task RunGenericScenario(EventProcessorHost eventPro try { - TestUtility.Log($"Calling RegisterEventProcessorAsync"); + TestUtility.Log("Calling RegisterEventProcessorAsync"); var processorFactory = new TestEventProcessorFactory(); processorFactory.OnCreateProcessor += (f, createArgs) => @@ -1067,7 +1097,7 @@ async Task RunGenericScenario(EventProcessorHost eventPro async Task GetOptionsAsync() { var partitions = await DiscoverEndOfStream(); - return new EventProcessorOptions() + return new EventProcessorOptions { MaxBatchSize = 100, InitialOffsetProvider = pId => EventPosition.FromOffset(partitions[pId].Item1) @@ -1080,7 +1110,7 @@ class GenericScenarioResult public ConcurrentDictionary> ReceivedEvents = new ConcurrentDictionary>(); public int NumberOfFailures = 0; - object listLock = new object(); + readonly object listLock = new object(); public void AddEvents(string partitionId, IEnumerable addEvents) { diff --git a/test/Microsoft.Azure.EventHubs.Tests/Processor/TestEventProcessor.cs b/test/Microsoft.Azure.EventHubs.Tests/Processor/TestEventProcessor.cs index 523be49..325ab9b 100644 --- a/test/Microsoft.Azure.EventHubs.Tests/Processor/TestEventProcessor.cs +++ b/test/Microsoft.Azure.EventHubs.Tests/Processor/TestEventProcessor.cs @@ -59,6 +59,29 @@ Task IEventProcessor.OpenAsync(PartitionContext context) } } + class SecondTestEventProcessor : IEventProcessor + { + Task IEventProcessor.CloseAsync(PartitionContext context, CloseReason reason) + { + return Task.CompletedTask; + } + + Task IEventProcessor.ProcessErrorAsync(PartitionContext context, Exception error) + { + return Task.CompletedTask; + } + + Task IEventProcessor.ProcessEventsAsync(PartitionContext context, IEnumerable events) + { + return Task.CompletedTask; + } + + Task IEventProcessor.OpenAsync(PartitionContext context) + { + return Task.CompletedTask; + } + } + class TestEventProcessorFactory : IEventProcessorFactory { public event EventHandler> OnCreateProcessor; diff --git a/test/Microsoft.Azure.EventHubs.Tests/TestUtility.cs b/test/Microsoft.Azure.EventHubs.Tests/TestUtility.cs index 7db0954..4db6d06 100644 --- a/test/Microsoft.Azure.EventHubs.Tests/TestUtility.cs +++ b/test/Microsoft.Azure.EventHubs.Tests/TestUtility.cs @@ -38,6 +38,7 @@ static TestUtility() // Update operation timeout on ConnectionStringBuilder. ehCsb.OperationTimeout = TimeSpan.FromSeconds(30); + EventHubsConnectionString = ehCsb.ToString(); } @@ -62,7 +63,7 @@ internal static Task SendToPartitionAsync(EventHubClient ehClient, string partit internal static async Task SendToPartitionAsync(EventHubClient ehClient, string partitionId, EventData eventData, int numberOfMessages = 1) { - TestUtility.Log($"Starting to send {numberOfMessages} to partition {partitionId}."); + TestUtility.Log($"Starting to send {numberOfMessages} messages to partition {partitionId}."); var partitionSender = ehClient.CreatePartitionSender(partitionId); for (int i = 0; i < numberOfMessages; i++)