Skip to content

Commit

Permalink
Add BootstrapRefreshInterval (#2052)
Browse files Browse the repository at this point in the history
Add a new timer with exponential backoff for failing to fetch metadata at bootstrap til we get the first config
  • Loading branch information
ciaozhang authored Apr 13, 2023
1 parent 2eb488a commit 90da399
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ public class ConfigurationManager<T> : BaseConfigurationManager, IConfigurationM
private readonly IConfigurationValidator<T> _configValidator;
private T _currentConfiguration;
private Exception _fetchMetadataFailure;
private TimeSpan _bootstrapRefreshInterval = TimeSpan.FromSeconds(1);

/// <summary>
/// Static initializer for a new object. Static initializers run before the first instance of the type is created.
/// </summary>
static ConfigurationManager()
/// <summary>
/// Static initializer for a new object. Static initializers run before the first instance of the type is created.
/// </summary>
static ConfigurationManager()
{
}

Expand Down Expand Up @@ -182,15 +183,33 @@ public async Task<T> GetConfigurationAsync(CancellationToken cancel)
catch (Exception ex)
{
_fetchMetadataFailure = ex;
_syncAfter = DateTimeUtil.Add(now.UtcDateTime, AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval);

if (_currentConfiguration == null) // Throw an exception if there's no configuration to return.
{
if (_bootstrapRefreshInterval < RefreshInterval)
{
// Adopt exponential backoff for bootstrap refresh interval with a decorrelated jitter if it is not longer than the refresh interval.
TimeSpan _bootstrapRefreshIntervalWithJitter = TimeSpan.FromSeconds(new Random().Next((int)_bootstrapRefreshInterval.TotalSeconds));
_bootstrapRefreshInterval += _bootstrapRefreshInterval;
_syncAfter = DateTimeUtil.Add(now.UtcDateTime, _bootstrapRefreshIntervalWithJitter);
}
else
{
_syncAfter = DateTimeUtil.Add(now.UtcDateTime, AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval);
}

throw LogHelper.LogExceptionMessage(
new InvalidOperationException(
LogHelper.FormatInvariant(LogMessages.IDX20803, LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), LogHelper.MarkAsNonPII(ex)), ex));
LogHelper.FormatInvariant(LogMessages.IDX20803, LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), LogHelper.MarkAsNonPII(_syncAfter), LogHelper.MarkAsNonPII(ex)), ex));
}
else
{
_syncAfter = DateTimeUtil.Add(now.UtcDateTime, AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval);

LogHelper.LogExceptionMessage(
new InvalidOperationException(
LogHelper.FormatInvariant(LogMessages.IDX20806, LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), LogHelper.MarkAsNonPII(ex)), ex));
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.IdentityModel.Protocols/LogMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal static class LogMessages
internal const string IDX20108 = "IDX20108: The address specified '{0}' is not valid as per HTTPS scheme. Please specify an https address for security reasons. If you want to test with http address, set the RequireHttps property on IDocumentRetriever to false.";

// configuration retrieval errors
internal const string IDX20803 = "IDX20803: Unable to obtain configuration from: '{0}'. Exception: '{1}.";
internal const string IDX20803 = "IDX20803: Unable to obtain configuration from: '{0}'. Will retry at '{1}'. Exception: '{2}'.";
internal const string IDX20804 = "IDX20804: Unable to retrieve document from: '{0}'.";
internal const string IDX20805 = "IDX20805: Obtaining information from metadata endpoint: '{0}'.";
internal const string IDX20806 = "IDX20806: Unable to obtain an updated configuration from: '{0}'. Returning the current configuration. Exception: '{1}.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,53 @@ public void FetchMetadataFailureTest()
TestUtilities.AssertFailIfErrors(context);
}

[Fact]
public void BootstrapRefreshIntervalTest()
{
var context = new CompareContext($"{this}.BootstrapRefreshIntervalTest");

var documentRetriever = new HttpDocumentRetriever(HttpResponseMessageUtils.SetupHttpClientThatReturns("OpenIdConnectMetadata.json", HttpStatusCode.NotFound));
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), documentRetriever) { RefreshInterval = TimeSpan.FromSeconds(2) };

// First time to fetch metadata.
try
{
var configuration = configManager.GetConfigurationAsync().Result;
}
catch (Exception firstFetchMetadataFailure)
{
// Refresh interval is BootstrapRefreshInterval
var syncAfter = configManager.GetType().GetField("_syncAfter", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(configManager);
if ((DateTimeOffset)syncAfter > DateTime.UtcNow + TimeSpan.FromSeconds(2))
context.AddDiff($"Expected the refresh interval is longer than 2 seconds.");

if (firstFetchMetadataFailure.InnerException == null)
context.AddDiff($"Expected exception to contain inner exception for fetch metadata failure.");

// Fetch metadata again during refresh interval, the exception should be same from above.
try
{
configManager.RequestRefresh();
var configuration = configManager.GetConfigurationAsync().Result;
}
catch (Exception secondFetchMetadataFailure)
{
if (secondFetchMetadataFailure.InnerException == null)
context.AddDiff($"Expected exception to contain inner exception for fetch metadata failure.");

syncAfter = configManager.GetType().GetField("_syncAfter", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(configManager);

// Refresh interval is RefreshInterval
if ((DateTimeOffset)syncAfter > DateTime.UtcNow + configManager.RefreshInterval)
context.AddDiff($"Expected the refresh interval is longer than 2 seconds.");

IdentityComparer.AreEqual(firstFetchMetadataFailure, secondFetchMetadataFailure, context);
}
}

TestUtilities.AssertFailIfErrors(context);
}

[Fact]
public void GetSets()
{
Expand Down

0 comments on commit 90da399

Please sign in to comment.