diff --git a/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/ManagementApiE2ETests.cs b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/ManagementApiE2ETests.cs index 54b84b0..2927ee2 100644 --- a/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/ManagementApiE2ETests.cs +++ b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/ManagementApiE2ETests.cs @@ -28,7 +28,7 @@ public class ManagementApiE2ETests private const string ContainerName = "ContainerName"; private NamespaceManager _namespaceManager; - private NamespaceManagerSettings _namespaceManagerSettings; + private NotificationHubSettings _notificationHubSettings; private string _notificationHubName; private readonly Uri _inputFileSasUri; private readonly Uri _outputContainerSasUri; @@ -190,22 +190,22 @@ public void ManagementApi_FailsWithAuthorizationException() var namespaceManager = CreateNamespaceManager(_testServer.RecordingMode, IncorrectConnectionString); // Check that CreateNotificationHub returns UnauthorizedAccessException when connection string is incorrect - Assert.Throws(() => namespaceManager.CreateNotificationHub(_notificationHubName)); + Assert.Throws(() => namespaceManager.CreateNotificationHub(_notificationHubName)); // We must create hub to recieve UnauthorizedAccessException when GetNotificationHub and DeleteNotificationHub execute var notificationHubDescription = _namespaceManager.CreateNotificationHub(_notificationHubName); // Check that GetNotificationHub returns UnauthorizedAccessException when connection string is incorrect - Assert.Throws(() => namespaceManager.GetNotificationHub(_notificationHubName)); + Assert.Throws(() => namespaceManager.GetNotificationHub(_notificationHubName)); // Check that NotificationHubExists returns UnauthorizedAccessException when connection string is incorrect - Assert.Throws(() => namespaceManager.NotificationHubExists(_notificationHubName)); + Assert.Throws(() => namespaceManager.NotificationHubExists(_notificationHubName)); // Check that UpdateNotificationHub returns UnauthorizedAccessException when connection string is incorrect - Assert.Throws(() => namespaceManager.UpdateNotificationHub(notificationHubDescription)); + Assert.Throws(() => namespaceManager.UpdateNotificationHub(notificationHubDescription)); // Check that DeleteNotificationHub returns UnauthorizedAccessException when connection string is incorrect - Assert.Throws(() => namespaceManager.DeleteNotificationHub(_notificationHubName)); + Assert.Throws(() => namespaceManager.DeleteNotificationHub(_notificationHubName)); } finally { @@ -299,10 +299,9 @@ private NamespaceManager CreateNamespaceManager(RecordingMode recordingMode, str _namespaceUriString = "https://sample.servicebus.windows.net/"; } - var namespaceManagerSettings = new NamespaceManagerSettings(); - namespaceManagerSettings.TokenProvider = SharedAccessSignatureTokenProvider.CreateSharedAccessSignatureTokenProvider(connectionString); + var namespaceManagerSettings = new NotificationHubSettings(); namespaceManagerSettings.MessageHandler = _testServer; - return new NamespaceManager(new Uri(_namespaceUriString), namespaceManagerSettings); + return new NamespaceManager(connectionString, namespaceManagerSettings); } } } diff --git a/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/Microsoft.Azure.NotificationHubs.DotNetCore.Tests.csproj b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/Microsoft.Azure.NotificationHubs.DotNetCore.Tests.csproj index 2fd36a8..3a8aa1b 100644 --- a/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/Microsoft.Azure.NotificationHubs.DotNetCore.Tests.csproj +++ b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/Microsoft.Azure.NotificationHubs.DotNetCore.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NamespaceManagerRetryPolicyTests.cs b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NamespaceManagerRetryPolicyTests.cs new file mode 100644 index 0000000..c9707ef --- /dev/null +++ b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NamespaceManagerRetryPolicyTests.cs @@ -0,0 +1,123 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. +//------------------------------------------------------------ + +namespace Microsoft.Azure.NotificationHubs.DotNetCore.Tests +{ + using System; + using System.Collections.Generic; + using System.Net; + using System.Net.Http; + using System.Net.Sockets; + using System.Threading.Tasks; + using Microsoft.Azure.NotificationHubs.Messaging; + using RichardSzalay.MockHttp; + using Xunit; + + public class NamespaceManagerRetryPolicyTests + { + private readonly string _connectionString; + private readonly string _hubName; + private NamespaceManager _namespaceClient; + private MockHttpMessageHandler _mockHttp; + private const string _hubResponse = "https://sample.servicebus.windows.net/sample?api-version=2017-04sample2020-07-02T18:03:10Z2020-07-02T18:03:11ZsampleP10675199DT2H48M5.4775807SSharedAccessKeyNoneListen2020-07-02T18:03:10.772227Z2020-07-02T18:03:10.772227ZDefaultListenSharedAccessSignaturexxxxxxxxSharedAccessKeyNoneListenManageSend2020-07-02T18:03:10.772227Z2020-07-02T18:03:10.772227ZDefaultFullSharedAccessSignaturexxxxxxxx"; + + public NamespaceManagerRetryPolicyTests() + { + _connectionString = "Endpoint=sb://sample.servicebus.windows.net/;SharedAccessKeyName=DefaultListenSharedAccessSignature;SharedAccessKey=xxxxxx"; + _hubName = "hub-name"; + _mockHttp = new MockHttpMessageHandler(); + _namespaceClient = new NamespaceManager(_connectionString, new NotificationHubSettings + { + HttpClient = _mockHttp.ToHttpClient(), + RetryOptions = new NotificationHubRetryOptions + { + Delay = TimeSpan.FromMilliseconds(10) + } + }); + } + + [Theory] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + [InlineData(HttpStatusCode.GatewayTimeout)] + [InlineData(HttpStatusCode.RequestTimeout)] + public async Task RetryPolicyRetriesOnTransientErrorInPut(HttpStatusCode errorCode) + { + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Respond(errorCode); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Respond(HttpStatusCode.OK, "application/xml", _hubResponse); + + await _namespaceClient.CreateNotificationHubAsync(_hubName); + + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task RetryPolicyRetriesConnectionErrors() + { + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Throw(new TimeoutException()); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Throw(new HttpRequestException("", new SocketException((int)SocketError.TimedOut))); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Respond(HttpStatusCode.OK, "application/xml", _hubResponse); + + await _namespaceClient.CreateNotificationHubAsync(_hubName); + + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task RetryPolicyRetriesOnThrottling() + { + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Respond((HttpStatusCode)403, new Dictionary { { "Retry-After", "1" }}, new StringContent("")); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Respond((HttpStatusCode)429, new Dictionary { { "Retry-After", "1" } }, new StringContent("")); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Respond(HttpStatusCode.OK, "application/xml", _hubResponse); + + await _namespaceClient.CreateNotificationHubAsync(_hubName); + + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task RetryPolicyRethrowsNonTransientErrors() + { + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Respond(HttpStatusCode.NotFound); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Respond(HttpStatusCode.OK, "application/xml", _hubResponse); + + await Assert.ThrowsAsync(() => _namespaceClient.CreateNotificationHubAsync(_hubName)); + } + + [Fact] + public async Task RetryPolicyGivesUpAfterTimeout() + { + _namespaceClient = new NamespaceManager(_connectionString, new NotificationHubSettings + { + MessageHandler = _mockHttp, + RetryOptions = new NotificationHubRetryOptions + { + Delay = TimeSpan.FromMilliseconds(10), + MaxRetries = 1 + } + }); + + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Throw(new TimeoutException()); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Throw(new TimeoutException()); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name") + .Respond(HttpStatusCode.OK, "application/xml", _hubResponse); + + await Assert.ThrowsAsync(() => _namespaceClient.CreateNotificationHubAsync(_hubName)); + } + } +} diff --git a/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NotificationHubClientRetryPolicyTests.cs b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NotificationHubClientRetryPolicyTests.cs new file mode 100644 index 0000000..3342fea --- /dev/null +++ b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NotificationHubClientRetryPolicyTests.cs @@ -0,0 +1,140 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. +//------------------------------------------------------------ + +namespace Microsoft.Azure.NotificationHubs.DotNetCore.Tests +{ + using System; + using System.Collections.Generic; + using System.Net; + using System.Net.Http; + using System.Net.Sockets; + using System.Threading.Tasks; + using Microsoft.Azure.NotificationHubs.Messaging; + using RichardSzalay.MockHttp; + using Xunit; + + public class NotificationHubClientRetryPolicyTests + { + private readonly string _connectionString; + private readonly string _hubName; + private NotificationHubClient _nhClient; + private MockHttpMessageHandler _mockHttp; + + public NotificationHubClientRetryPolicyTests() + { + _connectionString = "Endpoint=sb://sample.servicebus.windows.net/;SharedAccessKeyName=DefaultListenSharedAccessSignature;SharedAccessKey=xxxxxx"; + _hubName = "hub-name"; + _mockHttp = new MockHttpMessageHandler(); + _nhClient = new NotificationHubClient(_connectionString, _hubName, new NotificationHubSettings + { + HttpClient = _mockHttp.ToHttpClient(), + RetryOptions = new NotificationHubRetryOptions + { + Delay = TimeSpan.FromMilliseconds(10) + } + }); + } + + [Theory] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + [InlineData(HttpStatusCode.GatewayTimeout)] + [InlineData(HttpStatusCode.RequestTimeout)] + public async Task RetryPolicyRetriesOnTransientErrorInSend(HttpStatusCode errorCode) + { + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Respond(errorCode); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Respond(HttpStatusCode.OK); + + await _nhClient.SendDirectNotificationAsync(new FcmNotification("{}"), "123"); + + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Theory] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + [InlineData(HttpStatusCode.GatewayTimeout)] + [InlineData(HttpStatusCode.RequestTimeout)] + public async Task RetryPolicyRetriesOnTransientErrorInRegister(HttpStatusCode errorCode) + { + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/registrations") + .Respond(errorCode); + var registrationXml = "https://sample.servicebus.windows.net/hub-name/registrations/123456?api-version=2017-044757098718499783238-6462592605842469809-12019-05-13T17:12:18Z2019-05-13T17:12:18Z29999-12-31T23:59:59.9994757098718499783238-6462592605842469809-1tag2amzn1.adm-registration.v2.123"; + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/registrations") + .Respond("application/atom+xml", registrationXml); + + var registration = await _nhClient.CreateFcmNativeRegistrationAsync("123456"); + + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task RetryPolicyRetriesConnectionErrors() + { + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Throw(new TimeoutException()); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Throw(new HttpRequestException("", new SocketException((int)SocketError.TimedOut))); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Respond(HttpStatusCode.OK); + + await _nhClient.SendDirectNotificationAsync(new FcmNotification("{}"), "123"); + + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task RetryPolicyRetriesOnThrottling() + { + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Respond((HttpStatusCode)403, new Dictionary { { "Retry-After", "1" }}, new StringContent("")); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Respond((HttpStatusCode)429, new Dictionary { { "Retry-After", "1" } }, new StringContent("")); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Respond(HttpStatusCode.OK); + + await _nhClient.SendDirectNotificationAsync(new FcmNotification("{}"), "123"); + + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task RetryPolicyRethrowsNonTransientErrors() + { + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Respond(HttpStatusCode.NotFound); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Respond(HttpStatusCode.OK); + + await Assert.ThrowsAsync(() => _nhClient.SendDirectNotificationAsync(new FcmNotification("{}"), "123")); + } + + [Fact] + public async Task RetryPolicyGivesUpAfterTimeout() + { + _nhClient = new NotificationHubClient(_connectionString, _hubName, new NotificationHubSettings + { + MessageHandler = _mockHttp, + RetryOptions = new NotificationHubRetryOptions + { + Delay = TimeSpan.FromMilliseconds(10), + MaxRetries = 1 + } + }); + + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Throw(new TimeoutException()); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Throw(new TimeoutException()); + _mockHttp.Expect("https://sample.servicebus.windows.net/hub-name/messages") + .Respond(HttpStatusCode.OK); + + await Assert.ThrowsAsync(() => _nhClient.SendDirectNotificationAsync(new FcmNotification("{}"), "123")); + } + } +} diff --git a/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NotificationHubClientTest.cs b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NotificationHubClientTest.cs index 4cc048c..e50b8df 100644 --- a/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NotificationHubClientTest.cs +++ b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NotificationHubClientTest.cs @@ -32,7 +32,7 @@ public NotificationHubClientTest() _configuration = builder.Build(); _testServer = new TestServerProxy(); - var settings = new NotificationHubClientSettings + var settings = new NotificationHubSettings { MessageHandler = _testServer }; diff --git a/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NotificationHubConnectionStringBuilderTests.cs b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NotificationHubConnectionStringBuilderTests.cs index a3bd046..65365ed 100644 --- a/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NotificationHubConnectionStringBuilderTests.cs +++ b/src/Microsoft.Azure.NotificationHubs.DotNetCore.Tests/NotificationHubConnectionStringBuilderTests.cs @@ -1,7 +1,13 @@ -using Xunit; +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. +//------------------------------------------------------------ namespace Microsoft.Azure.NotificationHubs.Tests { + using Xunit; + public class NotificationHubConnectionStringBuilderTests { [Fact] diff --git a/src/Microsoft.Azure.NotificationHubs.DotNetFramework.Tests/Microsoft.Azure.NotificationHubs.DotNetFramework.Tests.csproj b/src/Microsoft.Azure.NotificationHubs.DotNetFramework.Tests/Microsoft.Azure.NotificationHubs.DotNetFramework.Tests.csproj index 9888b7f..62bf636 100644 --- a/src/Microsoft.Azure.NotificationHubs.DotNetFramework.Tests/Microsoft.Azure.NotificationHubs.DotNetFramework.Tests.csproj +++ b/src/Microsoft.Azure.NotificationHubs.DotNetFramework.Tests/Microsoft.Azure.NotificationHubs.DotNetFramework.Tests.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Microsoft.Azure.NotificationHubs/BasicRetryPolicy.cs b/src/Microsoft.Azure.NotificationHubs/BasicRetryPolicy.cs new file mode 100644 index 0000000..7f5eb08 --- /dev/null +++ b/src/Microsoft.Azure.NotificationHubs/BasicRetryPolicy.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Globalization; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using System.Transactions; +using Microsoft.Azure.NotificationHubs.Messaging; + +namespace Microsoft.Azure.NotificationHubs +{ + /// + /// The default retry policy for the Service Bus client library, respecting the + /// configuration specified as a set of . + /// + /// + /// + /// + internal class BasicRetryPolicy : NotificationHubRetryPolicy + { + /// The seed to use for initializing random number generated for a given thread-specific instance. + private static int s_randomSeed = Environment.TickCount; + + /// The random number generator to use for a specific thread. + private static readonly ThreadLocal RandomNumberGenerator = new ThreadLocal(() => new Random(Interlocked.Increment(ref s_randomSeed)), false); + + /// + /// The set of options responsible for configuring the retry + /// behavior. + /// + /// + public NotificationHubRetryOptions Options { get; } + + /// + /// The factor to apply to the base delay for use as a base jitter value. + /// + /// + /// This factor is used as the basis for random jitter to apply to the calculated retry duration. + /// It defines the magnitude of a random offset for retry backoff intervals, to amortize request spikes from this client. + /// + public double JitterFactor { get; } = 0.08; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The options which control the retry approach. + /// + public BasicRetryPolicy(NotificationHubRetryOptions retryOptions) + { + Options = retryOptions ?? throw new ArgumentNullException(nameof(retryOptions)); + } + + /// + /// Calculates the amount of time to wait before another attempt should be made. + /// + /// + /// The last exception that was observed for the operation to be retried. + /// The number of total attempts that have been made, including the initial attempt before any retries. + /// + /// The amount of time to delay before retrying the associated operation; if null, then the operation is no longer eligible to be retried. + /// + public override TimeSpan? CalculateRetryDelay( + Exception lastException, + int attemptCount) + { + if (Options.MaxRetries <= 0 + || Options.Delay == TimeSpan.Zero + || Options.MaxDelay == TimeSpan.Zero + || attemptCount > Options.MaxRetries + || !ShouldRetryException(lastException)) + { + return null; + } + + var baseJitterSeconds = Options.Delay.TotalSeconds * JitterFactor; + + TimeSpan retryDelay; + + if (lastException is MessagingException messagingException && messagingException.RetryAfter.HasValue) + { + retryDelay = messagingException.RetryAfter.Value; + } + else + { + switch (Options.Mode) + { + case NotificationHubRetryMode.Fixed: + retryDelay = CalculateFixedDelay(Options.Delay.TotalSeconds, baseJitterSeconds, RandomNumberGenerator.Value); + break; + case NotificationHubRetryMode.Exponential: + retryDelay = CalculateExponentialDelay(attemptCount, Options.Delay.TotalSeconds, baseJitterSeconds, RandomNumberGenerator.Value); + break; + default: + throw new NotSupportedException($"Unsupported retry mode {Options.Mode}"); + }; + } + + // Adjust the delay, if needed, to keep within the maximum + // duration. + + if (Options.MaxDelay < retryDelay) + { + return Options.MaxDelay; + } + + return retryDelay; + } + + /// + /// Determines if an exception should be retried. + /// + /// + /// The exception to consider. + /// + /// true to retry the exception; otherwise, false. + /// + private static bool ShouldRetryException(Exception exception) + { + if (exception is TaskCanceledException || exception is OperationCanceledException) + { + exception = exception?.InnerException; + } + else if (exception is AggregateException aggregateEx) + { + exception = aggregateEx?.Flatten().InnerException; + } + + switch (exception) + { + case null: + return false; + + case MessagingException ex: + return ex.IsTransient; + + case TimeoutException _: + case SocketException _: + return true; + + default: + return false; + } + } + + /// + /// Calculates the delay for an exponential back-off. + /// + /// + /// The number of total attempts that have been made, including the initial attempt before any retries. + /// The delay to use as a basis for the exponential back-off, in seconds. + /// The delay to use as the basis for a random jitter value, in seconds. + /// The random number generator to use for the calculation. + /// + /// The recommended duration to delay before retrying; this value does not take the maximum delay or eligibility for retry into account. + /// + private static TimeSpan CalculateExponentialDelay( + int attemptCount, + double baseDelaySeconds, + double baseJitterSeconds, + Random random) => + TimeSpan.FromSeconds(Math.Pow(2, attemptCount) * baseDelaySeconds + random.NextDouble() * baseJitterSeconds); + + /// + /// Calculates the delay for a fixed back-off. + /// + /// + /// The delay to use as a basis for the fixed back-off, in seconds. + /// The delay to use as the basis for a random jitter value, in seconds. + /// The random number generator to use for the calculation. + /// + /// The recommended duration to delay before retrying; this value does not take the maximum delay or eligibility for retry into account. + /// + private static TimeSpan CalculateFixedDelay( + double baseDelaySeconds, + double baseJitterSeconds, + Random random) => + TimeSpan.FromSeconds(baseDelaySeconds + random.NextDouble() * baseJitterSeconds); + } +} diff --git a/src/Microsoft.Azure.NotificationHubs/ExceptionErrorCodes.cs b/src/Microsoft.Azure.NotificationHubs/ExceptionErrorCodes.cs index ce51e26..5a2241f 100644 --- a/src/Microsoft.Azure.NotificationHubs/ExceptionErrorCodes.cs +++ b/src/Microsoft.Azure.NotificationHubs/ExceptionErrorCodes.cs @@ -91,6 +91,11 @@ public enum ExceptionErrorCodes /// NamespaceNotFound = 40402, + /// + /// Specifies that resource was not found + /// + ResourceNotFound = 40403, + /// /// Specifies that store lock was lost /// @@ -137,6 +142,11 @@ public enum ExceptionErrorCodes /// EntityGone = 41000, + /// + /// Specifies that request has been throttled + /// + Throttled = 42900, + /// /// Specifies that unknown internal error ocured @@ -168,7 +178,6 @@ public enum ExceptionErrorCodes /// BadGatewayFailure = 50200, - /// /// Specifies that gateway timeout error ocured /// diff --git a/src/Microsoft.Azure.NotificationHubs/ExceptionUtility.cs b/src/Microsoft.Azure.NotificationHubs/ExceptionUtility.cs index 94725e9..902f6cc 100644 --- a/src/Microsoft.Azure.NotificationHubs/ExceptionUtility.cs +++ b/src/Microsoft.Azure.NotificationHubs/ExceptionUtility.cs @@ -12,8 +12,10 @@ namespace Microsoft.Azure.NotificationHubs using System.IO; using System.Net; using System.Net.Http; + using System.Net.Sockets; using System.Threading.Tasks; using System.Xml; + using static Microsoft.Azure.NotificationHubs.Messaging.MessagingExceptionDetail; internal static class ExceptionsUtility { @@ -24,90 +26,58 @@ internal static class ExceptionsUtility public const string HttpStatusCodeTag = "Code"; public const string DetailTag = "Detail"; - public static Exception HandleXmlException(XmlException exception) + public static Exception HandleXmlException(XmlException exception, string trackingId) { - return new MessagingException(SRClient.InvalidXmlFormat, false, exception); + var details = new MessagingExceptionDetail(ExceptionErrorCodes.InternalFailure, SRClient.InvalidXmlFormat, ErrorLevelType.ServerError, null, trackingId); + return new MessagingException(details, false, exception); } - public static Exception TranslateToMessagingException(this Exception ex, int timeoutInMilliseconds = 0, string trackingId = null) + public static async Task TranslateToMessagingExceptionAsync(this HttpResponseMessage response, string trackingId) { - if(ex is XmlException) + var responseBody = string.Empty; + if (response.Content != null) { - return HandleXmlException((XmlException)ex); + responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); } - - if(ex is HttpRequestException) - { - if (ex.InnerException is WebException) - { - return HandleWebException((WebException)ex.InnerException, timeoutInMilliseconds, trackingId); - } - else - { - return HandleUnexpectedException(ex, trackingId); - } - } - - return HandleUnexpectedException(ex, trackingId); - } - public static async Task TranslateToMessagingExceptionAsync(this HttpResponseMessage response, string method, int timeoutInMilliseconds, string trackingId) - { - var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var exceptionMessage = FormatExceptionMessage(responseBody, response.StatusCode, response.ReasonPhrase, trackingId); - var code = response.StatusCode; - - if (code == HttpStatusCode.NotFound || code == HttpStatusCode.NoContent) - { - return new MessagingEntityNotFoundException(exceptionMessage); - } - else if (code == HttpStatusCode.Conflict) - { - if (method.Equals(HttpMethod.Delete.ToString(), StringComparison.OrdinalIgnoreCase)) - { - return new MessagingException(exceptionMessage); - } - if (method.Equals(HttpMethod.Put.ToString(), StringComparison.OrdinalIgnoreCase)) - { - return new MessagingException(exceptionMessage); - } - else if (exceptionMessage.Contains(ConflictOperationInProgressSubCode)) - { - return new MessagingException(exceptionMessage); - } - else - { - return new MessagingEntityAlreadyExistsException(exceptionMessage, null); - } - } - else if (code == HttpStatusCode.Unauthorized) - { - return new UnauthorizedAccessException(exceptionMessage); - } - else if (code == HttpStatusCode.Forbidden) - { - // TODO: It is not always correct to assume Forbidden - // equals QuotaExceeded, but Gateway currently has no additional information - // for us to make better judgment. - return new QuotaExceededException(exceptionMessage); - } - else if (code == HttpStatusCode.BadRequest) - { - return new ArgumentException(exceptionMessage); - } - else if (code == HttpStatusCode.ServiceUnavailable) - { - return new ServerBusyException(exceptionMessage); - } - else if (code == HttpStatusCode.GatewayTimeout) + var statusCode = response.StatusCode; + var retryAfter = response.Headers.RetryAfter?.Delta; + + switch (statusCode) { - // mainly a test hook, but also a valid contract by itself - return new MessagingCommunicationException(exceptionMessage); + case HttpStatusCode.NotFound: + case HttpStatusCode.NoContent: + return new MessagingEntityNotFoundException(new MessagingExceptionDetail(ExceptionErrorCodes.EndpointNotFound, exceptionMessage, ErrorLevelType.UserError, statusCode, trackingId)); + case HttpStatusCode.Conflict: + if (response.RequestMessage.Method.Equals(HttpMethod.Delete) || + response.RequestMessage.Method.Equals(HttpMethod.Put) || + exceptionMessage.Contains(ConflictOperationInProgressSubCode)) + { + return new MessagingException(new MessagingExceptionDetail(ExceptionErrorCodes.ConflictGeneric, exceptionMessage, ErrorLevelType.UserError, statusCode, trackingId), false); + } + else + { + return new MessagingEntityAlreadyExistsException(new MessagingExceptionDetail(ExceptionErrorCodes.ConflictGeneric, exceptionMessage, ErrorLevelType.UserError, statusCode, trackingId)); + } + + case HttpStatusCode.Unauthorized: + return new UnauthorizedException(new MessagingExceptionDetail(ExceptionErrorCodes.UnauthorizedGeneric, exceptionMessage, ErrorLevelType.UserError, statusCode, trackingId)); + case HttpStatusCode.Forbidden: + case (HttpStatusCode)429: + return new QuotaExceededException(new MessagingExceptionDetail(ExceptionErrorCodes.Throttled, exceptionMessage, ErrorLevelType.UserError, statusCode, trackingId), retryAfter); + case HttpStatusCode.BadRequest: + return new BadRequestException(new MessagingExceptionDetail(ExceptionErrorCodes.BadRequest, exceptionMessage, ErrorLevelType.UserError, statusCode, trackingId)); + case HttpStatusCode.InternalServerError: + case HttpStatusCode.ServiceUnavailable: + case HttpStatusCode.GatewayTimeout: + case HttpStatusCode.RequestTimeout: + return new ServerBusyException(new MessagingExceptionDetail(ExceptionErrorCodes.ServerBusy, exceptionMessage, ErrorLevelType.ServerError, statusCode, trackingId)); } - return new MessagingException(exceptionMessage); + return new MessagingException(new MessagingExceptionDetail(ExceptionErrorCodes.UnknownExceptionDetail, exceptionMessage, ErrorLevelType.ServerError, statusCode, trackingId), false); } - private static string FormatExceptionMessage(string responseBody, HttpStatusCode code, string reasonPhrase, string trackingId) + internal static string FormatExceptionMessage(string responseBody, HttpStatusCode code, string reasonPhrase, string trackingId) { var exceptionMessage = string.Empty; using(var stringReader = new StringReader(responseBody)) @@ -125,9 +95,9 @@ private static string FormatExceptionMessage(string responseBody, HttpStatusCode reader.ReadStartElement(DetailTag); exceptionMessage = string.Format(CultureInfo.InvariantCulture, "{0} {1}", exceptionMessage, reader.ReadString()); } - catch (XmlException) + catch (XmlException ex) { - //Ignore this exception + // Ignore this exception } } @@ -139,43 +109,30 @@ private static string FormatExceptionMessage(string responseBody, HttpStatusCode return exceptionMessage; } - public static Exception HandleWebException(WebException webException, int timeoutInMilliseconds, string trackingId) + public static Exception HandleSocketException(SocketException socketException, int timeoutInMilliseconds, string trackingId) { - var webResponse = (HttpWebResponse)webException.Response; - var exceptionMessage = webException.Message; - + var exceptionMessage = socketException.Message; - if (webResponse == null) - { - switch (webException.Status) - { - case WebExceptionStatus.RequestCanceled: - case WebExceptionStatus.Timeout: - exceptionMessage = string.Format(SRClient.TrackableExceptionMessageFormat, string.Format(SRClient.OperationRequestTimedOut, timeoutInMilliseconds), CreateClientTrackingExceptionInfo(trackingId)); - return new TimeoutException(exceptionMessage, webException); - - case WebExceptionStatus.ConnectFailure: - case WebExceptionStatus.NameResolutionFailure: - exceptionMessage = string.Format(SRClient.TrackableExceptionMessageFormat, exceptionMessage, CreateClientTrackingExceptionInfo(trackingId)); - return new MessagingCommunicationException(exceptionMessage, webException); - } - } - else + switch (socketException.SocketErrorCode) { - throw webException; + case SocketError.AddressNotAvailable: + case SocketError.ConnectionRefused: + case SocketError.AccessDenied: + case SocketError.HostUnreachable: + case SocketError.HostNotFound: + exceptionMessage = string.Format(SRClient.TrackableExceptionMessageFormat, exceptionMessage, CreateClientTrackingExceptionInfo(trackingId)); + return new MessagingCommunicationException(new MessagingExceptionDetail(ExceptionErrorCodes.ProviderUnreachable, exceptionMessage, ErrorLevelType.ClientConnection, null, trackingId), false, socketException); + default: + exceptionMessage = string.Format(SRClient.TrackableExceptionMessageFormat, string.Format(SRClient.OperationRequestTimedOut, timeoutInMilliseconds), CreateClientTrackingExceptionInfo(trackingId)); + return new MessagingCommunicationException(new MessagingExceptionDetail(ExceptionErrorCodes.GatewayTimeoutFailure, exceptionMessage, ErrorLevelType.ClientConnection, null, trackingId), true, socketException); } - - return new MessagingException(exceptionMessage, webException); } public static Exception HandleUnexpectedException(Exception ex, string trackingId) { - throw new MessagingException($"Unexpected exception encountered {CreateClientTrackingExceptionInfo(trackingId)}", ex); - } - - public static bool IsMessagingException(this Exception e) - { - return (e is MessagingException || e is UnauthorizedAccessException || e is ArgumentException); + var exceptionMessage = $"Unexpected exception encountered {CreateClientTrackingExceptionInfo(trackingId)}"; + var details = new MessagingExceptionDetail(ExceptionErrorCodes.UnknownExceptionDetail, exceptionMessage, ErrorLevelType.ClientConnection, null, trackingId); + throw new MessagingException(details, false, ex); } internal static string CreateClientTrackingExceptionInfo(string trackingId) diff --git a/src/Microsoft.Azure.NotificationHubs/KeyValueConfigurationManager.cs b/src/Microsoft.Azure.NotificationHubs/KeyValueConfigurationManager.cs index b7324f3..bfec13a 100644 --- a/src/Microsoft.Azure.NotificationHubs/KeyValueConfigurationManager.cs +++ b/src/Microsoft.Azure.NotificationHubs/KeyValueConfigurationManager.cs @@ -3,12 +3,8 @@ // Licensed under the MIT License. See License.txt in the project root for // license information. //------------------------------------------------------------ -using System; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Configuration; using System.Text.RegularExpressions; -using Microsoft.Azure.NotificationHubs.Auth; namespace Microsoft.Azure.NotificationHubs { @@ -113,99 +109,5 @@ public void Validate() throw new ConfigurationException(string.Format(SRClient.AppSettingsConfigMissingSetting, EndpointConfigName)); } } - - public NamespaceManager CreateNamespaceManager() - { - Validate(); - - string operationTimeout = connectionProperties[OperationTimeoutConfigName]; - IEnumerable endpoints = GetEndpointAddresses(connectionProperties[EndpointConfigName], connectionProperties[ManagementPortConfigName]); - string sasKeyName = connectionProperties[SharedAccessKeyName]; - string sasKey = connectionProperties[SharedAccessValueName]; - - try - { - TokenProvider provider = CreateTokenProvider(sasKeyName, sasKey); - if (string.IsNullOrEmpty(operationTimeout)) - { - return new NamespaceManager(endpoints, provider); - } - - return new NamespaceManager( - endpoints, - new NamespaceManagerSettings() - { - TokenProvider = provider - }); - } - catch (ArgumentException e) - { - throw new ArgumentException( - string.Format(SRClient.AppSettingsCreateManagerWithInvalidConnectionString, e.Message), - e); - } - catch (UriFormatException e) - { - throw new ArgumentException( - string.Format(SRClient.AppSettingsCreateManagerWithInvalidConnectionString, e.Message), - e); - } - } - - internal TokenProvider CreateTokenProvider() - { - var connectionProperty3 = connectionProperties["SharedAccessKeyName"]; - var connectionProperty4 = connectionProperties["SharedAccessKey"]; - var sharedAccessKeyName = connectionProperty3; - var sharedAccessKey = connectionProperty4; - return CreateTokenProvider( - sharedAccessKeyName, - sharedAccessKey); - } - - private static TokenProvider CreateTokenProvider( - string sharedAccessKeyName, - string sharedAccessKey) - { - if (string.IsNullOrWhiteSpace(sharedAccessKey)) - { - throw new ArgumentException(nameof(sharedAccessKey)); - } - - return new SharedAccessSignatureTokenProvider(sharedAccessKeyName, sharedAccessKey); - } - - public static IList GetEndpointAddresses(string uriEndpoints, string portString) - { - List addresses = new List(); - if (string.IsNullOrWhiteSpace(uriEndpoints)) - { - return addresses; - } - - string[] endpoints = uriEndpoints.Split(new string[] { ValueSeparator }, StringSplitOptions.RemoveEmptyEntries); - if (endpoints == null || endpoints.Length == 0) - { - return addresses; - } - - if (!int.TryParse(portString, out int port)) - { - port = -1; - } - - foreach (string endpoint in endpoints) - { - var address = new UriBuilder(endpoint); - if (port > 0) - { - address.Port = port; - } - - addresses.Add(address.Uri); - } - - return addresses; - } } } diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/BadRequestException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/BadRequestException.cs new file mode 100644 index 0000000..c5cfda9 --- /dev/null +++ b/src/Microsoft.Azure.NotificationHubs/Messaging/BadRequestException.cs @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. +//----------------------------------------------------------------------------- + +namespace Microsoft.Azure.NotificationHubs.Messaging +{ + using System; + using System.Runtime.Serialization; + + /// Exception for signaling bad request data errors. + [Serializable] + public class BadRequestException : MessagingException + { + /// Constructor. + /// Detail about the cause of the exception. + internal BadRequestException(MessagingExceptionDetail detail) : + base(detail, false) + { + } + + /// Exception Constructor for additional details embedded in a serializable stream. + /// The serialization information object. + /// The streaming context/source. + protected BadRequestException(SerializationInfo info, StreamingContext context) : + base(info, context) + { + } + } +} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/DuplicateMessageException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/DuplicateMessageException.cs deleted file mode 100644 index 63f28de..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/DuplicateMessageException.cs +++ /dev/null @@ -1,59 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - - /// Exception for signalling that a duplicate message was appended. - [Serializable] - public sealed class DuplicateMessageException : MessagingException - { - /// Constructor. - /// The message. - public DuplicateMessageException(string message) : - base(message) - { - this.IsTransient = false; - } - - /// Constructor. - /// The message. - /// The inner exception. - public DuplicateMessageException(string message, Exception innerException) : - base(message, innerException) - { - this.IsTransient = false; - } - - /// Constructor. - /// Detail about the cause of the exception. - internal DuplicateMessageException(MessagingExceptionDetail detail) : - base(detail) - { - this.IsTransient = false; - } - - /// Constructor. - /// Detail about the cause of the exception. - /// The inner exception. - internal DuplicateMessageException(MessagingExceptionDetail detail, Exception innerException) : - base(detail, innerException) - { - this.IsTransient = false; - } - - /// Constructor. - /// The information. - /// The context. - DuplicateMessageException(SerializationInfo info, StreamingContext context) : - base(info, context) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/InternalServerErrorException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/InternalServerErrorException.cs deleted file mode 100644 index 59dd7b1..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/InternalServerErrorException.cs +++ /dev/null @@ -1,42 +0,0 @@ -//---------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//---------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using Microsoft.Azure.NotificationHubs; - - [Serializable] - sealed class InternalServerErrorException : MessagingException - { - public InternalServerErrorException() : this((Exception)null) - { - } - - public InternalServerErrorException(string message) - : base(message) - { - this.Initialize(); - } - - public InternalServerErrorException(Exception innerException) - : base(SRClient.InternalServerError, true, innerException) - { - this.Initialize(); - } - - public InternalServerErrorException(MessagingExceptionDetail detail) - : base(detail) - { - this.Initialize(); - } - - void Initialize() - { - this.IsTransient = true; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/InvalidLinkTypeException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/InvalidLinkTypeException.cs deleted file mode 100644 index ce89c71..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/InvalidLinkTypeException.cs +++ /dev/null @@ -1,20 +0,0 @@ -//------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//------------------------------------------------------------ - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - - [Serializable] - sealed class InvalidLinkTypeException : MessagingException - { - public InvalidLinkTypeException(string message, Exception innerException) : - base(message, innerException) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/MessageLockLostException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/MessageLockLostException.cs deleted file mode 100644 index 4789cf1..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/MessageLockLostException.cs +++ /dev/null @@ -1,48 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - - /// Exception for signaling message lock lost errors. - [Serializable] - public sealed class MessageLockLostException : MessagingException - { - /// - /// Initializes a new instance of the class. - /// - /// The string message that will be propagated to the caller. - public MessageLockLostException(string message) : - base(message) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The string exception message. - /// The inner exception to be propagated with this exception to the caller. - public MessageLockLostException(string message, Exception innerException) : - base(message, innerException) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The serialization information object of for the streaming context. - /// The stream context providing exception details. - MessageLockLostException(SerializationInfo info, StreamingContext context) : - base(info, context) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/MessageNotFoundException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/MessageNotFoundException.cs deleted file mode 100644 index 9b60fb9..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/MessageNotFoundException.cs +++ /dev/null @@ -1,48 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - - /// Exception for signalling message not found errors. - [Serializable] - public sealed class MessageNotFoundException : MessagingException - { - /// - /// Initializes a new instance of the class. - /// - /// The string message that will be propagated to the caller.. - public MessageNotFoundException(string message) : - base(message) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The string exception message. - /// The inner exception to be propagated with this exception to the caller.. - public MessageNotFoundException(string message, Exception innerException) : - base(message, innerException) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// Holds all the data needed to serialize or deserialize an object. - /// The stream context providing exception details. - MessageNotFoundException(SerializationInfo info, StreamingContext context) : - base(info, context) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/MessageSizeExceededException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/MessageSizeExceededException.cs deleted file mode 100644 index 9016c60..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/MessageSizeExceededException.cs +++ /dev/null @@ -1,45 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - - /// - /// Exception for signalling message size excess - /// - [Serializable] - public sealed class MessageSizeExceededException : MessagingException - { - /// - /// Initializes a new instance of the class. - /// - /// The string message that will be propagated to the caller.. - public MessageSizeExceededException(string message) : - base(message) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The string exception message. - /// The inner exception to be propagated with this exception to the caller.. - public MessageSizeExceededException(string message, Exception innerException) : - base(message, innerException) - { - this.IsTransient = false; - } - - MessageSizeExceededException(SerializationInfo info, StreamingContext context) : - base(info, context) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/MessageStoreLockLostException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/MessageStoreLockLostException.cs deleted file mode 100644 index b0f6aef..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/MessageStoreLockLostException.cs +++ /dev/null @@ -1,65 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - - /// Exception for signalling message store lock lost errors. - [Serializable] - public sealed class MessageStoreLockLostException : MessagingException - { - /// - /// Initializes a new instance of the class. - /// - /// The string message that will be propagated to the caller.. - public MessageStoreLockLostException(string message) : - base(message) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The string exception message. - /// The inner exception to be propagated with this exception to the caller.. - public MessageStoreLockLostException(string message, Exception innerException) : - base(message, innerException) - { - this.IsTransient = false; - } - - /// Constructor. - /// Detail about the cause of the exception. - /// The TrackingContext. - internal MessageStoreLockLostException(MessagingExceptionDetail detail) : - base(detail) - { - this.IsTransient = false; - } - - /// Constructor. - /// Detail about the cause of the exception. - /// The TrackingContext. - /// The inner exception. - internal MessageStoreLockLostException(MessagingExceptionDetail detail, Exception innerException) : - base(detail, innerException) - { - this.IsTransient = false; - } - - /// Constructor. - /// The information. - /// The context. - MessageStoreLockLostException(SerializationInfo info, StreamingContext context) : - base(info, context) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingCommunicationException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingCommunicationException.cs index 2ea9691..16e87b0 100644 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingCommunicationException.cs +++ b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingCommunicationException.cs @@ -8,8 +8,6 @@ namespace Microsoft.Azure.NotificationHubs.Messaging { using System; using System.Runtime.Serialization; - using System.Security; - using Microsoft.Azure.NotificationHubs; /// /// Exception for signaling general communication errors related to messaging operations. @@ -17,22 +15,14 @@ namespace Microsoft.Azure.NotificationHubs.Messaging [Serializable] public sealed class MessagingCommunicationException : MessagingException { - /// - /// Initializes a new instance of the class. - /// - /// Name of the entity. - public MessagingCommunicationException(string communicationPath) - : this(string.Format(SRClient.MessagingEndpointCommunicationError, communicationPath), null) - { - } - /// /// Initializes a new instance of the class. /// /// The string exception message. /// The inner exception to be propagated with this exception to the caller.. - public MessagingCommunicationException(string message, Exception innerException) - : base(message, innerException) + /// If set to true, indicates it is a transient error. + public MessagingCommunicationException(MessagingExceptionDetail message, bool isTransientError, Exception innerException) + : base(message, isTransientError, innerException) { } diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityAlreadyExistsException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityAlreadyExistsException.cs index 796ff81..2c58277 100644 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityAlreadyExistsException.cs +++ b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityAlreadyExistsException.cs @@ -8,48 +8,24 @@ namespace Microsoft.Azure.NotificationHubs.Messaging { using System; using System.Runtime.Serialization; - using Microsoft.Azure.NotificationHubs; /// Exception for signalling messaging entity already exists errors. [Serializable] public sealed class MessagingEntityAlreadyExistsException : MessagingException { - /// - /// Initializes a new instance of the class. - /// - /// Name of the entity. - public MessagingEntityAlreadyExistsException(string entityName) - : this(MessagingExceptionDetail.EntityConflict(string.Format(SRClient.MessagingEntityAlreadyExists, entityName))) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The string exception message. - /// The inner exception to be propagated with this exception to the caller.. - public MessagingEntityAlreadyExistsException(string message, Exception innerException) - : base(MessagingExceptionDetail.EntityConflict(message), innerException) - { - this.IsTransient = false; - } - /// Constructor. /// Detail about the cause of the exception. internal MessagingEntityAlreadyExistsException(MessagingExceptionDetail detail) : - base(detail) + base(detail, false) { - this.IsTransient = false; } /// Constructor. /// Detail about the cause of the exception. /// The inner exception. internal MessagingEntityAlreadyExistsException(MessagingExceptionDetail detail, Exception innerException) : - base(detail, innerException) + base(detail, false, innerException) { - this.IsTransient = false; } /// Constructor. @@ -58,7 +34,6 @@ internal MessagingEntityAlreadyExistsException(MessagingExceptionDetail detail, MessagingEntityAlreadyExistsException(SerializationInfo info, StreamingContext context) : base(info, context) { - this.IsTransient = false; } } } diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityIsDisabledException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityIsDisabledException.cs deleted file mode 100644 index 48b7c31..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityIsDisabledException.cs +++ /dev/null @@ -1,65 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - using System.Security; - using Microsoft.Azure.NotificationHubs; - - /// Exception for signalling messaging entity not found errors. - [Serializable] - public sealed class MessagingEntityDisabledException : MessagingException - { - /// - /// Initializes a new instance of the class. - /// - /// Name of the entity. - public MessagingEntityDisabledException(string entityName) - : this(string.Format(SRClient.MessagingEntityIsDisabledException, entityName), null) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The string exception message. - /// The inner exception to be propagated with this exception to the caller.. - public MessagingEntityDisabledException(string message, Exception innerException) - : base(message, innerException) - { - this.IsTransient = false; - } - - /// Constructor. - /// Detail about the cause of the exception. - internal MessagingEntityDisabledException(MessagingExceptionDetail detail) : - base(detail) - { - this.IsTransient = false; - } - - /// Constructor. - /// Detail about the cause of the exception. - /// The inner exception. - internal MessagingEntityDisabledException(MessagingExceptionDetail detail, Exception innerException) : - base(detail, innerException) - { - this.IsTransient = false; - } - - /// Constructor. - /// The information. - /// The context. - MessagingEntityDisabledException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityMovedException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityMovedException.cs deleted file mode 100644 index 8371687..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityMovedException.cs +++ /dev/null @@ -1,46 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - using Microsoft.Azure.NotificationHubs; - - [Serializable] - sealed class MessagingEntityMovedException : MessagingException - { - public MessagingEntityMovedException(string entityName) - : base(string.Format(SRClient.MessagingEntityMoved, entityName), null) - { - this.IsTransient = false; - } - - public MessagingEntityMovedException(string mesage, Exception innerException) - : base(mesage, innerException) - { - this.IsTransient = false; - } - - internal MessagingEntityMovedException(MessagingExceptionDetail detail) : - base(detail) - { - this.IsTransient = false; - } - - internal MessagingEntityMovedException(MessagingExceptionDetail detail, Exception innerException) : - base(detail, innerException) - { - this.IsTransient = false; - } - - MessagingEntityMovedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityNotFoundException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityNotFoundException.cs index 36dc790..f11d002 100644 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityNotFoundException.cs +++ b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingEntityNotFoundException.cs @@ -8,51 +8,24 @@ namespace Microsoft.Azure.NotificationHubs.Messaging { using System; using System.Runtime.Serialization; - using System.Security; - using Microsoft.Azure.NotificationHubs; /// Exception for signalling messaging entity not found errors. [Serializable] public sealed class MessagingEntityNotFoundException : MessagingException { - /// - /// Initializes a new instance of the class. - /// - /// Name of the entity. - public MessagingEntityNotFoundException(string entityName) - : this(MessagingExceptionDetail.EntityNotFound(string.Format(SRClient.MessagingEntityCouldNotBeFound, entityName)), null) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The string exception message. - /// The inner exception to be propagated with this exception to the caller.. - public MessagingEntityNotFoundException(string message, Exception innerException) - : base(MessagingExceptionDetail.EntityNotFound(message), innerException) - { - this.IsTransient = false; - } - /// Constructor. /// Detail about the cause of the exception. - /// The TrackingContext. internal MessagingEntityNotFoundException(MessagingExceptionDetail detail) : - base(detail) + base(detail, false) { - this.IsTransient = false; } /// Constructor. /// Detail about the cause of the exception. - /// The TrackingContext. /// The inner exception. internal MessagingEntityNotFoundException(MessagingExceptionDetail detail, Exception innerException) : - base(detail, innerException) + base(detail, false, innerException) { - this.IsTransient = false; } /// Constructor. @@ -61,7 +34,6 @@ internal MessagingEntityNotFoundException(MessagingExceptionDetail detail, Excep MessagingEntityNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) { - this.IsTransient = false; } /// diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingException.cs index e2b6327..1dc0e44 100644 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingException.cs +++ b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingException.cs @@ -15,61 +15,23 @@ namespace Microsoft.Azure.NotificationHubs.Messaging [Serializable] public class MessagingException : Exception { - // TODO 228780 Remove MessagingException constructor that does not take an exception detail and context information - - - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - public MessagingException(string message) : - base(message) - { - MessagingExceptionDetail detail = MessagingExceptionDetail.UnknownDetail(message); - this.Initialize(detail, DateTime.UtcNow); - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - /// The inner exception. - public MessagingException(string message, Exception innerException) : - base(message, innerException) - { - MessagingExceptionDetail detail = MessagingExceptionDetail.UnknownDetail(message); - this.Initialize(detail, DateTime.UtcNow); - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - /// If set to true, indicates it is a transient error. - /// The inner exception. - public MessagingException(string message, bool isTransientError, Exception innerException) : - base(message, innerException) - { - MessagingExceptionDetail detail = MessagingExceptionDetail.UnknownDetail(message); - this.Initialize(detail, DateTime.UtcNow); - this.IsTransient = isTransientError; - } - /// Constructor. - /// - internal MessagingException(MessagingExceptionDetail detail) : - base(detail.Message) + /// Detail about the cause of the exception. + /// If set to true, indicates it is a transient error. + internal MessagingException(MessagingExceptionDetail detail, bool isTransientError) : + this(detail, isTransientError, null) { - this.Initialize(detail, DateTime.UtcNow); } /// Constructor. /// Detail about the cause of the exception. + /// If set to true, indicates it is a transient error. /// The inner exception. - internal MessagingException(MessagingExceptionDetail detail, Exception innerException) : + internal MessagingException(MessagingExceptionDetail detail, bool isTransientError, Exception innerException) : base(detail.Message, innerException) { this.Initialize(detail, DateTime.UtcNow); + this.IsTransient = isTransientError; } /// Constructor. @@ -81,6 +43,7 @@ protected MessagingException(SerializationInfo info, StreamingContext context) : this.Initialize( (MessagingExceptionDetail)info.GetValue("Detail", typeof(MessagingExceptionDetail)), (DateTime)info.GetValue("Timestamp", typeof(DateTime))); + this.IsTransient = (bool)info.GetValue("IsTransient", typeof(DateTime)); } /// @@ -98,7 +61,12 @@ protected MessagingException(SerializationInfo info, StreamingContext context) : /// getting a true from this property implies that user can retry the operation that /// generated the exception without additional intervention. /// - public bool IsTransient { get; protected set; } + public bool IsTransient { get; private set; } + + /// + /// If set, indicates recommended time for waiting before retrying transient errors. + /// + public TimeSpan? RetryAfter { get; protected set; } /// /// Sets the with information about the exception. @@ -110,6 +78,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont base.GetObjectData(info, context); info.AddValue("Detail", this.Detail); + info.AddValue("IsTransient", this.IsTransient); info.AddValue("Timestamp", this.Timestamp.ToString()); } diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingExceptionDetail.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingExceptionDetail.cs index 828842c..77df3fd 100644 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingExceptionDetail.cs +++ b/src/Microsoft.Azure.NotificationHubs/Messaging/MessagingExceptionDetail.cs @@ -7,7 +7,7 @@ namespace Microsoft.Azure.NotificationHubs.Messaging { using System; - using Microsoft.Azure.NotificationHubs; + using System.Net; /// /// Details about the cause of a Messaging Exception that map errors to specific exceptions. @@ -15,16 +15,13 @@ namespace Microsoft.Azure.NotificationHubs.Messaging [Serializable] public sealed class MessagingExceptionDetail { - private MessagingExceptionDetail(int errorCode, string message) - : this(errorCode, message, ErrorLevelType.UserError) + internal MessagingExceptionDetail(ExceptionErrorCodes errorCode, string message, ErrorLevelType errorLevel, HttpStatusCode? httpStatusCode, string trackingId) { - } - - private MessagingExceptionDetail(int errorCode, string message, ErrorLevelType errorLevel) - { - this.ErrorCode = errorCode; + this.ErrorCode = (int)errorCode; this.Message = message; this.ErrorLevel = errorLevel; + this.HttpStatusCode = httpStatusCode; + this.TrackingId = trackingId; } /// @@ -41,6 +38,11 @@ public enum ErrorLevelType /// The server error /// ServerError, + + /// + /// Error related to client connectivity + /// + ClientConnection } /// @@ -49,163 +51,23 @@ public enum ErrorLevelType public int ErrorCode { get; private set; } /// - /// A human-readable message that gives more detail about the cause of this error. - /// - public string Message { get; private set; } - - /// - /// An enumerated value indicating the type of error. - /// - public ErrorLevelType ErrorLevel { get; private set; } - - /// - /// Creates a new instance of the class with UnknownExceptionDetail error code. - /// - /// The exception message. - /// The exception class instance - public static MessagingExceptionDetail UnknownDetail(string message) - { - return new MessagingExceptionDetail((int)ExceptionErrorCodes.UnknownExceptionDetail, message); - } - - /// - /// Creates a new instance of the class with EntityGone error code. - /// - /// The exception message. - /// The exception class instance - public static MessagingExceptionDetail EntityGone(string message) - { - return new MessagingExceptionDetail((int)ExceptionErrorCodes.EntityGone, message); - } - - /// - /// Creates a new instance of the class with EndpointNotFound error code. - /// - /// The exception message. - /// The exception class instance - public static MessagingExceptionDetail EntityNotFound(string message) - { - return new MessagingExceptionDetail((int)ExceptionErrorCodes.EndpointNotFound, message); - } - - /// - /// Creates a new instance of the class with ConflictGeneric error code. - /// - /// The exception message. - /// The exception class instance - public static MessagingExceptionDetail EntityConflict(string message) - { - return new MessagingExceptionDetail((int)ExceptionErrorCodes.ConflictGeneric, message); - } - - /// - /// Creates a new instance of the class with ServerBusy error code. - /// - /// The exception message. - /// The exception class instance - public static MessagingExceptionDetail ServerBusy(string message) - { - return new MessagingExceptionDetail((int)ExceptionErrorCodes.ServerBusy, message, ErrorLevelType.ServerError); - } - - /// - /// Creates a new instance of the class with StoreLockLost error code. - /// - /// The exception message. - /// The exception class instance - public static MessagingExceptionDetail StoreLockLost(string message) - { - return new MessagingExceptionDetail((int)ExceptionErrorCodes.StoreLockLost, message); - } - - /// - /// Creates a new instance of the class with UnspecifiedInternalError error code. - /// - /// The exception message. - /// The exception class instance - public static MessagingExceptionDetail UnspecifiedInternalError(string message) - { - return new MessagingExceptionDetail((int)ExceptionErrorCodes.UnspecifiedInternalError, message, ErrorLevelType.ServerError); - } - - /// - /// Creates a new instance of the class with SqlFiltersExceeded error code. + /// Http status code of the response. /// - /// The exception message. - /// The exception class instance - public static MessagingExceptionDetail SqlFiltersExceeded(string message) - { - return new MessagingExceptionDetail((int)ExceptionErrorCodes.SqlFiltersExceeded, message); - } + public HttpStatusCode? HttpStatusCode { get; private set; } /// - /// Creates a new instance of the class with CorrelationFiltersExceeded error code. + /// Tracking ID of the request. /// - /// The exception message. - /// The exception class instance - public static MessagingExceptionDetail CorrelationFiltersExceeded(string message) - { - return new MessagingExceptionDetail((int)ExceptionErrorCodes.CorrelationFiltersExceeded, message); - } + public string TrackingId { get; private set; } /// - /// Creates a new instance of the class with SubscriptionsExceeded error code. - /// - /// The exception message. - /// The exception class instance - public static MessagingExceptionDetail SubscriptionsExceeded(string message) - { - return new MessagingExceptionDetail((int)ExceptionErrorCodes.SubscriptionsExceeded, message); - } - - /// - /// Creates a new instance of the class with EventHubAtFullCapacity error code. - /// - /// The exception message. - /// The exception class instance - public static MessagingExceptionDetail EventHubAtFullCapacity(string message) - { - return new MessagingExceptionDetail((int)ExceptionErrorCodes.EventHubAtFullCapacity, message, ErrorLevelType.ServerError); - } - - /// - /// Creates a new instance of the class with UpdateConflict error code. - /// - /// Name of the entity. - /// - /// The exception class instance - /// - public static MessagingExceptionDetail EntityUpdateConflict(string entityName) - { - string message = string.Format(SRClient.MessagingEntityUpdateConflict, entityName); - return new MessagingExceptionDetail((int)ExceptionErrorCodes.UpdateConflict, message); - } - - /// - /// Creates a new instance of the class with ConflictOperationInProgress error code. + /// A human-readable message that gives more detail about the cause of this error. /// - /// Name of the entity. - /// - /// The exception class instance - /// - public static MessagingExceptionDetail EntityConflictOperationInProgress(string entityName) - { - string message = string.Format(SRClient.MessagingEntityRequestConflict, entityName); - return new MessagingExceptionDetail((int)ExceptionErrorCodes.ConflictOperationInProgress, message); - } + public string Message { get; private set; } /// - /// Creates a new instance of the class with a custom error code. + /// An enumerated value indicating the type of error. /// - /// The error code. - /// The exception message. - /// The error level. - /// - /// The exception class instance - /// - public static MessagingExceptionDetail ReconstructExceptionDetail(int errorCode, string message, ErrorLevelType errorLevel) - { - return new MessagingExceptionDetail(errorCode, message, errorLevel); - } + public ErrorLevelType ErrorLevel { get; private set; } } } diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/NoMatchingSubscriptionException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/NoMatchingSubscriptionException.cs deleted file mode 100644 index 2eea5d8..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/NoMatchingSubscriptionException.cs +++ /dev/null @@ -1,49 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - - /// - /// The exception that is thrown when subscription matching resulted no match. - /// - [Serializable] - public sealed class NoMatchingSubscriptionException : MessagingException - { - /// - /// Initializes a new instance of the class with error message. - /// - /// The error message about the exception. - public NoMatchingSubscriptionException(string message) - : base(message) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class with error message and inner exception. - /// - /// The error message about the exception.The inner exception that is the cause of the current exception. - public NoMatchingSubscriptionException(string message, Exception innerException) - : base(message, innerException) - { - this.IsTransient = false; - } - - /// - /// Returns the string representation of the . - /// - /// - /// - /// The string representation of the . - /// - public override string ToString() - { - return this.Message; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/PairedMessagingFactoryException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/PairedMessagingFactoryException.cs deleted file mode 100644 index 39e13c8..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/PairedMessagingFactoryException.cs +++ /dev/null @@ -1,27 +0,0 @@ -//---------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//---------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - - /// - /// Represents the exception occurred for the paired messaging factory. - /// - [Serializable] - public class PairedMessagingFactoryException : MessagingException - { - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - public PairedMessagingFactoryException(string message) - : base(message) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/PartitionNotOwnedException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/PartitionNotOwnedException.cs deleted file mode 100644 index 487d4c0..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/PartitionNotOwnedException.cs +++ /dev/null @@ -1,51 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - using System.Security; - using Microsoft.Azure.NotificationHubs.Messaging; - - /// Exception for signalling partition not owned errors. - [Serializable] - public sealed class PartitionNotOwnedException : MessagingException - { - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - public PartitionNotOwnedException(string message) - : base(message) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - /// The inner exception. - public PartitionNotOwnedException(string message, Exception innerException) - : base(message, innerException) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The serialization information. - /// The streaming context. - PartitionNotOwnedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - this.IsTransient = false; - } - } -} - diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/QuotaExceededException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/QuotaExceededException.cs index 2da7f8a..efff5d4 100644 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/QuotaExceededException.cs +++ b/src/Microsoft.Azure.NotificationHubs/Messaging/QuotaExceededException.cs @@ -13,42 +13,15 @@ namespace Microsoft.Azure.NotificationHubs.Messaging [Serializable] public class QuotaExceededException : MessagingException { - /// - /// Initializes a new instance of the class. - /// - /// The exception message included with the base exception. - public QuotaExceededException(string message) : - base(message) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message included with the base exception. - /// The inner exception. - public QuotaExceededException(string message, Exception innerException) : - base(message, innerException) - { - this.IsTransient = false; - } - - /// Constructor. - /// Detail about the cause of the exception. - internal QuotaExceededException(MessagingExceptionDetail detail) : - base(detail) - { - this.IsTransient = false; - } + internal readonly TimeSpan DefaultRetryTimeout = TimeSpan.FromSeconds(10); /// Constructor. /// Detail about the cause of the exception. - /// The inner exception. - internal QuotaExceededException(MessagingExceptionDetail detail, Exception innerException) : - base(detail, innerException) + /// Retry after value. + internal QuotaExceededException(MessagingExceptionDetail detail, TimeSpan? retryAfter) : + base(detail, true) { - this.IsTransient = false; + RetryAfter = retryAfter ?? DefaultRetryTimeout; } /// Exception Constructor for additional details embedded in a serializable stream. @@ -57,7 +30,6 @@ internal QuotaExceededException(MessagingExceptionDetail detail, Exception inner protected QuotaExceededException(SerializationInfo info, StreamingContext context) : base(info, context) { - this.IsTransient = false; } } } diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/ReceiverDisconnectedException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/ReceiverDisconnectedException.cs deleted file mode 100644 index ea1804e..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/ReceiverDisconnectedException.cs +++ /dev/null @@ -1,57 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - - /// - /// This exception is thrown if two or more EventHubReceiver connect - /// to the same partition with different epoch values. - /// - [Serializable] - public sealed class ReceiverDisconnectedException : MessagingException - { - /// - /// Initializes a new instance of the class with the specified exception message. - /// - /// The exception message. - public ReceiverDisconnectedException(string message) - : base(message) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class with the specified exception message and inner exception text. - /// - /// The exception message.The inner exception text. - public ReceiverDisconnectedException(string message, Exception innerException) - : base(message, innerException) - { - this.IsTransient = false; - } - - internal ReceiverDisconnectedException(MessagingExceptionDetail detail) : - base(detail) - { - this.IsTransient = false; - } - - internal ReceiverDisconnectedException(MessagingExceptionDetail detail, Exception innerException) : - base(detail, innerException) - { - this.IsTransient = false; - } - - ReceiverDisconnectedException(SerializationInfo info, StreamingContext context) : - base(info, context) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/RequestQuotaExceededException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/RequestQuotaExceededException.cs deleted file mode 100644 index c3ba6ea..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/RequestQuotaExceededException.cs +++ /dev/null @@ -1,55 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - - /// Exception for signaling receive request quota exceeded errors. - [Serializable] - class RequestQuotaExceededException : QuotaExceededException - { - /// Constructor. - /// The exception message included with the base exception. - public RequestQuotaExceededException(string message) : - base(message) - { - } - - /// Exception Constructor. - /// The exception message included with the base exception. - /// The inner exception. - public RequestQuotaExceededException(string message, Exception innerException) : - base(message, innerException) - { - } - - /// Constructor. - /// Detail about the cause of the exception. - internal RequestQuotaExceededException(MessagingExceptionDetail detail) : - base(detail) - { - } - - /// Constructor. - /// Detail about the cause of the exception. - /// The inner exception. - internal RequestQuotaExceededException(MessagingExceptionDetail detail, Exception innerException) : - base(detail, innerException) - { - } - - /// Exception Constructor for additional details embedded in a serializable stream. - /// The serialization information object. - /// The streaming context/source. - protected RequestQuotaExceededException(SerializationInfo info, StreamingContext context) : - base(info, context) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/SendAvailabilityBacklogException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/SendAvailabilityBacklogException.cs deleted file mode 100644 index 224bd61..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/SendAvailabilityBacklogException.cs +++ /dev/null @@ -1,27 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - - - /// - /// Represents the exception occurred during the sending of availability backlogs. - /// - [Serializable] - public class SendAvailabilityBacklogException : Exception - { - /// - /// Initializes a new instance of the class. - /// - /// The message associated with the exception. - public SendAvailabilityBacklogException(string message) : base(message) - { - - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/SendAvailabilityMessagingException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/SendAvailabilityMessagingException.cs deleted file mode 100644 index 0f3b5f4..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/SendAvailabilityMessagingException.cs +++ /dev/null @@ -1,27 +0,0 @@ -//---------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//---------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using Microsoft.Azure.NotificationHubs; - /// - /// Represents the exceptions occurred during the sending the availability for the messaging. - /// - [Serializable] - public class SendAvailabilityMessagingException : MessagingException - { - /// - /// Initializes a new instance of the class. - /// - /// The error that caused the exception. - public SendAvailabilityMessagingException(Exception innerException) - : base(SRClient.PairedNamespacePrimaryEntityUnreachable, innerException) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/ServerBusyException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/ServerBusyException.cs index b186b7f..e39bde5 100644 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/ServerBusyException.cs +++ b/src/Microsoft.Azure.NotificationHubs/Messaging/ServerBusyException.cs @@ -15,29 +15,10 @@ namespace Microsoft.Azure.NotificationHubs.Messaging [Serializable] public sealed class ServerBusyException : MessagingException { - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - public ServerBusyException(string message) - : this(message, null) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - /// The inner exception. - public ServerBusyException(string message, Exception innerException) - : base(MessagingExceptionDetail.ServerBusy(message), innerException) - { - } - /// Constructor. /// Detail about the cause of the exception. - internal ServerBusyException(MessagingExceptionDetail detail) : - base(detail) + internal ServerBusyException(MessagingExceptionDetail detail) + : base(detail, true) { } @@ -46,4 +27,4 @@ internal ServerBusyException(MessagingExceptionDetail detail) : { } } -} \ No newline at end of file +} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/SessionCannotBeLockedException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/SessionCannotBeLockedException.cs deleted file mode 100644 index f240911..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/SessionCannotBeLockedException.cs +++ /dev/null @@ -1,45 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - - /// - /// - /// - [Serializable] - public sealed class SessionCannotBeLockedException : MessagingException - { - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - public SessionCannotBeLockedException(string message) - : base(message) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - /// The inner exception. - public SessionCannotBeLockedException(string message, Exception innerException) - : base(message, innerException) - { - this.IsTransient = false; - } - - SessionCannotBeLockedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - this.IsTransient = false; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/SessionLockLostException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/SessionLockLostException.cs deleted file mode 100644 index 21a5b30..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/SessionLockLostException.cs +++ /dev/null @@ -1,46 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - - /// Exception for signalling session lock lost errors. - [Serializable] - public sealed class SessionLockLostException : MessagingException - { - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - public SessionLockLostException(string message) : - base(message) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - /// The inner exception. - public SessionLockLostException(string message, Exception innerException) : - base(message, innerException) - { - this.IsTransient = false; - } - - /// Constructor. - /// The information. - /// The context. - SessionLockLostException(SerializationInfo info, StreamingContext context) : - base(info, context) - { - this.IsTransient = false; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/SoapActionNotSupportedException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/SoapActionNotSupportedException.cs deleted file mode 100644 index f60e7a6..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/SoapActionNotSupportedException.cs +++ /dev/null @@ -1,19 +0,0 @@ -//------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//------------------------------------------------------------ - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - - [Serializable] - sealed class SoapActionNotSupportedException : MessagingException - { - public SoapActionNotSupportedException(string message, Exception exception) : base(message, exception) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/TransactionMessagingException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/TransactionMessagingException.cs deleted file mode 100644 index 76b5009..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/TransactionMessagingException.cs +++ /dev/null @@ -1,31 +0,0 @@ -//------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//------------------------------------------------------------ - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - - [Serializable] - sealed class TransactionMessagingException : MessagingException - { - public TransactionMessagingException(string message, Exception innerException) - : base(message, innerException) - { - this.Initialize(); - } - - public TransactionMessagingException(string message) - : base(message) - { - this.Initialize(); - } - - void Initialize() - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/TransactionSizeExceededException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/TransactionSizeExceededException.cs deleted file mode 100644 index 636c39c..0000000 --- a/src/Microsoft.Azure.NotificationHubs/Messaging/TransactionSizeExceededException.cs +++ /dev/null @@ -1,63 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//----------------------------------------------------------------------------- - -namespace Microsoft.Azure.NotificationHubs.Messaging -{ - using System; - using System.Runtime.Serialization; - - /// Exception for signaling quota exceeded errors. - [Serializable] - public sealed class TransactionSizeExceededException : QuotaExceededException - { - /// - /// Initializes a new instance of the class. - /// - /// The exception message included with the base exception. - public TransactionSizeExceededException(string message) : - base(message) - { - this.IsTransient = false; - } - - /// - /// Initializes a new instance of the class. - /// - /// The exception message included with the base exception. - /// The inner exception. - public TransactionSizeExceededException(string message, Exception innerException) : - base(message, innerException) - { - this.IsTransient = false; - } - - /// Constructor. - /// Detail about the cause of the exception. - internal TransactionSizeExceededException(MessagingExceptionDetail detail) : - base(detail) - { - this.IsTransient = false; - } - - /// Constructor. - /// Detail about the cause of the exception. - /// The inner exception. - internal TransactionSizeExceededException(MessagingExceptionDetail detail, Exception innerException) : - base(detail, innerException) - { - this.IsTransient = false; - } - - /// Exception Constructor for additional details embedded in a serializable stream. - /// The serialization information object. - /// The streaming context/source. - TransactionSizeExceededException(SerializationInfo info, StreamingContext context) : - base(info, context) - { - this.IsTransient = false; - } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/Messaging/UnauthorizedException.cs b/src/Microsoft.Azure.NotificationHubs/Messaging/UnauthorizedException.cs new file mode 100644 index 0000000..0843f11 --- /dev/null +++ b/src/Microsoft.Azure.NotificationHubs/Messaging/UnauthorizedException.cs @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. +//----------------------------------------------------------------------------- + +namespace Microsoft.Azure.NotificationHubs.Messaging +{ + using System; + using System.Runtime.Serialization; + + /// Exception for signaling authorization errors. + [Serializable] + public class UnauthorizedException : MessagingException + { + /// Constructor. + /// Detail about the cause of the exception. + internal UnauthorizedException(MessagingExceptionDetail detail) : + base(detail, false) + { + } + + /// Exception Constructor for additional details embedded in a serializable stream. + /// The serialization information object. + /// The streaming context/source. + protected UnauthorizedException(SerializationInfo info, StreamingContext context) : + base(info, context) + { + } + } +} diff --git a/src/Microsoft.Azure.NotificationHubs/NamespaceManager.cs b/src/Microsoft.Azure.NotificationHubs/NamespaceManager.cs index b37ef18..5620ff7 100644 --- a/src/Microsoft.Azure.NotificationHubs/NamespaceManager.cs +++ b/src/Microsoft.Azure.NotificationHubs/NamespaceManager.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Sockets; using System.Runtime.Serialization; using System.Text; using System.Threading; @@ -17,6 +18,7 @@ using System.Xml; using Microsoft.Azure.NotificationHubs.Auth; using Microsoft.Azure.NotificationHubs.Messaging; +using static Microsoft.Azure.NotificationHubs.Messaging.MessagingExceptionDetail; namespace Microsoft.Azure.NotificationHubs { @@ -29,16 +31,10 @@ public sealed class NamespaceManager : INamespaceManager private const string Header = ""; private const string Footer = ""; private const string TrackingIdHeaderKey = "TrackingId"; - private readonly IEnumerable _addresses; private readonly HttpClient _httpClient; - - /// Gets the first namespace base address. - /// The namespace base address. - public Uri Address => _addresses.First(); - - /// Gets the namespace manager settings. - /// The namespace manager settings. - public NamespaceManagerSettings Settings { get; } + private readonly Uri _baseUri; + private readonly TokenProvider _tokenProvider; + private readonly NotificationHubRetryPolicy _retryPolicy; /// /// Gets operation timeout of the HTTP operations. @@ -57,152 +53,42 @@ public sealed class NamespaceManager : INamespaceManager /// An instance of the class public static NamespaceManager CreateFromConnectionString(string connectionString) { - KeyValueConfigurationManager manager = new KeyValueConfigurationManager(connectionString); - return manager.CreateNamespaceManager(); - } - - /// NamespaceManager Constructor. You must supply your base address to access your namespace. Anonymous credentials are assumed. - /// The full address of the namespace. - /// Thrown when address is null. - public NamespaceManager(string address) - : this(new Uri(address), (TokenProvider)null) - { - } - - /// NamespaceManager Constructor. You must supply your base addresses to access your namespace. Anonymous credentials are assumed. - /// The full addresses of the namespace. - /// Thrown when addresses field is null. - /// Thrown when addresses list is null or empty. - /// Thrown when address is not correctly formed. - public NamespaceManager(IList addresses) - : this(addresses, (TokenProvider)null) - { - } - - /// NamespaceManager Constructor. You must supply your base address to access your namespace. Anonymous credentials are assumed. - /// The full address of the namespace. - /// Thrown when address is null. - public NamespaceManager(Uri address) - : this(address, (TokenProvider)null) - { - } - - /// NamespaceManager Constructor. You must supply your base address to access your namespace. Anonymous credentials are assumed. - /// The full addresses of the namespace. - /// Thrown when addresses field is null. - /// Thrown when addresses list is null or empty. - public NamespaceManager(IEnumerable addresses) - : this(addresses, (TokenProvider)null) - { - } - - /// NamespaceManager Constructor. You must supply your base address and proper credentials to access your namespace. - /// The full address of the namespace. - /// The namespace access credentials. - /// Even though it is not allowed to include paths in the namespace address, you can specify a credential that authorizes you to perform actions only on - /// some sublevels off of the base address. - /// Thrown when address is null. - public NamespaceManager(string address, TokenProvider tokenProvider) - : this(new Uri(address), tokenProvider) - { - } - - /// NamespaceManager Constructor. You must supply your base address and proper credentials to access your namespace. - /// The full addresses of the namespace. - /// The namespace access credentials. - /// Even though it is not allowed to include paths in the namespace addresses, you can specify a credential that authorizes you to perform actions only on - /// some sublevels off of the base addresses. - /// Thrown when addresses field is null. - /// Thrown when addresses list is null or empty. - /// Thrown when address is not correctly formed. - - public NamespaceManager(IList addresses, TokenProvider tokenProvider) - : this(MessagingUtilities.GetUriList(addresses), tokenProvider) - { - } - - /// NamespaceManager Constructor. You must supply your base address and proper credentials to access your namespace. - /// The full address of the namespace. - /// A TokenProvider for the namespace - /// Even though it is not allowed to include paths in the namespace address, you can specify a credential that authorizes you to perform actions only on - /// some sublevels off of the base address, i.e. it is not a must that the credentials you specify be to the base adress itself - /// Thrown if address is null. - /// Thrown when the type is not a supported Credential type. - /// See to know more about the supported types. - public NamespaceManager(Uri address, TokenProvider tokenProvider) - : this(address, new NamespaceManagerSettings(tokenProvider)) - { - } - - /// NamespaceManager Constructor. You must supply your base address and proper credentials to access your namespace. - /// The full address of the namespace. - /// A TokenProvider for the namespace - /// Even though it is not allowed to include paths in the namespace addresses, you can specify a credential that authorizes you to perform actions only on - /// some sublevels off of the base addresses, i.e. it is not a must that the credentials you specify be to the base adresses itself - /// Thrown if addresses is null. - /// Thrown when the type is not a supported Credential type. - /// See to know more about the supported types. - /// Thrown when addresses list is null or empty. - public NamespaceManager(IEnumerable addresses, TokenProvider tokenProvider) - : this(addresses, new NamespaceManagerSettings(tokenProvider)) - { - } - - /// NamespaceManager Constructor. You must supply your base address and proper credentials to access your namespace. - /// The full address of the namespace. - /// Namespace manager settings. - /// Even though it is not allowed to include paths in the namespace address, you can specify a credential that authorizes you to perform actions only on - /// some sublevels off of the base address, i.e. it is not a must that the credentials you specify be to the base adress itself - /// Thrown when address or settings is null. - public NamespaceManager(string address, NamespaceManagerSettings settings) - : this(new Uri(address), settings) - { - } - - /// NamespaceManager Constructor. You must supply your base address and proper credentials to access your namespace. - /// The full addresses of the namespace. - /// Namespace manager settings. - /// Even though it is not allowed to include paths in the namespace address, you can specify a credential that authorizes you to perform actions only on - /// some sublevels off of the base addresses, i.e. it is not a must that the credentials you specify be to the base adresses itself - /// Thrown when address or settings is null. - /// Thrown when addresses list is null or empty. - /// Thrown when address is not correctly formed. - public NamespaceManager(IList addresses, NamespaceManagerSettings settings) - : this(MessagingUtilities.GetUriList(addresses), settings) - { + return new NamespaceManager(connectionString); } - /// NamespaceManager Constructor. You must supply your base address and proper credentials to access your namespace. - /// The full address of the namespace. - /// Namespace manager settings. - /// Even though it is not allowed to include paths in the namespace address, you can specify a credential that authorizes you to perform actions only on - /// some sublevels off of the base address, i.e. it is not a must that the credentials you specify be to the base adress itself - /// Thrown when address or settings is null. - public NamespaceManager(Uri address, NamespaceManagerSettings settings) - : this(new List() { address }, settings) + /// + /// Initializes a new instance of + /// + /// Namespace connection string + public NamespaceManager(string connectionString) : this(connectionString, null) { } - /// NamespaceManager Constructor. You must supply your base address and proper credentials to access your namespace. - /// The full address of the namespace. + /// Initializes a new instance of with settings + /// Namespace connection string /// Namespace manager settings. - /// Even though it is not allowed to include paths in the namespace addresses, you can specify a credential that authorizes you to perform actions only on - /// some sublevels off of the base addresses, i.e. it is not a must that the credentials you specify be to the base adresses itself - /// Thrown when addresses or settings is null. - /// Thrown when addresses list is null or empty. - public NamespaceManager(IEnumerable addresses, NamespaceManagerSettings settings) + public NamespaceManager(string connectionString, NotificationHubSettings settings) { - MessagingUtilities.ThrowIfNullAddressesOrPathExists(addresses); + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new ArgumentNullException(nameof(connectionString)); + } - _addresses = addresses.ToList(); - Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _tokenProvider = SharedAccessSignatureTokenProvider.CreateSharedAccessSignatureTokenProvider(connectionString); + var configurationManager = new KeyValueConfigurationManager(connectionString); + _baseUri = GetBaseUri(configurationManager); + settings = settings ?? new NotificationHubSettings(); - if (settings?.MessageHandler != null) + if (settings.HttpClient != null) { - var httpClientHandler = settings?.MessageHandler; + _httpClient = settings.HttpClient; + } + else if (settings.MessageHandler != null) + { + var httpClientHandler = settings.MessageHandler; _httpClient = new HttpClient(httpClientHandler); } - else if (settings?.Proxy != null) + else if (settings.Proxy != null) { var httpClientHandler = new HttpClientHandler(); httpClientHandler.UseProxy = true; @@ -214,7 +100,7 @@ public NamespaceManager(IEnumerable addresses, NamespaceManagerSettings set _httpClient = new HttpClient(); } - if (settings?.OperationTimeout == null) + if (settings.OperationTimeout == null) { OperationTimeout = TimeSpan.FromSeconds(60); } @@ -223,7 +109,10 @@ public NamespaceManager(IEnumerable addresses, NamespaceManagerSettings set OperationTimeout = settings.OperationTimeout.Value; } + _retryPolicy = settings.RetryOptions.ToRetryPolicy(); + _httpClient.Timeout = OperationTimeout; + SetUserAgent(); } /// @@ -311,32 +200,40 @@ public async Task GetNotificationHubAsync(string pat throw new ArgumentNullException(nameof(path)); } - var requestUri = new UriBuilder(Address) + var requestUri = new UriBuilder(_baseUri) { Scheme = Uri.UriSchemeHttps, Path = path, Query = $"api-version={ApiVersion}" }; - using (var response = await SendAsync(() => - { - var httpRequestMessage = CreateHttpRequest(HttpMethod.Get, requestUri.Uri); - - return httpRequestMessage; - }, cancellationToken).ConfigureAwait(false)) + return await _retryPolicy.RunOperation(async (ct) => { - var xmlResponse = await GetXmlContent(response).ConfigureAwait(false); - if (xmlResponse.NodeType != XmlNodeType.None) + using (var response = await SendAsync(() => { - var model = GetModelFromResponse(xmlResponse); - model.Path = path; - return model; - } - else + var httpRequestMessage = CreateHttpRequest(HttpMethod.Get, requestUri.Uri); + + return httpRequestMessage; + }, ct).ConfigureAwait(false)) { - throw new MessagingEntityNotFoundException("Notification Hub not found"); - } - }; + var trackingId = string.Empty; + if (response.Headers.TryGetValues(TrackingIdHeaderKey, out var values)) + { + trackingId = values.FirstOrDefault(); + } + var xmlResponse = await GetXmlContent(response, trackingId).ConfigureAwait(false); + if (xmlResponse.NodeType != XmlNodeType.None) + { + var model = GetModelFromResponse(xmlResponse, trackingId); + model.Path = path; + return model; + } + else + { + throw new MessagingEntityNotFoundException(new MessagingExceptionDetail(ExceptionErrorCodes.ConflictGeneric, "Notification Hub not found", ErrorLevelType.UserError, response.StatusCode, trackingId)); + } + }; + }, cancellationToken); } /// @@ -362,44 +259,47 @@ public Task> GetNotificationHubsAsync() /// A task that represents the asynchronous get hubs operation public async Task> GetNotificationHubsAsync(CancellationToken cancellationToken) { - var requestUri = new UriBuilder(Address) + var requestUri = new UriBuilder(_baseUri) { Scheme = Uri.UriSchemeHttps }; - using (var response = await SendAsync(() => - { - var httpRequestMessage = CreateHttpRequest(HttpMethod.Get, requestUri.Uri); - - return httpRequestMessage; - }, cancellationToken).ConfigureAwait(false)) + return await _retryPolicy.RunOperation(async (ct) => { - var result = new List(); + using (var response = await SendAsync(() => + { + var httpRequestMessage = CreateHttpRequest(HttpMethod.Get, requestUri.Uri); - using (var xmlReader = XmlReader.Create(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), new XmlReaderSettings { Async = true })) + return httpRequestMessage; + }, ct).ConfigureAwait(false)) { - // Advancing to the first element skipping non-content nodes - await xmlReader.MoveToContentAsync().ConfigureAwait(false); + var result = new List(); - if (!xmlReader.IsStartElement("feed")) + using (var xmlReader = XmlReader.Create(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), new XmlReaderSettings { Async = true })) { - throw new FormatException("Required 'feed' element is missing"); - } + // Advancing to the first element skipping non-content nodes + await xmlReader.MoveToContentAsync().ConfigureAwait(false); - while (xmlReader.ReadToFollowing("entry")) - { - if (xmlReader.ReadToDescendant("title")) + if (!xmlReader.IsStartElement("feed")) { - xmlReader.ReadStartElement(); - var hubName = xmlReader.Value; + throw new FormatException("Required 'feed' element is missing"); + } + + while (xmlReader.ReadToFollowing("entry")) + { + if (xmlReader.ReadToDescendant("title")) + { + xmlReader.ReadStartElement(); + var hubName = xmlReader.Value; - result.Add(await GetNotificationHubAsync(hubName, cancellationToken).ConfigureAwait(false)); + result.Add(await GetNotificationHubAsync(hubName, cancellationToken).ConfigureAwait(false)); + } } } - } - return result; - }; + return result; + }; + }, cancellationToken); } /// @@ -430,19 +330,22 @@ public async Task DeleteNotificationHubAsync(string path, CancellationToken canc throw new ArgumentNullException(nameof(path)); } - var requestUri = new UriBuilder(Address) + var requestUri = new UriBuilder(_baseUri) { Scheme = Uri.UriSchemeHttps, Path = path, Query = $"api-version={ApiVersion}" }; - await SendAsync(() => + await _retryPolicy.RunOperation(async (ct) => { - var httpRequestMessage = CreateHttpRequest(HttpMethod.Delete, requestUri.Uri); + return await SendAsync(() => + { + var httpRequestMessage = CreateHttpRequest(HttpMethod.Delete, requestUri.Uri); - return httpRequestMessage; - }, cancellationToken).ConfigureAwait(false); + return httpRequestMessage; + }, ct).ConfigureAwait(false); + }, cancellationToken); } /// @@ -516,7 +419,7 @@ private async Task CreateOrUpdateNotificationHubAsyn throw new ArgumentNullException(nameof(description)); } - var requestUri = new UriBuilder(Address) + var requestUri = new UriBuilder(_baseUri) { Scheme = Uri.UriSchemeHttps, Path = description.Path, @@ -524,24 +427,32 @@ private async Task CreateOrUpdateNotificationHubAsyn }; var xmlBody = CreateRequestBody(description); - using (var response = await SendAsync(() => - { - var httpRequestMessage = CreateHttpRequest(HttpMethod.Put, requestUri.Uri); - httpRequestMessage.Content = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(xmlBody))); + return await _retryPolicy.RunOperation(async (ct) => + { + using (var response = await SendAsync(() => + { + var httpRequestMessage = CreateHttpRequest(HttpMethod.Put, requestUri.Uri); + httpRequestMessage.Content = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(xmlBody))); - if (update) - { - httpRequestMessage.Headers.Add("If-Match", "*"); - } + if (update) + { + httpRequestMessage.Headers.Add("If-Match", "*"); + } - return httpRequestMessage; - }, cancellationToken).ConfigureAwait(false)) - { - var xmlResponse = await GetXmlContent(response).ConfigureAwait(false); - var model = GetModelFromResponse(xmlResponse); - model.Path = description.Path; - return model; - }; + return httpRequestMessage; + }, ct).ConfigureAwait(false)) + { + var trackingId = string.Empty; + if (response.Headers.TryGetValues(TrackingIdHeaderKey, out var values)) + { + trackingId = values.FirstOrDefault(); + } + var xmlResponse = await GetXmlContent(response, trackingId).ConfigureAwait(false); + var model = GetModelFromResponse(xmlResponse, trackingId); + model.Path = description.Path; + return model; + }; + }, cancellationToken); } /// Submits the notification hub job asynchronously. @@ -570,7 +481,7 @@ public async Task SubmitNotificationHubJobAsync(Notification throw new ArgumentNullException(nameof(job.OutputContainerUri)); } - var requestUri = new UriBuilder(Address) + var requestUri = new UriBuilder(_baseUri) { Scheme = Uri.UriSchemeHttps, Path = $"{notificationHubPath}/jobs", @@ -578,17 +489,25 @@ public async Task SubmitNotificationHubJobAsync(Notification }; var xmlBody = CreateRequestBody(job); - using (var response = await SendAsync(() => - { - var httpRequestMessage = CreateHttpRequest(HttpMethod.Post, requestUri.Uri); - httpRequestMessage.Content = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(xmlBody))); - - return httpRequestMessage; - }, cancellationToken).ConfigureAwait(false)) + return await _retryPolicy.RunOperation(async (ct) => { - var xmlResponse = await GetXmlContent(response).ConfigureAwait(false); - return GetModelFromResponse(xmlResponse); - }; + using (var response = await SendAsync(() => + { + var httpRequestMessage = CreateHttpRequest(HttpMethod.Post, requestUri.Uri); + httpRequestMessage.Content = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(xmlBody))); + + return httpRequestMessage; + }, ct).ConfigureAwait(false)) + { + var trackingId = string.Empty; + if (response.Headers.TryGetValues(TrackingIdHeaderKey, out var values)) + { + trackingId = values.FirstOrDefault(); + } + var xmlResponse = await GetXmlContent(response, trackingId).ConfigureAwait(false); + return GetModelFromResponse(xmlResponse, trackingId); + }; + }, cancellationToken); } /// Gets the notification hub job asynchronously. @@ -607,23 +526,31 @@ public Task GetNotificationHubJobAsync(string jobId, string /// A task that represents the asynchronous get job operation public async Task GetNotificationHubJobAsync(string jobId, string notificationHubPath, CancellationToken cancellationToken) { - var requestUri = new UriBuilder(Address) + var requestUri = new UriBuilder(_baseUri) { Scheme = Uri.UriSchemeHttps, Path = $"{notificationHubPath}/jobs/{jobId}", Query = $"api-version={ApiVersion}" }; - using (var response = await SendAsync(() => - { - var httpRequestMessage = CreateHttpRequest(HttpMethod.Get, requestUri.Uri); - - return httpRequestMessage; - }, cancellationToken).ConfigureAwait(false)) + return await _retryPolicy.RunOperation(async (ct) => { - var xmlResponse = await GetXmlContent(response).ConfigureAwait(false); - return GetModelFromResponse(xmlResponse); - }; + using (var response = await SendAsync(() => + { + var httpRequestMessage = CreateHttpRequest(HttpMethod.Get, requestUri.Uri); + + return httpRequestMessage; + }, ct).ConfigureAwait(false)) + { + var trackingId = string.Empty; + if (response.Headers.TryGetValues(TrackingIdHeaderKey, out var values)) + { + trackingId = values.FirstOrDefault(); + } + var xmlResponse = await GetXmlContent(response, trackingId).ConfigureAwait(false); + return GetModelFromResponse(xmlResponse, trackingId); + }; + }, cancellationToken); } @@ -641,36 +568,59 @@ public Task> GetNotificationHubJobsAsync(string /// A task that represents the asynchronous get jobs operation public async Task> GetNotificationHubJobsAsync(string notificationHubPath, CancellationToken cancellationToken) { - var requestUri = new UriBuilder(Address) + var requestUri = new UriBuilder(_baseUri) { Scheme = Uri.UriSchemeHttps, Path = $"{notificationHubPath}/jobs", Query = $"api-version={ApiVersion}" }; - using (var response = await SendAsync(() => CreateHttpRequest(HttpMethod.Get, requestUri.Uri), cancellationToken).ConfigureAwait(false)) + return await _retryPolicy.RunOperation(async (ct) => { - var result = new List(); - using (var xmlReader = XmlReader.Create(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), new XmlReaderSettings { Async = true })) + using (var response = await SendAsync(() => CreateHttpRequest(HttpMethod.Get, requestUri.Uri), ct).ConfigureAwait(false)) { - await xmlReader.MoveToContentAsync().ConfigureAwait(false); - - if (!xmlReader.IsStartElement("feed")) + var trackingId = string.Empty; + if (response.Headers.TryGetValues(TrackingIdHeaderKey, out var values)) { - throw new FormatException("Required 'feed' element is missing"); + trackingId = values.FirstOrDefault(); } - - while (xmlReader.ReadToFollowing("entry")) + var result = new List(); + using (var xmlReader = XmlReader.Create(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), new XmlReaderSettings { Async = true })) { - if (xmlReader.ReadToDescendant("content")) + await xmlReader.MoveToContentAsync().ConfigureAwait(false); + + if (!xmlReader.IsStartElement("feed")) { - xmlReader.ReadStartElement(); - result.Add(GetModelFromResponse(xmlReader)); + throw new FormatException("Required 'feed' element is missing"); + } + + while (xmlReader.ReadToFollowing("entry")) + { + if (xmlReader.ReadToDescendant("content")) + { + xmlReader.ReadStartElement(); + result.Add(GetModelFromResponse(xmlReader, trackingId)); + } } } + + return result; } + }, cancellationToken); + } + + private static Uri GetBaseUri(KeyValueConfigurationManager manager) + { + var endpointString = manager.connectionProperties[KeyValueConfigurationManager.EndpointConfigName]; + return new Uri(endpointString); + } - return result; + private void SetUserAgent() + { + if (!_httpClient.DefaultRequestHeaders.Contains(Constants.HttpUserAgentHeaderName)) + { + _httpClient.DefaultRequestHeaders.Add(Constants.HttpUserAgentHeaderName, + $"NHub/{ManagementStrings.ApiVersion} (api-origin=DotNetSdk;os={Environment.OSVersion.Platform};os-version={Environment.OSVersion.Version})"); } } @@ -694,36 +644,51 @@ private static string CreateRequestBody(T model) return AddHeaderAndFooterToXml(SerializeObject(model)); } - private static async Task GetXmlContent(HttpResponseMessage response) + private static async Task GetXmlContent(HttpResponseMessage response, string trackingId) { - var xmlReader = XmlReader.Create(await response.Content.ReadAsStreamAsync()); - if (xmlReader.ReadToFollowing("entry")) + try { - if (xmlReader.ReadToDescendant("content")) + if (response.Content == null) { - xmlReader.ReadStartElement(); + return XmlReader.Create(new StringReader(string.Empty)); } - } - return xmlReader; - } + var xmlReader = XmlReader.Create(await response.Content.ReadAsStreamAsync()); + if (xmlReader.ReadToFollowing("entry")) + { + if (xmlReader.ReadToDescendant("content")) + { + xmlReader.ReadStartElement(); + } + } - private static async Task GetXmlError(HttpResponseMessage response) => - XmlReader.Create(await response.Content.ReadAsStreamAsync().ConfigureAwait(false)); + return xmlReader; + } + catch (XmlException ex) + { + throw ExceptionsUtility.HandleXmlException(ex, trackingId); + } + } - private static T GetModelFromResponse(XmlReader xmlReader) where T : class + private static T GetModelFromResponse(XmlReader xmlReader, string trackingId) where T : class { var serializer = new DataContractSerializer(typeof(T)); - - using (xmlReader) + try { - return (T)serializer.ReadObject(xmlReader); + using (xmlReader) + { + return (T)serializer.ReadObject(xmlReader); + } + } + catch (SerializationException ex) when (ex.InnerException is XmlException xmlException) + { + throw ExceptionsUtility.HandleXmlException(xmlException, trackingId); } } private string CreateToken(Uri uri) { - return Settings.TokenProvider.GetToken(uri.ToString()); + return _tokenProvider.GetToken(uri.ToString()); } private HttpRequestMessage CreateHttpRequest(HttpMethod method, Uri uri) @@ -743,40 +708,36 @@ private async Task SendAsync(Func gener var httpRequestMessage = generateHttpRequestMessage(); httpRequestMessage.Headers.Add(TrackingIdHeaderKey, trackingId); - var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); - response.Headers.Add(TrackingIdHeaderKey, trackingId); - - if (response.IsSuccessStatusCode) + try { - return response; + var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); + response.Headers.Add(TrackingIdHeaderKey, trackingId); + + if (response.IsSuccessStatusCode) + { + return response; + } + else + { + throw await response.TranslateToMessagingExceptionAsync(trackingId).ConfigureAwait(false); + } } - else + catch (HttpRequestException ex) { - var xmlError = await GetXmlError(response).ConfigureAwait(false); - var error = GetModelFromResponse(xmlError); - trackingId = string.Empty; - - if (response.Headers.TryGetValues(TrackingIdHeaderKey, out var values)) + var innerException = ex.GetBaseException(); + if (innerException is SocketException socketException) { - trackingId = values.FirstOrDefault(); + throw ExceptionsUtility.HandleSocketException(socketException, OperationTimeout.Milliseconds, trackingId); } - - var innerException = new WebException($"The remote server returned an error: {error.Code}\nTrackingId: {trackingId}"); - - switch (response.StatusCode) + else { - case HttpStatusCode.NotFound: - throw new MessagingEntityNotFoundException(error.Detail, innerException); - case HttpStatusCode.Unauthorized: - throw new UnauthorizedAccessException(error.Detail, innerException); - case HttpStatusCode.BadRequest: - throw new MessagingCommunicationException(error.Detail, innerException); - case HttpStatusCode.Conflict: - throw new MessagingEntityAlreadyExistsException(error.Detail, innerException); - default: - throw new Exception(error.Detail, innerException); + throw ExceptionsUtility.HandleUnexpectedException(ex, trackingId); } } + catch (XmlException ex) + { + throw ExceptionsUtility.HandleXmlException(ex, trackingId); + } } } } diff --git a/src/Microsoft.Azure.NotificationHubs/NamespaceManagerSettings.cs b/src/Microsoft.Azure.NotificationHubs/NamespaceManagerSettings.cs deleted file mode 100644 index 86309fc..0000000 --- a/src/Microsoft.Azure.NotificationHubs/NamespaceManagerSettings.cs +++ /dev/null @@ -1,43 +0,0 @@ -//------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for -// license information. -//------------------------------------------------------------ - -using System; -using System.Net.Http; -using Microsoft.Azure.NotificationHubs.Auth; - -namespace Microsoft.Azure.NotificationHubs -{ - /// - /// Represents a namespace manager settings - /// - public sealed class NamespaceManagerSettings : NotificationHubClientSettings - { - /// - /// Initializes a new instance of the class. - /// - public NamespaceManagerSettings() - { - TokenProvider = null; - } - - /// - /// Initializes a new instance of the class. - /// - public NamespaceManagerSettings(TokenProvider tokenProvider) - { - TokenProvider = tokenProvider; - } - - /// - /// Gets or sets the security token provider. - /// - /// - /// - /// The security token provider. - /// - public TokenProvider TokenProvider { get; set; } - } -} diff --git a/src/Microsoft.Azure.NotificationHubs/NotificationHubClient.cs b/src/Microsoft.Azure.NotificationHubs/NotificationHubClient.cs index 90ca6d4..d1dc236 100644 --- a/src/Microsoft.Azure.NotificationHubs/NotificationHubClient.cs +++ b/src/Microsoft.Azure.NotificationHubs/NotificationHubClient.cs @@ -21,6 +21,7 @@ using System.Xml; using Microsoft.Azure.NotificationHubs.Auth; using Microsoft.Azure.NotificationHubs.Messaging; +using System.Net.Sockets; namespace Microsoft.Azure.NotificationHubs { @@ -38,6 +39,7 @@ public class NotificationHubClient : INotificationHubClient private readonly EntityDescriptionSerializer _entitySerializer = new EntityDescriptionSerializer(); private readonly string _notificationHubPath; private readonly TokenProvider _tokenProvider; + private readonly NotificationHubRetryPolicy _retryPolicy; private NamespaceManager _namespaceManager; /// @@ -55,7 +57,7 @@ public NotificationHubClient(string connectionString, string notificationHubPath /// Namespace connection string /// Hub name /// Settings - public NotificationHubClient(string connectionString, string notificationHubPath, NotificationHubClientSettings settings) + public NotificationHubClient(string connectionString, string notificationHubPath, NotificationHubSettings settings) { if (string.IsNullOrWhiteSpace(connectionString)) { @@ -68,17 +70,22 @@ public NotificationHubClient(string connectionString, string notificationHubPath } _notificationHubPath = notificationHubPath; - _tokenProvider = Auth.SharedAccessSignatureTokenProvider.CreateSharedAccessSignatureTokenProvider(connectionString); + _tokenProvider = SharedAccessSignatureTokenProvider.CreateSharedAccessSignatureTokenProvider(connectionString); var configurationManager = new KeyValueConfigurationManager(connectionString); _namespaceManager = NamespaceManager.CreateFromConnectionString(connectionString); _baseUri = GetBaseUri(configurationManager); - - if (settings?.MessageHandler != null) + settings = settings ?? new NotificationHubSettings(); + + if (settings.HttpClient != null) + { + _httpClient = settings.HttpClient; + } + else if (settings.MessageHandler != null) { - var httpClientHandler = settings?.MessageHandler; + var httpClientHandler = settings.MessageHandler; _httpClient = new HttpClient(httpClientHandler); } - else if (settings?.Proxy != null) + else if (settings.Proxy != null) { var httpClientHandler = new HttpClientHandler(); httpClientHandler.UseProxy = true; @@ -90,7 +97,7 @@ public NotificationHubClient(string connectionString, string notificationHubPath _httpClient = new HttpClient(); } - if (settings?.OperationTimeout == null) + if (settings.OperationTimeout == null) { OperationTimeout = TimeSpan.FromSeconds(60); } @@ -99,6 +106,8 @@ public NotificationHubClient(string connectionString, string notificationHubPath OperationTimeout = settings.OperationTimeout.Value; } + _retryPolicy = settings.RetryOptions.ToRetryPolicy(); + _httpClient.Timeout = OperationTimeout; SetUserAgent(); @@ -1030,16 +1039,19 @@ public async Task GetNotificationOutcomeDetailsAsync(string var requestUri = GetGenericRequestUriBuilder(); requestUri.Path += $"messages/{notificationId}"; - using (var request = CreateHttpRequest(HttpMethod.Get, requestUri.Uri, out var trackingId)) + return await _retryPolicy.RunOperation(async (ct) => { - using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, cancellationToken).ConfigureAwait(false)) + using (var request = CreateHttpRequest(HttpMethod.Get, requestUri.Uri, out var trackingId)) { - using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, ct).ConfigureAwait(false)) { - return (NotificationDetails)_notificationDetailsSerializer.ReadObject(responseStream); + using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + return (NotificationDetails)_notificationDetailsSerializer.ReadObject(responseStream); + } } } - } + }, cancellationToken); } /// @@ -1061,13 +1073,16 @@ public async Task GetFeedbackContainerUriAsync(CancellationToken cancellati var requestUri = GetGenericRequestUriBuilder(); requestUri.Path += "feedbackcontainer"; - using (var request = CreateHttpRequest(HttpMethod.Get, requestUri.Uri, out var trackingId)) + return await _retryPolicy.RunOperation(async (ct) => { - using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, cancellationToken).ConfigureAwait(false)) + using (var request = CreateHttpRequest(HttpMethod.Get, requestUri.Uri, out var trackingId)) { - return new Uri(await response.Content.ReadAsStringAsync().ConfigureAwait(false)); + using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, ct).ConfigureAwait(false)) + { + return new Uri(await response.Content.ReadAsStringAsync().ConfigureAwait(false)); + } } - } + }, cancellationToken); } /// @@ -1114,14 +1129,18 @@ public async Task CreateOrUpdateInstallationAsync(Installation installation, Can var requestUri = GetGenericRequestUriBuilder(); requestUri.Path += $"installations/{installation.InstallationId}"; - using (var request = CreateHttpRequest(HttpMethod.Put, requestUri.Uri, out var trackingId)) + await _retryPolicy.RunOperation(async (ct) => { - request.Content = new StringContent(installation.ToJson(), Encoding.UTF8, "application/json"); - - using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, cancellationToken).ConfigureAwait(false)) + using (var request = CreateHttpRequest(HttpMethod.Put, requestUri.Uri, out var trackingId)) { + request.Content = new StringContent(installation.ToJson(), Encoding.UTF8, "application/json"); + + using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, ct).ConfigureAwait(false)) + { + return true; + } } - } + }, cancellationToken); } /// @@ -1180,14 +1199,18 @@ public async Task PatchInstallationAsync(string installationId, IList { - request.Content = new StringContent(operations.ToJson(), Encoding.UTF8, "application/json-patch+json"); - - using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, cancellationToken).ConfigureAwait(false)) + using (var request = CreateHttpRequest(new HttpMethod("PATCH"), requestUri.Uri, out var trackingId)) { + request.Content = new StringContent(operations.ToJson(), Encoding.UTF8, "application/json-patch+json"); + + using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, ct).ConfigureAwait(false)) + { + return true; + } } - } + }, cancellationToken); } /// @@ -1227,12 +1250,16 @@ public async Task DeleteInstallationAsync(string installationId, CancellationTok var requestUri = GetGenericRequestUriBuilder(); requestUri.Path += $"installations/{installationId}"; - using (var request = CreateHttpRequest(HttpMethod.Delete, requestUri.Uri, out var trackingId)) + await _retryPolicy.RunOperation(async (ct) => { - using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.NoContent, cancellationToken).ConfigureAwait(false)) + using (var request = CreateHttpRequest(HttpMethod.Delete, requestUri.Uri, out var trackingId)) { + using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.NoContent, ct).ConfigureAwait(false)) + { + return true; + } } - } + }, cancellationToken); } /// @@ -1271,13 +1298,16 @@ public async Task InstallationExistsAsync(string installationId, Cancellat var requestUri = GetGenericRequestUriBuilder(); requestUri.Path += $"installations/{installationId}"; - using (var request = CreateHttpRequest(HttpMethod.Get, requestUri.Uri, out var trackingId)) + return await _retryPolicy.RunOperation(async (ct) => { - using (var response = await SendRequestAsync(request, trackingId, new [] { HttpStatusCode.OK, HttpStatusCode.NotFound }, cancellationToken)) + using (var request = CreateHttpRequest(HttpMethod.Get, requestUri.Uri, out var trackingId)) { - return response.StatusCode == HttpStatusCode.OK; + using (var response = await SendRequestAsync(request, trackingId, new[] { HttpStatusCode.OK, HttpStatusCode.NotFound }, ct)) + { + return response.StatusCode == HttpStatusCode.OK; + } } - } + }, cancellationToken); } /// @@ -1318,14 +1348,17 @@ public async Task GetInstallationAsync(string installationId, Canc var requestUri = GetGenericRequestUriBuilder(); requestUri.Path += $"installations/{installationId}"; - using (var request = CreateHttpRequest(HttpMethod.Get, requestUri.Uri, out var trackingId)) + return await _retryPolicy.RunOperation(async (ct) => { - using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, cancellationToken).ConfigureAwait(false)) + using (var request = CreateHttpRequest(HttpMethod.Get, requestUri.Uri, out var trackingId)) { - var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - return JsonConvert.DeserializeObject(responseContent); + using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, ct).ConfigureAwait(false)) + { + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return JsonConvert.DeserializeObject(responseContent); + } } - } + }, cancellationToken); } /// @@ -1353,20 +1386,23 @@ public async Task CreateRegistrationIdAsync(CancellationToken cancellati string registrationId = null; - using (var request = CreateHttpRequest(HttpMethod.Post, requestUri.Uri, out var trackingId)) - using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.Created, cancellationToken).ConfigureAwait(false)) + return await _retryPolicy.RunOperation(async (ct) => { - if (response.Headers.Location != null) + using (var request = CreateHttpRequest(HttpMethod.Post, requestUri.Uri, out var trackingId)) + using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.Created, ct).ConfigureAwait(false)) { - var location = response.Headers.Location; - if (location.Segments.Length == 4 && string.Equals(location.Segments[2], "registrationids/", StringComparison.OrdinalIgnoreCase)) + if (response.Headers.Location != null) { - registrationId = location.Segments[3]; + var location = response.Headers.Location; + if (location.Segments.Length == 4 && string.Equals(location.Segments[2], "registrationids/", StringComparison.OrdinalIgnoreCase)) + { + registrationId = location.Segments[3]; + } } } - } - return registrationId; + return registrationId; + }, cancellationToken); } /// @@ -2600,10 +2636,14 @@ public async Task CancelNotificationAsync(string scheduledNotificationId, Cancel var requestUri = GetGenericRequestUriBuilder(); requestUri.Path += $"schedulednotifications/{scheduledNotificationId}"; - using (var request = CreateHttpRequest(HttpMethod.Delete, requestUri.Uri, out var trackingId)) - using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, cancellationToken).ConfigureAwait(false)) + await _retryPolicy.RunOperation(async (ct) => { - } + using (var request = CreateHttpRequest(HttpMethod.Delete, requestUri.Uri, out var trackingId)) + using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.OK, ct).ConfigureAwait(false)) + { + return true; + } + }, cancellationToken); } /// @@ -2707,35 +2747,38 @@ public async Task SendDirectNotificationAsync(Notification notification.ValidateAndPopulateHeaders(); - using (var request = CreateHttpRequest(HttpMethod.Post, requestUri.Uri, out var trackingId)) + return await _retryPolicy.RunOperation(async (ct) => { - foreach (var item in notification.Headers) + using (var request = CreateHttpRequest(HttpMethod.Post, requestUri.Uri, out var trackingId)) { - request.Headers.Add(item.Key, item.Value); - } + foreach (var item in notification.Headers) + { + request.Headers.Add(item.Key, item.Value); + } - var content = new MultipartContent("mixed", "nh-batch-multipart-boundary"); + var content = new MultipartContent("mixed", "nh-batch-multipart-boundary"); - var notificationContent = new StringContent(notification.Body, Encoding.UTF8, notification.ContentType); - notificationContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("inline") { Name = "notification" }; - content.Add(notificationContent); + var notificationContent = new StringContent(notification.Body, Encoding.UTF8, notification.ContentType); + notificationContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("inline") { Name = "notification" }; + content.Add(notificationContent); - var devicesContent = new StringContent(JsonConvert.SerializeObject(deviceHandles), Encoding.UTF8, "application/json"); - devicesContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("inline") { Name = "devices" }; - content.Add(devicesContent); + var devicesContent = new StringContent(JsonConvert.SerializeObject(deviceHandles), Encoding.UTF8, "application/json"); + devicesContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("inline") { Name = "devices" }; + content.Add(devicesContent); - request.Content = content; + request.Content = content; - using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.Created, cancellationToken).ConfigureAwait(false)) - { - return new NotificationOutcome() + using (var response = await SendRequestAsync(request, trackingId, HttpStatusCode.Created, ct).ConfigureAwait(false)) { - State = NotificationOutcomeState.Enqueued, - TrackingId = trackingId, - NotificationId = GetNotificationIdFromResponse(response) - }; + return new NotificationOutcome() + { + State = NotificationOutcomeState.Enqueued, + TrackingId = trackingId, + NotificationId = GetNotificationIdFromResponse(response) + }; + } } - } + }, cancellationToken); } private T SyncOp(Func> func) @@ -2788,52 +2831,55 @@ private async Task SendNotificationImplAsync(Notification n notification.ValidateAndPopulateHeaders(); - using (var request = CreateHttpRequest(HttpMethod.Post, requestUri.Uri, out var trackingId)) + return await _retryPolicy.RunOperation(async (ct) => { - if (!string.IsNullOrWhiteSpace(deviceHandle)) + using (var request = CreateHttpRequest(HttpMethod.Post, requestUri.Uri, out var trackingId)) { - request.Headers.Add("ServiceBusNotification-DeviceHandle", deviceHandle); - } + if (!string.IsNullOrWhiteSpace(deviceHandle)) + { + request.Headers.Add("ServiceBusNotification-DeviceHandle", deviceHandle); + } - if (!string.IsNullOrWhiteSpace(tagExpression)) - { - request.Headers.Add("ServiceBusNotification-Tags", tagExpression); - } + if (!string.IsNullOrWhiteSpace(tagExpression)) + { + request.Headers.Add("ServiceBusNotification-Tags", tagExpression); + } - foreach (var item in notification.Headers) - { - request.Headers.Add(item.Key, item.Value); - } + foreach (var item in notification.Headers) + { + request.Headers.Add(item.Key, item.Value); + } - request.Content = new StringContent(notification.Body, Encoding.UTF8, notification.ContentType); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(notification.ContentType); + request.Content = new StringContent(notification.Body, Encoding.UTF8, notification.ContentType); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(notification.ContentType); - using (var response = await SendRequestAsync(request, trackingId, new[] { HttpStatusCode.OK, HttpStatusCode.Created }, cancellationToken).ConfigureAwait(false)) - { - if (EnableTestSend) + using (var response = await SendRequestAsync(request, trackingId, new[] { HttpStatusCode.OK, HttpStatusCode.Created }, ct).ConfigureAwait(false)) { - using (var responseContent = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) - using (var reader = XmlReader.Create(responseContent, new XmlReaderSettings { CloseInput = true })) + if (EnableTestSend) { - var result = (NotificationOutcome)_debugResponseSerializer.ReadObject(reader); - result.State = NotificationOutcomeState.DetailedStateAvailable; - result.TrackingId = trackingId; - return result; + using (var responseContent = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var reader = XmlReader.Create(responseContent, new XmlReaderSettings { CloseInput = true })) + { + var result = (NotificationOutcome)_debugResponseSerializer.ReadObject(reader); + result.State = NotificationOutcomeState.DetailedStateAvailable; + result.TrackingId = trackingId; + return result; + } } - } - else - { - var result = new NotificationOutcome + else { - State = NotificationOutcomeState.Enqueued, - TrackingId = trackingId, - NotificationId = GetNotificationIdFromResponse(response) - }; + var result = new NotificationOutcome + { + State = NotificationOutcomeState.Enqueued, + TrackingId = trackingId, + NotificationId = GetNotificationIdFromResponse(response) + }; - return result; + return result; + } } } - } + }, cancellationToken); } private async Task SendScheduledNotificationImplAsync(Notification notification, DateTimeOffset scheduledTime, string tagExpression, CancellationToken cancellationToken) @@ -2846,43 +2892,46 @@ private async Task SendScheduledNotificationImplAsync(Not notification.ValidateAndPopulateHeaders(); - using (var request = CreateHttpRequest(HttpMethod.Post, requestUri.Uri, out var trackingId)) + return await _retryPolicy.RunOperation(async (ct) => { - request.Headers.Add("ServiceBusNotification-ScheduleTime", scheduledTime.UtcDateTime.ToString("s", CultureInfo.InvariantCulture)); - - if (!string.IsNullOrWhiteSpace(tagExpression)) - { - request.Headers.Add("ServiceBusNotification-Tags", tagExpression); - } - - foreach (var item in notification.Headers) + using (var request = CreateHttpRequest(HttpMethod.Post, requestUri.Uri, out var trackingId)) { - request.Headers.Add(item.Key, item.Value); - } + request.Headers.Add("ServiceBusNotification-ScheduleTime", scheduledTime.UtcDateTime.ToString("s", CultureInfo.InvariantCulture)); - request.Content = new StringContent(notification.Body, Encoding.UTF8, notification.ContentType); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(notification.ContentType); + if (!string.IsNullOrWhiteSpace(tagExpression)) + { + request.Headers.Add("ServiceBusNotification-Tags", tagExpression); + } - using (var response = await SendRequestAsync(request, trackingId, new[] { HttpStatusCode.OK, HttpStatusCode.Created }, cancellationToken).ConfigureAwait(false)) - { - string notificationId = null; - if (response.Headers.Location != null) + foreach (var item in notification.Headers) { - notificationId = response.Headers.Location.Segments.Last().Trim('/'); + request.Headers.Add(item.Key, item.Value); } - var result = new ScheduledNotification() + request.Content = new StringContent(notification.Body, Encoding.UTF8, notification.ContentType); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(notification.ContentType); + + using (var response = await SendRequestAsync(request, trackingId, new[] { HttpStatusCode.OK, HttpStatusCode.Created }, ct).ConfigureAwait(false)) { - ScheduledNotificationId = notificationId, - Tags = tagExpression, - ScheduledTime = scheduledTime, - Payload = notification, - TrackingId = trackingId - }; - - return result; + string notificationId = null; + if (response.Headers.Location != null) + { + notificationId = response.Headers.Location.Segments.Last().Trim('/'); + } + + var result = new ScheduledNotification() + { + ScheduledNotificationId = notificationId, + Tags = tagExpression, + ScheduledTime = scheduledTime, + Payload = notification, + TrackingId = trackingId + }; + + return result; + } } - } + }, cancellationToken); } private async Task> GetAllEntitiesImplAsync(UriBuilder requestUri, string continuationToken, int top, CancellationToken cancellationToken) where TEntity : EntityDescription @@ -2978,24 +3027,26 @@ private async Task CreateOrUpdateRegistrationImplAsync { - if (operationType == EntityOperatonType.Update) + using (var request = CreateHttpRequest(operationType == EntityOperatonType.Create ? HttpMethod.Post : HttpMethod.Put, requestUri.Uri, out var trackingId)) { - request.Headers.Add(ManagementStrings.IfMatch, string.IsNullOrEmpty(registration.ETag) ? "*" : $"\"{registration.ETag}\""); - } + if (operationType == EntityOperatonType.Update) + { + request.Headers.Add(ManagementStrings.IfMatch, string.IsNullOrEmpty(registration.ETag) ? "*" : $"\"{registration.ETag}\""); + } - AddEntityToRequestContent(request, registration); + AddEntityToRequestContent(request, registration); - using (var response = await SendRequestAsync(request, trackingId, new[] { HttpStatusCode.OK, HttpStatusCode.Created }, cancellationToken).ConfigureAwait(false)) - { - using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var response = await SendRequestAsync(request, trackingId, new[] { HttpStatusCode.OK, HttpStatusCode.Created }, ct).ConfigureAwait(false)) { - return await ReadEntityAsync(responseStream).ConfigureAwait(false); + using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + return await ReadEntityAsync(responseStream).ConfigureAwait(false); + } } } - } + }, cancellationToken); } private async Task GetEntityImplAsync(string entityCollection, string entityId, CancellationToken cancellationToken, bool throwIfNotFound = true) where TEntity : EntityDescription @@ -3005,23 +3056,30 @@ private async Task GetEntityImplAsync(string entityCollection, HttpStatusCode[] successfulResponseStatuses; if (throwIfNotFound) - successfulResponseStatuses = new [] { HttpStatusCode.OK }; + { + successfulResponseStatuses = new[] { HttpStatusCode.OK }; + } else - successfulResponseStatuses = new [] { HttpStatusCode.OK, HttpStatusCode.NotFound }; + { + successfulResponseStatuses = new[] { HttpStatusCode.OK, HttpStatusCode.NotFound }; + } - using (var request = CreateHttpRequest(HttpMethod.Get, requestUri.Uri, out var trackingId)) + return await _retryPolicy.RunOperation(async (ct) => { - using (var response = await SendRequestAsync(request, trackingId, successfulResponseStatuses, cancellationToken).ConfigureAwait(false)) + using (var request = CreateHttpRequest(HttpMethod.Get, requestUri.Uri, out var trackingId)) { - if (response.StatusCode == HttpStatusCode.NotFound) - return null; - - using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var response = await SendRequestAsync(request, trackingId, successfulResponseStatuses, ct).ConfigureAwait(false)) { - return await ReadEntityAsync(responseStream).ConfigureAwait(false); + if (response.StatusCode == HttpStatusCode.NotFound) + return null; + + using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + return await ReadEntityAsync(responseStream).ConfigureAwait(false); + } } } - } + }, cancellationToken); } private HttpRequestMessage CreateHttpRequest(HttpMethod method, Uri requestUri, out string trackingId) @@ -3076,14 +3134,26 @@ private async Task SendRequestAsync(HttpRequestMessage requ var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); if (!successfulResponseStatuses.Any(s => s.Equals(response.StatusCode))) { - throw await response.TranslateToMessagingExceptionAsync(request.Method.Method, OperationTimeout.Milliseconds, trackingId).ConfigureAwait(false); + throw await response.TranslateToMessagingExceptionAsync(trackingId).ConfigureAwait(false); } return response; } - catch (Exception e) when (!e.IsMessagingException()) + catch (HttpRequestException ex) + { + var innerException = ex.GetBaseException(); + if (innerException is SocketException socketException) + { + throw ExceptionsUtility.HandleSocketException(socketException, OperationTimeout.Milliseconds, trackingId); + } + else + { + throw ExceptionsUtility.HandleUnexpectedException(ex, trackingId); + } + } + catch (XmlException ex) { - throw e.TranslateToMessagingException(OperationTimeout.Milliseconds, trackingId); + throw ExceptionsUtility.HandleXmlException(ex, trackingId); } } diff --git a/src/Microsoft.Azure.NotificationHubs/NotificationHubDescription.cs b/src/Microsoft.Azure.NotificationHubs/NotificationHubDescription.cs index 1c71042..7d27ce0 100644 --- a/src/Microsoft.Azure.NotificationHubs/NotificationHubDescription.cs +++ b/src/Microsoft.Azure.NotificationHubs/NotificationHubDescription.cs @@ -33,7 +33,7 @@ public NotificationHubDescription(string path) /// Gets the full path of the notificationHub. /// /// - /// This is a relative path to the . + /// This is a relative path to the . /// public string Path { diff --git a/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryMode.cs b/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryMode.cs new file mode 100644 index 0000000..8993a48 --- /dev/null +++ b/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryMode.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. +//------------------------------------------------------------ + +namespace Microsoft.Azure.NotificationHubs +{ + /// + /// The type of approach to apply when calculating the delay + /// between retry attempts. + /// + /// + public enum NotificationHubRetryMode + { + /// Retry attempts happen at fixed intervals; each delay is a consistent duration. + Fixed, + + /// Retry attempts will delay based on a back-off strategy, where each attempt will increase the duration that it waits before retrying. + Exponential + } +} diff --git a/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryOptions.cs b/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryOptions.cs new file mode 100644 index 0000000..f966c61 --- /dev/null +++ b/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryOptions.cs @@ -0,0 +1,103 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. +//------------------------------------------------------------ + +namespace Microsoft.Azure.NotificationHubs +{ + using System; + + /// + /// The set of options that can be specified to influence how + /// retry attempts are made, and a failure is eligible to be retried. + /// + /// + public class NotificationHubRetryOptions + { + /// The maximum number of retry attempts before considering the associated operation to have failed. + private int _maxRetries = 3; + + /// The delay or back-off factor to apply between retry attempts. + private TimeSpan _delay = TimeSpan.FromSeconds(1); + + /// The maximum delay to allow between retry attempts. + private TimeSpan _maxDelay = TimeSpan.FromMinutes(1); + + /// + /// The approach to use for calculating retry delays. + /// + /// + public NotificationHubRetryMode Mode { get; set; } = NotificationHubRetryMode.Exponential; + + /// + /// The maximum number of retry attempts before considering the associated operation + /// to have failed. + /// + /// + public int MaxRetries + { + get => _maxRetries; + + set + { + if (value < 0 || value > 100) + { + throw new ArgumentException("The maximum number of retry attempts has to be between 0 and 100", nameof(MaxRetries)); + } + + _maxRetries = value; + } + } + + /// + /// The delay between retry attempts for a fixed approach or the delay + /// on which to base calculations for a backoff-based approach. + /// + /// + public TimeSpan Delay + { + get => _delay; + + set + { + if (value < TimeSpan.FromMilliseconds(1) || value > TimeSpan.FromMinutes(5)) + { + throw new ArgumentException("The delay between retry attempts has to be between 1 millisecond and 5 minutes", nameof(Delay)); + } + + _delay = value; + } + } + + /// + /// The maximum permissible delay between retry attempts. + /// + /// + public TimeSpan MaxDelay + { + get => _maxDelay; + + set + { + if (value < TimeSpan.Zero) + { + throw new ArgumentException("The maximum permissible delay between retry attempts can not be negative", nameof(MaxDelay)); + } + + _maxDelay = value; + } + } + + /// + /// A custom retry policy to be used in place of the individual option values. + /// + /// + /// + /// When populated, this custom policy will take precedence over the individual retry + /// options provided. + /// + /// + public NotificationHubRetryPolicy CustomRetryPolicy { get; set; } + } +} diff --git a/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryOptionsExtensions.cs b/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryOptionsExtensions.cs new file mode 100644 index 0000000..7b199cf --- /dev/null +++ b/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryOptionsExtensions.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. +//------------------------------------------------------------ + +namespace Microsoft.Azure.NotificationHubs +{ + /// + /// The set of extension methods for the + /// class. + /// + /// + public static class NotificationHubRetryOptionsExtensions + { + /// + /// Converts the options into a retry policy for use. + /// + /// + /// The instance that this method was invoked on. + /// + /// The represented by the options. + public static NotificationHubRetryPolicy ToRetryPolicy(this NotificationHubRetryOptions instance) => + instance.CustomRetryPolicy ?? new BasicRetryPolicy(instance); + } +} diff --git a/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryPolicy.cs b/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryPolicy.cs new file mode 100644 index 0000000..402dc08 --- /dev/null +++ b/src/Microsoft.Azure.NotificationHubs/NotificationHubRetryPolicy.cs @@ -0,0 +1,114 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for +// license information. +//------------------------------------------------------------ + +namespace Microsoft.Azure.NotificationHubs +{ + using System; + using System.ComponentModel; + using System.Runtime.ExceptionServices; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Azure.NotificationHubs.Messaging; + + /// + /// An abstract representation of a policy to govern retrying of messaging operations. + /// + /// + /// + /// It is recommended that developers without advanced needs not implement custom retry + /// policies but instead configure the default policy by specifying the desired set of + /// retry options when creating one of the Service Bus clients. + /// + /// + /// + /// + public abstract class NotificationHubRetryPolicy + { + /// + /// Calculates the amount of time to wait before another attempt should be made. + /// + /// + /// The last exception that was observed for the operation to be retried. + /// The number of total attempts that have been made, including the initial attempt before any retries. + /// + /// The amount of time to delay before retrying the associated operation; if null, then the operation is no longer eligible to be retried. + /// + public abstract TimeSpan? CalculateRetryDelay( + Exception lastException, + int attemptCount); + + /// + /// Determines whether the specified is equal to this instance. + /// + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => base.Equals(obj); + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + /// Converts the instance to string representation. + /// + /// + /// A that represents this instance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => base.ToString(); + + /// + /// + /// + /// + /// + /// + internal async Task RunOperation( + Func> operation, + CancellationToken cancellationToken) + { + var failedAttemptCount = 0; + + + while (!cancellationToken.IsCancellationRequested) + { + try + { + return await operation(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + // Determine if there should be a retry for the next attempt; if so enforce the delay but do not quit the loop. + // Otherwise, throw the translated exception. + + ++failedAttemptCount; + TimeSpan? retryDelay = CalculateRetryDelay(ex, failedAttemptCount); + if (retryDelay.HasValue && !cancellationToken.IsCancellationRequested) + { + await Task.Delay(retryDelay.Value, cancellationToken).ConfigureAwait(false); + } + else + { + ExceptionDispatchInfo.Capture(ex) + .Throw(); + } + } + } + // If no value has been returned nor exception thrown by this point, + // then cancellation has been requested. + throw new TaskCanceledException(); + } + } +} diff --git a/src/Microsoft.Azure.NotificationHubs/NotificationHubClientSettings.cs b/src/Microsoft.Azure.NotificationHubs/NotificationHubSettings.cs similarity index 56% rename from src/Microsoft.Azure.NotificationHubs/NotificationHubClientSettings.cs rename to src/Microsoft.Azure.NotificationHubs/NotificationHubSettings.cs index eb0ab5f..29227b1 100644 --- a/src/Microsoft.Azure.NotificationHubs/NotificationHubClientSettings.cs +++ b/src/Microsoft.Azure.NotificationHubs/NotificationHubSettings.cs @@ -14,8 +14,10 @@ namespace Microsoft.Azure.NotificationHubs /// Notification Hubs client settings /// /// - public class NotificationHubClientSettings + public class NotificationHubSettings { + private NotificationHubRetryOptions _retryOptions = new NotificationHubRetryOptions(); + /// /// Gets or sets the proxy /// @@ -26,6 +28,11 @@ public class NotificationHubClientSettings /// public HttpMessageHandler MessageHandler { get; set; } + /// + /// Gets or sets HttpClient. If set will overwrite Proxy and MessageHandler. + /// + public HttpClient HttpClient { get; set; } + /// /// Gets or sets operation timeout of the HTTP operations. /// @@ -35,5 +42,19 @@ public class NotificationHubClientSettings /// /// public TimeSpan? OperationTimeout { get; set; } + + /// + /// The set of options to use for determining whether a failed operation should be retried and, + /// if so, the amount of time to wait between retry attempts. These options also control the + /// amount of time allowed for receiving messages and other interactions with the Service Bus service. + /// + public NotificationHubRetryOptions RetryOptions + { + get => _retryOptions; + set + { + _retryOptions = value ?? throw new ArgumentNullException(nameof(RetryOptions)); + } + } } } diff --git a/src/Microsoft.Azure.NotificationHubs/SRClient.Designer.cs b/src/Microsoft.Azure.NotificationHubs/SRClient.Designer.cs index 98b0341..26212d9 100644 --- a/src/Microsoft.Azure.NotificationHubs/SRClient.Designer.cs +++ b/src/Microsoft.Azure.NotificationHubs/SRClient.Designer.cs @@ -2967,6 +2967,15 @@ internal static string ServerDidNotReply { } } + /// + /// Looks up a localized string similar to Server is busy.. + /// + internal static string ServerIsBusy { + get { + return ResourceManager.GetString("ServerIsBusy", resourceCulture); + } + } + /// /// Looks up a localized string similar to A session handler is already registered or being registered.. /// diff --git a/src/Microsoft.Azure.NotificationHubs/SRClient.resx b/src/Microsoft.Azure.NotificationHubs/SRClient.resx index 749e561..d49cebb 100644 --- a/src/Microsoft.Azure.NotificationHubs/SRClient.resx +++ b/src/Microsoft.Azure.NotificationHubs/SRClient.resx @@ -1291,4 +1291,7 @@ The name of the notification hub cannot be longer than '{0}' characters + + Server is busy. + \ No newline at end of file