Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2720628
support startup retry and timeout
zhiyuanliang-ms Feb 17, 2025
c15cf1b
update
zhiyuanliang-ms Feb 17, 2025
15c7e54
update
zhiyuanliang-ms Feb 17, 2025
90c4159
update
zhiyuanliang-ms Feb 17, 2025
a0e6543
add testcase
zhiyuanliang-ms Feb 17, 2025
435ff08
clarify error type
zhiyuanliang-ms Feb 19, 2025
326bf46
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Feb 19, 2025
6a8ceb7
update
zhiyuanliang-ms Feb 19, 2025
fc1aa5b
update
zhiyuanliang-ms Feb 19, 2025
7de8a0d
update
zhiyuanliang-ms Feb 20, 2025
5d23399
fix lint
zhiyuanliang-ms Feb 20, 2025
233af51
handle keyvault error
zhiyuanliang-ms Feb 20, 2025
a0e0792
update
zhiyuanliang-ms Feb 20, 2025
9e32db4
update
zhiyuanliang-ms Feb 21, 2025
3a33738
update
zhiyuanliang-ms Feb 23, 2025
c637682
update
zhiyuanliang-ms Feb 23, 2025
a9bcea4
update
zhiyuanliang-ms Feb 23, 2025
00e2e6b
update
zhiyuanliang-ms Feb 23, 2025
1478e94
handle keyvault reference error
zhiyuanliang-ms Feb 23, 2025
9b50135
update
zhiyuanliang-ms Feb 23, 2025
39d9a3d
fix lint
zhiyuanliang-ms Feb 23, 2025
f8b76ed
update
zhiyuanliang-ms Feb 26, 2025
7e63ad5
update
zhiyuanliang-ms Feb 26, 2025
fe9ad2f
add boot loop protection
zhiyuanliang-ms Feb 27, 2025
1a10c89
Merge branch 'zhiyuanliang/startup-timeout' of https://github.com/Azu…
zhiyuanliang-ms Feb 27, 2025
48e1147
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Feb 27, 2025
b19732d
update
zhiyuanliang-ms Mar 4, 2025
80108c9
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Mar 13, 2025
009ccd5
update
zhiyuanliang-ms Mar 13, 2025
d61aba9
update testcase
zhiyuanliang-ms Mar 13, 2025
3d88c7a
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Apr 23, 2025
73a24d4
update
zhiyuanliang-ms Apr 23, 2025
2c362ce
update testcase
zhiyuanliang-ms Apr 23, 2025
56f6265
update
zhiyuanliang-ms Apr 24, 2025
aa037e5
update
zhiyuanliang-ms Apr 25, 2025
3afb300
update
zhiyuanliang-ms Apr 25, 2025
f69be0d
move error.ts to common folder
zhiyuanliang-ms Apr 25, 2025
2f6585c
handle transient network error
zhiyuanliang-ms Apr 25, 2025
894a00e
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Apr 25, 2025
4b22c86
update
zhiyuanliang-ms Apr 25, 2025
a0f5f1e
update
zhiyuanliang-ms Apr 27, 2025
f1e683e
keep error stack when fail to load
zhiyuanliang-ms Apr 28, 2025
4a31659
update testcase
zhiyuanliang-ms Apr 28, 2025
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
60 changes: 54 additions & 6 deletions src/AzureAppConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurat
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
import { getFixedBackoffDuration, calculateDynamicBackoffDuration } from "./failover.js";

type PagedSettingSelector = SettingSelector & {
/**
Expand All @@ -48,6 +49,9 @@ type PagedSettingSelector = SettingSelector & {
pageEtags?: string[];
};

const DEFAULT_STARTUP_TIMEOUT = 100 * 1000; // 100 seconds in milliseconds
const MAX_STARTUP_TIMEOUT = 60 * 60 * 1000; // 60 minutes in milliseconds

export class AzureAppConfigurationImpl implements AzureAppConfiguration {
/**
* Hosting key-value pairs in the configuration store.
Expand Down Expand Up @@ -229,13 +233,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
* Loads the configuration store for the first time.
*/
async load() {
await this.#inspectFmPackage();
await this.#loadSelectedAndWatchedKeyValues();
if (this.#featureFlagEnabled) {
await this.#loadFeatureFlags();
const startupTimeout = this.#options?.startupOptions?.timeoutInMs ?? DEFAULT_STARTUP_TIMEOUT;
let timer;
try {
await Promise.race([
new Promise((_, reject) => timer = setTimeout(() => reject(new Error("Load operation timed out.")), startupTimeout)),
this.#initialize()
]);
} catch (error) {
throw new Error(`Failed to load: ${error.message}`);
} finally {
clearTimeout(timer);
}
// Mark all settings have loaded at startup.
this.#isInitialLoadCompleted = true;
}

/**
Expand Down Expand Up @@ -320,6 +329,45 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
return new Disposable(remove);
}

/**
* Initializes the configuration provider.
*/
async #initialize() {
if (!this.#isInitialLoadCompleted) {
await this.#inspectFmPackage();
const retryEnabled = this.#options?.startupOptions?.retryEnabled ?? false;
const startTimestamp = Date.now();
while (startTimestamp + MAX_STARTUP_TIMEOUT > Date.now()) {
try {
await this.#loadSelectedAndWatchedKeyValues();
if (this.#featureFlagEnabled) {
await this.#loadFeatureFlags();
}
// Mark all settings have loaded at startup.
this.#isInitialLoadCompleted = true;
break;
} catch (error) {
if (retryEnabled) {
const timeElapsed = Date.now() - startTimestamp;
let postAttempts = 0;
let backoffDuration = getFixedBackoffDuration(timeElapsed);
if (backoffDuration === undefined) {
postAttempts += 1;
backoffDuration = calculateDynamicBackoffDuration(postAttempts);
}
await new Promise(resolve => setTimeout(resolve, backoffDuration));
console.warn("Failed to load configuration settings at startup. Retrying...");
} else {
throw error;
}
}
}
if (!this.#isInitialLoadCompleted) {
throw new Error("Load operation exceeded the maximum startup timeout limitation.");
}
}
}

/**
* Inspects the feature management package version.
*/
Expand Down
9 changes: 6 additions & 3 deletions src/AzureAppConfigurationOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import { KeyVaultOptions } from "./keyvault/KeyVaultOptions.js";
import { RefreshOptions } from "./RefreshOptions.js";
import { SettingSelector } from "./types.js";
import { FeatureFlagOptions } from "./featureManagement/FeatureFlagOptions.js";

export const MaxRetries = 2;
export const MaxRetryDelayInMs = 60000;
import { StartupOptions } from "./StartupOptions.js";

export interface AzureAppConfigurationOptions {
/**
Expand Down Expand Up @@ -48,6 +46,11 @@ export interface AzureAppConfigurationOptions {
*/
featureFlagOptions?: FeatureFlagOptions;

/**
* Specifies options used to configure provider startup.
*/
startupOptions?: StartupOptions;

/**
* Specifies whether to enable replica discovery or not.
*
Expand Down
22 changes: 13 additions & 9 deletions src/ConfigurationClientManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration";
import { ConfigurationClientWrapper } from "./ConfigurationClientWrapper.js";
import { TokenCredential } from "@azure/identity";
import { AzureAppConfigurationOptions, MaxRetries, MaxRetryDelayInMs } from "./AzureAppConfigurationOptions.js";
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
import { isBrowser, isWebWorker } from "./requestTracing/utils.js";
import * as RequestTracing from "./requestTracing/constants.js";
import { shuffleList } from "./common/utils.js";

// Configuration client retry options
const CLIENT_MAX_RETRIES = 2;
const CLIENT_MAX_RETRY_DELAY = 60_000; // 1 minute in milliseconds

const TCP_ORIGIN_KEY_NAME = "_origin._tcp";
const ALT_KEY_NAME = "_alt";
const TCP_KEY_NAME = "_tcp";
Expand All @@ -17,8 +21,8 @@ const ID_KEY_NAME = "Id";
const SECRET_KEY_NAME = "Secret";
const TRUSTED_DOMAIN_LABELS = [".azconfig.", ".appconfig."];
const FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds
const MINIMAL_CLIENT_REFRESH_INTERVAL = 30 * 1000; // 30 seconds in milliseconds
const SRV_QUERY_TIMEOUT = 30 * 1000; // 30 seconds in milliseconds
const MINIMAL_CLIENT_REFRESH_INTERVAL = 30_000; // 30 seconds in milliseconds
const SRV_QUERY_TIMEOUT = 30_000; // 30 seconds in milliseconds

export class ConfigurationClientManager {
#isFailoverable: boolean;
Expand Down Expand Up @@ -143,16 +147,16 @@ export class ConfigurationClientManager {

async #discoverFallbackClients(host: string) {
let result;
let timeout;
let timer;
try {
result = await Promise.race([
new Promise((_, reject) => timeout = setTimeout(() => reject(new Error("SRV record query timed out.")), SRV_QUERY_TIMEOUT)),
new Promise((_, reject) => timer = setTimeout(() => reject(new Error("SRV record query timed out.")), SRV_QUERY_TIMEOUT)),
this.#querySrvTargetHost(host)
]);
} catch (error) {
throw new Error(`Failed to build fallback clients, ${error.message}`);
throw new Error(`Failed to build fallback clients: ${error.message}`);
} finally {
clearTimeout(timeout);
clearTimeout(timer);
}

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

// retry options
const defaultRetryOptions = {
maxRetries: MaxRetries,
maxRetryDelayInMs: MaxRetryDelayInMs,
maxRetries: CLIENT_MAX_RETRIES,
maxRetryDelayInMs: CLIENT_MAX_RETRY_DELAY,
};
const retryOptions = Object.assign({}, defaultRetryOptions, options?.clientOptions?.retryOptions);

Expand Down
26 changes: 2 additions & 24 deletions src/ConfigurationClientWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@
// Licensed under the MIT license.

import { AppConfigurationClient } from "@azure/app-configuration";

const MaxBackoffDuration = 10 * 60 * 1000; // 10 minutes in milliseconds
const MinBackoffDuration = 30 * 1000; // 30 seconds in milliseconds
const MAX_SAFE_EXPONENTIAL = 30; // Used to avoid overflow. bitwise operations in JavaScript are limited to 32 bits. It overflows at 2^31 - 1.
const JITTER_RATIO = 0.25;
import { calculateDynamicBackoffDuration } from "./failover.js";

export class ConfigurationClientWrapper {
endpoint: string;
Expand All @@ -25,25 +21,7 @@ export class ConfigurationClientWrapper {
this.backoffEndTime = Date.now();
} else {
this.#failedAttempts += 1;
this.backoffEndTime = Date.now() + calculateBackoffDuration(this.#failedAttempts);
this.backoffEndTime = Date.now() + calculateDynamicBackoffDuration(this.#failedAttempts);
}
}
}

export function calculateBackoffDuration(failedAttempts: number) {
if (failedAttempts <= 1) {
return MinBackoffDuration;
}

// exponential: minBackoff * 2 ^ (failedAttempts - 1)
const exponential = Math.min(failedAttempts - 1, MAX_SAFE_EXPONENTIAL);
let calculatedBackoffDuration = MinBackoffDuration * (1 << exponential);
if (calculatedBackoffDuration > MaxBackoffDuration) {
calculatedBackoffDuration = MaxBackoffDuration;
}

// jitter: random value between [-1, 1) * jitterRatio * calculatedBackoffMs
const jitter = JITTER_RATIO * (Math.random() * 2 - 1);

return calculatedBackoffDuration * (1 + jitter);
}
20 changes: 20 additions & 0 deletions src/StartupOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export interface StartupOptions {
/**
* Specifies whether to enable retry on startup or not.
*
* @remarks
* If not specified, the default value is false.
*/
retryEnabled?: boolean;

/**
* The amount of time allowed to load data from Azure App Configuration on startup.
*
* @remarks
* If not specified, the default value is 100 seconds.
*/
timeoutInMs?: number;
}
10 changes: 5 additions & 5 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export function jsonSorter(key, value) {
}

export function shuffleList<T>(array: T[]): T[] {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
39 changes: 39 additions & 0 deletions src/failover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

const MIN_BACKOFF_DURATION = 30_000; // 30 seconds in milliseconds
const MAX_BACKOFF_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds
const MAX_SAFE_EXPONENTIAL = 30; // Used to avoid overflow. bitwise operations in JavaScript are limited to 32 bits. It overflows at 2^31 - 1.
const JITTER_RATIO = 0.25;

// Reference: https://github.com/Azure/AppConfiguration-DotnetProvider/blob/main/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/TimeSpanExtensions.cs#L14
export function getFixedBackoffDuration(timeElapsed: number): number | undefined {
if (timeElapsed <= 100_000) { // 100 seconds in milliseconds
return 5_000; // 5 seconds in milliseconds
}
if (timeElapsed <= 200_000) { // 200 seconds in milliseconds
return 10_000; // 10 seconds in milliseconds
}
if (timeElapsed <= 10 * 60 * 1000) { // 10 minutes in milliseconds
return MIN_BACKOFF_DURATION;
}
return undefined;
}

export function calculateDynamicBackoffDuration(failedAttempts: number) {
if (failedAttempts <= 1) {
return MIN_BACKOFF_DURATION;
}

// exponential: minBackoff * 2 ^ (failedAttempts - 1)
const exponential = Math.min(failedAttempts - 1, MAX_SAFE_EXPONENTIAL);
let calculatedBackoffDuration = MIN_BACKOFF_DURATION * (1 << exponential);
if (calculatedBackoffDuration > MAX_BACKOFF_DURATION) {
calculatedBackoffDuration = MAX_BACKOFF_DURATION;
}

// jitter: random value between [-1, 1) * jitterRatio * calculatedBackoffMs
const jitter = JITTER_RATIO * (Math.random() * 2 - 1);

return calculatedBackoffDuration * (1 + jitter);
}
2 changes: 1 addition & 1 deletion src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { AzureAppConfigurationImpl } from "./AzureAppConfigurationImpl.js";
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
import { ConfigurationClientManager, instanceOfTokenCredential } from "./ConfigurationClientManager.js";

const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5000; // 5 seconds
const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5_000; // 5 seconds

/**
* Loads the data from Azure App Configuration service and returns an instance of AzureAppConfiguration.
Expand Down
Loading