Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
e80a3fa
first draft time-based retries in startup
amerjusupovic Sep 14, 2023
031c473
set startupdelay from configureclientoptions, fix syntax and spacing
amerjusupovic Sep 18, 2023
7a0698e
allow configuring of startupclientoptions from options
amerjusupovic Sep 18, 2023
0ac28dd
rough draft startupoptions
amerjusupovic Sep 22, 2023
3070f42
fix draft
amerjusupovic Sep 22, 2023
40ffcbd
clarify summary for startupoptions
amerjusupovic Sep 22, 2023
2516420
testing out retryoptions settings
amerjusupovic Sep 22, 2023
e69a33c
update with new structure, need logic to calculate retry options for …
amerjusupovic Sep 25, 2023
565cbf5
fix usage of startupconfigclientmanager, logic for retry time to use …
amerjusupovic Sep 27, 2023
e09e934
in progress fixing connect tests
amerjusupovic Sep 28, 2023
e9bb2d7
fix connecttest, clientoptions in progress, need to fix maxretry logic
amerjusupovic Sep 28, 2023
b2b38c5
fix tests
amerjusupovic Oct 3, 2023
fdce806
extend startup timeout on test
amerjusupovic Oct 3, 2023
3242e9f
remove unused variable
amerjusupovic Oct 3, 2023
e099d5f
remove unused usings
amerjusupovic Oct 3, 2023
638ae77
remove max retries change
amerjusupovic Oct 3, 2023
7a56057
in progress working on fix for multiple clients with timeout
amerjusupovic Oct 3, 2023
313196c
in progress fix initializeasync
amerjusupovic Oct 4, 2023
4b4dad4
in progress fixing failover logic
amerjusupovic Oct 18, 2023
18f994e
progress
amerjusupovic Oct 18, 2023
c67243c
fix with updates to logic for replicas
amerjusupovic Oct 18, 2023
0ff1221
fix tests and logic in initializeasync
amerjusupovic Oct 18, 2023
823392e
remove unused using
amerjusupovic Oct 18, 2023
0f1e5b3
in progress fixing timing of cts
amerjusupovic Oct 19, 2023
9a3f139
stuck on logic for srv dns changes, iasyncenumerable
amerjusupovic Oct 19, 2023
a8f3698
progress
amerjusupovic Oct 19, 2023
34829cc
progress changing timeout to per store/replica, change options proper…
amerjusupovic Oct 31, 2023
df8a41c
fix logic to be per store/replica
amerjusupovic Nov 1, 2023
1dc561e
remove cancellationtoken from func
amerjusupovic Nov 1, 2023
6535cbb
progress on looping over clients
amerjusupovic Nov 1, 2023
a07e88b
fix iterating over clients, unit tests in progress
amerjusupovic Nov 2, 2023
cd722c0
add backoff to if statement
amerjusupovic Nov 2, 2023
9fc665c
working draft of design to retry replicas until timeout
amerjusupovic Nov 2, 2023
c92162a
updates to fix unit tests, bugs
amerjusupovic Nov 2, 2023
807c38b
fix variable naming hasNextClient
amerjusupovic Nov 2, 2023
4c4aa37
add comment to catch in initializeasync
amerjusupovic Nov 2, 2023
03bff55
prevent unnecessary delay when load finished
amerjusupovic Nov 2, 2023
418eb66
remove custom exception, fix logic in executewithfailover
amerjusupovic Nov 2, 2023
95979fd
move comment
amerjusupovic Nov 2, 2023
2df9787
fix small mistakes, rename variables
amerjusupovic Nov 6, 2023
4e98cb7
PR revisions
amerjusupovic Nov 7, 2023
609df3a
restructure startup retry logic, fix tests
amerjusupovic Nov 8, 2023
39da723
Update src/Microsoft.Extensions.Configuration.AzureAppConfiguration/S…
amerjusupovic Nov 8, 2023
3f2f466
remove unnecessary param isstartup, fix text
amerjusupovic Nov 8, 2023
b7b986d
PR revisions, fix configurestartupoptions summary
amerjusupovic Nov 8, 2023
5393333
Apply suggestions from code review
amerjusupovic Nov 8, 2023
17e013e
combine if statemenets
amerjusupovic Nov 9, 2023
e74a184
simplify logic for catching operationcanceledexception
amerjusupovic Nov 9, 2023
18f4b8b
move call to getclients
amerjusupovic Nov 9, 2023
6098a3a
PR revisions, make tests more specific
amerjusupovic Nov 9, 2023
c860a72
add new methods for jitter and startup backoff calculation, fix small…
amerjusupovic Nov 15, 2023
4ebb18a
use new fixed + exponential backoff function
amerjusupovic Nov 15, 2023
1fd54a0
remove debugging statemenets
amerjusupovic Nov 15, 2023
f9443d0
PR revisions
amerjusupovic Nov 15, 2023
21f6d51
PR revisions, remove new exponential backoff method and update old on…
amerjusupovic Nov 16, 2023
34173e6
update jitter range logic
amerjusupovic Nov 16, 2023
57f75f6
fix isFailoverable, change jitter ratio to 0.25
amerjusupovic Nov 16, 2023
8c34b8f
update IsFailoverable
amerjusupovic Nov 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ internal IEnumerable<IKeyValueAdapter> Adapters
/// </summary>
internal FeatureFilterTelemetry FeatureFilterTelemetry { get; set; } = new FeatureFilterTelemetry();

/// <summary>
/// Options used to configure provider startup.
/// </summary>
internal StartupOptions Startup { get; set; } = new StartupOptions();

/// <summary>
/// Specify what key-values to include in the configuration provider.
/// <see cref="Select"/> can be called multiple times to include multiple sets of key-values.
Expand Down Expand Up @@ -351,7 +356,7 @@ public AzureAppConfigurationOptions TrimKeyPrefix(string prefix)
}

/// <summary>
/// Configure the client used to communicate with Azure App Configuration.
/// Configure the client(s) used to communicate with Azure App Configuration.
/// </summary>
/// <param name="configure">A callback used to configure Azure App Configuration client options.</param>
public AzureAppConfigurationOptions ConfigureClientOptions(Action<ConfigurationClientOptions> configure)
Expand Down Expand Up @@ -429,6 +434,16 @@ public AzureAppConfigurationOptions Map(Func<ConfigurationSetting, ValueTask<Con
return this;
}

/// <summary>
/// Configure the provider behavior when loading data from Azure App Configuration on startup.
/// </summary>
/// <param name="configure">A callback used to configure Azure App Configuration startup options.</param>
public AzureAppConfigurationOptions ConfigureStartupOptions(Action<StartupOptions> configure)
{
configure?.Invoke(Startup);
return this;
}

private static ConfigurationClientOptions GetDefaultClientOptions()
{
var clientOptions = new ConfigurationClientOptions(ConfigurationClientOptions.ServiceVersion.V2022_11_01_Preview);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ public ILoggerFactory LoggerFactory
}
}

public AzureAppConfigurationProvider(IConfigurationClientManager clientManager, AzureAppConfigurationOptions options, bool optional)
public AzureAppConfigurationProvider(IConfigurationClientManager configClientManager, AzureAppConfigurationOptions options, bool optional)
{
_configClientManager = clientManager ?? throw new ArgumentNullException(nameof(clientManager));
_configClientManager = configClientManager ?? throw new ArgumentNullException(nameof(configClientManager));
_options = options ?? throw new ArgumentNullException(nameof(options));
_optional = optional;

Expand Down Expand Up @@ -127,16 +127,13 @@ public override void Load()
{
var watch = Stopwatch.StartNew();

var loadStartTime = DateTimeOffset.UtcNow;

// Guaranteed to have atleast one available client since it is a application startup path.
IEnumerable<ConfigurationClient> availableClients = _configClientManager.GetAvailableClients(loadStartTime);

try
{
using var startupCancellationTokenSource = new CancellationTokenSource(_options.Startup.Timeout);

// Load() is invoked only once during application startup. We don't need to check for concurrent network
// operations here because there can't be any other startup or refresh operation in progress at this time.
InitializeAsync(_optional, availableClients, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult();
LoadAsync(_optional, startupCancellationTokenSource.Token).ConfigureAwait(false).GetAwaiter().GetResult();
}
catch (ArgumentException)
{
Expand Down Expand Up @@ -205,7 +202,8 @@ public async Task RefreshAsync(CancellationToken cancellationToken)
if (InitializationCacheExpires < utcNow)
{
InitializationCacheExpires = utcNow.Add(MinCacheExpirationInterval);
await InitializeAsync(ignoreFailures: false, availableClients, cancellationToken).ConfigureAwait(false);

await InitializeAsync(availableClients, cancellationToken).ConfigureAwait(false);
}

return;
Expand Down Expand Up @@ -548,42 +546,135 @@ private async Task<Dictionary<string, string>> PrepareData(Dictionary<string, Co
return applicationData;
}

private async Task InitializeAsync(bool ignoreFailures, IEnumerable<ConfigurationClient> availableClients, CancellationToken cancellationToken = default)
private async Task LoadAsync(bool ignoreFailures, CancellationToken cancellationToken)
{
Dictionary<string, ConfigurationSetting> data = null;
Dictionary<KeyValueIdentifier, ConfigurationSetting> watchedSettings = null;

var startupStopwatch = Stopwatch.StartNew();

int postFixedWindowAttempts = 0;

var startupExceptions = new List<Exception>();

try
{
await ExecuteWithFailOverPolicyAsync(
availableClients,
async (client) =>
while (true)
{
IEnumerable<ConfigurationClient> clients = _configClientManager.GetAllClients();

if (await TryInitializeAsync(clients, startupExceptions, cancellationToken).ConfigureAwait(false))
{
data = await LoadSelectedKeyValues(
client,
cancellationToken)
.ConfigureAwait(false);

watchedSettings = await LoadKeyValuesRegisteredForRefresh(
client,
data,
cancellationToken)
.ConfigureAwait(false);

watchedSettings = UpdateWatchedKeyValueCollections(watchedSettings, data);
},
cancellationToken)
.ConfigureAwait(false);
break;
}

TimeSpan delay;

if (startupStopwatch.Elapsed.TryGetFixedBackoff(out TimeSpan backoff))
{
delay = backoff;
}
else
{
postFixedWindowAttempts++;

delay = FailOverConstants.MinStartupBackoffDuration.CalculateBackoffDuration(
FailOverConstants.MaxBackoffDuration,
postFixedWindowAttempts);
}

try
{
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw new TimeoutException(
$"The provider timed out while attempting to load.",
new AggregateException(startupExceptions));
}
}
}
catch (Exception exception) when (
ignoreFailures &&
(exception is RequestFailedException ||
exception is KeyVaultReferenceException ||
exception is TimeoutException ||
exception is OperationCanceledException ||
exception is InvalidOperationException ||
((exception as AggregateException)?.InnerExceptions?.Any(e =>
e is RequestFailedException ||
e is OperationCanceledException) ?? false)))
{ }
}

private async Task<bool> TryInitializeAsync(IEnumerable<ConfigurationClient> clients, List<Exception> startupExceptions, CancellationToken cancellationToken = default)
{
try
{
await InitializeAsync(clients, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return false;
}
catch (RequestFailedException exception)
{
if (IsFailOverable(exception))
{
startupExceptions.Add(exception);

return false;
}

throw;
}
catch (AggregateException exception)
{
if (exception.InnerExceptions?.Any(e => e is OperationCanceledException) ?? false)
{
if (!cancellationToken.IsCancellationRequested)
{
startupExceptions.Add(exception);
}

return false;
}

if (IsFailOverable(exception))
{
startupExceptions.Add(exception);

return false;
}

throw;
}

return true;
}

private async Task InitializeAsync(IEnumerable<ConfigurationClient> clients, CancellationToken cancellationToken = default)
{
Dictionary<string, ConfigurationSetting> data = null;
Dictionary<KeyValueIdentifier, ConfigurationSetting> watchedSettings = null;

await ExecuteWithFailOverPolicyAsync(
clients,
async (client) =>
{
data = await LoadSelectedKeyValues(
client,
cancellationToken)
.ConfigureAwait(false);

watchedSettings = await LoadKeyValuesRegisteredForRefresh(
client,
data,
cancellationToken)
.ConfigureAwait(false);

watchedSettings = UpdateWatchedKeyValueCollections(watchedSettings, data);
},
cancellationToken)
.ConfigureAwait(false);

// Update the cache expiration time for all refresh registered settings and feature flags
foreach (KeyValueWatcher changeWatcher in _options.ChangeWatchers.Concat(_options.MultiKeyWatchers))
Expand All @@ -599,17 +690,10 @@ e is RequestFailedException ||
adapter.InvalidateCache();
}

try
{
Dictionary<string, ConfigurationSetting> mappedData = await MapConfigurationSettings(data).ConfigureAwait(false);
SetData(await PrepareData(mappedData, cancellationToken).ConfigureAwait(false));
_watchedSettings = watchedSettings;
_mappedData = mappedData;
}
catch (KeyVaultReferenceException) when (ignoreFailures)
{
// ignore failures
}
Dictionary<string, ConfigurationSetting> mappedData = await MapConfigurationSettings(data).ConfigureAwait(false);
SetData(await PrepareData(mappedData, cancellationToken).ConfigureAwait(false));
_watchedSettings = watchedSettings;
_mappedData = mappedData;
}
}

Expand Down Expand Up @@ -856,7 +940,10 @@ private void UpdateCacheExpirationTime(KeyValueWatcher changeWatcher)
changeWatcher.CacheExpires = DateTimeOffset.UtcNow.Add(cacheExpirationTime);
}

private async Task<T> ExecuteWithFailOverPolicyAsync<T>(IEnumerable<ConfigurationClient> clients, Func<ConfigurationClient, Task<T>> funcToExecute, CancellationToken cancellationToken = default)
private async Task<T> ExecuteWithFailOverPolicyAsync<T>(
IEnumerable<ConfigurationClient> clients,
Func<ConfigurationClient, Task<T>> funcToExecute,
CancellationToken cancellationToken = default)
{
using IEnumerator<ConfigurationClient> clientEnumerator = clients.GetEnumerator();

Expand Down Expand Up @@ -929,7 +1016,10 @@ private async Task<T> ExecuteWithFailOverPolicyAsync<T>(IEnumerable<Configuratio
}
}

private async Task ExecuteWithFailOverPolicyAsync(IEnumerable<ConfigurationClient> clients, Func<ConfigurationClient, Task> funcToExecute, CancellationToken cancellationToken = default)
private async Task ExecuteWithFailOverPolicyAsync(
IEnumerable<ConfigurationClient> clients,
Func<ConfigurationClient, Task> funcToExecute,
CancellationToken cancellationToken = default)
{
await ExecuteWithFailOverPolicyAsync<object>(clients, async (client) =>
{
Expand All @@ -948,18 +1038,28 @@ private bool IsFailOverable(AggregateException ex)

private bool IsFailOverable(RequestFailedException rfe)
{
if (rfe.Status == HttpStatusCodes.TooManyRequests ||
rfe.Status == (int)HttpStatusCode.RequestTimeout ||
rfe.Status >= (int)HttpStatusCode.InternalServerError)
{
return true;
}

Exception innerException;

// The InnerException could be SocketException or WebException when endpoint is invalid and IOException if it is network issue.
if (rfe.InnerException != null && rfe.InnerException is HttpRequestException hre && hre.InnerException != null)
if (rfe.InnerException is HttpRequestException hre)
{
innerException = hre.InnerException;
}
else
{
return hre.InnerException is WebException ||
hre.InnerException is SocketException ||
hre.InnerException is IOException;
innerException = rfe.InnerException;
}

return rfe.Status == HttpStatusCodes.TooManyRequests ||
rfe.Status == (int)HttpStatusCode.RequestTimeout ||
rfe.Status >= (int)HttpStatusCode.InternalServerError;
// The InnerException could be SocketException or WebException when an endpoint is invalid and IOException if it's a network issue.
return innerException is WebException ||
innerException is SocketException ||
innerException is IOException;
}

private async Task<Dictionary<string, ConfigurationSetting>> MapConfigurationSettings(Dictionary<string, ConfigurationSetting> data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@

using Azure.Core;
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Constants;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
Expand Down Expand Up @@ -62,6 +60,11 @@ public IEnumerable<ConfigurationClient> GetAvailableClients(DateTimeOffset time)
return _clients.Where(client => client.BackoffEndTime <= time).Select(c => c.Client).ToList();
}

public IEnumerable<ConfigurationClient> GetAllClients()
{
return _clients.Select(c => c.Client).ToList();
}

public void UpdateClientStatus(ConfigurationClient client, bool successful)
{
if (client == null)
Expand Down Expand Up @@ -96,7 +99,7 @@ public bool UpdateSyncToken(Uri endpoint, string syncToken)
throw new ArgumentNullException(nameof(syncToken));
}

ConfigurationClientWrapper clientWrapper = this._clients.SingleOrDefault(c => new EndpointComparer().Equals(c.Endpoint, endpoint));
ConfigurationClientWrapper clientWrapper = _clients.SingleOrDefault(c => new EndpointComparer().Equals(c.Endpoint, endpoint));

if (clientWrapper != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

using System;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Constants
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
internal class FailOverConstants
{
// Timeouts to retry requests to config stores and their replicas after failure.

public static readonly TimeSpan MinBackoffDuration = TimeSpan.FromSeconds(30);
public static readonly TimeSpan MaxBackoffDuration = TimeSpan.FromMinutes(10);
// Minimum backoff duration for retries that occur after the fixed backoff window during startup.
public static readonly TimeSpan MinStartupBackoffDuration = TimeSpan.FromSeconds(30);
}
}
Loading