Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public class AzureAppConfigurationKeyVaultOptions
internal TimeSpan? DefaultSecretRefreshInterval = null;
internal bool IsKeyVaultRefreshConfigured = false;

/// <summary>
/// Specifies whether Key Vault references should be resolved in parallel.
/// Default value is false. Enabling this can reduce the time required to resolve Key Vault references
Comment thread
linglingye001 marked this conversation as resolved.
Outdated
/// when many references are loaded from Azure App Configuration.
/// </summary>
public bool ParallelSecretResolutionEnabled { get; set; }

/// <summary>
/// Sets the credentials used to authenticate to key vaults that have no registered <see cref="SecretClient"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ internal IEnumerable<IKeyValueAdapter> Adapters
/// </summary>
internal bool IsKeyVaultRefreshConfigured { get; private set; } = false;

/// <summary>
/// Flag to indicate whether Key Vault references should be resolved in parallel.
/// </summary>
internal bool IsParallelSecretResolutionEnabled { get; private set; } = false;
Comment thread
linglingye001 marked this conversation as resolved.
Outdated

/// <summary>
/// Indicates all feature flag features used by the application.
/// </summary>
Expand Down Expand Up @@ -520,6 +525,7 @@ public AzureAppConfigurationOptions ConfigureKeyVault(Action<AzureAppConfigurati
_adapters.Add(new AzureKeyVaultKeyValueAdapter(new AzureKeyVaultSecretProvider(keyVaultOptions)));

IsKeyVaultRefreshConfigured = keyVaultOptions.IsKeyVaultRefreshConfigured;
IsParallelSecretResolutionEnabled = keyVaultOptions.ParallelSecretResolutionEnabled;
IsKeyVaultConfigured = true;
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,37 +625,72 @@ private async Task<Dictionary<string, string>> PrepareData(Dictionary<string, Co
_requestTracingOptions.ResetAiConfigurationTracing();
}

foreach (KeyValuePair<string, ConfigurationSetting> kvp in data)
bool parallelSecretResolution = _options.IsParallelSecretResolutionEnabled;

if (parallelSecretResolution)
{
IEnumerable<KeyValuePair<string, string>> keyValuePairs = null;
// Dispatch adapter processing for all settings concurrently. Only Key Vault references
// perform network I/O during adapter processing; other adapters complete synchronously.
// Insertion order in 'data' is preserved when merging results so prefix-stripping and
// last-write-wins behavior remain unchanged.
var pendingTasks = new List<Task<IEnumerable<KeyValuePair<string, string>>>>(data.Count);

if (_requestTracingEnabled && _requestTracingOptions != null)
foreach (KeyValuePair<string, ConfigurationSetting> kvp in data)
{
_requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType);
if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType);
}

pendingTasks.Add(ProcessAdapters(kvp.Value, cancellationToken));
}

keyValuePairs = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false);
IEnumerable<KeyValuePair<string, string>>[] results = await Task.WhenAll(pendingTasks).ConfigureAwait(false);
Comment thread
linglingye001 marked this conversation as resolved.
Outdated

foreach (KeyValuePair<string, string> kv in keyValuePairs)
for (int i = 0; i < results.Length; i++)
{
string key = kv.Key;
MergeIntoApplicationData(applicationData, results[i]);
}
}
else
{
foreach (KeyValuePair<string, ConfigurationSetting> kvp in data)
{
IEnumerable<KeyValuePair<string, string>> keyValuePairs = null;

foreach (string prefix in _options.KeyPrefixes)
if (_requestTracingEnabled && _requestTracingOptions != null)
{
if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
key = key.Substring(prefix.Length);
break;
}
_requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType);
}

applicationData[key] = kv.Value;
keyValuePairs = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false);

MergeIntoApplicationData(applicationData, keyValuePairs);
}
}

return applicationData;
}

private void MergeIntoApplicationData(Dictionary<string, string> applicationData, IEnumerable<KeyValuePair<string, string>> keyValuePairs)
{
Comment thread
linglingye001 marked this conversation as resolved.
Outdated
foreach (KeyValuePair<string, string> kv in keyValuePairs)
{
string key = kv.Key;

foreach (string prefix in _options.KeyPrefixes)
{
if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
key = key.Substring(prefix.Length);
break;
}
}

applicationData[key] = kv.Value;
}
}

private async Task LoadAsync(bool ignoreFailures, CancellationToken cancellationToken)
{
var startupStopwatch = Stopwatch.StartNew();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal class AzureKeyVaultSecretProvider
private readonly AzureAppConfigurationKeyVaultOptions _keyVaultOptions;
private readonly IDictionary<string, SecretClient> _secretClients;
private readonly Dictionary<Uri, CachedKeyVaultSecret> _cachedKeyVaultSecrets;
private readonly object _cacheLock = new object();
Comment thread
linglingye001 marked this conversation as resolved.
Outdated
private Uri _nextRefreshSourceId;
Comment thread
linglingye001 marked this conversation as resolved.
private DateTimeOffset? _nextRefreshTime;

Expand All @@ -38,20 +39,25 @@ public AzureKeyVaultSecretProvider(AzureAppConfigurationKeyVaultOptions keyVault
public async Task<string> GetSecretValue(KeyVaultSecretIdentifier secretIdentifier, string key, string label, Logger logger, CancellationToken cancellationToken)
{
string secretValue = null;
SecretClient client;

if (_cachedKeyVaultSecrets.TryGetValue(secretIdentifier.SourceId, out CachedKeyVaultSecret cachedSecret) &&
(!cachedSecret.RefreshAt.HasValue || DateTimeOffset.UtcNow < cachedSecret.RefreshAt.Value))
lock (_cacheLock)
{
return cachedSecret.SecretValue;
}
if (_cachedKeyVaultSecrets.TryGetValue(secretIdentifier.SourceId, out CachedKeyVaultSecret cachedHit) &&
(!cachedHit.RefreshAt.HasValue || DateTimeOffset.UtcNow < cachedHit.RefreshAt.Value))
{
return cachedHit.SecretValue;
}

SecretClient client = GetSecretClient(secretIdentifier.SourceId);
client = GetSecretClient(secretIdentifier.SourceId);
}

if (client == null && _keyVaultOptions.SecretResolver == null)
{
throw new UnauthorizedAccessException("No key vault credential or secret resolver callback configured, and no matching secret client could be found.");
}

CachedKeyVaultSecret cachedSecret = null;
bool success = false;

try
Expand All @@ -73,49 +79,61 @@ public async Task<string> GetSecretValue(KeyVaultSecretIdentifier secretIdentifi
}
finally
{
SetSecretInCache(secretIdentifier.SourceId, key, cachedSecret, success);
lock (_cacheLock)
{
SetSecretInCache(secretIdentifier.SourceId, key, cachedSecret, success);
}
}

return secretValue;
}

public bool ShouldRefreshKeyVaultSecrets()
{
return _nextRefreshTime.HasValue && _nextRefreshTime.Value < DateTimeOffset.UtcNow;
lock (_cacheLock)
{
return _nextRefreshTime.HasValue && _nextRefreshTime.Value < DateTimeOffset.UtcNow;
}
}
Comment thread
linglingye001 marked this conversation as resolved.

public void ClearCache()
{
var sourceIdsToRemove = new List<Uri>();
lock (_cacheLock)
{
var sourceIdsToRemove = new List<Uri>();

var utcNow = DateTimeOffset.UtcNow;
var utcNow = DateTimeOffset.UtcNow;

foreach (KeyValuePair<Uri, CachedKeyVaultSecret> secret in _cachedKeyVaultSecrets)
{
if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < utcNow)
foreach (KeyValuePair<Uri, CachedKeyVaultSecret> secret in _cachedKeyVaultSecrets)
{
sourceIdsToRemove.Add(secret.Key);
if (secret.Value.LastRefreshTime + RefreshConstants.MinimumSecretRefreshInterval < utcNow)
{
sourceIdsToRemove.Add(secret.Key);
}
}
}

foreach (Uri sourceId in sourceIdsToRemove)
{
_cachedKeyVaultSecrets.Remove(sourceId);
}
foreach (Uri sourceId in sourceIdsToRemove)
{
_cachedKeyVaultSecrets.Remove(sourceId);
}

if (_cachedKeyVaultSecrets.Any())
{
UpdateNextRefreshableSecretFromCache();
if (_cachedKeyVaultSecrets.Any())
{
UpdateNextRefreshableSecretFromCache();
}
}
}

public void RemoveSecretFromCache(Uri sourceId)
{
_cachedKeyVaultSecrets.Remove(sourceId);

if (sourceId == _nextRefreshSourceId)
lock (_cacheLock)
{
UpdateNextRefreshableSecretFromCache();
_cachedKeyVaultSecrets.Remove(sourceId);

if (sourceId == _nextRefreshSourceId)
{
UpdateNextRefreshableSecretFromCache();
}
}
}

Expand Down
Loading
Loading