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 @@ -15,6 +15,7 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
Comment thread
linglingye001 marked this conversation as resolved.
Outdated
using System.Net.Sockets;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -625,37 +626,100 @@ 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;
// Only Key Vault references perform network I/O during adapter processing; other
// adapters complete synchronously. To avoid the overhead of wrapping non-I/O work
// in tasks, only Key Vault references are dispatched concurrently. Non-Key Vault
// settings are processed inline in their original order; their results, along with
// those of the in-flight Key Vault tasks, are merged in insertion order to preserve
// prefix-stripping and last-write-wins behavior.
var results = new IEnumerable<KeyValuePair<string, string>>[data.Count];
var pendingKeyVaultTasks = new List<(int Index, Task<IEnumerable<KeyValuePair<string, string>>> Task)>();

if (_requestTracingEnabled && _requestTracingOptions != null)
int index = 0;

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

if (IsKeyVaultReference(kvp.Value))
{
pendingKeyVaultTasks.Add((index, ProcessAdapters(kvp.Value, cancellationToken)));
}
else
{
results[index] = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false);
}

index++;
}

keyValuePairs = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false);
if (pendingKeyVaultTasks.Count > 0)
{
await Task.WhenAll(pendingKeyVaultTasks.Select(p => p.Task)).ConfigureAwait(false);

foreach (KeyValuePair<string, string> kv in keyValuePairs)
foreach ((int Index, Task<IEnumerable<KeyValuePair<string, string>>> Task) entry in pendingKeyVaultTasks)
{
results[entry.Index] = entry.Task.Result;
}
}

foreach (IEnumerable<KeyValuePair<string, string>> keyValuePairs in results)
{
string key = kv.Key;
MergeIntoApplicationData(applicationData, keyValuePairs);
}
}
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 static bool IsKeyVaultReference(ConfigurationSetting setting)
{
Comment thread
linglingye001 marked this conversation as resolved.
Outdated
return setting.ContentType.TryParseContentType(out ContentType contentType)
&& contentType.IsKeyVaultReference();
}

private void MergeIntoApplicationData(Dictionary<string, string> applicationData, IEnumerable<KeyValuePair<string, string>> keyValuePairs)
{
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