diff --git a/sdk/core/Azure.Core/CHANGELOG.md b/sdk/core/Azure.Core/CHANGELOG.md index 1f38e8e53ee11..ae3ba4a3eb98f 100644 --- a/sdk/core/Azure.Core/CHANGELOG.md +++ b/sdk/core/Azure.Core/CHANGELOG.md @@ -4,6 +4,9 @@ ### Features Added +- Added the `RetryPolicy` type which can be used to create a custom retry policy. +- Added the `Delay` type which can be used to customize delays. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 4437984695d8d..1f1934d30e0f8 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -398,6 +398,16 @@ public void AddPolicy(Azure.Core.Pipeline.HttpPipelinePolicy policy, Azure.Core. public static bool operator !=(Azure.Core.ContentType left, Azure.Core.ContentType right) { throw null; } public override string ToString() { throw null; } } + public abstract partial class Delay + { + protected Delay(System.TimeSpan? maxDelay = default(System.TimeSpan?), double jitterFactor = 0.2) { } + public static Azure.Core.Delay CreateExponentialDelay(System.TimeSpan? initialDelay = default(System.TimeSpan?), System.TimeSpan? maxDelay = default(System.TimeSpan?)) { throw null; } + public static Azure.Core.Delay CreateFixedDelay(System.TimeSpan? delay = default(System.TimeSpan?)) { throw null; } + public System.TimeSpan GetNextDelay(Azure.Response? response, int retryNumber) { throw null; } + protected abstract System.TimeSpan GetNextDelayCore(Azure.Response? response, int retryNumber); + protected static System.TimeSpan Max(System.TimeSpan val1, System.TimeSpan val2) { throw null; } + protected static System.TimeSpan Min(System.TimeSpan val1, System.TimeSpan val2) { throw null; } + } public static partial class DelegatedTokenCredential { public static Azure.Core.TokenCredential Create(System.Func getToken) { throw null; } @@ -1023,6 +1033,21 @@ public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemo public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } public static void SetAllowAutoRedirect(Azure.Core.HttpMessage message, bool allowAutoRedirect) { } } + public partial class RetryPolicy : Azure.Core.Pipeline.HttpPipelinePolicy + { + public RetryPolicy(int maxRetries = 3, Azure.Core.Delay? delay = null) { } + protected Azure.Core.Delay Delay { get { throw null; } } + protected virtual System.TimeSpan GetNextDelay(Azure.Core.HttpMessage message, System.TimeSpan? retryAfter) { throw null; } + protected virtual System.Threading.Tasks.ValueTask GetNextDelayAsync(Azure.Core.HttpMessage message, System.TimeSpan? retryAfter) { throw null; } + protected internal virtual void OnRequestSent(Azure.Core.HttpMessage message) { } + protected internal virtual System.Threading.Tasks.ValueTask OnRequestSentAsync(Azure.Core.HttpMessage message) { throw null; } + protected internal virtual void OnSendingRequest(Azure.Core.HttpMessage message) { } + protected internal virtual System.Threading.Tasks.ValueTask OnSendingRequestAsync(Azure.Core.HttpMessage message) { throw null; } + public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { } + public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } + protected internal virtual bool ShouldRetry(Azure.Core.HttpMessage message, System.Exception? exception) { throw null; } + protected internal virtual System.Threading.Tasks.ValueTask ShouldRetryAsync(Azure.Core.HttpMessage message, System.Exception? exception) { throw null; } + } public partial class ServerCertificateCustomValidationArgs { public ServerCertificateCustomValidationArgs(System.Security.Cryptography.X509Certificates.X509Certificate2? certificate, System.Security.Cryptography.X509Certificates.X509Chain? certificateAuthorityChain, System.Net.Security.SslPolicyErrors sslPolicyErrors) { } diff --git a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs index df69e6b57c0cc..0a1419b46789d 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs @@ -398,6 +398,16 @@ public void AddPolicy(Azure.Core.Pipeline.HttpPipelinePolicy policy, Azure.Core. public static bool operator !=(Azure.Core.ContentType left, Azure.Core.ContentType right) { throw null; } public override string ToString() { throw null; } } + public abstract partial class Delay + { + protected Delay(System.TimeSpan? maxDelay = default(System.TimeSpan?), double jitterFactor = 0.2) { } + public static Azure.Core.Delay CreateExponentialDelay(System.TimeSpan? initialDelay = default(System.TimeSpan?), System.TimeSpan? maxDelay = default(System.TimeSpan?)) { throw null; } + public static Azure.Core.Delay CreateFixedDelay(System.TimeSpan? delay = default(System.TimeSpan?)) { throw null; } + public System.TimeSpan GetNextDelay(Azure.Response? response, int retryNumber) { throw null; } + protected abstract System.TimeSpan GetNextDelayCore(Azure.Response? response, int retryNumber); + protected static System.TimeSpan Max(System.TimeSpan val1, System.TimeSpan val2) { throw null; } + protected static System.TimeSpan Min(System.TimeSpan val1, System.TimeSpan val2) { throw null; } + } public static partial class DelegatedTokenCredential { public static Azure.Core.TokenCredential Create(System.Func getToken) { throw null; } @@ -1023,6 +1033,21 @@ public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemo public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } public static void SetAllowAutoRedirect(Azure.Core.HttpMessage message, bool allowAutoRedirect) { } } + public partial class RetryPolicy : Azure.Core.Pipeline.HttpPipelinePolicy + { + public RetryPolicy(int maxRetries = 3, Azure.Core.Delay? delay = null) { } + protected Azure.Core.Delay Delay { get { throw null; } } + protected virtual System.TimeSpan GetNextDelay(Azure.Core.HttpMessage message, System.TimeSpan? retryAfter) { throw null; } + protected virtual System.Threading.Tasks.ValueTask GetNextDelayAsync(Azure.Core.HttpMessage message, System.TimeSpan? retryAfter) { throw null; } + protected internal virtual void OnRequestSent(Azure.Core.HttpMessage message) { } + protected internal virtual System.Threading.Tasks.ValueTask OnRequestSentAsync(Azure.Core.HttpMessage message) { throw null; } + protected internal virtual void OnSendingRequest(Azure.Core.HttpMessage message) { } + protected internal virtual System.Threading.Tasks.ValueTask OnSendingRequestAsync(Azure.Core.HttpMessage message) { throw null; } + public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { } + public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } + protected internal virtual bool ShouldRetry(Azure.Core.HttpMessage message, System.Exception? exception) { throw null; } + protected internal virtual System.Threading.Tasks.ValueTask ShouldRetryAsync(Azure.Core.HttpMessage message, System.Exception? exception) { throw null; } + } public partial class ServerCertificateCustomValidationArgs { public ServerCertificateCustomValidationArgs(System.Security.Cryptography.X509Certificates.X509Certificate2? certificate, System.Security.Cryptography.X509Certificates.X509Chain? certificateAuthorityChain, System.Net.Security.SslPolicyErrors sslPolicyErrors) { } diff --git a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs index df69e6b57c0cc..0a1419b46789d 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs @@ -398,6 +398,16 @@ public void AddPolicy(Azure.Core.Pipeline.HttpPipelinePolicy policy, Azure.Core. public static bool operator !=(Azure.Core.ContentType left, Azure.Core.ContentType right) { throw null; } public override string ToString() { throw null; } } + public abstract partial class Delay + { + protected Delay(System.TimeSpan? maxDelay = default(System.TimeSpan?), double jitterFactor = 0.2) { } + public static Azure.Core.Delay CreateExponentialDelay(System.TimeSpan? initialDelay = default(System.TimeSpan?), System.TimeSpan? maxDelay = default(System.TimeSpan?)) { throw null; } + public static Azure.Core.Delay CreateFixedDelay(System.TimeSpan? delay = default(System.TimeSpan?)) { throw null; } + public System.TimeSpan GetNextDelay(Azure.Response? response, int retryNumber) { throw null; } + protected abstract System.TimeSpan GetNextDelayCore(Azure.Response? response, int retryNumber); + protected static System.TimeSpan Max(System.TimeSpan val1, System.TimeSpan val2) { throw null; } + protected static System.TimeSpan Min(System.TimeSpan val1, System.TimeSpan val2) { throw null; } + } public static partial class DelegatedTokenCredential { public static Azure.Core.TokenCredential Create(System.Func getToken) { throw null; } @@ -1023,6 +1033,21 @@ public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemo public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } public static void SetAllowAutoRedirect(Azure.Core.HttpMessage message, bool allowAutoRedirect) { } } + public partial class RetryPolicy : Azure.Core.Pipeline.HttpPipelinePolicy + { + public RetryPolicy(int maxRetries = 3, Azure.Core.Delay? delay = null) { } + protected Azure.Core.Delay Delay { get { throw null; } } + protected virtual System.TimeSpan GetNextDelay(Azure.Core.HttpMessage message, System.TimeSpan? retryAfter) { throw null; } + protected virtual System.Threading.Tasks.ValueTask GetNextDelayAsync(Azure.Core.HttpMessage message, System.TimeSpan? retryAfter) { throw null; } + protected internal virtual void OnRequestSent(Azure.Core.HttpMessage message) { } + protected internal virtual System.Threading.Tasks.ValueTask OnRequestSentAsync(Azure.Core.HttpMessage message) { throw null; } + protected internal virtual void OnSendingRequest(Azure.Core.HttpMessage message) { } + protected internal virtual System.Threading.Tasks.ValueTask OnSendingRequestAsync(Azure.Core.HttpMessage message) { throw null; } + public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { } + public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } + protected internal virtual bool ShouldRetry(Azure.Core.HttpMessage message, System.Exception? exception) { throw null; } + protected internal virtual System.Threading.Tasks.ValueTask ShouldRetryAsync(Azure.Core.HttpMessage message, System.Exception? exception) { throw null; } + } public partial class ServerCertificateCustomValidationArgs { public ServerCertificateCustomValidationArgs(System.Security.Cryptography.X509Certificates.X509Certificate2? certificate, System.Security.Cryptography.X509Certificates.X509Chain? certificateAuthorityChain, System.Net.Security.SslPolicyErrors sslPolicyErrors) { } diff --git a/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs b/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs index 4437984695d8d..1f1934d30e0f8 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs @@ -398,6 +398,16 @@ public void AddPolicy(Azure.Core.Pipeline.HttpPipelinePolicy policy, Azure.Core. public static bool operator !=(Azure.Core.ContentType left, Azure.Core.ContentType right) { throw null; } public override string ToString() { throw null; } } + public abstract partial class Delay + { + protected Delay(System.TimeSpan? maxDelay = default(System.TimeSpan?), double jitterFactor = 0.2) { } + public static Azure.Core.Delay CreateExponentialDelay(System.TimeSpan? initialDelay = default(System.TimeSpan?), System.TimeSpan? maxDelay = default(System.TimeSpan?)) { throw null; } + public static Azure.Core.Delay CreateFixedDelay(System.TimeSpan? delay = default(System.TimeSpan?)) { throw null; } + public System.TimeSpan GetNextDelay(Azure.Response? response, int retryNumber) { throw null; } + protected abstract System.TimeSpan GetNextDelayCore(Azure.Response? response, int retryNumber); + protected static System.TimeSpan Max(System.TimeSpan val1, System.TimeSpan val2) { throw null; } + protected static System.TimeSpan Min(System.TimeSpan val1, System.TimeSpan val2) { throw null; } + } public static partial class DelegatedTokenCredential { public static Azure.Core.TokenCredential Create(System.Func getToken) { throw null; } @@ -1023,6 +1033,21 @@ public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemo public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } public static void SetAllowAutoRedirect(Azure.Core.HttpMessage message, bool allowAutoRedirect) { } } + public partial class RetryPolicy : Azure.Core.Pipeline.HttpPipelinePolicy + { + public RetryPolicy(int maxRetries = 3, Azure.Core.Delay? delay = null) { } + protected Azure.Core.Delay Delay { get { throw null; } } + protected virtual System.TimeSpan GetNextDelay(Azure.Core.HttpMessage message, System.TimeSpan? retryAfter) { throw null; } + protected virtual System.Threading.Tasks.ValueTask GetNextDelayAsync(Azure.Core.HttpMessage message, System.TimeSpan? retryAfter) { throw null; } + protected internal virtual void OnRequestSent(Azure.Core.HttpMessage message) { } + protected internal virtual System.Threading.Tasks.ValueTask OnRequestSentAsync(Azure.Core.HttpMessage message) { throw null; } + protected internal virtual void OnSendingRequest(Azure.Core.HttpMessage message) { } + protected internal virtual System.Threading.Tasks.ValueTask OnSendingRequestAsync(Azure.Core.HttpMessage message) { throw null; } + public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { } + public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } + protected internal virtual bool ShouldRetry(Azure.Core.HttpMessage message, System.Exception? exception) { throw null; } + protected internal virtual System.Threading.Tasks.ValueTask ShouldRetryAsync(Azure.Core.HttpMessage message, System.Exception? exception) { throw null; } + } public partial class ServerCertificateCustomValidationArgs { public ServerCertificateCustomValidationArgs(System.Security.Cryptography.X509Certificates.X509Certificate2? certificate, System.Security.Cryptography.X509Certificates.X509Chain? certificateAuthorityChain, System.Net.Security.SslPolicyErrors sslPolicyErrors) { } diff --git a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs index 4437984695d8d..1f1934d30e0f8 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -398,6 +398,16 @@ public void AddPolicy(Azure.Core.Pipeline.HttpPipelinePolicy policy, Azure.Core. public static bool operator !=(Azure.Core.ContentType left, Azure.Core.ContentType right) { throw null; } public override string ToString() { throw null; } } + public abstract partial class Delay + { + protected Delay(System.TimeSpan? maxDelay = default(System.TimeSpan?), double jitterFactor = 0.2) { } + public static Azure.Core.Delay CreateExponentialDelay(System.TimeSpan? initialDelay = default(System.TimeSpan?), System.TimeSpan? maxDelay = default(System.TimeSpan?)) { throw null; } + public static Azure.Core.Delay CreateFixedDelay(System.TimeSpan? delay = default(System.TimeSpan?)) { throw null; } + public System.TimeSpan GetNextDelay(Azure.Response? response, int retryNumber) { throw null; } + protected abstract System.TimeSpan GetNextDelayCore(Azure.Response? response, int retryNumber); + protected static System.TimeSpan Max(System.TimeSpan val1, System.TimeSpan val2) { throw null; } + protected static System.TimeSpan Min(System.TimeSpan val1, System.TimeSpan val2) { throw null; } + } public static partial class DelegatedTokenCredential { public static Azure.Core.TokenCredential Create(System.Func getToken) { throw null; } @@ -1023,6 +1033,21 @@ public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemo public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } public static void SetAllowAutoRedirect(Azure.Core.HttpMessage message, bool allowAutoRedirect) { } } + public partial class RetryPolicy : Azure.Core.Pipeline.HttpPipelinePolicy + { + public RetryPolicy(int maxRetries = 3, Azure.Core.Delay? delay = null) { } + protected Azure.Core.Delay Delay { get { throw null; } } + protected virtual System.TimeSpan GetNextDelay(Azure.Core.HttpMessage message, System.TimeSpan? retryAfter) { throw null; } + protected virtual System.Threading.Tasks.ValueTask GetNextDelayAsync(Azure.Core.HttpMessage message, System.TimeSpan? retryAfter) { throw null; } + protected internal virtual void OnRequestSent(Azure.Core.HttpMessage message) { } + protected internal virtual System.Threading.Tasks.ValueTask OnRequestSentAsync(Azure.Core.HttpMessage message) { throw null; } + protected internal virtual void OnSendingRequest(Azure.Core.HttpMessage message) { } + protected internal virtual System.Threading.Tasks.ValueTask OnSendingRequestAsync(Azure.Core.HttpMessage message) { throw null; } + public override void Process(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { } + public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message, System.ReadOnlyMemory pipeline) { throw null; } + protected internal virtual bool ShouldRetry(Azure.Core.HttpMessage message, System.Exception? exception) { throw null; } + protected internal virtual System.Threading.Tasks.ValueTask ShouldRetryAsync(Azure.Core.HttpMessage message, System.Exception? exception) { throw null; } + } public partial class ServerCertificateCustomValidationArgs { public ServerCertificateCustomValidationArgs(System.Security.Cryptography.X509Certificates.X509Certificate2? certificate, System.Security.Cryptography.X509Certificates.X509Chain? certificateAuthorityChain, System.Net.Security.SslPolicyErrors sslPolicyErrors) { } diff --git a/sdk/core/Azure.Core/samples/Configuration.md b/sdk/core/Azure.Core/samples/Configuration.md index 4ec91e89f48c2..c07af462ce08a 100644 --- a/sdk/core/Azure.Core/samples/Configuration.md +++ b/sdk/core/Azure.Core/samples/Configuration.md @@ -4,9 +4,9 @@ ## Configuring retry options -To modify the retry options use the `Retry` property of client options class. +To modify the retry options, use the `Retry` property of the `ClientOptions` class. -Be default clients are setup to retry 3 times with exponential retry kind and initial delay of 0.8 sec. +By default, clients are setup to retry 3 times using an exponential retry strategy with an initial delay of 0.8 sec, and a max delay of 1 minute. ```C# Snippet:RetryOptions SecretClientOptions options = new SecretClientOptions() @@ -22,14 +22,16 @@ SecretClientOptions options = new SecretClientOptions() ## Setting a custom retry policy -Using `RetryOptions` to configure retry behavior is sufficient for the vast majority of scenarios. For more advanced scenarios, it is possible to create a custom retry policy and set it to the `RetryPolicy` property of client options class. This can be accomplished by implementing a retry policy that derives from the abstract `RetryPolicy` class. The `RetryPolicy` class contains hooks to determine if a request should be retried and how long to wait before retrying. In the following example, we implement a policy that will prevent retries from taking place if the overall processing time has exceeded some threshold. Notice that the policy takes in `RetryOptions` as one of the constructor parameters and passes it to the base constructor. By doing this, we are able to delegate to the base `RetryPolicy` as needed (either by explicitly invoking the base methods, or by not overriding methods that we do not need to customize) which will respect the `RetryOptions`. +Using `RetryOptions` to configure retry behavior is sufficient for the vast majority of scenarios. For more advanced scenarios, it's possible to use a custom retry policy by setting the `RetryPolicy` property of client options class. This can be accomplished by implementing a retry policy that derives from the `RetryPolicy` class, or by passing in a `DelayStrategy` into the existing `RetryPolicy` constructor. The `RetryPolicy` class contains hooks to determine if a request should be retried and how long to wait before retrying. + +In the following example, we implement a policy that will prevent retries from taking place if the overall processing time has exceeded a configured threshold. Notice that the policy takes in `RetryOptions` as one of the constructor parameters and passes it to the base constructor. By doing this, we are able to delegate to the base `RetryPolicy` as needed (either by explicitly invoking the base methods, or by not overriding methods that we do not need to customize) which will respect the `RetryOptions`. ```C# Snippet:GlobalTimeoutRetryPolicy internal class GlobalTimeoutRetryPolicy : RetryPolicy { private readonly TimeSpan _timeout; - public GlobalTimeoutRetryPolicy(RetryOptions options, TimeSpan timeout) : base(options) + public GlobalTimeoutRetryPolicy(int maxRetries, Delay delay, TimeSpan timeout) : base(maxRetries, delay) { _timeout = timeout; } @@ -59,15 +61,20 @@ internal class GlobalTimeoutRetryPolicy : RetryPolicy Here is how we would configure the client to use the policy we just created. ```C# Snippet:SetGlobalTimeoutRetryPolicy -var retryOptions = new RetryOptions +var delay = Delay.CreateFixedDelay(TimeSpan.FromSeconds(2)); +SecretClientOptions options = new SecretClientOptions() { - Delay = TimeSpan.FromSeconds(2), - MaxRetries = 10, - Mode = RetryMode.Fixed + RetryPolicy = new GlobalTimeoutRetryPolicy(maxRetries: 4, delay: delay, timeout: TimeSpan.FromSeconds(30)) }; +``` + +Another scenario where it may be helpful to use a custom retry policy is when you need to customize the delay behavior, but don't need to adjust the logic used to determine whether a request should be retried or not. In this case, it isn't necessary to create a custom `RetryPolicy` class - instead, you can pass in a `DelayStrategy` into the `RetryPolicy` constructor. + +In the below example, we create a customized exponential delay strategy that uses different jitter factors from the default values. We then pass the strategy into the `RetryPolicy` constructor and set the constructed policy in our options. +```C# Snippet:CustomizeExponentialDelay SecretClientOptions options = new SecretClientOptions() { - RetryPolicy = new GlobalTimeoutRetryPolicy(retryOptions, timeout: TimeSpan.FromSeconds(30)) + RetryPolicy = new RetryPolicy(delay: new MyCustomDelay()) }; ``` diff --git a/sdk/core/Azure.Core/src/Delay.cs b/sdk/core/Azure.Core/src/Delay.cs new file mode 100644 index 0000000000000..5f2014b44fac8 --- /dev/null +++ b/sdk/core/Azure.Core/src/Delay.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Azure.Core.Pipeline; + +namespace Azure.Core +{ + /// + /// An abstraction to control delay behavior. + /// +#pragma warning disable AZC0012 // Avoid single word type names + public abstract class Delay +#pragma warning restore AZC0012 // Avoid single word type names + { + private readonly Random _random = new ThreadSafeRandom(); + private readonly double _minJitterFactor; + private readonly double _maxJitterFactor; + private readonly TimeSpan _maxDelay; + + /// + /// Constructs a new instance of . + /// + /// The max delay value to apply on an individual delay. + /// The jitter factor to apply to each delay. For example, if the delay is 1 second with a jitterFactor of 0.2, the actual + /// delay used will be a random double between 0.8 and 1.2. If set to 0, no jitter will be applied. + protected Delay(TimeSpan? maxDelay = default, double jitterFactor = 0.2) + { + // use same defaults as RetryOptions + _minJitterFactor = 1 - jitterFactor; + _maxJitterFactor = 1 + jitterFactor; + _maxDelay = maxDelay ?? TimeSpan.FromMinutes(1); + } + + /// + /// Create an exponential delay with the default jitter factor. + /// + /// The initial delay to use. + /// The maximum delay to use. + /// The instance. + public static Delay CreateExponentialDelay( + TimeSpan? initialDelay = default, + TimeSpan? maxDelay = default) + { + return new ExponentialDelay(initialDelay ?? TimeSpan.FromSeconds(0.8), maxDelay ?? TimeSpan.FromMinutes(1)); + } + + /// + /// Create a fixed delay with jitter. + /// + /// The delay to use. + /// The instance. + public static Delay CreateFixedDelay( + TimeSpan? delay = default) + { + return new FixedDelay(delay ?? TimeSpan.FromSeconds(0.8)); + } + + /// + /// Gets the next delay interval. + /// + /// The response, if any, returned from the service. + /// The retry number. + /// A representing the next delay interval. + protected abstract TimeSpan GetNextDelayCore(Response? response, int retryNumber); + + /// + /// Gets the next delay interval taking into account the Max Delay, jitter, and any Retry-After headers. + /// + /// The response, if any, returned from the service. + /// The retry number. + /// A representing the next delay interval. + public TimeSpan GetNextDelay(Response? response, int retryNumber) => + Max( + response?.Headers.RetryAfter ?? TimeSpan.Zero, + Min( + ApplyJitter(GetNextDelayCore(response, retryNumber)), + _maxDelay)); + + private TimeSpan ApplyJitter(TimeSpan delay) + { + return TimeSpan.FromMilliseconds(_random.Next((int)(delay.TotalMilliseconds * _minJitterFactor), (int)(delay.TotalMilliseconds * _maxJitterFactor))); + } + + /// + /// Gets the maximum of two values. + /// + /// The first value. + /// The second value. + /// The maximum of the two values. + protected static TimeSpan Max(TimeSpan val1, TimeSpan val2) => val1 > val2 ? val1 : val2; + + /// + /// Gets the minimum of two values. + /// + /// The first value. + /// The second value. + /// The minimum of the two values. + protected static TimeSpan Min(TimeSpan val1, TimeSpan val2) => val1 < val2 ? val1 : val2; + } +} diff --git a/sdk/core/Azure.Core/src/ExponentialDelay.cs b/sdk/core/Azure.Core/src/ExponentialDelay.cs new file mode 100644 index 0000000000000..8517c234dd9be --- /dev/null +++ b/sdk/core/Azure.Core/src/ExponentialDelay.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Core +{ + internal class ExponentialDelay : Delay + { + private readonly TimeSpan _delay; + + public ExponentialDelay( + TimeSpan delay, + TimeSpan maxDelay) : base(maxDelay) + { + _delay = delay; + } + + protected override TimeSpan GetNextDelayCore(Response? response, int retryNumber) => + TimeSpan.FromMilliseconds((1 << retryNumber) * _delay.TotalMilliseconds); + } +} \ No newline at end of file diff --git a/sdk/core/Azure.Core/src/FixedDelay.cs b/sdk/core/Azure.Core/src/FixedDelay.cs new file mode 100644 index 0000000000000..a7546150d5f60 --- /dev/null +++ b/sdk/core/Azure.Core/src/FixedDelay.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Core +{ + internal class FixedDelay : Delay + { + private readonly TimeSpan _delay; + + public FixedDelay(TimeSpan delay) : base(TimeSpan.FromMilliseconds(delay.TotalMilliseconds)) + { + _delay = delay; + } + + protected override TimeSpan GetNextDelayCore(Response? response, int retryNumber) => _delay; + } +} \ No newline at end of file diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineBuilder.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineBuilder.cs index 16b57d790f269..9b762dae7dd2e 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineBuilder.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineBuilder.cs @@ -152,7 +152,14 @@ void AddNonNullPolicies(HttpPipelinePolicy[] policiesToAdd) policies.Add(CreateTelemetryPolicy(buildOptions.ClientOptions)); } - policies.Add(buildOptions.ClientOptions.RetryPolicy ?? new DefaultRetryPolicy(buildOptions.ClientOptions.Retry)); + var retryOptions = buildOptions.ClientOptions.Retry; + policies.Add( + buildOptions.ClientOptions.RetryPolicy ?? + new RetryPolicy( + retryOptions.MaxRetries, + retryOptions.Mode == RetryMode.Exponential ? + Delay.CreateExponentialDelay(retryOptions.Delay, retryOptions.MaxDelay) : + Delay.CreateFixedDelay(retryOptions.Delay))); policies.Add(RedirectPolicy.Shared); diff --git a/sdk/core/Azure.Core/src/Pipeline/Internal/DefaultRetryPolicy.cs b/sdk/core/Azure.Core/src/Pipeline/Internal/DefaultRetryPolicy.cs deleted file mode 100644 index 4b2b0e85fc13d..0000000000000 --- a/sdk/core/Azure.Core/src/Pipeline/Internal/DefaultRetryPolicy.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Azure.Core.Pipeline -{ - /// - /// The default retry policy that will be used by the pipeline if no policy is specified in . - /// All logic for the default retry policy is implemented in and this class is only used to provide a concrete type - /// that can be instantiated. - /// - internal class DefaultRetryPolicy : RetryPolicy - { - public DefaultRetryPolicy(RetryOptions options) : base(options) - { - } - } -} \ No newline at end of file diff --git a/sdk/core/Azure.Core/src/Pipeline/RetryPolicy.cs b/sdk/core/Azure.Core/src/Pipeline/RetryPolicy.cs index f7b51af37d4ea..a9cc49fe16dcd 100644 --- a/sdk/core/Azure.Core/src/Pipeline/RetryPolicy.cs +++ b/sdk/core/Azure.Core/src/Pipeline/RetryPolicy.cs @@ -14,30 +14,24 @@ namespace Azure.Core.Pipeline /// /// Represents a policy that can be overriden to customize whether or not a request will be retried and how long to wait before retrying. /// - internal abstract class RetryPolicy : HttpPipelinePolicy + public class RetryPolicy : HttpPipelinePolicy { - private readonly RetryMode _mode; - private readonly TimeSpan _delay; - private readonly TimeSpan _maxDelay; private readonly int _maxRetries; - private readonly Random _random = new ThreadSafeRandom(); - - private const string RetryAfterHeaderName = "Retry-After"; - private const string RetryAfterMsHeaderName = "retry-after-ms"; - private const string XRetryAfterMsHeaderName = "x-ms-retry-after-ms"; + /// + /// Gets the delay to use for computing the interval between retry attempts. + /// + protected Delay Delay { get; } /// /// Initializes a new instance of the class. /// - /// The set of options to use for configuring the policy. - protected RetryPolicy(RetryOptions? options = default) + /// The maximum number of retries to attempt. + /// The delay to use for computing the interval between retry attempts. + public RetryPolicy(int maxRetries = 3, Delay? delay = default) { - options ??= ClientOptions.Default.Retry; - _mode = options.Mode; - _delay = options.Delay; - _maxDelay = options.MaxDelay; - _maxRetries = options.MaxRetries; + _maxRetries = maxRetries; + Delay = delay ?? Delay.CreateExponentialDelay(); } /// @@ -127,7 +121,8 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory TimeSpan.Zero) { if (async) @@ -221,15 +216,17 @@ private bool ShouldRetryInternal(HttpMessage message, Exception? exception) /// This method can be overriden to control how long to delay before retrying. This method will only be called for sync methods. /// /// The message containing the request and response. + /// The Retry-After header value, if any, returned from the service. /// The amount of time to delay before retrying. - protected internal virtual TimeSpan CalculateNextDelay(HttpMessage message) => CalculateNextDelayInternal(message); + protected virtual TimeSpan GetNextDelay(HttpMessage message, TimeSpan? retryAfter) => GetNextDelayInternal(message); /// /// This method can be overriden to control how long to delay before retrying. This method will only be called for async methods. /// /// The message containing the request and response. + /// The Retry-After header value, if any, returned from the service. /// The amount of time to delay before retrying. - protected internal virtual ValueTask CalculateNextDelayAsync(HttpMessage message) => new(CalculateNextDelayInternal(message)); + protected virtual ValueTask GetNextDelayAsync(HttpMessage message, TimeSpan? retryAfter) => new(GetNextDelayInternal(message)); /// /// This method can be overridden to introduce logic before each request attempt is sent. This will run even for the first attempt. @@ -263,73 +260,11 @@ protected internal virtual void OnRequestSent(HttpMessage message) /// The message containing the request and response. protected internal virtual ValueTask OnRequestSentAsync(HttpMessage message) => default; - private TimeSpan CalculateNextDelayInternal(HttpMessage message) - { - TimeSpan delay = TimeSpan.Zero; - - switch (_mode) - { - case RetryMode.Fixed: - delay = _delay; - break; - case RetryMode.Exponential: - delay = CalculateExponentialDelay(message.RetryNumber + 1); - break; - } - - TimeSpan serverDelay = GetServerDelay(message); - if (serverDelay > delay) - { - delay = serverDelay; - } - - return delay; - } - - /// - /// Gets the server specified delay. If the message has no response, is returned. - /// This method can be used to help calculate the next delay when overriding , i.e. - /// implementors may want to add the server delay to their own custom delay. - /// - /// The message to inspect for the server specified delay. - /// The server specified delay. - protected static TimeSpan GetServerDelay(HttpMessage message) - { - if (!message.HasResponse) - { - return TimeSpan.Zero; - } - - if (message.Response.TryGetHeader(RetryAfterMsHeaderName, out var retryAfterValue) || - message.Response.TryGetHeader(XRetryAfterMsHeaderName, out retryAfterValue)) - { - if (int.TryParse(retryAfterValue, out var delaySeconds)) - { - return TimeSpan.FromMilliseconds(delaySeconds); - } - } - - if (message.Response.TryGetHeader(RetryAfterHeaderName, out retryAfterValue)) - { - if (int.TryParse(retryAfterValue, out var delaySeconds)) - { - return TimeSpan.FromSeconds(delaySeconds); - } - if (DateTimeOffset.TryParse(retryAfterValue, out DateTimeOffset delayTime)) - { - return delayTime - DateTimeOffset.Now; - } - } - - return TimeSpan.Zero; - } - - private TimeSpan CalculateExponentialDelay(int attempted) + private TimeSpan GetNextDelayInternal(HttpMessage message) { - return TimeSpan.FromMilliseconds( - Math.Min( - (1 << (attempted - 1)) * _random.Next((int)(_delay.TotalMilliseconds * 0.8), (int)(_delay.TotalMilliseconds * 1.2)), - _maxDelay.TotalMilliseconds)); + return Delay.GetNextDelay( + message.HasResponse ? message.Response : default, + message.RetryNumber); } } } diff --git a/sdk/core/Azure.Core/src/ResponseHeaders.cs b/sdk/core/Azure.Core/src/ResponseHeaders.cs index d3c0ed54743b3..b3ab4cf2efd9b 100644 --- a/sdk/core/Azure.Core/src/ResponseHeaders.cs +++ b/sdk/core/Azure.Core/src/ResponseHeaders.cs @@ -14,6 +14,10 @@ namespace Azure.Core /// public readonly struct ResponseHeaders : IEnumerable { + private const string RetryAfterHeaderName = "Retry-After"; + private const string RetryAfterMsHeaderName = "retry-after-ms"; + private const string XRetryAfterMsHeaderName = "x-ms-retry-after-ms"; + private readonly Response _response; internal ResponseHeaders(Response response) @@ -50,6 +54,39 @@ internal ResponseHeaders(Response response) /// public string? RequestId => TryGetValue(HttpHeader.Names.XMsRequestId, out string? value) ? value : null; + /// + /// Gets the value of the retry after header, one of "Retry-After", "retry-after-ms", or "x-ms-retry-after-ms". + /// + internal TimeSpan? RetryAfter + { + get + { + if (TryGetValue(RetryAfterMsHeaderName, out var retryAfterValue) || + TryGetValue(XRetryAfterMsHeaderName, out retryAfterValue)) + { + if (int.TryParse(retryAfterValue, out var delaySeconds)) + { + return TimeSpan.FromMilliseconds(delaySeconds); + } + } + + if (TryGetValue(RetryAfterHeaderName, out retryAfterValue)) + { + if (int.TryParse(retryAfterValue, out var delaySeconds)) + { + return TimeSpan.FromSeconds(delaySeconds); + } + + if (DateTimeOffset.TryParse(retryAfterValue, out DateTimeOffset delayTime)) + { + return delayTime - DateTimeOffset.Now; + } + } + + return default; + } + } + /// /// Returns an enumerator that iterates through the . /// diff --git a/sdk/core/Azure.Core/tests/AzureSasCredentialSynchronousPolicyTests.cs b/sdk/core/Azure.Core/tests/AzureSasCredentialSynchronousPolicyTests.cs index f9a827a2cbf0f..fcd4dc16de492 100644 --- a/sdk/core/Azure.Core/tests/AzureSasCredentialSynchronousPolicyTests.cs +++ b/sdk/core/Azure.Core/tests/AzureSasCredentialSynchronousPolicyTests.cs @@ -126,7 +126,7 @@ public async Task VerifyRetryAfterMultipleSasCredentialUpdate() { request.Method = RequestMethod.Get; request.Uri.Query = INITIAL_QUERY_URI; - var pipeline = new HttpPipeline(transport, new HttpPipelinePolicy[] { new DefaultRetryPolicy(new RetryOptions() { Delay = TimeSpan.FromMilliseconds(100) }), sasPolicy }); + var pipeline = new HttpPipeline(transport, new HttpPipelinePolicy[] { new RetryPolicy(delay: Delay.CreateExponentialDelay(TimeSpan.FromMilliseconds(100))), sasPolicy }); Response response = await pipeline.SendRequestAsync(request, CancellationToken.None).ConfigureAwait(false); Assert.AreEqual((int)HttpStatusCode.OK, response.Status); diff --git a/sdk/core/Azure.Core/tests/FixedRetryPolicyTests.cs b/sdk/core/Azure.Core/tests/FixedRetryPolicyTests.cs index 1839c40ef1750..a0bcf4c441e3e 100644 --- a/sdk/core/Azure.Core/tests/FixedRetryPolicyTests.cs +++ b/sdk/core/Azure.Core/tests/FixedRetryPolicyTests.cs @@ -25,7 +25,7 @@ public async Task WaitsBetweenRetries() await mockTransport.RequestGate.Cycle(new MockResponse(500)); TimeSpan delay = await policy.DelayGate.Cycle(); - Assert.AreEqual(delay, TimeSpan.FromSeconds(3)); + Assert.That(delay, Is.EqualTo(TimeSpan.FromSeconds(3)).Within(TimeSpan.FromSeconds(0.2 * 3))); await mockTransport.RequestGate.Cycle(new MockResponse(200)); @@ -46,7 +46,7 @@ public async Task WaitsSameAmountEveryTime() for (int i = 0; i < 3; i++) { TimeSpan delay = await policy.DelayGate.Cycle(); - Assert.AreEqual(delay, TimeSpan.FromSeconds(3)); + Assert.That(delay, Is.EqualTo(TimeSpan.FromSeconds(3)).Within(TimeSpan.FromSeconds(0.2 * 3))); await mockTransport.RequestGate.Cycle(new MockResponse(500)); } @@ -68,7 +68,7 @@ public async Task WaitsSameAmountBetweenTries_Exceptions() for (int i = 0; i < 3; i++) { TimeSpan delay = await policy.DelayGate.Cycle(); - Assert.AreEqual(delay, TimeSpan.FromSeconds(3)); + Assert.That(delay, Is.EqualTo(TimeSpan.FromSeconds(3)).Within(TimeSpan.FromSeconds(0.2 * 3))); await mockTransport.RequestGate.Cycle(new MockResponse(500)); } @@ -98,7 +98,7 @@ public async Task UsesLargerOfDelayAndServerDelay(int delay, int retryAfter, int Response response = await task.TimeoutAfterDefault(); - Assert.AreEqual(TimeSpan.FromSeconds(expected), retryDelay); + Assert.That(retryDelay, Is.EqualTo(TimeSpan.FromSeconds(expected)).Within(TimeSpan.FromSeconds(0.2 * expected))); Assert.AreEqual(501, response.Status); } } diff --git a/sdk/core/Azure.Core/tests/HttpPipelineTests.cs b/sdk/core/Azure.Core/tests/HttpPipelineTests.cs index 4a3570f2be452..4c297eefd8610 100644 --- a/sdk/core/Azure.Core/tests/HttpPipelineTests.cs +++ b/sdk/core/Azure.Core/tests/HttpPipelineTests.cs @@ -20,13 +20,8 @@ public async Task CanBuildPipelineAndSendMessage() new MockResponse(500), new MockResponse(1)); - var pipeline = new HttpPipeline(mockTransport, new[] { - new DefaultRetryPolicy( - new RetryOptions { - Mode = RetryMode.Exponential, - Delay = TimeSpan.Zero, - MaxDelay = TimeSpan.Zero, - MaxRetries = 5}) + var pipeline = new HttpPipeline(mockTransport, new HttpPipelinePolicy[] { + new RetryPolicy(5) }, responseClassifier: new CustomResponseClassifier()); Request request = pipeline.CreateRequest(); diff --git a/sdk/core/Azure.Core/tests/RetryPolicyTestBase.cs b/sdk/core/Azure.Core/tests/RetryPolicyTestBase.cs index cc2ad976940ca..da0f32bf281fb 100644 --- a/sdk/core/Azure.Core/tests/RetryPolicyTestBase.cs +++ b/sdk/core/Azure.Core/tests/RetryPolicyTestBase.cs @@ -355,7 +355,7 @@ public async Task MsHeadersArePreferredOverRetryAfter(string headerName) Response response = await task.TimeoutAfterDefault(); - Assert.AreEqual(TimeSpan.FromMilliseconds(120000), retryDelay); + Assert.That(retryDelay, Is.EqualTo(TimeSpan.FromMilliseconds(120000)).Within(TimeSpan.FromMilliseconds(0.2 * 120000))); Assert.AreEqual(501, response.Status); } @@ -662,14 +662,12 @@ internal class RetryPolicyMock : RetryPolicy internal Exception LastException { get; set; } - public RetryPolicyMock(RetryMode mode, int maxRetries = 3, TimeSpan delay = default, TimeSpan maxDelay = default) : base( - new RetryOptions - { - Mode = mode, - Delay = delay, - MaxDelay = maxDelay, - MaxRetries = maxRetries - }) + public RetryPolicyMock(RetryMode mode, int maxRetries = 3, TimeSpan delay = default, TimeSpan maxDelay = default) + : base( + maxRetries, + mode == RetryMode.Exponential ? + Delay.CreateExponentialDelay(delay, maxDelay) : + Delay.CreateFixedDelay(delay)) { } diff --git a/sdk/core/Azure.Core/tests/samples/ConfigurationSamples.cs b/sdk/core/Azure.Core/tests/samples/ConfigurationSamples.cs index 6ea84dd9660c9..b4bc60cd574b6 100644 --- a/sdk/core/Azure.Core/tests/samples/ConfigurationSamples.cs +++ b/sdk/core/Azure.Core/tests/samples/ConfigurationSamples.cs @@ -4,6 +4,7 @@ using System; using System.Net; using System.Net.Http; +using System.Threading.Tasks; using Azure.Core.Pipeline; using Azure.Identity; using Azure.Security.KeyVault.Secrets; @@ -103,17 +104,32 @@ public void SetPollyRetryPolicy() public void SetGlobalTimeoutRetryPolicy() { #region Snippet:SetGlobalTimeoutRetryPolicy - var retryOptions = new RetryOptions + + var delay = Delay.CreateFixedDelay(TimeSpan.FromSeconds(2)); + SecretClientOptions options = new SecretClientOptions() { - Delay = TimeSpan.FromSeconds(2), - MaxRetries = 10, - Mode = RetryMode.Fixed + RetryPolicy = new GlobalTimeoutRetryPolicy(maxRetries: 4, delay: delay, timeout: TimeSpan.FromSeconds(30)) }; + #endregion + } + + [Test] + public void CustomizedJitterExponentialDelay() + { + #region Snippet:CustomizeExponentialDelay SecretClientOptions options = new SecretClientOptions() { - RetryPolicy = new GlobalTimeoutRetryPolicy(retryOptions, timeout: TimeSpan.FromSeconds(30)) + RetryPolicy = new RetryPolicy(delay: new MyCustomDelay()) }; #endregion } + + private class MyCustomDelay : Delay + { + protected override TimeSpan GetNextDelayCore(Response response, int retryNumber) + { + throw new NotImplementedException(); + } + } } } diff --git a/sdk/core/Azure.Core/tests/samples/GlobalTimeoutRetryPolicy.cs b/sdk/core/Azure.Core/tests/samples/GlobalTimeoutRetryPolicy.cs index 89336b53568de..0f5ff54c8d220 100644 --- a/sdk/core/Azure.Core/tests/samples/GlobalTimeoutRetryPolicy.cs +++ b/sdk/core/Azure.Core/tests/samples/GlobalTimeoutRetryPolicy.cs @@ -12,7 +12,7 @@ internal class GlobalTimeoutRetryPolicy : RetryPolicy { private readonly TimeSpan _timeout; - public GlobalTimeoutRetryPolicy(RetryOptions options, TimeSpan timeout) : base(options) + public GlobalTimeoutRetryPolicy(int maxRetries, Delay delay, TimeSpan timeout) : base(maxRetries, delay) { _timeout = timeout; }