@@ -35,6 +35,7 @@ import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAd
3535import { RefreshTimer } from "./refresh/RefreshTimer.js" ;
3636import { getConfigurationSettingWithTrace , listConfigurationSettingsWithTrace , requestTracingEnabled } from "./requestTracing/utils.js" ;
3737import { KeyFilter , LabelFilter , SettingSelector } from "./types.js" ;
38+ import { ConfigurationClientManager } from "./ConfigurationClientManager.js" ;
3839
3940type 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+ }
0 commit comments