Skip to content

Commit 0e635bb

Browse files
committed
dedup key vault ref in preload stage
1 parent ccfbe12 commit 0e635bb

3 files changed

Lines changed: 57 additions & 36 deletions

File tree

src/appConfigurationImpl.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
111111
#secretReferences: ConfigurationSetting[] = []; // cached key vault references
112112
#secretRefreshTimer: RefreshTimer | undefined = undefined;
113113
#resolveSecretsInParallel: boolean = false;
114+
#keyVaultAdapter: AzureKeyVaultKeyValueAdapter;
114115

115116
/**
116117
* Selectors of key-values obtained from @see AzureAppConfigurationOptions.selectors
@@ -204,7 +205,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
204205
}
205206
this.#resolveSecretsInParallel = options.keyVaultOptions.parallelSecretResolutionEnabled ?? false;
206207
}
207-
this.#adapters.push(new AzureKeyVaultKeyValueAdapter(options?.keyVaultOptions, this.#secretRefreshTimer));
208+
this.#keyVaultAdapter = new AzureKeyVaultKeyValueAdapter(options?.keyVaultOptions, this.#secretRefreshTimer);
209+
this.#adapters.push(this.#keyVaultAdapter);
208210
this.#adapters.push(new JsonKeyValueAdapter());
209211
}
210212

@@ -715,6 +717,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
715717
return Promise.resolve(false);
716718
}
717719

720+
// Invalidate cached secret values so the refresh round re-fetches them from Key Vault.
721+
this.#keyVaultAdapter.clearCache();
718722
await this.#resolveSecretReferences(this.#secretReferences, (key, value) => {
719723
this.#configMap.set(key, value);
720724
});
@@ -895,22 +899,30 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
895899

896900
async #resolveSecretReferences(secretReferences: ConfigurationSetting[], resultHandler: (key: string, value: unknown) => void): Promise<void> {
897901
if (this.#resolveSecretsInParallel) {
898-
const secretResolutionPromises: Promise<void>[] = [];
902+
// Preload phase: resolve each unique secret exactly once, in parallel, to warm the cache.
903+
// References that resolve to the same secret identifier are deduplicated so that only a
904+
// single Key Vault request is issued per unique secret.
905+
const seenSecretIds = new Set<string>();
906+
const uniqueSecretReferences: ConfigurationSetting[] = [];
899907
for (const setting of secretReferences) {
900-
const secretResolutionPromise = this.#processKeyValue(setting)
901-
.then(([key, value]) => {
902-
resultHandler(key, value);
903-
});
904-
secretResolutionPromises.push(secretResolutionPromise);
908+
const secretId = this.#keyVaultAdapter.getSecretReferenceId(setting);
909+
if (secretId === undefined) {
910+
// The reference cannot be parsed; resolve it individually so the appropriate error
911+
// is surfaced when it is processed below.
912+
uniqueSecretReferences.push(setting);
913+
} else if (!seenSecretIds.has(secretId)) {
914+
seenSecretIds.add(secretId);
915+
uniqueSecretReferences.push(setting);
916+
}
905917
}
918+
await Promise.all(uniqueSecretReferences.map(setting => this.#processKeyValue(setting)));
919+
}
906920

907-
// Wait for all secret resolution promises to be resolved
908-
await Promise.all(secretResolutionPromises);
909-
} else {
910-
for (const setting of secretReferences) {
911-
const [key, value] = await this.#processKeyValue(setting);
912-
resultHandler(key, value);
913-
}
921+
// Resolve every reference. In parallel mode the values are served from the warmed cache
922+
// without any additional I/O.
923+
for (const setting of secretReferences) {
924+
const [key, value] = await this.#processKeyValue(setting);
925+
resultHandler(key, value);
914926
}
915927
}
916928

src/keyvault/keyVaultKeyValueAdapter.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,28 @@ export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter {
4949
}
5050
}
5151

52+
/**
53+
* Returns the normalized Key Vault secret identifier (sourceId) for a secret reference setting,
54+
* or undefined if the reference cannot be parsed. Used to deduplicate references that resolve to
55+
* the same secret before resolving them.
56+
*/
57+
getSecretReferenceId(setting: ConfigurationSetting): string | undefined {
58+
try {
59+
return parseKeyVaultSecretIdentifier(
60+
parseSecretReference(setting).value.secretId
61+
).sourceId;
62+
} catch {
63+
return undefined;
64+
}
65+
}
66+
67+
/**
68+
* Clears the cached secret values, throttled by the minimum secret refresh interval.
69+
*/
70+
clearCache(): void {
71+
this.#keyVaultSecretProvider.clearCache();
72+
}
73+
5274
async onChangeDetected(): Promise<void> {
5375
this.#keyVaultSecretProvider.clearCache();
5476
return;

src/keyvault/keyVaultSecretProvider.ts

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@ import { KeyVaultReferenceErrorMessages } from "../common/errorMessages.js";
99

1010
export class AzureKeyVaultSecretProvider {
1111
#keyVaultOptions: KeyVaultOptions | undefined;
12-
#secretRefreshTimer: RefreshTimer | undefined;
1312
#minSecretRefreshTimer: RefreshTimer;
1413
#secretClients: Map<string, SecretClient>; // map key vault hostname to corresponding secret client
1514
#cachedSecretValues: Map<string, any> = new Map<string, any>(); // map secret identifier to secret value
16-
#inflightRequests: Map<string, Promise<unknown>> = new Map<string, Promise<unknown>>(); // map secret identifier to in-flight Key Vault request
1715

1816
constructor(keyVaultOptions?: KeyVaultOptions, refreshTimer?: RefreshTimer) {
1917
if (keyVaultOptions?.secretRefreshIntervalInMs !== undefined) {
@@ -25,7 +23,6 @@ export class AzureKeyVaultSecretProvider {
2523
}
2624
}
2725
this.#keyVaultOptions = keyVaultOptions;
28-
this.#secretRefreshTimer = refreshTimer;
2926
this.#minSecretRefreshTimer = new RefreshTimer(MIN_SECRET_REFRESH_INTERVAL_IN_MS);
3027
this.#secretClients = new Map();
3128
for (const client of this.#keyVaultOptions?.secretClients ?? []) {
@@ -37,27 +34,17 @@ export class AzureKeyVaultSecretProvider {
3734
async getSecretValue(secretIdentifier: KeyVaultSecretIdentifier): Promise<unknown> {
3835
const identifierKey = secretIdentifier.sourceId;
3936

40-
// If the refresh interval is not expired, return the cached value if available.
41-
if (this.#cachedSecretValues.has(identifierKey) &&
42-
(!this.#secretRefreshTimer || !this.#secretRefreshTimer.canRefresh())) {
43-
return this.#cachedSecretValues.get(identifierKey);
37+
// Return the cached value if available. The cache is invalidated externally (on secret refresh
38+
// or when a key-value change is detected) so a stale value is never served.
39+
if (this.#cachedSecretValues.has(identifierKey)) {
40+
return this.#cachedSecretValues.get(identifierKey);
4441
}
4542

46-
// Deduplicate concurrent requests for the same secret: if a request is already in-flight, await it.
47-
let pendingRequest = this.#inflightRequests.get(identifierKey);
48-
if (pendingRequest === undefined) {
49-
pendingRequest = this.#getSecretValueFromKeyVault(secretIdentifier)
50-
.then((secretValue) => {
51-
this.#cachedSecretValues.set(identifierKey, secretValue);
52-
return secretValue;
53-
})
54-
.finally(() => {
55-
// Failures are not cached so subsequent calls can retry.
56-
this.#inflightRequests.delete(identifierKey);
57-
});
58-
this.#inflightRequests.set(identifierKey, pendingRequest);
59-
}
60-
return pendingRequest;
43+
// Fetch the secret value from Key Vault and cache it. Failures are not cached, so a subsequent
44+
// call will retry.
45+
const secretValue = await this.#getSecretValueFromKeyVault(secretIdentifier);
46+
this.#cachedSecretValues.set(identifierKey, secretValue);
47+
return secretValue;
6148
}
6249

6350
clearCache(): void {

0 commit comments

Comments
 (0)