Skip to content

Commit 7db665f

Browse files
Failover support (#98)
* resolve conflicts * resolve conflicts * resolve conflicts * add tests * resolve conflicts and update * fix lint * resolve conflicts * resolve comments * update package-lock * update * update * update failover error * update * update * update failoverable error with 'ENOTFOUND' * fix lint * update * added ENOENT error * update * update error message in test * update test * update test * update * resolve conflicts
1 parent a251f82 commit 7db665f

13 files changed

+700
-285
lines changed

package-lock.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/AzureAppConfigurationImpl.ts

Lines changed: 128 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAd
3535
import { RefreshTimer } from "./refresh/RefreshTimer.js";
3636
import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
3737
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
38+
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
3839

3940
type PagedSettingSelector = SettingSelector & {
4041
/**
@@ -56,10 +57,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
5657
*/
5758
#sortedTrimKeyPrefixes: string[] | undefined;
5859
readonly #requestTracingEnabled: boolean;
59-
#client: AppConfigurationClient;
60-
#clientEndpoint: string | undefined;
60+
#clientManager: ConfigurationClientManager;
6161
#options: AzureAppConfigurationOptions | undefined;
6262
#isInitialLoadCompleted: boolean = false;
63+
#isFailoverRequest: boolean = false;
6364

6465
// Refresh
6566
#refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
@@ -78,13 +79,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
7879
#featureFlagSelectors: PagedSettingSelector[] = [];
7980

8081
constructor(
81-
client: AppConfigurationClient,
82-
clientEndpoint: string | undefined,
83-
options: AzureAppConfigurationOptions | undefined
82+
clientManager: ConfigurationClientManager,
83+
options: AzureAppConfigurationOptions | undefined,
8484
) {
85-
this.#client = client;
86-
this.#clientEndpoint = clientEndpoint;
8785
this.#options = options;
86+
this.#clientManager = clientManager;
8887

8988
// Enable request tracing if not opt-out
9089
this.#requestTracingEnabled = requestTracingEnabled();
@@ -197,35 +196,66 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
197196
return {
198197
requestTracingEnabled: this.#requestTracingEnabled,
199198
initialLoadCompleted: this.#isInitialLoadCompleted,
200-
appConfigOptions: this.#options
199+
appConfigOptions: this.#options,
200+
isFailoverRequest: this.#isFailoverRequest
201201
};
202202
}
203203

204-
async #loadSelectedKeyValues(): Promise<ConfigurationSetting[]> {
205-
const loadedSettings: ConfigurationSetting[] = [];
204+
async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise<any>): Promise<any> {
205+
const clientWrappers = await this.#clientManager.getClients();
206206

207-
// validate selectors
208-
const selectors = getValidKeyValueSelectors(this.#options?.selectors);
207+
let successful: boolean;
208+
for (const clientWrapper of clientWrappers) {
209+
successful = false;
210+
try {
211+
const result = await funcToExecute(clientWrapper.client);
212+
this.#isFailoverRequest = false;
213+
successful = true;
214+
clientWrapper.updateBackoffStatus(successful);
215+
return result;
216+
} catch (error) {
217+
if (isFailoverableError(error)) {
218+
clientWrapper.updateBackoffStatus(successful);
219+
this.#isFailoverRequest = true;
220+
continue;
221+
}
209222

210-
for (const selector of selectors) {
211-
const listOptions: ListConfigurationSettingsOptions = {
212-
keyFilter: selector.keyFilter,
213-
labelFilter: selector.labelFilter
214-
};
223+
throw error;
224+
}
225+
}
215226

216-
const settings = listConfigurationSettingsWithTrace(
217-
this.#requestTraceOptions,
218-
this.#client,
219-
listOptions
220-
);
227+
this.#clientManager.refreshClients();
228+
throw new Error("All clients failed to get configuration settings.");
229+
}
221230

222-
for await (const setting of settings) {
223-
if (!isFeatureFlag(setting)) { // exclude feature flags
224-
loadedSettings.push(setting);
231+
async #loadSelectedKeyValues(): Promise<ConfigurationSetting[]> {
232+
// validate selectors
233+
const selectors = getValidKeyValueSelectors(this.#options?.selectors);
234+
235+
const funcToExecute = async (client) => {
236+
const loadedSettings: ConfigurationSetting[] = [];
237+
for (const selector of selectors) {
238+
const listOptions: ListConfigurationSettingsOptions = {
239+
keyFilter: selector.keyFilter,
240+
labelFilter: selector.labelFilter
241+
};
242+
243+
const settings = listConfigurationSettingsWithTrace(
244+
this.#requestTraceOptions,
245+
client,
246+
listOptions
247+
);
248+
249+
for await (const setting of settings) {
250+
if (!isFeatureFlag(setting)) { // exclude feature flags
251+
loadedSettings.push(setting);
252+
}
225253
}
226254
}
227-
}
228-
return loadedSettings;
255+
return loadedSettings;
256+
};
257+
258+
return await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[];
229259
}
230260

231261
/**
@@ -279,29 +309,42 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
279309
}
280310

281311
async #loadFeatureFlags() {
282-
const featureFlagSettings: ConfigurationSetting[] = [];
283-
for (const selector of this.#featureFlagSelectors) {
284-
const listOptions: ListConfigurationSettingsOptions = {
285-
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
286-
labelFilter: selector.labelFilter
287-
};
312+
// Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting
313+
const funcToExecute = async (client) => {
314+
const featureFlagSettings: ConfigurationSetting[] = [];
315+
// deep copy selectors to avoid modification if current client fails
316+
const selectors = JSON.parse(
317+
JSON.stringify(this.#featureFlagSelectors)
318+
);
288319

289-
const pageEtags: string[] = [];
290-
const pageIterator = listConfigurationSettingsWithTrace(
291-
this.#requestTraceOptions,
292-
this.#client,
293-
listOptions
294-
).byPage();
295-
for await (const page of pageIterator) {
296-
pageEtags.push(page.etag ?? "");
297-
for (const setting of page.items) {
298-
if (isFeatureFlag(setting)) {
299-
featureFlagSettings.push(setting);
320+
for (const selector of selectors) {
321+
const listOptions: ListConfigurationSettingsOptions = {
322+
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
323+
labelFilter: selector.labelFilter
324+
};
325+
326+
const pageEtags: string[] = [];
327+
const pageIterator = listConfigurationSettingsWithTrace(
328+
this.#requestTraceOptions,
329+
client,
330+
listOptions
331+
).byPage();
332+
for await (const page of pageIterator) {
333+
pageEtags.push(page.etag ?? "");
334+
for (const setting of page.items) {
335+
if (isFeatureFlag(setting)) {
336+
featureFlagSettings.push(setting);
337+
}
300338
}
301339
}
340+
selector.pageEtags = pageEtags;
302341
}
303-
selector.pageEtags = pageEtags;
304-
}
342+
343+
this.#featureFlagSelectors = selectors;
344+
return featureFlagSettings;
345+
};
346+
347+
const featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[];
305348

306349
// parse feature flags
307350
const featureFlags = await Promise.all(
@@ -389,7 +432,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
389432
// check if any refresh task failed
390433
for (const result of results) {
391434
if (result.status === "rejected") {
392-
throw result.reason;
435+
console.warn("Refresh failed:", result.reason);
393436
}
394437
}
395438

@@ -430,13 +473,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
430473
}
431474

432475
if (needRefresh) {
433-
try {
434-
await this.#loadSelectedAndWatchedKeyValues();
435-
} catch (error) {
436-
// if refresh failed, backoff
437-
this.#refreshTimer.backoff();
438-
throw error;
439-
}
476+
await this.#loadSelectedAndWatchedKeyValues();
440477
}
441478

442479
this.#refreshTimer.reset();
@@ -454,39 +491,32 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
454491
}
455492

456493
// check if any feature flag is changed
457-
let needRefresh = false;
458-
for (const selector of this.#featureFlagSelectors) {
459-
const listOptions: ListConfigurationSettingsOptions = {
460-
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
461-
labelFilter: selector.labelFilter,
462-
pageEtags: selector.pageEtags
463-
};
464-
const pageIterator = listConfigurationSettingsWithTrace(
465-
this.#requestTraceOptions,
466-
this.#client,
467-
listOptions
468-
).byPage();
469-
470-
for await (const page of pageIterator) {
471-
if (page._response.status === 200) { // created or changed
472-
needRefresh = true;
473-
break;
494+
const funcToExecute = async (client) => {
495+
for (const selector of this.#featureFlagSelectors) {
496+
const listOptions: ListConfigurationSettingsOptions = {
497+
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
498+
labelFilter: selector.labelFilter,
499+
pageEtags: selector.pageEtags
500+
};
501+
502+
const pageIterator = listConfigurationSettingsWithTrace(
503+
this.#requestTraceOptions,
504+
client,
505+
listOptions
506+
).byPage();
507+
508+
for await (const page of pageIterator) {
509+
if (page._response.status === 200) { // created or changed
510+
return true;
511+
}
474512
}
475513
}
514+
return false;
515+
};
476516

477-
if (needRefresh) {
478-
break; // short-circuit if result from any of the selectors is changed
479-
}
480-
}
481-
517+
const needRefresh: boolean = await this.#executeWithFailoverPolicy(funcToExecute);
482518
if (needRefresh) {
483-
try {
484-
await this.#loadFeatureFlags();
485-
} catch (error) {
486-
// if refresh failed, backoff
487-
this.#featureFlagRefreshTimer.backoff();
488-
throw error;
489-
}
519+
await this.#loadFeatureFlags();
490520
}
491521

492522
this.#featureFlagRefreshTimer.reset();
@@ -540,14 +570,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
540570
* Get a configuration setting by key and label. If the setting is not found, return undefine instead of throwing an error.
541571
*/
542572
async #getConfigurationSetting(configurationSettingId: ConfigurationSettingId, customOptions?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse | undefined> {
543-
let response: GetConfigurationSettingResponse | undefined;
544-
try {
545-
response = await getConfigurationSettingWithTrace(
573+
const funcToExecute = async (client) => {
574+
return getConfigurationSettingWithTrace(
546575
this.#requestTraceOptions,
547-
this.#client,
576+
client,
548577
configurationSettingId,
549578
customOptions
550579
);
580+
};
581+
582+
let response: GetConfigurationSettingResponse | undefined;
583+
try {
584+
response = await this.#executeWithFailoverPolicy(funcToExecute);
551585
} catch (error) {
552586
if (isRestError(error) && error.statusCode === 404) {
553587
response = undefined;
@@ -634,7 +668,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
634668
}
635669

636670
#createFeatureFlagReference(setting: ConfigurationSetting<string>): string {
637-
let featureFlagReference = `${this.#clientEndpoint}kv/${setting.key}`;
671+
let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`;
638672
if (setting.label && setting.label.trim().length !== 0) {
639673
featureFlagReference += `?label=${setting.label}`;
640674
}
@@ -794,3 +828,9 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
794828
return getValidSelectors(selectors);
795829
}
796830
}
831+
832+
function isFailoverableError(error: any): boolean {
833+
// ENOTFOUND: DNS lookup failed, ENOENT: no such file or directory
834+
return isRestError(error) && (error.code === "ENOTFOUND" || error.code === "ENOENT" ||
835+
(error.statusCode !== undefined && (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500)));
836+
}

src/AzureAppConfigurationOptions.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const MaxRetryDelayInMs = 60000;
1212

1313
export interface AzureAppConfigurationOptions {
1414
/**
15-
* Specify what key-values to include in the configuration provider.
15+
* Specifies what key-values to include in the configuration provider.
1616
*
1717
* @remarks
1818
* If no selectors are specified then all key-values with no label will be included.
@@ -47,4 +47,12 @@ export interface AzureAppConfigurationOptions {
4747
* Specifies options used to configure feature flags.
4848
*/
4949
featureFlagOptions?: FeatureFlagOptions;
50+
51+
/**
52+
* Specifies whether to enable replica discovery or not.
53+
*
54+
* @remarks
55+
* If not specified, the default value is true.
56+
*/
57+
replicaDiscoveryEnabled?: boolean;
5058
}

0 commit comments

Comments
 (0)