diff --git a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
index a5d4dee4..fda406b9 100644
--- a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
+++ b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj
@@ -21,7 +21,7 @@
- 8.1.0-preview
+ 8.2.0-preview
diff --git a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
index 4354b77e..eb43831e 100644
--- a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
+++ b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj
@@ -24,7 +24,7 @@
- 8.1.0-preview
+ 8.2.0-preview
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationClientFactory.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationClientFactory.cs
new file mode 100644
index 00000000..6127822d
--- /dev/null
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationClientFactory.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Azure.Core;
+using Azure.Data.AppConfiguration;
+using Microsoft.Extensions.Azure;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
+{
+ internal class AzureAppConfigurationClientFactory : IAzureClientFactory
+ {
+ private readonly ConfigurationClientOptions _clientOptions;
+
+ private readonly TokenCredential _credential;
+ private readonly IEnumerable _connectionStrings;
+
+ public AzureAppConfigurationClientFactory(
+ IEnumerable connectionStrings,
+ ConfigurationClientOptions clientOptions)
+ {
+ if (connectionStrings == null || !connectionStrings.Any())
+ {
+ throw new ArgumentNullException(nameof(connectionStrings));
+ }
+
+ _connectionStrings = connectionStrings;
+
+ _clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions));
+ }
+
+ public AzureAppConfigurationClientFactory(
+ TokenCredential credential,
+ ConfigurationClientOptions clientOptions)
+ {
+ _credential = credential ?? throw new ArgumentNullException(nameof(credential));
+ _clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions));
+ }
+
+ public ConfigurationClient CreateClient(string endpoint)
+ {
+ if (string.IsNullOrEmpty(endpoint))
+ {
+ throw new ArgumentNullException(nameof(endpoint));
+ }
+
+ if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri uriResult))
+ {
+ throw new ArgumentException("Invalid host URI.");
+ }
+
+ if (_credential != null)
+ {
+ return new ConfigurationClient(uriResult, _credential, _clientOptions);
+ }
+
+ string connectionString = _connectionStrings.FirstOrDefault(cs => ConnectionStringUtils.Parse(cs, ConnectionStringUtils.EndpointSection) == endpoint);
+
+ //
+ // falback to the first connection string
+ if (connectionString == null)
+ {
+ string id = ConnectionStringUtils.Parse(_connectionStrings.First(), ConnectionStringUtils.IdSection);
+ string secret = ConnectionStringUtils.Parse(_connectionStrings.First(), ConnectionStringUtils.SecretSection);
+
+ connectionString = ConnectionStringUtils.Build(uriResult, id, secret);
+ }
+
+ return new ConfigurationClient(connectionString, _clientOptions);
+ }
+ }
+}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationExtensions.cs
index dc022bac..8b4bf8c8 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationExtensions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationExtensions.cs
@@ -33,7 +33,9 @@ private static bool IsProviderDisabled()
///
/// The configuration builder to add key-values to.
/// The connection string used to connect to the configuration store.
- /// Determines the behavior of the App Configuration provider when an exception occurs while loading data from server. If false, the exception is thrown. If true, the exception is suppressed and no settings are populated from Azure App Configuration.
+ /// Determines the behavior of the App Configuration provider when an exception occurs while loading data from server. If false, the exception is thrown. If true, the exception is suppressed and no settings are populated from Azure App Configuration.
+ /// will always be thrown when the caller gives an invalid input configuration (connection strings, endpoints, key/label filters...etc).
+ ///
/// The provided configuration builder.
public static IConfigurationBuilder AddAzureAppConfiguration(
this IConfigurationBuilder configurationBuilder,
@@ -48,7 +50,9 @@ public static IConfigurationBuilder AddAzureAppConfiguration(
///
/// The configuration builder to add key-values to.
/// The list of connection strings used to connect to the configuration store and its replicas.
- /// Determines the behavior of the App Configuration provider when an exception occurs while loading data from server. If false, the exception is thrown. If true, the exception is suppressed and no settings are populated from Azure App Configuration.
+ /// Determines the behavior of the App Configuration provider when an exception occurs while loading data from server. If false, the exception is thrown. If true, the exception is suppressed and no settings are populated from Azure App Configuration.
+ /// will always be thrown when the caller gives an invalid input configuration (connection strings, endpoints, key/label filters...etc).
+ ///
/// The provided configuration builder.
public static IConfigurationBuilder AddAzureAppConfiguration(
this IConfigurationBuilder configurationBuilder,
@@ -63,7 +67,9 @@ public static IConfigurationBuilder AddAzureAppConfiguration(
///
/// The configuration builder to add key-values to.
/// A callback used to configure Azure App Configuration options.
- /// Determines the behavior of the App Configuration provider when an exception occurs while loading data from server. If false, the exception is thrown. If true, the exception is suppressed and no settings are populated from Azure App Configuration.
+ /// Determines the behavior of the App Configuration provider when an exception occurs while loading data from server. If false, the exception is thrown. If true, the exception is suppressed and no settings are populated from Azure App Configuration.
+ /// will always be thrown when the caller gives an invalid input configuration (connection strings, endpoints, key/label filters...etc).
+ ///
/// The provided configuration builder.
public static IConfigurationBuilder AddAzureAppConfiguration(
this IConfigurationBuilder configurationBuilder,
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
index 7d9a9cad..804d5d88 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
@@ -2,7 +2,9 @@
// Licensed under the MIT license.
//
using Azure.Core;
+using Azure.Core.Pipeline;
using Azure.Data.AppConfiguration;
+using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
@@ -10,6 +12,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
@@ -22,13 +25,16 @@ public class AzureAppConfigurationOptions
{
private const int MaxRetries = 2;
private static readonly TimeSpan MaxRetryDelay = TimeSpan.FromMinutes(1);
+ private static readonly TimeSpan NetworkTimeout = TimeSpan.FromSeconds(10);
+ private static readonly KeyValueSelector DefaultQuery = new KeyValueSelector { KeyFilter = KeyFilter.Any, LabelFilter = LabelFilter.Null };
- private List _changeWatchers = new List();
- private List _multiKeyWatchers = new List();
+ private List _individualKvWatchers = new List();
+ private List _ffWatchers = new List();
private List _adapters;
private List>> _mappers = new List>>();
- private List _kvSelectors = new List();
+ private List _selectors;
private IConfigurationRefresher _refresher = new AzureAppConfigurationRefresher();
+ private bool _selectCalled = false;
// The following set is sorted in descending order.
// Since multiple prefixes could start with the same characters, we need to trim the longest prefix first.
@@ -62,19 +68,29 @@ public class AzureAppConfigurationOptions
internal TokenCredential Credential { get; private set; }
///
- /// A collection of .
+ /// A collection of specified by user.
///
- internal IEnumerable KeyValueSelectors => _kvSelectors;
+ internal IEnumerable Selectors => _selectors;
+
+ ///
+ /// Indicates if was called.
+ ///
+ internal bool RegisterAllEnabled { get; private set; }
+
+ ///
+ /// Refresh interval for selected key-value collections when is called.
+ ///
+ internal TimeSpan KvCollectionRefreshInterval { get; private set; }
///
/// A collection of .
///
- internal IEnumerable ChangeWatchers => _changeWatchers;
+ internal IEnumerable IndividualKvWatchers => _individualKvWatchers;
///
/// A collection of .
///
- internal IEnumerable MultiKeyWatchers => _multiKeyWatchers;
+ internal IEnumerable FeatureFlagWatchers => _ffWatchers;
///
/// A collection of .
@@ -96,11 +112,15 @@ internal IEnumerable Adapters
internal IEnumerable KeyPrefixes => _keyPrefixes;
///
- /// An optional configuration client manager that can be used to provide clients to communicate with Azure App Configuration.
+ /// For use in tests only. An optional configuration client manager that can be used to provide clients to communicate with Azure App Configuration.
///
- /// This property is used only for unit testing.
internal IConfigurationClientManager ClientManager { get; set; }
+ ///
+ /// For use in tests only. An optional class used to process pageable results from Azure App Configuration.
+ ///
+ internal IConfigurationSettingPageIterator ConfigurationSettingPageIterator { get; set; }
+
///
/// An optional timespan value to set the minimum backoff duration to a value other than the default.
///
@@ -131,6 +151,11 @@ internal IEnumerable Adapters
///
internal StartupOptions Startup { get; set; } = new StartupOptions();
+ ///
+ /// Client factory that is responsible for creating instances of ConfigurationClient.
+ ///
+ internal IAzureClientFactory ClientFactory { get; private set; }
+
///
/// Initializes a new instance of the class.
///
@@ -142,6 +167,20 @@ public AzureAppConfigurationOptions()
new JsonKeyValueAdapter(),
new FeatureManagementKeyValueAdapter(FeatureFlagTracing)
};
+
+ // Adds the default query to App Configuration if and are never called.
+ _selectors = new List { DefaultQuery };
+ }
+
+ ///
+ /// Sets the client factory used to create ConfigurationClient instances.
+ ///
+ /// The client factory.
+ /// The current instance.
+ public AzureAppConfigurationOptions SetClientFactory(IAzureClientFactory factory)
+ {
+ ClientFactory = factory ?? throw new ArgumentNullException(nameof(factory));
+ return this;
}
///
@@ -170,22 +209,30 @@ public AzureAppConfigurationOptions Select(string keyFilter, string labelFilter
throw new ArgumentNullException(nameof(keyFilter));
}
+ // Do not support * and , for label filter for now.
+ if (labelFilter != null && (labelFilter.Contains('*') || labelFilter.Contains(',')))
+ {
+ throw new ArgumentException("The characters '*' and ',' are not supported in label filters.", nameof(labelFilter));
+ }
+
if (string.IsNullOrWhiteSpace(labelFilter))
{
labelFilter = LabelFilter.Null;
}
- // Do not support * and , for label filter for now.
- if (labelFilter.Contains('*') || labelFilter.Contains(','))
+ if (!_selectCalled)
{
- throw new ArgumentException("The characters '*' and ',' are not supported in label filters.", nameof(labelFilter));
+ _selectors.Remove(DefaultQuery);
+
+ _selectCalled = true;
}
- _kvSelectors.AppendUnique(new KeyValueSelector
+ _selectors.AppendUnique(new KeyValueSelector
{
KeyFilter = keyFilter,
LabelFilter = labelFilter
});
+
return this;
}
@@ -201,7 +248,14 @@ public AzureAppConfigurationOptions SelectSnapshot(string name)
throw new ArgumentNullException(nameof(name));
}
- _kvSelectors.AppendUnique(new KeyValueSelector
+ if (!_selectCalled)
+ {
+ _selectors.Remove(DefaultQuery);
+
+ _selectCalled = true;
+ }
+
+ _selectors.AppendUnique(new KeyValueSelector
{
SnapshotName = name
});
@@ -212,7 +266,7 @@ public AzureAppConfigurationOptions SelectSnapshot(string name)
///
/// Configures options for Azure App Configuration feature flags that will be parsed and transformed into feature management configuration.
/// If no filtering is specified via the then all feature flags with no label are loaded.
- /// All loaded feature flags will be automatically registered for refresh on an individual flag level.
+ /// All loaded feature flags will be automatically registered for refresh as a collection.
///
/// A callback used to configure feature flag options.
public AzureAppConfigurationOptions UseFeatureFlags(Action configure = null)
@@ -237,25 +291,22 @@ public AzureAppConfigurationOptions UseFeatureFlags(Action c
options.FeatureFlagSelectors.Add(new KeyValueSelector
{
KeyFilter = FeatureManagementConstants.FeatureFlagMarker + "*",
- LabelFilter = options.Label == null ? LabelFilter.Null : options.Label
+ LabelFilter = string.IsNullOrWhiteSpace(options.Label) ? LabelFilter.Null : options.Label,
+ IsFeatureFlagSelector = true
});
}
- foreach (var featureFlagSelector in options.FeatureFlagSelectors)
+ foreach (KeyValueSelector featureFlagSelector in options.FeatureFlagSelectors)
{
- var featureFlagFilter = featureFlagSelector.KeyFilter;
- var labelFilter = featureFlagSelector.LabelFilter;
+ _selectors.AppendUnique(featureFlagSelector);
- Select(featureFlagFilter, labelFilter);
-
- _multiKeyWatchers.AppendUnique(new KeyValueWatcher
+ _ffWatchers.AppendUnique(new KeyValueWatcher
{
- Key = featureFlagFilter,
- Label = labelFilter,
+ Key = featureFlagSelector.KeyFilter,
+ Label = featureFlagSelector.LabelFilter,
// If UseFeatureFlags is called multiple times for the same key and label filters, last refresh interval wins
RefreshInterval = options.RefreshInterval
});
-
}
return this;
@@ -376,18 +427,41 @@ public AzureAppConfigurationOptions ConfigureClientOptions(ActionA callback used to configure Azure App Configuration refresh options.
public AzureAppConfigurationOptions ConfigureRefresh(Action configure)
{
+ if (RegisterAllEnabled)
+ {
+ throw new InvalidOperationException($"{nameof(ConfigureRefresh)}() cannot be invoked multiple times when {nameof(AzureAppConfigurationRefreshOptions.RegisterAll)} has been invoked.");
+ }
+
var refreshOptions = new AzureAppConfigurationRefreshOptions();
configure?.Invoke(refreshOptions);
- if (!refreshOptions.RefreshRegistrations.Any())
+ bool isRegisterCalled = refreshOptions.RefreshRegistrations.Any();
+ RegisterAllEnabled = refreshOptions.RegisterAllEnabled;
+
+ if (!isRegisterCalled && !RegisterAllEnabled)
{
- throw new ArgumentException($"{nameof(ConfigureRefresh)}() must have at least one key-value registered for refresh.");
+ throw new InvalidOperationException($"{nameof(ConfigureRefresh)}() must call either {nameof(AzureAppConfigurationRefreshOptions.Register)}()" +
+ $" or {nameof(AzureAppConfigurationRefreshOptions.RegisterAll)}()");
}
- foreach (var item in refreshOptions.RefreshRegistrations)
+ // Check if both register methods are called at any point
+ if (RegisterAllEnabled && (_individualKvWatchers.Any() || isRegisterCalled))
{
- item.RefreshInterval = refreshOptions.RefreshInterval;
- _changeWatchers.Add(item);
+ throw new InvalidOperationException($"Cannot call both {nameof(AzureAppConfigurationRefreshOptions.RegisterAll)} and "
+ + $"{nameof(AzureAppConfigurationRefreshOptions.Register)}.");
+ }
+
+ if (RegisterAllEnabled)
+ {
+ KvCollectionRefreshInterval = refreshOptions.RefreshInterval;
+ }
+ else
+ {
+ foreach (KeyValueWatcher item in refreshOptions.RefreshRegistrations)
+ {
+ item.RefreshInterval = refreshOptions.RefreshInterval;
+ _individualKvWatchers.Add(item);
+ }
}
return this;
@@ -456,6 +530,10 @@ private static ConfigurationClientOptions GetDefaultClientOptions()
clientOptions.Retry.MaxDelay = MaxRetryDelay;
clientOptions.Retry.Mode = RetryMode.Exponential;
clientOptions.AddPolicy(new UserAgentHeaderPolicy(), HttpPipelinePosition.PerCall);
+ clientOptions.Transport = new HttpClientTransport(new HttpClient()
+ {
+ Timeout = NetworkTimeout
+ });
return clientOptions;
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
index b5dd42f1..795aeb10 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
@@ -4,7 +4,6 @@
using Azure;
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
-using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models;
using Microsoft.Extensions.Logging;
using System;
@@ -32,9 +31,13 @@ internal class AzureAppConfigurationProvider : ConfigurationProvider, IConfigura
private Uri _lastSuccessfulEndpoint;
private AzureAppConfigurationOptions _options;
private Dictionary _mappedData;
- private Dictionary _watchedSettings = new Dictionary();
+ private Dictionary _watchedIndividualKvs = new Dictionary();
+ private HashSet _ffKeys = new HashSet();
+ private Dictionary> _kvEtags = new Dictionary>();
+ private Dictionary> _ffEtags = new Dictionary>();
private RequestTracingOptions _requestTracingOptions;
private Dictionary _configClientBackoffs = new Dictionary();
+ private DateTimeOffset _nextCollectionRefreshTime;
private readonly TimeSpan MinRefreshInterval;
@@ -108,11 +111,25 @@ public AzureAppConfigurationProvider(IConfigurationClientManager configClientMan
_options = options ?? throw new ArgumentNullException(nameof(options));
_optional = optional;
- IEnumerable watchers = options.ChangeWatchers.Union(options.MultiKeyWatchers);
+ IEnumerable watchers = options.IndividualKvWatchers.Union(options.FeatureFlagWatchers);
- if (watchers.Any())
+ bool hasWatchers = watchers.Any();
+ TimeSpan minWatcherRefreshInterval = hasWatchers ? watchers.Min(w => w.RefreshInterval) : TimeSpan.MaxValue;
+
+ if (options.RegisterAllEnabled)
+ {
+ if (options.KvCollectionRefreshInterval <= TimeSpan.Zero)
+ {
+ throw new ArgumentException(
+ $"{nameof(options.KvCollectionRefreshInterval)} must be greater than zero seconds when using RegisterAll for refresh",
+ nameof(options));
+ }
+
+ MinRefreshInterval = TimeSpan.FromTicks(Math.Min(minWatcherRefreshInterval.Ticks, options.KvCollectionRefreshInterval.Ticks));
+ }
+ else if (hasWatchers)
{
- MinRefreshInterval = watchers.Min(w => w.RefreshInterval);
+ MinRefreshInterval = minWatcherRefreshInterval;
}
else
{
@@ -194,13 +211,15 @@ public async Task RefreshAsync(CancellationToken cancellationToken)
EnsureAssemblyInspected();
var utcNow = DateTimeOffset.UtcNow;
- IEnumerable refreshableWatchers = _options.ChangeWatchers.Where(changeWatcher => utcNow >= changeWatcher.NextRefreshTime);
- IEnumerable refreshableMultiKeyWatchers = _options.MultiKeyWatchers.Where(changeWatcher => utcNow >= changeWatcher.NextRefreshTime);
+ IEnumerable refreshableIndividualKvWatchers = _options.IndividualKvWatchers.Where(kvWatcher => utcNow >= kvWatcher.NextRefreshTime);
+ IEnumerable refreshableFfWatchers = _options.FeatureFlagWatchers.Where(ffWatcher => utcNow >= ffWatcher.NextRefreshTime);
+ bool isRefreshDue = _options.RegisterAllEnabled && utcNow >= _nextCollectionRefreshTime;
// Skip refresh if mappedData is loaded, but none of the watchers or adapters are refreshable.
if (_mappedData != null &&
- !refreshableWatchers.Any() &&
- !refreshableMultiKeyWatchers.Any() &&
+ !refreshableIndividualKvWatchers.Any() &&
+ !refreshableFfWatchers.Any() &&
+ !isRefreshDue &&
!_options.Adapters.Any(adapter => adapter.NeedsRefresh()))
{
return;
@@ -208,6 +227,11 @@ public async Task RefreshAsync(CancellationToken cancellationToken)
IEnumerable clients = _configClientManager.GetClients();
+ if (_requestTracingOptions != null)
+ {
+ _requestTracingOptions.ReplicaCount = clients.Count() - 1;
+ }
+
//
// Filter clients based on their backoff status
clients = clients.Where(client =>
@@ -249,179 +273,166 @@ public async Task RefreshAsync(CancellationToken cancellationToken)
//
// Avoid instance state modification
- Dictionary watchedSettings = null;
+ Dictionary> kvEtags = null;
+ Dictionary> ffEtags = null;
+ HashSet ffKeys = null;
+ Dictionary watchedIndividualKvs = null;
List keyValueChanges = null;
- List changedKeyValuesCollection = null;
Dictionary data = null;
+ Dictionary ffCollectionData = null;
+ bool ffCollectionUpdated = false;
bool refreshAll = false;
StringBuilder logInfoBuilder = new StringBuilder();
StringBuilder logDebugBuilder = new StringBuilder();
await ExecuteWithFailOverPolicyAsync(clients, async (client) =>
- {
- data = null;
- watchedSettings = null;
- keyValueChanges = new List();
- changedKeyValuesCollection = null;
- refreshAll = false;
- Uri endpoint = _configClientManager.GetEndpointForClient(client);
- logDebugBuilder.Clear();
- logInfoBuilder.Clear();
-
- foreach (KeyValueWatcher changeWatcher in refreshableWatchers)
- {
- string watchedKey = changeWatcher.Key;
- string watchedLabel = changeWatcher.Label;
-
- KeyValueIdentifier watchedKeyLabel = new KeyValueIdentifier(watchedKey, watchedLabel);
-
- KeyValueChange change = default;
-
- //
- // Find if there is a change associated with watcher
- if (_watchedSettings.TryGetValue(watchedKeyLabel, out ConfigurationSetting watchedKv))
- {
- await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions,
- async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
- }
- else
- {
- // Load the key-value in case the previous load attempts had failed
-
- try
- {
- await CallWithRequestTracing(
- async () => watchedKv = await client.GetConfigurationSettingAsync(watchedKey, watchedLabel, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
- }
- catch (RequestFailedException e) when (e.Status == (int)HttpStatusCode.NotFound)
- {
- watchedKv = null;
- }
-
- if (watchedKv != null)
- {
- change = new KeyValueChange()
- {
- Key = watchedKv.Key,
- Label = watchedKv.Label.NormalizeNull(),
- Current = watchedKv,
- ChangeType = KeyValueChangeType.Modified
- };
- }
- }
-
- // Check if a change has been detected in the key-value registered for refresh
- if (change.ChangeType != KeyValueChangeType.None)
- {
- logDebugBuilder.AppendLine(LogHelper.BuildKeyValueReadMessage(change.ChangeType, change.Key, change.Label, endpoint.ToString()));
- logInfoBuilder.AppendLine(LogHelper.BuildKeyValueSettingUpdatedMessage(change.Key));
- keyValueChanges.Add(change);
-
- if (changeWatcher.RefreshAll)
- {
- refreshAll = true;
- break;
- }
- }
- else
- {
- logDebugBuilder.AppendLine(LogHelper.BuildKeyValueReadMessage(change.ChangeType, change.Key, change.Label, endpoint.ToString()));
- }
- }
+ {
+ kvEtags = null;
+ ffEtags = null;
+ ffKeys = null;
+ watchedIndividualKvs = null;
+ keyValueChanges = new List();
+ data = null;
+ ffCollectionData = null;
+ ffCollectionUpdated = false;
+ refreshAll = false;
+ logDebugBuilder.Clear();
+ logInfoBuilder.Clear();
+ Uri endpoint = _configClientManager.GetEndpointForClient(client);
- if (refreshAll)
+ if (_options.RegisterAllEnabled)
+ {
+ // Get key value collection changes if RegisterAll was called
+ if (isRefreshDue)
{
- // Trigger a single load-all operation if a change was detected in one or more key-values with refreshAll: true
- data = await LoadSelectedKeyValues(client, cancellationToken).ConfigureAwait(false);
- watchedSettings = await LoadKeyValuesRegisteredForRefresh(client, data, cancellationToken).ConfigureAwait(false);
- watchedSettings = UpdateWatchedKeyValueCollections(watchedSettings, data);
- logInfoBuilder.AppendLine(LogHelper.BuildConfigurationUpdatedMessage());
- return;
+ refreshAll = await HaveCollectionsChanged(
+ _options.Selectors.Where(selector => !selector.IsFeatureFlagSelector),
+ _kvEtags,
+ client,
+ cancellationToken).ConfigureAwait(false);
}
+ }
+ else
+ {
+ refreshAll = await RefreshIndividualKvWatchers(
+ client,
+ keyValueChanges,
+ refreshableIndividualKvWatchers,
+ endpoint,
+ logDebugBuilder,
+ logInfoBuilder,
+ cancellationToken).ConfigureAwait(false);
+ }
- changedKeyValuesCollection = await GetRefreshedKeyValueCollections(refreshableMultiKeyWatchers, client, logDebugBuilder, logInfoBuilder, endpoint, cancellationToken).ConfigureAwait(false);
+ if (refreshAll)
+ {
+ // Trigger a single load-all operation if a change was detected in one or more key-values with refreshAll: true,
+ // or if any key-value collection change was detected.
+ kvEtags = new Dictionary>();
+ ffEtags = new Dictionary>();
+ ffKeys = new HashSet();
+
+ data = await LoadSelected(client, kvEtags, ffEtags, _options.Selectors, ffKeys, cancellationToken).ConfigureAwait(false);
+ watchedIndividualKvs = await LoadKeyValuesRegisteredForRefresh(client, data, cancellationToken).ConfigureAwait(false);
+ logInfoBuilder.AppendLine(LogHelper.BuildConfigurationUpdatedMessage());
+ return;
+ }
- if (!changedKeyValuesCollection.Any())
+ // Get feature flag changes
+ ffCollectionUpdated = await HaveCollectionsChanged(
+ refreshableFfWatchers.Select(watcher => new KeyValueSelector
{
- logDebugBuilder.AppendLine(LogHelper.BuildFeatureFlagsUnchangedMessage(endpoint.ToString()));
- }
- },
- cancellationToken)
- .ConfigureAwait(false);
+ KeyFilter = watcher.Key,
+ LabelFilter = watcher.Label,
+ IsFeatureFlagSelector = true
+ }),
+ _ffEtags,
+ client,
+ cancellationToken).ConfigureAwait(false);
+
+ if (ffCollectionUpdated)
+ {
+ ffEtags = new Dictionary>();
+ ffKeys = new HashSet();
+
+ ffCollectionData = await LoadSelected(
+ client,
+ new Dictionary>(),
+ ffEtags,
+ _options.Selectors.Where(selector => selector.IsFeatureFlagSelector),
+ ffKeys,
+ cancellationToken).ConfigureAwait(false);
+
+ logInfoBuilder.Append(LogHelper.BuildFeatureFlagsUpdatedMessage());
+ }
+ else
+ {
+ logDebugBuilder.AppendLine(LogHelper.BuildFeatureFlagsUnchangedMessage(endpoint.ToString()));
+ }
+ },
+ cancellationToken)
+ .ConfigureAwait(false);
- if (!refreshAll)
+ if (refreshAll)
{
- watchedSettings = new Dictionary(_watchedSettings);
+ _mappedData = await MapConfigurationSettings(data).ConfigureAwait(false);
- foreach (KeyValueWatcher changeWatcher in refreshableWatchers.Concat(refreshableMultiKeyWatchers))
+ // Invalidate all the cached KeyVault secrets
+ foreach (IKeyValueAdapter adapter in _options.Adapters)
{
- UpdateNextRefreshTime(changeWatcher);
+ adapter.OnChangeDetected();
}
- foreach (KeyValueChange change in keyValueChanges.Concat(changedKeyValuesCollection))
+ // Update the next refresh time for all refresh registered settings and feature flags
+ foreach (KeyValueWatcher changeWatcher in _options.IndividualKvWatchers.Concat(_options.FeatureFlagWatchers))
{
- KeyValueIdentifier changeIdentifier = new KeyValueIdentifier(change.Key, change.Label);
- if (change.ChangeType == KeyValueChangeType.Modified)
- {
- ConfigurationSetting setting = change.Current;
- ConfigurationSetting settingCopy = new ConfigurationSetting(setting.Key, setting.Value, setting.Label, setting.ETag);
- watchedSettings[changeIdentifier] = settingCopy;
+ UpdateNextRefreshTime(changeWatcher);
+ }
+ }
+ else
+ {
+ watchedIndividualKvs = new Dictionary(_watchedIndividualKvs);
- foreach (Func> func in _options.Mappers)
- {
- setting = await func(setting).ConfigureAwait(false);
- }
+ await ProcessKeyValueChangesAsync(keyValueChanges, _mappedData, watchedIndividualKvs).ConfigureAwait(false);
- if (setting == null)
- {
- _mappedData.Remove(change.Key);
- }
- else
- {
- _mappedData[change.Key] = setting;
- }
- }
- else if (change.ChangeType == KeyValueChangeType.Deleted)
+ if (ffCollectionUpdated)
+ {
+ // Remove all feature flag keys that are not present in the latest loading of feature flags, but were loaded previously
+ foreach (string key in _ffKeys.Except(ffKeys))
{
- _mappedData.Remove(change.Key);
- watchedSettings.Remove(changeIdentifier);
+ _mappedData.Remove(key);
}
- // Invalidate the cached Key Vault secret (if any) for this ConfigurationSetting
- foreach (IKeyValueAdapter adapter in _options.Adapters)
+ Dictionary mappedFfData = await MapConfigurationSettings(ffCollectionData).ConfigureAwait(false);
+
+ foreach (KeyValuePair kvp in mappedFfData)
{
- // If the current setting is null, try to pass the previous setting instead
- if (change.Current != null)
- {
- adapter.OnChangeDetected(change.Current);
- }
- else if (change.Previous != null)
- {
- adapter.OnChangeDetected(change.Previous);
- }
+ _mappedData[kvp.Key] = kvp.Value;
}
}
- }
- else
- {
- _mappedData = await MapConfigurationSettings(data).ConfigureAwait(false);
-
- // Invalidate all the cached KeyVault secrets
- foreach (IKeyValueAdapter adapter in _options.Adapters)
- {
- adapter.OnChangeDetected();
- }
- // Update the next refresh time for all refresh registered settings and feature flags
- foreach (KeyValueWatcher changeWatcher in _options.ChangeWatchers.Concat(_options.MultiKeyWatchers))
+ //
+ // update the next refresh time for all refresh registered settings and feature flags
+ foreach (KeyValueWatcher changeWatcher in refreshableIndividualKvWatchers.Concat(refreshableFfWatchers))
{
UpdateNextRefreshTime(changeWatcher);
}
}
- if (_options.Adapters.Any(adapter => adapter.NeedsRefresh()) || changedKeyValuesCollection?.Any() == true || keyValueChanges.Any())
+ if (_options.RegisterAllEnabled && isRefreshDue)
+ {
+ _nextCollectionRefreshTime = DateTimeOffset.UtcNow.Add(_options.KvCollectionRefreshInterval);
+ }
+
+ if (_options.Adapters.Any(adapter => adapter.NeedsRefresh()) || keyValueChanges.Any() || refreshAll || ffCollectionUpdated)
{
- _watchedSettings = watchedSettings;
+ _watchedIndividualKvs = watchedIndividualKvs ?? _watchedIndividualKvs;
+
+ _ffEtags = ffEtags ?? _ffEtags;
+
+ _kvEtags = kvEtags ?? _kvEtags;
+
+ _ffKeys = ffKeys ?? _ffKeys;
if (logDebugBuilder.Length > 0)
{
@@ -432,6 +443,7 @@ await CallWithRequestTracing(
{
_logger.LogInformation(logInfoBuilder.ToString().Trim());
}
+
// PrepareData makes calls to KeyVault and may throw exceptions. But, we still update watchers before
// SetData because repeating appconfig calls (by not updating watchers) won't help anything for keyvault calls.
// As long as adapter.NeedsRefresh is true, we will attempt to update keyvault again the next time RefreshAsync is called.
@@ -543,6 +555,11 @@ public void ProcessPushNotification(PushNotification pushNotification, TimeSpan?
if (_configClientManager.UpdateSyncToken(pushNotification.ResourceUri, pushNotification.SyncToken))
{
+ if (_requestTracingEnabled && _requestTracingOptions != null)
+ {
+ _requestTracingOptions.IsPushRefreshUsed = true;
+ }
+
SetDirty(maxDelay);
}
else
@@ -555,14 +572,21 @@ private void SetDirty(TimeSpan? maxDelay)
{
DateTimeOffset nextRefreshTime = AddRandomDelay(DateTimeOffset.UtcNow, maxDelay ?? DefaultMaxSetDirtyDelay);
- foreach (KeyValueWatcher changeWatcher in _options.ChangeWatchers)
+ if (_options.RegisterAllEnabled)
{
- changeWatcher.NextRefreshTime = nextRefreshTime;
+ _nextCollectionRefreshTime = nextRefreshTime;
+ }
+ else
+ {
+ foreach (KeyValueWatcher kvWatcher in _options.IndividualKvWatchers)
+ {
+ kvWatcher.NextRefreshTime = nextRefreshTime;
+ }
}
- foreach (KeyValueWatcher changeWatcher in _options.MultiKeyWatchers)
+ foreach (KeyValueWatcher featureFlagWatcher in _options.FeatureFlagWatchers)
{
- changeWatcher.NextRefreshTime = nextRefreshTime;
+ featureFlagWatcher.NextRefreshTime = nextRefreshTime;
}
}
@@ -612,6 +636,11 @@ private async Task LoadAsync(bool ignoreFailures, CancellationToken cancellation
{
IEnumerable clients = _configClientManager.GetClients();
+ if (_requestTracingOptions != null)
+ {
+ _requestTracingOptions.ReplicaCount = clients.Count() - 1;
+ }
+
if (await TryInitializeAsync(clients, startupExceptions, cancellationToken).ConfigureAwait(false))
{
break;
@@ -707,34 +736,44 @@ private async Task TryInitializeAsync(IEnumerable cli
private async Task InitializeAsync(IEnumerable clients, CancellationToken cancellationToken = default)
{
Dictionary data = null;
- Dictionary watchedSettings = null;
+ Dictionary> kvEtags = new Dictionary>();
+ Dictionary> ffEtags = new Dictionary>();
+ Dictionary watchedIndividualKvs = null;
+ HashSet ffKeys = new HashSet();
await ExecuteWithFailOverPolicyAsync(
clients,
async (client) =>
{
- data = await LoadSelectedKeyValues(
+ data = await LoadSelected(
client,
+ kvEtags,
+ ffEtags,
+ _options.Selectors,
+ ffKeys,
cancellationToken)
.ConfigureAwait(false);
- watchedSettings = await LoadKeyValuesRegisteredForRefresh(
+ watchedIndividualKvs = await LoadKeyValuesRegisteredForRefresh(
client,
data,
cancellationToken)
.ConfigureAwait(false);
-
- watchedSettings = UpdateWatchedKeyValueCollections(watchedSettings, data);
},
cancellationToken)
.ConfigureAwait(false);
// Update the next refresh time for all refresh registered settings and feature flags
- foreach (KeyValueWatcher changeWatcher in _options.ChangeWatchers.Concat(_options.MultiKeyWatchers))
+ foreach (KeyValueWatcher changeWatcher in _options.IndividualKvWatchers.Concat(_options.FeatureFlagWatchers))
{
UpdateNextRefreshTime(changeWatcher);
}
+ if (_options.RegisterAllEnabled)
+ {
+ _nextCollectionRefreshTime = DateTimeOffset.UtcNow.Add(_options.KvCollectionRefreshInterval);
+ }
+
if (data != null)
{
// Invalidate all the cached KeyVault secrets
@@ -744,51 +783,71 @@ await ExecuteWithFailOverPolicyAsync(
}
Dictionary mappedData = await MapConfigurationSettings(data).ConfigureAwait(false);
+
SetData(await PrepareData(mappedData, cancellationToken).ConfigureAwait(false));
- _watchedSettings = watchedSettings;
+
_mappedData = mappedData;
+ _kvEtags = kvEtags;
+ _ffEtags = ffEtags;
+ _watchedIndividualKvs = watchedIndividualKvs;
+ _ffKeys = ffKeys;
}
}
- private async Task> LoadSelectedKeyValues(ConfigurationClient client, CancellationToken cancellationToken)
+ private async Task> LoadSelected(
+ ConfigurationClient client,
+ Dictionary> kvEtags,
+ Dictionary> ffEtags,
+ IEnumerable selectors,
+ HashSet ffKeys,
+ CancellationToken cancellationToken)
{
- var serverData = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- // Use default query if there are no key-values specified for use other than the feature flags
- bool useDefaultQuery = !_options.KeyValueSelectors.Any(selector => selector.KeyFilter == null ||
- !selector.KeyFilter.StartsWith(FeatureManagementConstants.FeatureFlagMarker));
+ Dictionary data = new Dictionary();
- if (useDefaultQuery)
+ foreach (KeyValueSelector loadOption in selectors)
{
- // Load all key-values with the null label.
- var selector = new SettingSelector
- {
- KeyFilter = KeyFilter.Any,
- LabelFilter = LabelFilter.Null
- };
-
- await CallWithRequestTracing(async () =>
+ if (string.IsNullOrEmpty(loadOption.SnapshotName))
{
- await foreach (ConfigurationSetting setting in client.GetConfigurationSettingsAsync(selector, cancellationToken).ConfigureAwait(false))
+ var selector = new SettingSelector()
{
- serverData[setting.Key] = setting;
- }
- }).ConfigureAwait(false);
- }
+ KeyFilter = loadOption.KeyFilter,
+ LabelFilter = loadOption.LabelFilter
+ };
- foreach (KeyValueSelector loadOption in _options.KeyValueSelectors)
- {
- IAsyncEnumerable settingsEnumerable;
+ var matchConditions = new List();
- if (string.IsNullOrEmpty(loadOption.SnapshotName))
- {
- settingsEnumerable = client.GetConfigurationSettingsAsync(
- new SettingSelector
+ await CallWithRequestTracing(async () =>
+ {
+ AsyncPageable pageableSettings = client.GetConfigurationSettingsAsync(selector, cancellationToken);
+
+ await foreach (Page page in pageableSettings.AsPages(_options.ConfigurationSettingPageIterator).ConfigureAwait(false))
{
- KeyFilter = loadOption.KeyFilter,
- LabelFilter = loadOption.LabelFilter
- },
- cancellationToken);
+ using Response response = page.GetRawResponse();
+
+ foreach (ConfigurationSetting setting in page.Values)
+ {
+ data[setting.Key] = setting;
+
+ if (loadOption.IsFeatureFlagSelector)
+ {
+ ffKeys.Add(setting.Key);
+ }
+ }
+
+ // The ETag will never be null here because it's not a conditional request
+ // Each successful response should have 200 status code and an ETag
+ matchConditions.Add(new MatchConditions { IfNoneMatch = response.Headers.ETag });
+ }
+ }).ConfigureAwait(false);
+
+ if (loadOption.IsFeatureFlagSelector)
+ {
+ ffEtags[loadOption] = matchConditions;
+ }
+ else
+ {
+ kvEtags[loadOption] = matchConditions;
+ }
}
else
{
@@ -808,38 +867,42 @@ await CallWithRequestTracing(async () =>
throw new InvalidOperationException($"{nameof(snapshot.SnapshotComposition)} for the selected snapshot with name '{snapshot.Name}' must be 'key', found '{snapshot.SnapshotComposition}'.");
}
- settingsEnumerable = client.GetConfigurationSettingsForSnapshotAsync(
+ IAsyncEnumerable settingsEnumerable = client.GetConfigurationSettingsForSnapshotAsync(
loadOption.SnapshotName,
cancellationToken);
- }
- await CallWithRequestTracing(async () =>
- {
- await foreach (ConfigurationSetting setting in settingsEnumerable.ConfigureAwait(false))
+ await CallWithRequestTracing(async () =>
{
- serverData[setting.Key] = setting;
- }
- }).ConfigureAwait(false);
+ await foreach (ConfigurationSetting setting in settingsEnumerable.ConfigureAwait(false))
+ {
+ data[setting.Key] = setting;
+ }
+ }).ConfigureAwait(false);
+ }
}
- return serverData;
+ return data;
}
- private async Task> LoadKeyValuesRegisteredForRefresh(ConfigurationClient client, IDictionary existingSettings, CancellationToken cancellationToken)
+ private async Task> LoadKeyValuesRegisteredForRefresh(
+ ConfigurationClient client,
+ IDictionary existingSettings,
+ CancellationToken cancellationToken)
{
- Dictionary watchedSettings = new Dictionary();
+ var watchedIndividualKvs = new Dictionary();
- foreach (KeyValueWatcher changeWatcher in _options.ChangeWatchers)
+ foreach (KeyValueWatcher kvWatcher in _options.IndividualKvWatchers)
{
- string watchedKey = changeWatcher.Key;
- string watchedLabel = changeWatcher.Label;
+ string watchedKey = kvWatcher.Key;
+ string watchedLabel = kvWatcher.Label;
+
KeyValueIdentifier watchedKeyLabel = new KeyValueIdentifier(watchedKey, watchedLabel);
// Skip the loading for the key-value in case it has already been loaded
if (existingSettings.TryGetValue(watchedKey, out ConfigurationSetting loadedKv)
&& watchedKeyLabel.Equals(new KeyValueIdentifier(loadedKv.Key, loadedKv.Label)))
{
- watchedSettings[watchedKeyLabel] = new ConfigurationSetting(loadedKv.Key, loadedKv.Value, loadedKv.Label, loadedKv.ETag);
+ watchedIndividualKvs[watchedKeyLabel] = new ConfigurationSetting(loadedKv.Key, loadedKv.Value, loadedKv.Label, loadedKv.ETag);
continue;
}
@@ -857,61 +920,84 @@ private async Task> LoadKey
// If the key-value was found, store it for updating the settings
if (watchedKv != null)
{
- watchedSettings[watchedKeyLabel] = new ConfigurationSetting(watchedKv.Key, watchedKv.Value, watchedKv.Label, watchedKv.ETag);
+ watchedIndividualKvs[watchedKeyLabel] = new ConfigurationSetting(watchedKv.Key, watchedKv.Value, watchedKv.Label, watchedKv.ETag);
existingSettings[watchedKey] = watchedKv;
}
}
- return watchedSettings;
+ return watchedIndividualKvs;
}
- private Dictionary UpdateWatchedKeyValueCollections(Dictionary watchedSettings, IDictionary existingSettings)
+ private async Task RefreshIndividualKvWatchers(
+ ConfigurationClient client,
+ List keyValueChanges,
+ IEnumerable refreshableIndividualKvWatchers,
+ Uri endpoint,
+ StringBuilder logDebugBuilder,
+ StringBuilder logInfoBuilder,
+ CancellationToken cancellationToken)
{
- foreach (KeyValueWatcher changeWatcher in _options.MultiKeyWatchers)
+ foreach (KeyValueWatcher kvWatcher in refreshableIndividualKvWatchers)
{
- IEnumerable currentKeyValues = GetCurrentKeyValueCollection(changeWatcher.Key, changeWatcher.Label, existingSettings.Values);
+ string watchedKey = kvWatcher.Key;
+ string watchedLabel = kvWatcher.Label;
+
+ KeyValueIdentifier watchedKeyLabel = new KeyValueIdentifier(watchedKey, watchedLabel);
+
+ KeyValueChange change = default;
- foreach (ConfigurationSetting setting in currentKeyValues)
+ //
+ // Find if there is a change associated with watcher
+ if (_watchedIndividualKvs.TryGetValue(watchedKeyLabel, out ConfigurationSetting watchedKv))
{
- watchedSettings[new KeyValueIdentifier(setting.Key, setting.Label)] = new ConfigurationSetting(setting.Key, setting.Value, setting.Label, setting.ETag);
+ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions,
+ async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
}
- }
+ else
+ {
+ // Load the key-value in case the previous load attempts had failed
- return watchedSettings;
- }
+ try
+ {
+ await CallWithRequestTracing(
+ async () => watchedKv = await client.GetConfigurationSettingAsync(watchedKey, watchedLabel, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
+ }
+ catch (RequestFailedException e) when (e.Status == (int)HttpStatusCode.NotFound)
+ {
+ watchedKv = null;
+ }
- private async Task> GetRefreshedKeyValueCollections(
- IEnumerable multiKeyWatchers,
- ConfigurationClient client,
- StringBuilder logDebugBuilder,
- StringBuilder logInfoBuilder,
- Uri endpoint,
- CancellationToken cancellationToken)
- {
- var keyValueChanges = new List();
+ if (watchedKv != null)
+ {
+ change = new KeyValueChange()
+ {
+ Key = watchedKv.Key,
+ Label = watchedKv.Label.NormalizeNull(),
+ Current = watchedKv,
+ ChangeType = KeyValueChangeType.Modified
+ };
+ }
+ }
- foreach (KeyValueWatcher changeWatcher in multiKeyWatchers)
- {
- IEnumerable currentKeyValues = GetCurrentKeyValueCollection(changeWatcher.Key, changeWatcher.Label, _watchedSettings.Values);
+ // Check if a change has been detected in the key-value registered for refresh
+ if (change.ChangeType != KeyValueChangeType.None)
+ {
+ logDebugBuilder.AppendLine(LogHelper.BuildKeyValueReadMessage(change.ChangeType, change.Key, change.Label, endpoint.ToString()));
+ logInfoBuilder.AppendLine(LogHelper.BuildKeyValueSettingUpdatedMessage(change.Key));
+ keyValueChanges.Add(change);
- keyValueChanges.AddRange(
- await client.GetKeyValueChangeCollection(
- currentKeyValues,
- new GetKeyValueChangeCollectionOptions
- {
- KeyFilter = changeWatcher.Key,
- Label = changeWatcher.Label.NormalizeNull(),
- RequestTracingEnabled = _requestTracingEnabled,
- RequestTracingOptions = _requestTracingOptions
- },
- logDebugBuilder,
- logInfoBuilder,
- endpoint,
- cancellationToken)
- .ConfigureAwait(false));
+ if (kvWatcher.RefreshAll)
+ {
+ return true;
+ }
+ }
+ else
+ {
+ logDebugBuilder.AppendLine(LogHelper.BuildKeyValueReadMessage(change.ChangeType, change.Key, change.Label, endpoint.ToString()));
+ }
}
- return keyValueChanges;
+ return false;
}
private void SetData(IDictionary data)
@@ -966,7 +1052,6 @@ private void SetRequestTracingOptions()
IsDevEnvironment = TracingUtils.IsDevEnvironment(),
IsKeyVaultConfigured = _options.IsKeyVaultConfigured,
IsKeyVaultRefreshConfigured = _options.IsKeyVaultRefreshConfigured,
- ReplicaCount = _options.Endpoints?.Count() - 1 ?? _options.ConnectionStrings?.Count() - 1 ?? 0,
FeatureFlagTracing = _options.FeatureFlagTracing,
IsLoadBalancingEnabled = _options.LoadBalancingEnabled
};
@@ -1124,6 +1209,13 @@ await ExecuteWithFailOverPolicyAsync