Skip to content

Commit 305fb0b

Browse files
Merge branch 'main' of https://github.com/Azure/AppConfiguration-JavaScriptProvider into zhiyuanliang/secret-refresh
2 parents 4201e6c + 5c1f4a3 commit 305fb0b

12 files changed

+172
-96
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOp
3535
import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js";
3636
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
3737
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
38-
import { getFixedBackoffDuration, calculateBackoffDuration } from "./failover.js";
39-
import { InvalidOperationError, ArgumentError, isFailoverableError, isRetriableError, isArgumentError } from "./error.js";
38+
import { getFixedBackoffDuration, getExponentialBackoffDuration } from "./common/backoffUtils.js";
39+
import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/error.js";
4040

4141
const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000; // 5 seconds
4242

@@ -258,7 +258,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
258258
})
259259
]);
260260
} catch (error) {
261-
if (!isArgumentError(error)) {
261+
if (!isInputError(error)) {
262262
const timeElapsed = Date.now() - startTimestamp;
263263
if (timeElapsed < MIN_DELAY_FOR_UNHANDLED_FAILURE) {
264264
// load() method is called in the application's startup code path.
@@ -267,7 +267,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
267267
await new Promise(resolve => setTimeout(resolve, MIN_DELAY_FOR_UNHANDLED_FAILURE - timeElapsed));
268268
}
269269
}
270-
throw new Error(`Failed to load: ${error.message}`);
270+
throw new Error("Failed to load.", { cause: error });
271271
} finally {
272272
clearTimeout(timeoutId); // cancel the timeout promise
273273
}
@@ -372,7 +372,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
372372
this.#isInitialLoadCompleted = true;
373373
break;
374374
} catch (error) {
375-
if (!isRetriableError(error)) {
375+
if (isInputError(error)) {
376376
throw error;
377377
}
378378
if (abortSignal.aborted) {
@@ -382,7 +382,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
382382
let backoffDuration = getFixedBackoffDuration(timeElapsed);
383383
if (backoffDuration === undefined) {
384384
postAttempts += 1;
385-
backoffDuration = calculateBackoffDuration(postAttempts);
385+
backoffDuration = getExponentialBackoffDuration(postAttempts);
386386
}
387387
console.warn(`Failed to load. Error message: ${error.message}. Retrying in ${backoffDuration} ms.`);
388388
await new Promise(resolve => setTimeout(resolve, backoffDuration));

src/ConfigurationClientManager.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import { TokenCredential } from "@azure/identity";
77
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
88
import { isBrowser, isWebWorker } from "./requestTracing/utils.js";
99
import * as RequestTracing from "./requestTracing/constants.js";
10-
import { instanceOfTokenCredential, shuffleList } from "./common/utils.js";
11-
import { ArgumentError } from "./error.js";
10+
import { shuffleList, instanceOfTokenCredential } from "./common/utils.js";
11+
import { ArgumentError } from "./common/error.js";
1212

1313
// Configuration client retry options
1414
const CLIENT_MAX_RETRIES = 2;
15-
const CLIENT_MAX_RETRY_DELAY = 60_000; // 1 minute in milliseconds
15+
const CLIENT_MAX_RETRY_DELAY_IN_MS = 60_000;
1616

1717
const TCP_ORIGIN_KEY_NAME = "_origin._tcp";
1818
const ALT_KEY_NAME = "_alt";
@@ -21,9 +21,9 @@ const ENDPOINT_KEY_NAME = "Endpoint";
2121
const ID_KEY_NAME = "Id";
2222
const SECRET_KEY_NAME = "Secret";
2323
const TRUSTED_DOMAIN_LABELS = [".azconfig.", ".appconfig."];
24-
const FALLBACK_CLIENT_EXPIRE_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds
25-
const MINIMAL_CLIENT_REFRESH_INTERVAL = 30_000; // 30 seconds in milliseconds
26-
const DNS_RESOLVER_TIMEOUT = 3_000; // 3 seconds in milliseconds, in most cases, dns resolution should be within 200 milliseconds
24+
const FALLBACK_CLIENT_EXPIRE_INTERVAL_IN_MS = 60 * 60 * 1000;
25+
const MINIMAL_CLIENT_REFRESH_INTERVAL_IN_MS = 30_000;
26+
const DNS_RESOLVER_TIMEOUT_IN_MS = 3_000;
2727
const DNS_RESOLVER_TRIES = 2;
2828
const MAX_ALTNATIVE_SRV_COUNT = 10;
2929

@@ -120,11 +120,11 @@ export class ConfigurationClientManager {
120120
const currentTime = Date.now();
121121
// Filter static clients whose backoff time has ended
122122
let availableClients = this.#staticClients.filter(client => client.backoffEndTime <= currentTime);
123-
if (currentTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL &&
123+
if (currentTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL_IN_MS &&
124124
(!this.#dynamicClients ||
125125
// All dynamic clients are in backoff means no client is available
126126
this.#dynamicClients.every(client => currentTime < client.backoffEndTime) ||
127-
currentTime >= this.#lastFallbackClientUpdateTime + FALLBACK_CLIENT_EXPIRE_INTERVAL)) {
127+
currentTime >= this.#lastFallbackClientUpdateTime + FALLBACK_CLIENT_EXPIRE_INTERVAL_IN_MS)) {
128128
await this.#discoverFallbackClients(this.endpoint.hostname);
129129
return availableClients.concat(this.#dynamicClients);
130130
}
@@ -142,7 +142,7 @@ export class ConfigurationClientManager {
142142
async refreshClients() {
143143
const currentTime = Date.now();
144144
if (this.#isFailoverable &&
145-
currentTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL) {
145+
currentTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL_IN_MS) {
146146
await this.#discoverFallbackClients(this.endpoint.hostname);
147147
}
148148
}
@@ -185,7 +185,7 @@ export class ConfigurationClientManager {
185185

186186
try {
187187
// https://nodejs.org/api/dns.html#dnspromisesresolvesrvhostname
188-
const resolver = new this.#dns.Resolver({timeout: DNS_RESOLVER_TIMEOUT, tries: DNS_RESOLVER_TRIES});
188+
const resolver = new this.#dns.Resolver({timeout: DNS_RESOLVER_TIMEOUT_IN_MS, tries: DNS_RESOLVER_TRIES});
189189
// On success, resolveSrv() returns an array of SrvRecord
190190
// On failure, resolveSrv() throws an error with code 'ENOTFOUND'.
191191
const originRecords = await resolver.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`); // look up SRV records for the origin host
@@ -266,7 +266,7 @@ function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurat
266266
// retry options
267267
const defaultRetryOptions = {
268268
maxRetries: CLIENT_MAX_RETRIES,
269-
maxRetryDelayInMs: CLIENT_MAX_RETRY_DELAY,
269+
maxRetryDelayInMs: CLIENT_MAX_RETRY_DELAY_IN_MS,
270270
};
271271
const retryOptions = Object.assign({}, defaultRetryOptions, options?.clientOptions?.retryOptions);
272272

src/ConfigurationClientWrapper.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { AppConfigurationClient } from "@azure/app-configuration";
5-
import { calculateBackoffDuration } from "./failover.js";
5+
import { getExponentialBackoffDuration } from "./common/backoffUtils.js";
66

77
export class ConfigurationClientWrapper {
88
endpoint: string;
@@ -21,7 +21,7 @@ export class ConfigurationClientWrapper {
2121
this.backoffEndTime = Date.now();
2222
} else {
2323
this.#failedAttempts += 1;
24-
this.backoffEndTime = Date.now() + calculateBackoffDuration(this.#failedAttempts);
24+
this.backoffEndTime = Date.now() + getExponentialBackoffDuration(this.#failedAttempts);
2525
}
2626
}
2727
}

src/StartupOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
export const DEFAULT_STARTUP_TIMEOUT_IN_MS = 100_000; // 100 seconds in milliseconds
4+
export const DEFAULT_STARTUP_TIMEOUT_IN_MS = 100_000;
55

66
export interface StartupOptions {
77
/**

src/common/backoffUtils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
const MIN_BACKOFF_DURATION_IN_MS = 30_000;
5+
const MAX_BACKOFF_DURATION_IN_MS = 10 * 60 * 1000;
6+
const JITTER_RATIO = 0.25;
7+
8+
export function getFixedBackoffDuration(timeElapsedInMs: number): number | undefined {
9+
if (timeElapsedInMs < 100_000) {
10+
return 5_000;
11+
}
12+
if (timeElapsedInMs < 200_000) {
13+
return 10_000;
14+
}
15+
if (timeElapsedInMs < 10 * 60 * 1000) {
16+
return MIN_BACKOFF_DURATION_IN_MS;
17+
}
18+
return undefined;
19+
}
20+
21+
export function getExponentialBackoffDuration(failedAttempts: number): number {
22+
if (failedAttempts <= 1) {
23+
return MIN_BACKOFF_DURATION_IN_MS;
24+
}
25+
26+
// exponential: minBackoff * 2 ^ (failedAttempts - 1)
27+
// The right shift operator is not used in order to avoid potential overflow. Bitwise operations in JavaScript are limited to 32 bits.
28+
let calculatedBackoffDuration = MIN_BACKOFF_DURATION_IN_MS * Math.pow(2, failedAttempts - 1);
29+
if (calculatedBackoffDuration > MAX_BACKOFF_DURATION_IN_MS) {
30+
calculatedBackoffDuration = MAX_BACKOFF_DURATION_IN_MS;
31+
}
32+
33+
// jitter: random value between [-1, 1) * jitterRatio * calculatedBackoffMs
34+
const jitter = JITTER_RATIO * (Math.random() * 2 - 1);
35+
36+
return calculatedBackoffDuration * (1 + jitter);
37+
}

src/error.ts renamed to src/common/error.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class InvalidOperationError extends Error {
1414
}
1515

1616
/**
17-
* Error thrown when an argument or configuration is invalid.
17+
* Error thrown when an input argument is invalid.
1818
*/
1919
export class ArgumentError extends Error {
2020
constructor(message: string) {
@@ -24,11 +24,11 @@ export class ArgumentError extends Error {
2424
}
2525

2626
/**
27-
* Error thrown when it fails to get the secret from the Key Vault.
27+
* Error thrown when a Key Vault reference cannot be resolved.
2828
*/
2929
export class KeyVaultReferenceError extends Error {
30-
constructor(message: string) {
31-
super(message);
30+
constructor(message: string, options?: ErrorOptions) {
31+
super(message, options);
3232
this.name = "KeyVaultReferenceError";
3333
}
3434
}
@@ -37,8 +37,10 @@ export function isFailoverableError(error: any): boolean {
3737
if (!isRestError(error)) {
3838
return false;
3939
}
40-
// ENOTFOUND: DNS lookup failed, ENOENT: no such file or directory
41-
if (error.code === "ENOTFOUND" || error.code === "ENOENT") {
40+
// https://nodejs.org/api/errors.html#common-system-errors
41+
// ENOTFOUND: DNS lookup failed, ENOENT: no such file or directory, ECONNREFUSED: connection refused, ECONNRESET: connection reset by peer, ETIMEDOUT: connection timed out
42+
if (error.code !== undefined &&
43+
(error.code === "ENOTFOUND" || error.code === "ENOENT" || error.code === "ECONNREFUSED" || error.code === "ECONNRESET" || error.code === "ETIMEDOUT")) {
4244
return true;
4345
}
4446
// 401 Unauthorized, 403 Forbidden, 408 Request Timeout, 429 Too Many Requests, 5xx Server Errors
@@ -50,19 +52,11 @@ export function isFailoverableError(error: any): boolean {
5052
return false;
5153
}
5254

53-
export function isRetriableError(error: any): boolean {
54-
if (isArgumentError(error) ||
55-
error instanceof RangeError) {
56-
return false;
57-
}
58-
return true;
59-
}
60-
61-
export function isArgumentError(error: any): boolean {
62-
if (error instanceof ArgumentError ||
55+
/**
56+
* Check if the error is an instance of ArgumentError, TypeError, or RangeError.
57+
*/
58+
export function isInputError(error: any): boolean {
59+
return error instanceof ArgumentError ||
6360
error instanceof TypeError ||
64-
error instanceof RangeError) {
65-
return true;
66-
}
67-
return false;
61+
error instanceof RangeError;
6862
}

src/failover.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
const MIN_BACKOFF_DURATION = 30_000; // 30 seconds in milliseconds
5-
const MAX_BACKOFF_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds
4+
const MIN_BACKOFF_DURATION_IN_MS = 30_000;
5+
const MAX_BACKOFF_DURATION_IN_MS = 10 * 60 * 1000;
66
const JITTER_RATIO = 0.25;
77

8-
export function getFixedBackoffDuration(timeElapsed: number): number | undefined {
9-
if (timeElapsed <= 100_000) { // 100 seconds in milliseconds
10-
return 5_000; // 5 seconds in milliseconds
8+
export function getFixedBackoffDuration(timeElapsedInMs: number): number | undefined {
9+
if (timeElapsedInMs <= 100_000) {
10+
return 5_000;
1111
}
12-
if (timeElapsed <= 200_000) { // 200 seconds in milliseconds
13-
return 10_000; // 10 seconds in milliseconds
12+
if (timeElapsedInMs <= 200_000) {
13+
return 10_000;
1414
}
15-
if (timeElapsed <= 10 * 60 * 1000) { // 10 minutes in milliseconds
16-
return MIN_BACKOFF_DURATION;
15+
if (timeElapsedInMs <= 10 * 60 * 1000) {
16+
return MIN_BACKOFF_DURATION_IN_MS;
1717
}
1818
return undefined;
1919
}
2020

2121
export function calculateBackoffDuration(failedAttempts: number) {
2222
if (failedAttempts <= 1) {
23-
return MIN_BACKOFF_DURATION;
23+
return MIN_BACKOFF_DURATION_IN_MS;
2424
}
2525

2626
// exponential: minBackoff * 2 ^ (failedAttempts - 1)
2727
// The right shift operator is not used in order to avoid potential overflow. Bitwise operations in JavaScript are limited to 32 bits.
28-
let calculatedBackoffDuration = MIN_BACKOFF_DURATION * Math.pow(2, failedAttempts - 1);
29-
if (calculatedBackoffDuration > MAX_BACKOFF_DURATION) {
30-
calculatedBackoffDuration = MAX_BACKOFF_DURATION;
28+
let calculatedBackoffDuration = MIN_BACKOFF_DURATION_IN_MS * Math.pow(2, failedAttempts - 1);
29+
if (calculatedBackoffDuration > MAX_BACKOFF_DURATION_IN_MS) {
30+
calculatedBackoffDuration = MAX_BACKOFF_DURATION_IN_MS;
3131
}
3232

3333
// jitter: random value between [-1, 1) * jitterRatio * calculatedBackoffMs

src/keyvault/AzureKeyVaultKeyValueAdapter.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import { IKeyValueAdapter } from "../IKeyValueAdapter.js";
66
import { AzureKeyVaultSecretProvider } from "./AzureKeyVaultSecretProvider.js";
77
import { KeyVaultOptions } from "./KeyVaultOptions.js";
88
import { RefreshTimer } from "../refresh/RefreshTimer.js";
9-
import { ArgumentError, KeyVaultReferenceError } from "../error.js";
10-
import { parseKeyVaultSecretIdentifier, KeyVaultSecretIdentifier } from "@azure/keyvault-secrets";
9+
import { ArgumentError, KeyVaultReferenceError } from "../common/error.js";
10+
import { KeyVaultSecretIdentifier, parseKeyVaultSecretIdentifier } from "@azure/keyvault-secrets";
11+
import { isRestError } from "@azure/core-rest-pipeline";
12+
import { AuthenticationError } from "@azure/identity";
1113

1214
export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter {
1315
#keyVaultOptions: KeyVaultOptions | undefined;
@@ -24,20 +26,25 @@ export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter {
2426

2527
async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> {
2628
if (!this.#keyVaultOptions) {
27-
throw new ArgumentError("Failed to process the key vault reference. The keyVaultOptions is not configured.");
29+
throw new ArgumentError("Failed to process the Key Vault reference because Key Vault options are not configured.");
30+
}
31+
let secretIdentifier: KeyVaultSecretIdentifier;
32+
try {
33+
secretIdentifier = parseKeyVaultSecretIdentifier(
34+
parseSecretReference(setting).value.secretId
35+
);
36+
} catch (error) {
37+
throw new KeyVaultReferenceError(buildKeyVaultReferenceErrorMessage("Invalid Key Vault reference.", setting), { cause: error });
2838
}
2939

30-
const secretIdentifier: KeyVaultSecretIdentifier = parseKeyVaultSecretIdentifier(
31-
parseSecretReference(setting).value.secretId
32-
);
3340
try {
3441
const secretValue = await this.#keyVaultSecretProvider.getSecretValue(secretIdentifier);
3542
return [setting.key, secretValue];
3643
} catch (error) {
37-
if (error instanceof ArgumentError) {
38-
throw error;
44+
if (isRestError(error) || error instanceof AuthenticationError) {
45+
throw new KeyVaultReferenceError(buildKeyVaultReferenceErrorMessage("Failed to resolve Key Vault reference.", setting, secretIdentifier.sourceId), { cause: error });
3946
}
40-
throw new KeyVaultReferenceError(buildKeyVaultReferenceErrorMessage(error.message, setting, secretIdentifier.sourceId));
47+
throw error;
4148
}
4249
}
4350

@@ -48,5 +55,5 @@ export class AzureKeyVaultKeyValueAdapter implements IKeyValueAdapter {
4855
}
4956

5057
function buildKeyVaultReferenceErrorMessage(message: string, setting: ConfigurationSetting, secretIdentifier?: string ): string {
51-
return `${message} Key: '${setting.key}' Label: '${setting.label ?? ""}' ETag: '${setting.etag ?? ""}' SecretIdentifier: '${secretIdentifier ?? ""}'`;
58+
return `${message} Key: '${setting.key}' Label: '${setting.label ?? ""}' ETag: '${setting.etag ?? ""}' ${secretIdentifier ? ` SecretIdentifier: '${secretIdentifier}'` : ""}`;
5259
}

src/keyvault/AzureKeyVaultSecretProvider.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { KeyVaultOptions } from "./KeyVaultOptions.js";
55
import { RefreshTimer } from "../refresh/RefreshTimer.js";
6-
import { ArgumentError } from "../error.js";
6+
import { ArgumentError } from "../common/error.js";
77
import { SecretClient, KeyVaultSecretIdentifier } from "@azure/keyvault-secrets";
88

99
export class AzureKeyVaultSecretProvider {
@@ -68,8 +68,7 @@ export class AzureKeyVaultSecretProvider {
6868
return await this.#keyVaultOptions.secretResolver(new URL(sourceId));
6969
}
7070
// When code reaches here, it means that the key vault reference cannot be resolved in all possible ways.
71-
throw new ArgumentError("Failed to get secret value. No key vault secret client, credential or secret resolver callback is available to resolve the secret.");
72-
71+
throw new ArgumentError("Failed to process the key vault reference. No key vault secret client, credential or secret resolver callback is available to resolve the secret.");
7372
}
7473

7574
#getSecretClient(vaultUrl: URL): SecretClient | undefined {

src/load.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js"
88
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
99
import { instanceOfTokenCredential } from "./common/utils.js";
1010

11-
const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5_000; // 5 seconds
11+
const MIN_DELAY_FOR_UNHANDLED_ERROR_IN_MS: number = 5_000;
1212

1313
/**
1414
* Loads the data from Azure App Configuration service and returns an instance of AzureAppConfiguration.
@@ -49,7 +49,7 @@ export async function load(
4949
// load() method is called in the application's startup code path.
5050
// Unhandled exceptions cause application crash which can result in crash loops as orchestrators attempt to restart the application.
5151
// Knowing the intended usage of the provider in startup code path, we mitigate back-to-back crash loops from overloading the server with requests by waiting a minimum time to propagate fatal errors.
52-
const delay = MIN_DELAY_FOR_UNHANDLED_ERROR - (Date.now() - startTimestamp);
52+
const delay = MIN_DELAY_FOR_UNHANDLED_ERROR_IN_MS - (Date.now() - startTimestamp);
5353
if (delay > 0) {
5454
await new Promise((resolve) => setTimeout(resolve, delay));
5555
}

0 commit comments

Comments
 (0)