@@ -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,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+ }
0 commit comments