@@ -35,6 +35,7 @@ import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAd
35
35
import { RefreshTimer } from "./refresh/RefreshTimer.js" ;
36
36
import { getConfigurationSettingWithTrace , listConfigurationSettingsWithTrace , requestTracingEnabled } from "./requestTracing/utils.js" ;
37
37
import { KeyFilter , LabelFilter , SettingSelector } from "./types.js" ;
38
+ import { ConfigurationClientManager } from "./ConfigurationClientManager.js" ;
38
39
39
40
type PagedSettingSelector = SettingSelector & {
40
41
/**
@@ -56,10 +57,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
56
57
*/
57
58
#sortedTrimKeyPrefixes: string [ ] | undefined ;
58
59
readonly #requestTracingEnabled: boolean ;
59
- #client: AppConfigurationClient ;
60
- #clientEndpoint: string | undefined ;
60
+ #clientManager: ConfigurationClientManager ;
61
61
#options: AzureAppConfigurationOptions | undefined ;
62
62
#isInitialLoadCompleted: boolean = false ;
63
+ #isFailoverRequest: boolean = false ;
63
64
64
65
// Refresh
65
66
#refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS ;
@@ -78,13 +79,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
78
79
#featureFlagSelectors: PagedSettingSelector [ ] = [ ] ;
79
80
80
81
constructor (
81
- client : AppConfigurationClient ,
82
- clientEndpoint : string | undefined ,
83
- options : AzureAppConfigurationOptions | undefined
82
+ clientManager : ConfigurationClientManager ,
83
+ options : AzureAppConfigurationOptions | undefined ,
84
84
) {
85
- this . #client = client ;
86
- this . #clientEndpoint = clientEndpoint ;
87
85
this . #options = options ;
86
+ this . #clientManager = clientManager ;
88
87
89
88
// Enable request tracing if not opt-out
90
89
this . #requestTracingEnabled = requestTracingEnabled ( ) ;
@@ -197,35 +196,66 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
197
196
return {
198
197
requestTracingEnabled : this . #requestTracingEnabled,
199
198
initialLoadCompleted : this . #isInitialLoadCompleted,
200
- appConfigOptions : this . #options
199
+ appConfigOptions : this . #options,
200
+ isFailoverRequest : this . #isFailoverRequest
201
201
} ;
202
202
}
203
203
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 ( ) ;
206
206
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
+ }
209
222
210
- for ( const selector of selectors ) {
211
- const listOptions : ListConfigurationSettingsOptions = {
212
- keyFilter : selector . keyFilter ,
213
- labelFilter : selector . labelFilter
214
- } ;
223
+ throw error ;
224
+ }
225
+ }
215
226
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
+ }
221
230
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
+ }
225
253
}
226
254
}
227
- }
228
- return loadedSettings ;
255
+ return loadedSettings ;
256
+ } ;
257
+
258
+ return await this . #executeWithFailoverPolicy( funcToExecute ) as ConfigurationSetting [ ] ;
229
259
}
230
260
231
261
/**
@@ -279,29 +309,42 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
279
309
}
280
310
281
311
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
+ ) ;
288
319
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
+ }
300
338
}
301
339
}
340
+ selector . pageEtags = pageEtags ;
302
341
}
303
- selector . pageEtags = pageEtags ;
304
- }
342
+
343
+ this . #featureFlagSelectors = selectors ;
344
+ return featureFlagSettings ;
345
+ } ;
346
+
347
+ const featureFlagSettings = await this . #executeWithFailoverPolicy( funcToExecute ) as ConfigurationSetting [ ] ;
305
348
306
349
// parse feature flags
307
350
const featureFlags = await Promise . all (
@@ -389,7 +432,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
389
432
// check if any refresh task failed
390
433
for ( const result of results ) {
391
434
if ( result . status === "rejected" ) {
392
- throw result . reason ;
435
+ console . warn ( "Refresh failed:" , result . reason ) ;
393
436
}
394
437
}
395
438
@@ -430,13 +473,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
430
473
}
431
474
432
475
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( ) ;
440
477
}
441
478
442
479
this . #refreshTimer. reset ( ) ;
@@ -454,39 +491,32 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
454
491
}
455
492
456
493
// 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
+ }
474
512
}
475
513
}
514
+ return false ;
515
+ } ;
476
516
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 ) ;
482
518
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( ) ;
490
520
}
491
521
492
522
this . #featureFlagRefreshTimer. reset ( ) ;
@@ -540,14 +570,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
540
570
* Get a configuration setting by key and label. If the setting is not found, return undefine instead of throwing an error.
541
571
*/
542
572
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 (
546
575
this . #requestTraceOptions,
547
- this . # client,
576
+ client ,
548
577
configurationSettingId ,
549
578
customOptions
550
579
) ;
580
+ } ;
581
+
582
+ let response : GetConfigurationSettingResponse | undefined ;
583
+ try {
584
+ response = await this . #executeWithFailoverPolicy( funcToExecute ) ;
551
585
} catch ( error ) {
552
586
if ( isRestError ( error ) && error . statusCode === 404 ) {
553
587
response = undefined ;
@@ -634,7 +668,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
634
668
}
635
669
636
670
#createFeatureFlagReference( setting : ConfigurationSetting < string > ) : string {
637
- let featureFlagReference = `${ this . #clientEndpoint } kv/${ setting . key } ` ;
671
+ let featureFlagReference = `${ this . #clientManager . endpoint . origin } / kv/${ setting . key } ` ;
638
672
if ( setting . label && setting . label . trim ( ) . length !== 0 ) {
639
673
featureFlagReference += `?label=${ setting . label } ` ;
640
674
}
@@ -794,3 +828,9 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
794
828
return getValidSelectors ( selectors ) ;
795
829
}
796
830
}
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