@@ -15,6 +15,7 @@ import { RefreshTimer } from "./refresh/RefreshTimer.js";
1515import { getConfigurationSettingWithTrace , listConfigurationSettingsWithTrace , requestTracingEnabled } from "./requestTracing/utils.js" ;
1616import { KeyFilter , LabelFilter , SettingSelector } from "./types.js" ;
1717import { ConfigurationClientManager } from "./ConfigurationClientManager.js" ;
18+ import { updateClientBackoffStatus } from "./ConfigurationClientWrapper.js" ;
1819
1920type PagedSettingSelector = SettingSelector & {
2021 /**
@@ -41,6 +42,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
4142 #clientEndpoint: string | undefined ;
4243 #options: AzureAppConfigurationOptions | undefined ;
4344 #isInitialLoadCompleted: boolean = false ;
45+ #isFailoverRequest: boolean = false ;
4446
4547 // Refresh
4648 #refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS ;
@@ -59,13 +61,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
5961 #featureFlagSelectors: PagedSettingSelector [ ] = [ ] ;
6062
6163 constructor (
62- client : AppConfigurationClient ,
63- clientEndpoint : string | undefined ,
64- options : AzureAppConfigurationOptions | undefined
64+ clientManager : ConfigurationClientManager ,
65+ options : AzureAppConfigurationOptions | undefined ,
6566 ) {
66- this . #client = client ;
67- this . #clientEndpoint = clientEndpoint ;
6867 this . #options = options ;
68+ this . #clientManager = clientManager ;
6969
7070 // Enable request tracing if not opt-out
7171 this . #requestTracingEnabled = options ?. requestTracingOptions ?. enabled ?? requestTracingEnabled ( ) ;
@@ -178,34 +178,70 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
178178 return {
179179 requestTracingEnabled : this . #requestTracingEnabled,
180180 initialLoadCompleted : this . #isInitialLoadCompleted,
181- appConfigOptions : this . #options
181+ appConfigOptions : this . #options,
182+ isFailoverRequest : this . #isFailoverRequest
182183 } ;
183184 }
184185
185- async #loadSelectedKeyValues( ) : Promise < ConfigurationSetting [ ] > {
186- const loadedSettings : ConfigurationSetting [ ] = [ ] ;
186+ async #executeWithFailoverPolicy( funcToExecute ) {
187+ const clients = await this . #clientManager. getClients ( ) ;
188+ if ( clients . length === 0 ) {
189+ this . #clientManager. refreshClients ( ) ;
190+ throw new Error ( "No client is available to connect to the target App Configuration store." ) ;
191+ }
187192
188- // validate selectors
189- const selectors = getValidKeyValueSelectors ( this . #options?. selectors ) ;
193+ for ( const client of clients ) {
194+ let successful = false ;
195+ try {
196+ const result = await funcToExecute ( client . client ) ;
197+ this . #isFailoverRequest = false ;
198+ successful = true ;
199+ updateClientBackoffStatus ( client , successful ) ;
200+ return result ;
201+ } catch ( error ) {
202+ if ( isFailoverableError ( error ) ) {
203+ updateClientBackoffStatus ( client , successful ) ;
204+ this . #isFailoverRequest = true ;
205+ continue ;
206+ }
190207
191- for ( const selector of selectors ) {
192- const listOptions : ListConfigurationSettingsOptions = {
193- keyFilter : selector . keyFilter ,
194- labelFilter : selector . labelFilter
195- } ;
208+ throw error ;
209+ }
210+ }
196211
197- const settings = listConfigurationSettingsWithTrace (
198- this . #requestTraceOptions,
199- this . #client,
200- listOptions
201- ) ;
212+ this . #clientManager. refreshClients ( ) ;
213+ throw new Error ( "All app configuration clients failed to get settings." ) ;
214+ }
202215
203- for await ( const setting of settings ) {
204- if ( ! isFeatureFlag ( setting ) ) { // exclude feature flags
205- loadedSettings . push ( setting ) ;
216+ async #loadSelectedKeyValues( ) : Promise < ConfigurationSetting [ ] > {
217+ // validate selectors
218+ const selectors = getValidKeyValueSelectors ( this . #options?. selectors ) ;
219+ let loadedSettings : ConfigurationSetting [ ] = [ ] ;
220+
221+ const funcToExecute = async ( client ) => {
222+ const loadedSettings : ConfigurationSetting [ ] = [ ] ;
223+ for ( const selector of selectors ) {
224+ const listOptions : ListConfigurationSettingsOptions = {
225+ keyFilter : selector . keyFilter ,
226+ labelFilter : selector . labelFilter
227+ } ;
228+
229+ const settings = listConfigurationSettingsWithTrace (
230+ this . #requestTraceOptions,
231+ client ,
232+ listOptions
233+ ) ;
234+
235+ for await ( const setting of settings ) {
236+ if ( ! isFeatureFlag ( setting ) ) { // exclude feature flags
237+ loadedSettings . push ( setting ) ;
238+ }
206239 }
207240 }
208- }
241+ return loadedSettings ;
242+ } ;
243+
244+ loadedSettings = await this . #executeWithFailoverPolicy( funcToExecute ) ;
209245 return loadedSettings ;
210246 }
211247
@@ -260,30 +296,43 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
260296 }
261297
262298 async #loadFeatureFlags( ) {
263- const featureFlagSettings : ConfigurationSetting [ ] = [ ] ;
264- for ( const selector of this . #featureFlagSelectors ) {
265- const listOptions : ListConfigurationSettingsOptions = {
266- keyFilter : ` ${ featureFlagPrefix } ${ selector . keyFilter } ` ,
267- labelFilter : selector . labelFilter
268- } ;
299+ // Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting
300+ const funcToExecute = async ( client ) => {
301+ const featureFlagSettings : ConfigurationSetting [ ] = [ ] ;
302+ const selectors = JSON . parse (
303+ JSON . stringify ( this . #featureFlagSelectors )
304+ ) ;
269305
270- const pageEtags : string [ ] = [ ] ;
271- const pageIterator = listConfigurationSettingsWithTrace (
272- this . #requestTraceOptions,
273- this . #client,
274- listOptions
275- ) . byPage ( ) ;
276- for await ( const page of pageIterator ) {
277- pageEtags . push ( page . etag ?? "" ) ;
278- for ( const setting of page . items ) {
279- if ( isFeatureFlag ( setting ) ) {
280- featureFlagSettings . push ( setting ) ;
306+ for ( const selector of selectors ) {
307+ const listOptions : ListConfigurationSettingsOptions = {
308+ keyFilter : `${ featureFlagPrefix } ${ selector . keyFilter } ` ,
309+ labelFilter : selector . labelFilter
310+ } ;
311+
312+ const pageEtags : string [ ] = [ ] ;
313+ const pageIterator = listConfigurationSettingsWithTrace (
314+ this . #requestTraceOptions,
315+ client ,
316+ listOptions
317+ ) . byPage ( ) ;
318+ for await ( const page of pageIterator ) {
319+ pageEtags . push ( page . etag ?? "" ) ;
320+ for ( const setting of page . items ) {
321+ if ( isFeatureFlag ( setting ) ) {
322+ featureFlagSettings . push ( setting ) ;
323+ }
281324 }
282325 }
326+ selector . pageEtags = pageEtags ;
283327 }
284- selector . pageEtags = pageEtags ;
328+
329+ this . #featureFlagSelectors = selectors ;
330+ return featureFlagSettings ;
285331 }
286332
333+ let featureFlagSettings : ConfigurationSetting [ ] = [ ] ;
334+ featureFlagSettings = await this . #executeWithFailoverPolicy( funcToExecute ) ;
335+
287336 // parse feature flags
288337 const featureFlags = await Promise . all (
289338 featureFlagSettings . map ( setting => this . #parseFeatureFlag( setting ) )
@@ -435,30 +484,32 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
435484 }
436485
437486 // check if any feature flag is changed
438- let needRefresh = false ;
439- for ( const selector of this . #featureFlagSelectors) {
440- const listOptions : ListConfigurationSettingsOptions = {
441- keyFilter : `${ featureFlagPrefix } ${ selector . keyFilter } ` ,
442- labelFilter : selector . labelFilter ,
443- pageEtags : selector . pageEtags
444- } ;
445- const pageIterator = listConfigurationSettingsWithTrace (
446- this . #requestTraceOptions,
447- this . #client,
448- listOptions
449- ) . byPage ( ) ;
450-
451- for await ( const page of pageIterator ) {
452- if ( page . _response . status === 200 ) { // created or changed
453- needRefresh = true ;
454- break ;
487+ const funcToExecute = async ( client ) => {
488+ const needRefresh = false ;
489+ for ( const selector of this . #featureFlagSelectors) {
490+ const listOptions : ListConfigurationSettingsOptions = {
491+ keyFilter : `${ featureFlagPrefix } ${ selector . keyFilter } ` ,
492+ labelFilter : selector . labelFilter ,
493+ pageEtags : selector . pageEtags
494+ } ;
495+
496+ const pageIterator = listConfigurationSettingsWithTrace (
497+ this . #requestTraceOptions,
498+ client ,
499+ listOptions
500+ ) . byPage ( ) ;
501+
502+ for await ( const page of pageIterator ) {
503+ if ( page . _response . status === 200 ) { // created or changed
504+ return true ;
505+ }
455506 }
456507 }
457-
458- if ( needRefresh ) {
459- break ; // short-circuit if result from any of the selectors is changed
460- }
461- }
508+ return needRefresh ;
509+ } ;
510+
511+ let needRefresh : boolean ;
512+ needRefresh = await this . #executeWithFailoverPolicy ( funcToExecute ) ;
462513
463514 if ( needRefresh ) {
464515 try {
@@ -521,14 +572,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
521572 * Get a configuration setting by key and label. If the setting is not found, return undefine instead of throwing an error.
522573 */
523574 async #getConfigurationSetting( configurationSettingId : ConfigurationSettingId , customOptions ?: GetConfigurationSettingOptions ) : Promise < GetConfigurationSettingResponse | undefined > {
524- let response : GetConfigurationSettingResponse | undefined ;
525- try {
526- response = await getConfigurationSettingWithTrace (
575+ const funcToExecute = async ( client ) => {
576+ return getConfigurationSettingWithTrace (
527577 this . #requestTraceOptions,
528- this . # client,
578+ client ,
529579 configurationSettingId ,
530580 customOptions
531581 ) ;
582+ } ;
583+
584+ let response : GetConfigurationSettingResponse | undefined ;
585+ try {
586+ response = await this . #executeWithFailoverPolicy( funcToExecute ) ;
532587 } catch ( error ) {
533588 if ( isRestError ( error ) && error . statusCode === 404 ) {
534589 response = undefined ;
@@ -659,3 +714,7 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
659714 return getValidSelectors ( selectors ) ;
660715 }
661716}
717+
718+ function isFailoverableError ( error : any ) : boolean {
719+ return isRestError ( error ) && ( error . statusCode === 408 || error . statusCode === 429 || ( error . statusCode !== undefined && error . statusCode >= 500 ) ) ;
720+ }
0 commit comments