Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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
/// 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;

/// <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;
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)
{
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();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using lock, we should use ConcurrentDictionary for atomic operation

private Uri _nextRefreshSourceId;
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;
}
}

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