Skip to content

Commit 2a7399b

Browse files
merge preview
2 parents 2b78b27 + 477f18d commit 2a7399b

14 files changed

+716
-281
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.

rollup.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import dts from "rollup-plugin-dts";
44

55
export default [
66
{
7-
external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto"],
7+
external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto", "dns/promises"],
88
input: "src/index.ts",
99
output: [
1010
{

src/AzureAppConfigurationImpl.ts

Lines changed: 127 additions & 87 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,11 +57,11 @@ 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
#isCdnUsed: boolean;
6363
#isInitialLoadCompleted: boolean = false;
64+
#isFailoverRequest: boolean = false;
6465

6566
// Refresh
6667
#refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
@@ -79,15 +80,13 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
7980
#featureFlagSelectors: PagedSettingSelector[] = [];
8081

8182
constructor(
82-
client: AppConfigurationClient,
83-
clientEndpoint: string | undefined,
83+
clientManager: ConfigurationClientManager,
8484
options: AzureAppConfigurationOptions | undefined,
8585
isCdnUsed: boolean
8686
) {
87-
this.#client = client;
88-
this.#clientEndpoint = clientEndpoint;
8987
this.#options = options;
9088
this.#isCdnUsed = isCdnUsed;
89+
this.#clientManager = clientManager;
9190

9291
// Enable request tracing if not opt-out
9392
this.#requestTracingEnabled = requestTracingEnabled();
@@ -201,35 +200,66 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
201200
requestTracingEnabled: this.#requestTracingEnabled,
202201
initialLoadCompleted: this.#isInitialLoadCompleted,
203202
isCdnUsed: this.#isCdnUsed,
204-
appConfigOptions: this.#options
203+
appConfigOptions: this.#options,
204+
isFailoverRequest: this.#isFailoverRequest
205205
};
206206
}
207207

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

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

214-
for (const selector of selectors) {
215-
const listOptions: ListConfigurationSettingsOptions = {
216-
keyFilter: selector.keyFilter,
217-
labelFilter: selector.labelFilter
218-
};
227+
throw error;
228+
}
229+
}
219230

220-
const settings = listConfigurationSettingsWithTrace(
221-
this.#requestTraceOptions,
222-
this.#client,
223-
listOptions
224-
);
231+
this.#clientManager.refreshClients();
232+
throw new Error("All clients failed to get configuration settings.");
233+
}
225234

226-
for await (const setting of settings) {
227-
if (!isFeatureFlag(setting)) { // exclude feature flags
228-
loadedSettings.push(setting);
235+
async #loadSelectedKeyValues(): Promise<ConfigurationSetting[]> {
236+
// validate selectors
237+
const selectors = getValidKeyValueSelectors(this.#options?.selectors);
238+
239+
const funcToExecute = async (client) => {
240+
const loadedSettings: ConfigurationSetting[] = [];
241+
for (const selector of selectors) {
242+
const listOptions: ListConfigurationSettingsOptions = {
243+
keyFilter: selector.keyFilter,
244+
labelFilter: selector.labelFilter
245+
};
246+
247+
const settings = listConfigurationSettingsWithTrace(
248+
this.#requestTraceOptions,
249+
client,
250+
listOptions
251+
);
252+
253+
for await (const setting of settings) {
254+
if (!isFeatureFlag(setting)) { // exclude feature flags
255+
loadedSettings.push(setting);
256+
}
229257
}
230258
}
231-
}
232-
return loadedSettings;
259+
return loadedSettings;
260+
};
261+
262+
return await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[];
233263
}
234264

235265
/**
@@ -283,29 +313,42 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
283313
}
284314

285315
async #loadFeatureFlags() {
286-
const featureFlagSettings: ConfigurationSetting[] = [];
287-
for (const selector of this.#featureFlagSelectors) {
288-
const listOptions: ListConfigurationSettingsOptions = {
289-
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
290-
labelFilter: selector.labelFilter
291-
};
316+
// Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting
317+
const funcToExecute = async (client) => {
318+
const featureFlagSettings: ConfigurationSetting[] = [];
319+
// deep copy selectors to avoid modification if current client fails
320+
const selectors = JSON.parse(
321+
JSON.stringify(this.#featureFlagSelectors)
322+
);
292323

293-
const pageEtags: string[] = [];
294-
const pageIterator = listConfigurationSettingsWithTrace(
295-
this.#requestTraceOptions,
296-
this.#client,
297-
listOptions
298-
).byPage();
299-
for await (const page of pageIterator) {
300-
pageEtags.push(page.etag ?? "");
301-
for (const setting of page.items) {
302-
if (isFeatureFlag(setting)) {
303-
featureFlagSettings.push(setting);
324+
for (const selector of selectors) {
325+
const listOptions: ListConfigurationSettingsOptions = {
326+
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
327+
labelFilter: selector.labelFilter
328+
};
329+
330+
const pageEtags: string[] = [];
331+
const pageIterator = listConfigurationSettingsWithTrace(
332+
this.#requestTraceOptions,
333+
client,
334+
listOptions
335+
).byPage();
336+
for await (const page of pageIterator) {
337+
pageEtags.push(page.etag ?? "");
338+
for (const setting of page.items) {
339+
if (isFeatureFlag(setting)) {
340+
featureFlagSettings.push(setting);
341+
}
304342
}
305343
}
344+
selector.pageEtags = pageEtags;
306345
}
307-
selector.pageEtags = pageEtags;
308-
}
346+
347+
this.#featureFlagSelectors = selectors;
348+
return featureFlagSettings;
349+
};
350+
351+
const featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[];
309352

310353
// parse feature flags
311354
const featureFlags = await Promise.all(
@@ -393,7 +436,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
393436
// check if any refresh task failed
394437
for (const result of results) {
395438
if (result.status === "rejected") {
396-
throw result.reason;
439+
console.warn("Refresh failed:", result.reason);
397440
}
398441
}
399442

@@ -434,13 +477,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
434477
}
435478

436479
if (needRefresh) {
437-
try {
438-
await this.#loadSelectedAndWatchedKeyValues();
439-
} catch (error) {
440-
// if refresh failed, backoff
441-
this.#refreshTimer.backoff();
442-
throw error;
443-
}
480+
await this.#loadSelectedAndWatchedKeyValues();
444481
}
445482

446483
this.#refreshTimer.reset();
@@ -458,39 +495,32 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
458495
}
459496

460497
// check if any feature flag is changed
461-
let needRefresh = false;
462-
for (const selector of this.#featureFlagSelectors) {
463-
const listOptions: ListConfigurationSettingsOptions = {
464-
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
465-
labelFilter: selector.labelFilter,
466-
pageEtags: selector.pageEtags
467-
};
468-
const pageIterator = listConfigurationSettingsWithTrace(
469-
this.#requestTraceOptions,
470-
this.#client,
471-
listOptions
472-
).byPage();
473-
474-
for await (const page of pageIterator) {
475-
if (page._response.status === 200) { // created or changed
476-
needRefresh = true;
477-
break;
498+
const funcToExecute = async (client) => {
499+
for (const selector of this.#featureFlagSelectors) {
500+
const listOptions: ListConfigurationSettingsOptions = {
501+
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
502+
labelFilter: selector.labelFilter,
503+
pageEtags: selector.pageEtags
504+
};
505+
506+
const pageIterator = listConfigurationSettingsWithTrace(
507+
this.#requestTraceOptions,
508+
client,
509+
listOptions
510+
).byPage();
511+
512+
for await (const page of pageIterator) {
513+
if (page._response.status === 200) { // created or changed
514+
return true;
515+
}
478516
}
479517
}
518+
return false;
519+
};
480520

481-
if (needRefresh) {
482-
break; // short-circuit if result from any of the selectors is changed
483-
}
484-
}
485-
521+
const needRefresh: boolean = await this.#executeWithFailoverPolicy(funcToExecute);
486522
if (needRefresh) {
487-
try {
488-
await this.#loadFeatureFlags();
489-
} catch (error) {
490-
// if refresh failed, backoff
491-
this.#featureFlagRefreshTimer.backoff();
492-
throw error;
493-
}
523+
await this.#loadFeatureFlags();
494524
}
495525

496526
this.#featureFlagRefreshTimer.reset();
@@ -544,14 +574,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
544574
* Get a configuration setting by key and label. If the setting is not found, return undefine instead of throwing an error.
545575
*/
546576
async #getConfigurationSetting(configurationSettingId: ConfigurationSettingId, customOptions?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse | undefined> {
547-
let response: GetConfigurationSettingResponse | undefined;
548-
try {
549-
response = await getConfigurationSettingWithTrace(
577+
const funcToExecute = async (client) => {
578+
return getConfigurationSettingWithTrace(
550579
this.#requestTraceOptions,
551-
this.#client,
580+
client,
552581
configurationSettingId,
553582
customOptions
554583
);
584+
};
585+
586+
let response: GetConfigurationSettingResponse | undefined;
587+
try {
588+
response = await this.#executeWithFailoverPolicy(funcToExecute);
555589
} catch (error) {
556590
if (isRestError(error) && error.statusCode === 404) {
557591
response = undefined;
@@ -638,7 +672,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
638672
}
639673

640674
#createFeatureFlagReference(setting: ConfigurationSetting<string>): string {
641-
let featureFlagReference = `${this.#clientEndpoint}kv/${setting.key}`;
675+
let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`;
642676
if (setting.label && setting.label.trim().length !== 0) {
643677
featureFlagReference += `?label=${setting.label}`;
644678
}
@@ -798,3 +832,9 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
798832
return getValidSelectors(selectors);
799833
}
800834
}
835+
836+
function isFailoverableError(error: any): boolean {
837+
// ENOTFOUND: DNS lookup failed, ENOENT: no such file or directory
838+
return isRestError(error) && (error.code === "ENOTFOUND" || error.code === "ENOENT" ||
839+
(error.statusCode !== undefined && (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 408 || error.statusCode === 429 || error.statusCode >= 500)));
840+
}

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)