Skip to content

Commit 9fd9500

Browse files
Reworked retry policy functionality & Created IMDS retry policy (#5231)
1 parent 7a8f398 commit 9fd9500

35 files changed

+1407
-268
lines changed

src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.Identity.Client.Core;
1212
using Microsoft.Identity.Client.Extensibility;
1313
using Microsoft.Identity.Client.Http;
14+
using Microsoft.Identity.Client.Http.Retry;
1415
using Microsoft.Identity.Client.Instance;
1516
using Microsoft.Identity.Client.Instance.Discovery;
1617
using Microsoft.Identity.Client.Internal.Broker;
@@ -124,6 +125,8 @@ public string ClientVersion
124125

125126
public Func<AppTokenProviderParameters, Task<AppTokenProviderResult>> AppTokenProvider;
126127

128+
internal IRetryPolicyFactory RetryPolicyFactory { get; set; }
129+
127130
#region ClientCredentials
128131

129132
// Indicates if claims or assertions are used within the configuration
@@ -207,6 +210,5 @@ public X509Certificate2 ClientCredentialCertificate
207210
public IDeviceAuthManager DeviceAuthManagerForTest { get; set; }
208211
public bool IsInstanceDiscoveryEnabled { get; internal set; } = true;
209212
#endregion
210-
211213
}
212214
}

src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
using Microsoft.Identity.Client.Utils;
1515
using Microsoft.IdentityModel.Abstractions;
1616
using Microsoft.Identity.Client.Internal;
17+
using Microsoft.Identity.Client.Http.Retry;
18+
1719
#if SUPPORTS_SYSTEM_TEXT_JSON
1820
using System.Text.Json;
1921
#else
@@ -31,6 +33,12 @@ public abstract class BaseAbstractApplicationBuilder<T>
3133
internal BaseAbstractApplicationBuilder(ApplicationConfiguration configuration)
3234
{
3335
Config = configuration;
36+
37+
// Ensure the default retry policy factory is set if the test factory was not provided
38+
if (Config.RetryPolicyFactory == null)
39+
{
40+
Config.RetryPolicyFactory = new RetryPolicyFactory();
41+
}
3442
}
3543

3644
internal ApplicationConfiguration Config { get; }
@@ -227,6 +235,17 @@ public T WithClientVersion(string clientVersion)
227235
return this as T;
228236
}
229237

238+
/// <summary>
239+
/// Internal only: Allows tests to inject a custom retry policy factory.
240+
/// </summary>
241+
/// <param name="factory">The retry policy factory to use.</param>
242+
/// <returns>The builder for chaining.</returns>
243+
internal T WithRetryPolicyFactory(IRetryPolicyFactory factory)
244+
{
245+
Config.RetryPolicyFactory = factory;
246+
return (T)this;
247+
}
248+
230249
internal virtual ApplicationConfiguration BuildConfiguration()
231250
{
232251
ResolveAuthority();

src/client/Microsoft.Identity.Client/Http/HttpManager.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using System.Threading;
1414
using System.Threading.Tasks;
1515
using Microsoft.Identity.Client.Core;
16+
using Microsoft.Identity.Client.Http.Retry;
1617

1718
namespace Microsoft.Identity.Client.Http
1819
{
@@ -110,10 +111,11 @@ public async Task<HttpResponse> SendRequestAsync(
110111
logger.Error("The HTTP request failed. " + exception.Message);
111112
timeoutException = exception;
112113
}
113-
114-
while (!_disableInternalRetries && retryPolicy.PauseForRetry(response, timeoutException, retryCount))
114+
115+
while (!_disableInternalRetries && await retryPolicy.PauseForRetryAsync(response, timeoutException, retryCount, logger).ConfigureAwait(false))
115116
{
116-
logger.Warning($"Retry condition met. Retry count: {retryCount++} after waiting {retryPolicy.DelayInMilliseconds}ms.");
117+
retryCount++;
118+
117119
return await SendRequestAsync(
118120
endpoint,
119121
headers,

src/client/Microsoft.Identity.Client/Http/IHttpManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Threading;
1010
using System.Threading.Tasks;
1111
using Microsoft.Identity.Client.Core;
12+
using Microsoft.Identity.Client.Http.Retry;
1213

1314
namespace Microsoft.Identity.Client.Http
1415
{

src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using Microsoft.Identity.Client.Core;
7+
8+
namespace Microsoft.Identity.Client.Http.Retry
9+
{
10+
class DefaultRetryPolicy : IRetryPolicy
11+
{
12+
// referenced in unit tests
13+
public const int DefaultStsMaxRetries = 1;
14+
public const int DefaultManagedIdentityMaxRetries = 3;
15+
16+
private const int DefaultStsRetryDelayMs = 1000;
17+
private const int DefaultManagedIdentityRetryDelayMs = 1000;
18+
19+
public readonly int _defaultRetryDelayMs;
20+
private readonly int _maxRetries;
21+
private readonly Func<HttpResponse, Exception, bool> _retryCondition;
22+
private readonly LinearRetryStrategy _linearRetryStrategy = new LinearRetryStrategy();
23+
24+
public DefaultRetryPolicy(RequestType requestType)
25+
{
26+
switch (requestType)
27+
{
28+
case RequestType.ManagedIdentityDefault:
29+
_defaultRetryDelayMs = DefaultManagedIdentityRetryDelayMs;
30+
_maxRetries = DefaultManagedIdentityMaxRetries;
31+
_retryCondition = HttpRetryConditions.DefaultManagedIdentity;
32+
break;
33+
case RequestType.STS:
34+
_defaultRetryDelayMs = DefaultStsRetryDelayMs;
35+
_maxRetries = DefaultStsMaxRetries;
36+
_retryCondition = HttpRetryConditions.Sts;
37+
break;
38+
default:
39+
throw new ArgumentOutOfRangeException(nameof(requestType), requestType, "Unknown request type");
40+
}
41+
}
42+
43+
internal virtual Task DelayAsync(int milliseconds)
44+
{
45+
return Task.Delay(milliseconds);
46+
}
47+
48+
public async Task<bool> PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger)
49+
{
50+
// Check if the status code is retriable and if the current retry count is less than max retries
51+
if (_retryCondition(response, exception) &&
52+
retryCount < _maxRetries)
53+
{
54+
// Use HeadersAsDictionary to check for "Retry-After" header
55+
string retryAfter = string.Empty;
56+
if (response?.HeadersAsDictionary != null)
57+
{
58+
response.HeadersAsDictionary.TryGetValue("Retry-After", out retryAfter);
59+
}
60+
61+
int retryAfterDelay = _linearRetryStrategy.CalculateDelay(retryAfter, _defaultRetryDelayMs);
62+
63+
logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})");
64+
65+
// Pause execution for the calculated delay
66+
await DelayAsync(retryAfterDelay).ConfigureAwait(false);
67+
68+
return true;
69+
}
70+
71+
// If the status code is not retriable or max retries have been reached, do not retry
72+
return false;
73+
}
74+
}
75+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
6+
namespace Microsoft.Identity.Client.Http.Retry
7+
{
8+
internal class ExponentialRetryStrategy
9+
{
10+
// Minimum backoff time in milliseconds
11+
private int _minExponentialBackoff;
12+
// Maximum backoff time in milliseconds
13+
private int _maxExponentialBackoff;
14+
// Maximum backoff time in milliseconds
15+
private int _exponentialDeltaBackoff;
16+
17+
public ExponentialRetryStrategy(int minExponentialBackoff, int maxExponentialBackoff, int exponentialDeltaBackoff)
18+
{
19+
_minExponentialBackoff = minExponentialBackoff;
20+
_maxExponentialBackoff = maxExponentialBackoff;
21+
_exponentialDeltaBackoff = exponentialDeltaBackoff;
22+
}
23+
24+
/// <summary>
25+
/// Calculates the exponential delay based on the current retry attempt.
26+
/// </summary>
27+
/// <param name="currentRetry">The current retry attempt number.</param>
28+
/// <returns>The calculated exponential delay in milliseconds.</returns>
29+
/// <remarks>
30+
/// The delay is calculated using the formula:
31+
/// - If <paramref name="currentRetry"/> is 0, it returns the minimum backoff time.
32+
/// - Otherwise, it calculates the delay as the minimum of:
33+
/// - (2^(currentRetry - 1)) * deltaBackoff
34+
/// - maxBackoff
35+
/// This ensures that the delay increases exponentially with each retry attempt,
36+
/// but does not exceed the maximum backoff time.
37+
/// </remarks>
38+
public int CalculateDelay(int currentRetry)
39+
{
40+
// Attempt 1
41+
if (currentRetry == 0)
42+
{
43+
return _minExponentialBackoff;
44+
}
45+
46+
// Attempt 2+
47+
int exponentialDelay = Math.Min(
48+
(int)(Math.Pow(2, currentRetry - 1) * _exponentialDeltaBackoff),
49+
_maxExponentialBackoff
50+
);
51+
52+
return exponentialDelay;
53+
}
54+
}
55+
}

src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs renamed to src/client/Microsoft.Identity.Client/Http/Retry/HttpRetryCondition.cs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
using System;
55
using System.Threading.Tasks;
66

7-
namespace Microsoft.Identity.Client.Http
7+
namespace Microsoft.Identity.Client.Http.Retry
88
{
99
internal static class HttpRetryConditions
1010
{
1111
/// <summary>
1212
/// Retry policy specific to managed identity flow.
13-
/// Avoid changing this, as it's breaking change.
13+
/// Avoid changing this, as it's a breaking change.
1414
/// </summary>
15-
public static bool ManagedIdentity(HttpResponse response, Exception exception)
15+
public static bool DefaultManagedIdentity(HttpResponse response, Exception exception)
1616
{
1717
if (exception != null)
1818
{
@@ -21,12 +21,32 @@ public static bool ManagedIdentity(HttpResponse response, Exception exception)
2121

2222
return (int)response.StatusCode switch
2323
{
24-
//Not Found
24+
// Not Found, Request Timeout, Too Many Requests, Server Error, Service Unavailable, Gateway Timeout
2525
404 or 408 or 429 or 500 or 503 or 504 => true,
2626
_ => false,
2727
};
2828
}
2929

30+
/// <summary>
31+
/// Retry policy specific to IMDS Managed Identity.
32+
/// </summary>
33+
public static bool Imds(HttpResponse response, Exception exception)
34+
{
35+
if (exception != null)
36+
{
37+
return exception is TaskCanceledException ? true : false;
38+
}
39+
40+
return (int)response.StatusCode switch
41+
{
42+
// Not Found, Request Timeout, Gone, Too Many Requests
43+
404 or 408 or 410 or 429 => true,
44+
// Server Error range
45+
>= 500 and <= 599 => true,
46+
_ => false,
47+
};
48+
}
49+
3050
/// <summary>
3151
/// Retry condition for /token and /authorize endpoints
3252
/// </summary>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using Microsoft.Identity.Client.Core;
7+
8+
namespace Microsoft.Identity.Client.Http.Retry
9+
{
10+
// Interface for implementing retry logic for HTTP requests.
11+
// Determines if a retry should occur and handles pause logic between retries.
12+
internal interface IRetryPolicy
13+
{
14+
/// <summary>
15+
/// Determines whether a retry should be attempted for a given HTTP response or exception,
16+
/// and performs any necessary pause or delay logic before the next retry attempt.
17+
/// </summary>
18+
/// <param name="response">The HTTP response received from the request.</param>
19+
/// <param name="exception">The exception encountered during the request.</param>
20+
/// <param name="retryCount">The current retry attempt count.</param>
21+
/// <param name="logger">The logger used for diagnostic and informational messages.</param>
22+
/// <returns>A task that returns true if a retry should be performed; otherwise, false.</returns>
23+
Task<bool> PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger);
24+
}
25+
}

0 commit comments

Comments
 (0)