Skip to content

Commit 2720628

Browse files
support startup retry and timeout
1 parent e9fe900 commit 2720628

File tree

8 files changed

+124
-48
lines changed

8 files changed

+124
-48
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurat
4040
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
4141
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
4242
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
43+
import { getFixedBackoffDuration, calculateDynamicBackoffDuration } from "./failover.js";
4344

4445
type PagedSettingSelector = SettingSelector & {
4546
/**
@@ -48,6 +49,9 @@ type PagedSettingSelector = SettingSelector & {
4849
pageEtags?: string[];
4950
};
5051

52+
const DEFAULT_STARTUP_TIMEOUT = 100 * 1000; // 100 seconds in milliseconds
53+
const MAX_STARTUP_TIMEOUT = 30 * 60 * 1000; // 15 minutes in milliseconds
54+
5155
export class AzureAppConfigurationImpl implements AzureAppConfiguration {
5256
/**
5357
* Hosting key-value pairs in the configuration store.
@@ -229,13 +233,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
229233
* Loads the configuration store for the first time.
230234
*/
231235
async load() {
232-
await this.#inspectFmPackage();
233-
await this.#loadSelectedAndWatchedKeyValues();
234-
if (this.#featureFlagEnabled) {
235-
await this.#loadFeatureFlags();
236+
const startupTimeout = this.#options?.startupOptions?.timeoutInMs ?? DEFAULT_STARTUP_TIMEOUT;
237+
let timer;
238+
try {
239+
await Promise.race([
240+
new Promise((_, reject) => timer = setTimeout(() => reject(new Error("Load operation timed out.")), startupTimeout)),
241+
this.#initialize()
242+
]);
243+
} catch (error) {
244+
throw new Error(`Failed to load: ${error.message}`);
245+
} finally {
246+
clearTimeout(timer);
236247
}
237-
// Mark all settings have loaded at startup.
238-
this.#isInitialLoadCompleted = true;
239248
}
240249

241250
/**
@@ -320,6 +329,37 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
320329
return new Disposable(remove);
321330
}
322331

332+
/**
333+
* Initializes the configuration provider.
334+
*/
335+
async #initialize() {
336+
if (this.#isInitialLoadCompleted) {
337+
await this.#inspectFmPackage();
338+
const startTimestamp = Date.now();
339+
while (startTimestamp + MAX_STARTUP_TIMEOUT > Date.now()) {
340+
try {
341+
await this.#loadSelectedAndWatchedKeyValues();
342+
if (this.#featureFlagEnabled) {
343+
await this.#loadFeatureFlags();
344+
}
345+
// Mark all settings have loaded at startup.
346+
this.#isInitialLoadCompleted = true;
347+
break;
348+
} catch (error) {
349+
const timeElapsed = Date.now() - startTimestamp;
350+
let postAttempts = 0;
351+
let backoffDuration = getFixedBackoffDuration(timeElapsed);
352+
if (backoffDuration === undefined) {
353+
postAttempts += 1;
354+
backoffDuration = calculateDynamicBackoffDuration(postAttempts);
355+
}
356+
await new Promise(resolve => setTimeout(resolve, backoffDuration));
357+
console.warn("Failed to load configuration settings at startup. Retrying...");
358+
}
359+
}
360+
}
361+
}
362+
323363
/**
324364
* Inspects the feature management package version.
325365
*/

src/AzureAppConfigurationOptions.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ import { KeyVaultOptions } from "./keyvault/KeyVaultOptions.js";
66
import { RefreshOptions } from "./RefreshOptions.js";
77
import { SettingSelector } from "./types.js";
88
import { FeatureFlagOptions } from "./featureManagement/FeatureFlagOptions.js";
9-
10-
export const MaxRetries = 2;
11-
export const MaxRetryDelayInMs = 60000;
9+
import { StartupOptions } from "./StartupOptions.js";
1210

1311
export interface AzureAppConfigurationOptions {
1412
/**
@@ -48,6 +46,11 @@ export interface AzureAppConfigurationOptions {
4846
*/
4947
featureFlagOptions?: FeatureFlagOptions;
5048

49+
/**
50+
* Specifies options used to configure provider startup.
51+
*/
52+
startupOptions?: StartupOptions;
53+
5154
/**
5255
* Specifies whether to enable replica discovery or not.
5356
*

src/ConfigurationClientManager.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration";
55
import { ConfigurationClientWrapper } from "./ConfigurationClientWrapper.js";
66
import { TokenCredential } from "@azure/identity";
7-
import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions.js";
7+
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
88
import { isBrowser, isWebWorker } from "./requestTracing/utils.js";
99
import * as RequestTracing from "./requestTracing/constants.js";
1010
import { shuffleList } from "./common/utils.js";
1111

12+
// Configuration client retry options
13+
const CLIENT_MAX_RETRIES = 2;
14+
const CLIENT_MAX_RETRY_DELAY = 60_000; // 1 minute in milliseconds
15+
1216
const TCP_ORIGIN_KEY_NAME = "_origin._tcp";
1317
const ALT_KEY_NAME = "_alt";
1418
const TCP_KEY_NAME = "_tcp";
@@ -17,8 +21,8 @@ const ID_KEY_NAME = "Id";
1721
const SECRET_KEY_NAME = "Secret";
1822
const TRUSTED_DOMAIN_LABELS = [".azconfig.", ".appconfig."];
1923
const FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds
20-
const MINIMAL_CLIENT_REFRESH_INTERVAL = 30 * 1000; // 30 seconds in milliseconds
21-
const SRV_QUERY_TIMEOUT = 30 * 1000; // 30 seconds in milliseconds
24+
const MINIMAL_CLIENT_REFRESH_INTERVAL = 30_000; // 30 seconds in milliseconds
25+
const SRV_QUERY_TIMEOUT = 30_000; // 30 seconds in milliseconds
2226

2327
export class ConfigurationClientManager {
2428
#isFailoverable: boolean;
@@ -143,16 +147,16 @@ export class ConfigurationClientManager {
143147

144148
async #discoverFallbackClients(host: string) {
145149
let result;
146-
let timeout;
150+
let timer;
147151
try {
148152
result = await Promise.race([
149-
new Promise((_, reject) => timeout = setTimeout(() => reject(new Error("SRV record query timed out.")), SRV_QUERY_TIMEOUT)),
153+
new Promise((_, reject) => timer = setTimeout(() => reject(new Error("SRV record query timed out.")), SRV_QUERY_TIMEOUT)),
150154
this.#querySrvTargetHost(host)
151155
]);
152156
} catch (error) {
153-
throw new Error(`Failed to build fallback clients, ${error.message}`);
157+
throw new Error(`Failed to build fallback clients: ${error.message}`);
154158
} finally {
155-
clearTimeout(timeout);
159+
clearTimeout(timer);
156160
}
157161

158162
const srvTargetHosts = shuffleList(result) as string[];
@@ -269,8 +273,8 @@ function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurat
269273

270274
// retry options
271275
const defaultRetryOptions = {
272-
maxRetries: MaxRetries,
273-
maxRetryDelayInMs: MaxRetryDelayInMs,
276+
maxRetries: CLIENT_MAX_RETRIES,
277+
maxRetryDelayInMs: CLIENT_MAX_RETRY_DELAY,
274278
};
275279
const retryOptions = Object.assign({}, defaultRetryOptions, options?.clientOptions?.retryOptions);
276280

src/ConfigurationClientWrapper.ts

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

44
import { AppConfigurationClient } from "@azure/app-configuration";
5-
6-
const MaxBackoffDuration = 10 * 60 * 1000; // 10 minutes in milliseconds
7-
const MinBackoffDuration = 30 * 1000; // 30 seconds in milliseconds
8-
const MAX_SAFE_EXPONENTIAL = 30; // Used to avoid overflow. bitwise operations in JavaScript are limited to 32 bits. It overflows at 2^31 - 1.
9-
const JITTER_RATIO = 0.25;
5+
import { calculateDynamicBackoffDuration } from "./failover.js";
106

117
export class ConfigurationClientWrapper {
128
endpoint: string;
@@ -25,25 +21,7 @@ export class ConfigurationClientWrapper {
2521
this.backoffEndTime = Date.now();
2622
} else {
2723
this.#failedAttempts += 1;
28-
this.backoffEndTime = Date.now() + calculateBackoffDuration(this.#failedAttempts);
24+
this.backoffEndTime = Date.now() + calculateDynamicBackoffDuration(this.#failedAttempts);
2925
}
3026
}
3127
}
32-
33-
export function calculateBackoffDuration(failedAttempts: number) {
34-
if (failedAttempts <= 1) {
35-
return MinBackoffDuration;
36-
}
37-
38-
// exponential: minBackoff * 2 ^ (failedAttempts - 1)
39-
const exponential = Math.min(failedAttempts - 1, MAX_SAFE_EXPONENTIAL);
40-
let calculatedBackoffDuration = MinBackoffDuration * (1 << exponential);
41-
if (calculatedBackoffDuration > MaxBackoffDuration) {
42-
calculatedBackoffDuration = MaxBackoffDuration;
43-
}
44-
45-
// jitter: random value between [-1, 1) * jitterRatio * calculatedBackoffMs
46-
const jitter = JITTER_RATIO * (Math.random() * 2 - 1);
47-
48-
return calculatedBackoffDuration * (1 + jitter);
49-
}

src/StartupOptions.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
export interface StartupOptions {
5+
/**
6+
* The amount of time allowed to load data from Azure App Configuration on startup.
7+
*
8+
* @remarks
9+
* If not specified, the default value is 100 seconds.
10+
*/
11+
timeoutInMs?: number;
12+
}

src/common/utils.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ export function jsonSorter(key, value) {
2424
}
2525

2626
export function shuffleList<T>(array: T[]): T[] {
27-
for (let i = array.length - 1; i > 0; i--) {
28-
const j = Math.floor(Math.random() * (i + 1));
29-
[array[i], array[j]] = [array[j], array[i]];
30-
}
31-
return array;
27+
for (let i = array.length - 1; i > 0; i--) {
28+
const j = Math.floor(Math.random() * (i + 1));
29+
[array[i], array[j]] = [array[j], array[i]];
30+
}
31+
return array;
3232
}

src/failover.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
const MIN_BACKOFF_DURATION = 30_000; // 30 seconds in milliseconds
5+
const MAX_BACKOFF_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds
6+
const MAX_SAFE_EXPONENTIAL = 30; // Used to avoid overflow. bitwise operations in JavaScript are limited to 32 bits. It overflows at 2^31 - 1.
7+
const JITTER_RATIO = 0.25;
8+
9+
// Reference: https://github.com/Azure/AppConfiguration-DotnetProvider/blob/main/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/TimeSpanExtensions.cs#L14
10+
export function getFixedBackoffDuration(timeElapsed: number): number | undefined {
11+
if (timeElapsed <= 100_000) { // 100 seconds in milliseconds
12+
return 5_000; // 5 seconds in milliseconds
13+
}
14+
if (timeElapsed <= 200_000) { // 200 seconds in milliseconds
15+
return 10_000; // 10 seconds in milliseconds
16+
}
17+
if (timeElapsed <= 10 * 60 * 1000) { // 10 minutes in milliseconds
18+
return MIN_BACKOFF_DURATION;
19+
}
20+
return undefined;
21+
}
22+
23+
export function calculateDynamicBackoffDuration(failedAttempts: number) {
24+
if (failedAttempts <= 1) {
25+
return MIN_BACKOFF_DURATION;
26+
}
27+
28+
// exponential: minBackoff * 2 ^ (failedAttempts - 1)
29+
const exponential = Math.min(failedAttempts - 1, MAX_SAFE_EXPONENTIAL);
30+
let calculatedBackoffDuration = MIN_BACKOFF_DURATION * (1 << exponential);
31+
if (calculatedBackoffDuration > MAX_BACKOFF_DURATION) {
32+
calculatedBackoffDuration = MAX_BACKOFF_DURATION;
33+
}
34+
35+
// jitter: random value between [-1, 1) * jitterRatio * calculatedBackoffMs
36+
const jitter = JITTER_RATIO * (Math.random() * 2 - 1);
37+
38+
return calculatedBackoffDuration * (1 + jitter);
39+
}

src/load.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { AzureAppConfigurationImpl } from "./AzureAppConfigurationImpl.js";
77
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
88
import { ConfigurationClientManager, instanceOfTokenCredential } from "./ConfigurationClientManager.js";
99

10-
const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5000; // 5 seconds
10+
const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5_000; // 5 seconds
1111

1212
/**
1313
* Loads the data from Azure App Configuration service and returns an instance of AzureAppConfiguration.

0 commit comments

Comments
 (0)