Skip to content

Commit 487b1ec

Browse files
Retry policies are now per request instead of per HttpManager (#5252)
1 parent a520deb commit 487b1ec

File tree

31 files changed

+457
-353
lines changed

31 files changed

+457
-353
lines changed

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,8 @@ public string ClientVersion
114114
public bool CacheSynchronizationEnabled { get; internal set; } = true;
115115
public bool MultiCloudSupportEnabled { get; set; } = false;
116116

117-
public bool RetryOnServerErrors { get; set; } = true;
118-
119117
public ManagedIdentityId ManagedIdentityId { get; internal set; }
118+
public bool DisableInternalRetries { get; internal set; } = false;
120119

121120
public bool IsManagedIdentity { get; }
122121
public bool IsConfidentialClient { get; }

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory)
6161
/// </summary>
6262
/// <param name="httpClientFactory">HTTP client factory</param>
6363
/// <param name="retryOnceOn5xx">Configures MSAL to retry on 5xx server errors. When enabled (on by default), MSAL will wait 1 second after receiving
64-
/// a 5xx error and then retry the http request again.</param>
64+
/// a 5xx error and then retry the http request again.
65+
/// When disabled, the developer will be responsible for configuring their own retry policy in their custom IMsalHttpClientFactory.</param>
6566
/// <remarks>MSAL does not guarantee that it will not modify the HttpClient, for example by adding new headers.
6667
/// Prior to the changes needed in order to make MSAL's httpClients thread safe (https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/2046/files),
6768
/// the httpClient had the possibility of throwing an exception stating "Properties can only be modified before sending the first request".
@@ -73,7 +74,7 @@ public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory)
7374
public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx)
7475
{
7576
Config.HttpClientFactory = httpClientFactory;
76-
Config.RetryOnServerErrors = retryOnceOn5xx;
77+
Config.DisableInternalRetries = !retryOnceOn5xx;
7778
return (T)this;
7879
}
7980

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

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,29 @@ namespace Microsoft.Identity.Client.Http
2727
internal class HttpManager : IHttpManager
2828
{
2929
protected readonly IMsalHttpClientFactory _httpClientFactory;
30-
private readonly IRetryPolicy _retryPolicy;
30+
private readonly bool _disableInternalRetries;
3131
public long LastRequestDurationInMs { get; private set; }
3232

3333
/// <summary>
34-
/// A new instance of the HTTP manager with a retry *condition*. The retry policy hardcodes:
35-
/// - the number of retries (1)
36-
/// - the delay between retries (1 second)
34+
/// Initializes a new instance of the <see cref="HttpManager"/> class.
3735
/// </summary>
36+
/// <param name="httpClientFactory">
37+
/// An instance of <see cref="IMsalHttpClientFactory"/> used to create and manage <see cref="HttpClient"/> instances.
38+
/// This factory ensures proper reuse of <see cref="HttpClient"/> to avoid socket exhaustion.
39+
/// </param>
40+
/// <param name="disableInternalRetries">
41+
/// A boolean flag indicating whether the HTTP manager should enable retry logic for transient failures.
42+
/// </param>
43+
/// <exception cref="ArgumentNullException">
44+
/// Thrown when <paramref name="httpClientFactory"/> is null.
45+
/// </exception>
3846
public HttpManager(
3947
IMsalHttpClientFactory httpClientFactory,
40-
IRetryPolicy retryPolicy)
48+
bool disableInternalRetries)
4149
{
4250
_httpClientFactory = httpClientFactory ??
4351
throw new ArgumentNullException(nameof(httpClientFactory));
44-
_retryPolicy = retryPolicy;
52+
_disableInternalRetries = disableInternalRetries;
4553
}
4654

4755
public async Task<HttpResponse> SendRequestAsync(
@@ -54,6 +62,7 @@ public async Task<HttpResponse> SendRequestAsync(
5462
X509Certificate2 bindingCertificate,
5563
Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> validateServerCert,
5664
CancellationToken cancellationToken,
65+
IRetryPolicy retryPolicy,
5766
int retryCount = 0)
5867
{
5968
Exception timeoutException = null;
@@ -102,9 +111,9 @@ public async Task<HttpResponse> SendRequestAsync(
102111
timeoutException = exception;
103112
}
104113

105-
while (_retryPolicy.pauseForRetry(response, timeoutException, retryCount))
114+
while (!_disableInternalRetries && retryPolicy.PauseForRetry(response, timeoutException, retryCount))
106115
{
107-
logger.Warning($"Retry condition met. Retry count: {retryCount++} after waiting {_retryPolicy.DelayInMilliseconds}ms.");
116+
logger.Warning($"Retry condition met. Retry count: {retryCount++} after waiting {retryPolicy.DelayInMilliseconds}ms.");
108117
return await SendRequestAsync(
109118
endpoint,
110119
headers,
@@ -113,8 +122,10 @@ public async Task<HttpResponse> SendRequestAsync(
113122
logger,
114123
doNotThrow,
115124
bindingCertificate,
116-
validateServerCert, cancellationToken: cancellationToken,
117-
retryCount: retryCount) // Pass the updated retry count
125+
validateServerCert,
126+
cancellationToken,
127+
retryPolicy,
128+
retryCount) // Pass the updated retry count
118129
.ConfigureAwait(false);
119130
}
120131

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,18 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Linq;
7-
using System.Text;
8-
using System.Threading.Tasks;
9-
104
namespace Microsoft.Identity.Client.Http
115
{
126
/// <summary>
13-
/// Factory to return the instance of HttpManager based on retry configuration and type of MSAL application.
7+
/// Factory to return the instance of HttpManager based on type of MSAL application.
148
/// </summary>
159
internal sealed class HttpManagerFactory
1610
{
1711
public static IHttpManager GetHttpManager(
1812
IMsalHttpClientFactory httpClientFactory,
19-
bool withRetry,
20-
bool isManagedIdentity)
13+
bool disableInternalRetries = false)
2114
{
22-
if (!withRetry)
23-
{
24-
return new HttpManager(httpClientFactory, new NoRetryPolicy());
25-
}
26-
27-
return isManagedIdentity ?
28-
new HttpManager(httpClientFactory, new LinearRetryPolicy(1000, 3, HttpRetryConditions.ManagedIdentity)) :
29-
new HttpManager(httpClientFactory, new LinearRetryPolicy(1000, 1, HttpRetryConditions.Sts));
15+
return new HttpManager(httpClientFactory, disableInternalRetries);
3016
}
3117
}
3218
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ internal interface IHttpManager
2828
/// <param name="mtlsCertificate">Certificate used for MTLS authentication.</param>
2929
/// <param name="validateServerCertificate">Callback to validate the server cert for service fabric managed identity flow.</param>
3030
/// <param name="cancellationToken"></param>
31+
/// <param name="retryPolicy">Retry policy to be used for the request.</param>
3132
/// <param name="retryCount">Number of retries to be attempted in case of retriable status codes.</param>
3233
/// <returns></returns>
3334
Task<HttpResponse> SendRequestAsync(
@@ -40,6 +41,7 @@ Task<HttpResponse> SendRequestAsync(
4041
X509Certificate2 mtlsCertificate,
4142
Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> validateServerCertificate,
4243
CancellationToken cancellationToken,
44+
IRetryPolicy retryPolicy,
4345
int retryCount = 0);
4446
}
4547
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ namespace Microsoft.Identity.Client.Http
1212
internal interface IRetryPolicy
1313
{
1414
int DelayInMilliseconds { get; }
15-
bool pauseForRetry(HttpResponse response, Exception exception, int retryCount);
15+
bool PauseForRetry(HttpResponse response, Exception exception, int retryCount);
1616
}
1717
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ namespace Microsoft.Identity.Client.Http
99
{
1010
internal class LinearRetryPolicy : IRetryPolicy
1111
{
12-
12+
// referenced in unit tests, cannot be private
13+
public static int numRetries { get; private set; } = 0;
14+
public const int DefaultStsMaxRetries = 1;
15+
// this will be overridden in the unit tests so that they run faster
16+
public static int DefaultStsRetryDelayMs { get; set; } = 1000;
17+
1318
private int _maxRetries;
1419
private readonly Func<HttpResponse, Exception, bool> _retryCondition;
1520
public int DelayInMilliseconds { private set; get; }
@@ -21,8 +26,11 @@ public LinearRetryPolicy(int delayMilliseconds, int maxRetries, Func<HttpRespons
2126
_retryCondition = retryCondition;
2227
}
2328

24-
public bool pauseForRetry(HttpResponse response, Exception exception, int retryCount)
29+
public bool PauseForRetry(HttpResponse response, Exception exception, int retryCount)
2530
{
31+
// referenced in the unit tests
32+
numRetries = retryCount + 1;
33+
2634
return retryCount < _maxRetries && _retryCondition(response, exception);
2735
}
2836
}

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

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

src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ public RegionInfo(string region, RegionAutodetectionSource regionSource, string
4646
private static bool s_failedAutoDiscovery = false;
4747
private static string s_regionDiscoveryDetails;
4848

49+
private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy(
50+
LinearRetryPolicy.DefaultStsRetryDelayMs,
51+
LinearRetryPolicy.DefaultStsMaxRetries,
52+
HttpRetryConditions.Sts);
53+
4954
public RegionManager(
5055
IHttpManager httpManager,
5156
int imdsCallTimeout = 2000,
@@ -206,7 +211,9 @@ private async Task<RegionInfo> DiscoverAsync(ILoggerAdapter logger, Cancellation
206211
logger: logger,
207212
doNotThrow: false,
208213
mtlsCertificate: null,
209-
validateServerCertificate: null, cancellationToken: GetCancellationToken(requestCancellationToken))
214+
validateServerCertificate: null,
215+
cancellationToken: GetCancellationToken(requestCancellationToken),
216+
retryPolicy: _linearRetryPolicy)
210217
.ConfigureAwait(false);
211218

212219
// A bad request occurs when the version in the IMDS call is no longer supported.
@@ -222,7 +229,9 @@ private async Task<RegionInfo> DiscoverAsync(ILoggerAdapter logger, Cancellation
222229
logger: logger,
223230
doNotThrow: false,
224231
mtlsCertificate: null,
225-
validateServerCertificate: null, cancellationToken: GetCancellationToken(requestCancellationToken))
232+
validateServerCertificate: null,
233+
cancellationToken: GetCancellationToken(requestCancellationToken),
234+
retryPolicy: _linearRetryPolicy)
226235
.ConfigureAwait(false); // Call again with updated version
227236
}
228237

@@ -324,7 +333,8 @@ private async Task<string> GetImdsUriApiVersionAsync(ILoggerAdapter logger, Dict
324333
doNotThrow: false,
325334
mtlsCertificate: null,
326335
validateServerCertificate: null,
327-
cancellationToken: GetCancellationToken(userCancellationToken))
336+
cancellationToken: GetCancellationToken(userCancellationToken),
337+
retryPolicy: _linearRetryPolicy)
328338
.ConfigureAwait(false);
329339

330340
// When IMDS endpoint is called without the api version query param, bad request response comes back with latest version.

src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Net;
77
using System.Threading.Tasks;
88
using Microsoft.Identity.Client.Core;
9+
using Microsoft.Identity.Client.Http;
910
using Microsoft.Identity.Client.Internal;
1011
using Microsoft.Identity.Client.OAuth2;
1112

@@ -28,6 +29,11 @@ public async Task ValidateAuthorityAsync(
2829
var resource = $"https://{authorityInfo.Host}";
2930
string webFingerUrl = Constants.FormatAdfsWebFingerUrl(authorityInfo.Host, resource);
3031

32+
LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy(
33+
LinearRetryPolicy.DefaultStsRetryDelayMs,
34+
LinearRetryPolicy.DefaultStsMaxRetries,
35+
HttpRetryConditions.Sts);
36+
3137
Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync(
3238
new Uri(webFingerUrl),
3339
null,
@@ -36,7 +42,10 @@ public async Task ValidateAuthorityAsync(
3642
logger: _requestContext.Logger,
3743
doNotThrow: false,
3844
mtlsCertificate: null,
39-
validateServerCertificate: null, cancellationToken: _requestContext.UserCancellationToken)
45+
validateServerCertificate: null,
46+
cancellationToken: _requestContext.UserCancellationToken,
47+
retryPolicy: _linearRetryPolicy
48+
)
4049
.ConfigureAwait(false);
4150

4251
if (httpResponse.StatusCode != HttpStatusCode.OK)

0 commit comments

Comments
 (0)