@@ -15,6 +15,7 @@ import { RefreshTimer } from "./refresh/RefreshTimer";
1515import { getConfigurationSettingWithTrace , listConfigurationSettingsWithTrace , requestTracingEnabled } from "./requestTracing/utils" ;
1616import { KeyFilter , LabelFilter , SettingSelector } from "./types" ;
1717import { ConfigurationClientManager } from "./ConfigurationClientManager" ;
18+ import { updateClientBackoffStatus } from "./ConfigurationClientWrapper" ;
1819
1920type PagedSettingSelector = SettingSelector & {
2021 /**
@@ -40,6 +41,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
4041 #client: AppConfigurationClient ;
4142 #options: AzureAppConfigurationOptions | undefined ;
4243 #isInitialLoadCompleted: boolean = false ;
44+ #isFailoverRequest: boolean = false ;
4345
4446 // Refresh
4547 #refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS ;
@@ -58,11 +60,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
5860 #featureFlagSelectors: PagedSettingSelector [ ] = [ ] ;
5961
6062 constructor (
61- client : AppConfigurationClient ,
62- options : AzureAppConfigurationOptions | undefined
63+ clientManager : ConfigurationClientManager ,
64+ options : AzureAppConfigurationOptions | undefined ,
6365 ) {
64- this . #client = client ;
6566 this . #options = options ;
67+ this . #clientManager = clientManager ;
6668
6769 // Enable request tracing if not opt-out
6870 this . #requestTracingEnabled = requestTracingEnabled ( ) ;
@@ -175,34 +177,70 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
175177 return {
176178 requestTracingEnabled : this . #requestTracingEnabled,
177179 initialLoadCompleted : this . #isInitialLoadCompleted,
178- appConfigOptions : this . #options
180+ appConfigOptions : this . #options,
181+ isFailoverRequest : this . #isFailoverRequest
179182 } ;
180183 }
181184
182- async #loadSelectedKeyValues( ) : Promise < ConfigurationSetting [ ] > {
183- const loadedSettings : ConfigurationSetting [ ] = [ ] ;
185+ async #executeWithFailoverPolicy( funcToExecute ) {
186+ const clients = await this . #clientManager. getClients ( ) ;
187+ if ( clients . length === 0 ) {
188+ this . #clientManager. refreshClients ( ) ;
189+ throw new Error ( "No client is available to connect to the target App Configuration store." ) ;
190+ }
184191
185- // validate selectors
186- const selectors = getValidKeyValueSelectors ( this . #options?. selectors ) ;
192+ for ( const client of clients ) {
193+ let successful = false ;
194+ try {
195+ const result = await funcToExecute ( client . client ) ;
196+ this . #isFailoverRequest = false ;
197+ successful = true ;
198+ updateClientBackoffStatus ( client , successful ) ;
199+ return result ;
200+ } catch ( error ) {
201+ if ( isFailoverableError ( error ) ) {
202+ updateClientBackoffStatus ( client , successful ) ;
203+ this . #isFailoverRequest = true ;
204+ continue ;
205+ }
187206
188- for ( const selector of selectors ) {
189- const listOptions : ListConfigurationSettingsOptions = {
190- keyFilter : selector . keyFilter ,
191- labelFilter : selector . labelFilter
192- } ;
207+ throw error ;
208+ }
209+ }
193210
194- const settings = listConfigurationSettingsWithTrace (
195- this . #requestTraceOptions,
196- this . #client,
197- listOptions
198- ) ;
211+ this . #clientManager. refreshClients ( ) ;
212+ throw new Error ( "All app configuration clients failed to get settings." ) ;
213+ }
199214
200- for await ( const setting of settings ) {
201- if ( ! isFeatureFlag ( setting ) ) { // exclude feature flags
202- loadedSettings . push ( setting ) ;
215+ async #loadSelectedKeyValues( ) : Promise < ConfigurationSetting [ ] > {
216+ // validate selectors
217+ const selectors = getValidKeyValueSelectors ( this . #options?. selectors ) ;
218+ let loadedSettings : ConfigurationSetting [ ] = [ ] ;
219+
220+ const funcToExecute = async ( client ) => {
221+ const loadedSettings : ConfigurationSetting [ ] = [ ] ;
222+ for ( const selector of selectors ) {
223+ const listOptions : ListConfigurationSettingsOptions = {
224+ keyFilter : selector . keyFilter ,
225+ labelFilter : selector . labelFilter
226+ } ;
227+
228+ const settings = listConfigurationSettingsWithTrace (
229+ this . #requestTraceOptions,
230+ client ,
231+ listOptions
232+ ) ;
233+
234+ for await ( const setting of settings ) {
235+ if ( ! isFeatureFlag ( setting ) ) { // exclude feature flags
236+ loadedSettings . push ( setting ) ;
237+ }
203238 }
204239 }
205- }
240+ return loadedSettings ;
241+ } ;
242+
243+ loadedSettings = await this . #executeWithFailoverPolicy( funcToExecute ) ;
206244 return loadedSettings ;
207245 }
208246
@@ -258,30 +296,42 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
258296
259297 async #loadFeatureFlags( ) {
260298 // Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting
261- const featureFlagsMap = new Map < string , any > ( ) ;
262- for ( const selector of this . #featureFlagSelectors) {
263- const listOptions : ListConfigurationSettingsOptions = {
264- keyFilter : `${ featureFlagPrefix } ${ selector . keyFilter } ` ,
265- labelFilter : selector . labelFilter
266- } ;
267-
268- const pageEtags : string [ ] = [ ] ;
269- const pageIterator = listConfigurationSettingsWithTrace (
270- this . #requestTraceOptions,
271- this . #client,
272- listOptions
273- ) . byPage ( ) ;
274- for await ( const page of pageIterator ) {
275- pageEtags . push ( page . etag ?? "" ) ;
276- for ( const setting of page . items ) {
277- if ( isFeatureFlag ( setting ) ) {
278- featureFlagsMap . set ( setting . key , setting . value ) ;
299+ const funcToExecute = async ( client ) => {
300+ const featureFlagsMap = new Map < string , any > ( ) ;
301+ const selectors = JSON . parse (
302+ JSON . stringify ( this . #featureFlagSelectors)
303+ ) ;
304+
305+ for ( const selector of selectors ) {
306+ const listOptions : ListConfigurationSettingsOptions = {
307+ keyFilter : `${ featureFlagPrefix } ${ selector . keyFilter } ` ,
308+ labelFilter : selector . labelFilter
309+ } ;
310+
311+ const pageEtags : string [ ] = [ ] ;
312+ const pageIterator = listConfigurationSettingsWithTrace (
313+ this . #requestTraceOptions,
314+ client ,
315+ listOptions
316+ ) . byPage ( ) ;
317+ for await ( const page of pageIterator ) {
318+ pageEtags . push ( page . etag ?? "" ) ;
319+ for ( const setting of page . items ) {
320+ if ( isFeatureFlag ( setting ) ) {
321+ featureFlagsMap . set ( setting . key , setting . value ) ;
322+ }
279323 }
280324 }
325+ selector . pageEtags = pageEtags ;
281326 }
282- selector . pageEtags = pageEtags ;
327+
328+ this . #featureFlagSelectors = selectors ;
329+ return featureFlagsMap ;
283330 }
284331
332+ let featureFlagsMap = new Map < string , any > ( ) ;
333+ featureFlagsMap = await this . #executeWithFailoverPolicy( funcToExecute ) ;
334+
285335 // parse feature flags
286336 const featureFlags = Array . from ( featureFlagsMap . values ( ) ) . map ( rawFlag => JSON . parse ( rawFlag ) ) ;
287337
@@ -431,30 +481,32 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
431481 }
432482
433483 // check if any feature flag is changed
434- let needRefresh = false ;
435- for ( const selector of this . #featureFlagSelectors) {
436- const listOptions : ListConfigurationSettingsOptions = {
437- keyFilter : `${ featureFlagPrefix } ${ selector . keyFilter } ` ,
438- labelFilter : selector . labelFilter ,
439- pageEtags : selector . pageEtags
440- } ;
441- const pageIterator = listConfigurationSettingsWithTrace (
442- this . #requestTraceOptions,
443- this . #client,
444- listOptions
445- ) . byPage ( ) ;
446-
447- for await ( const page of pageIterator ) {
448- if ( page . _response . status === 200 ) { // created or changed
449- needRefresh = true ;
450- break ;
484+ const funcToExecute = async ( client ) => {
485+ const needRefresh = false ;
486+ for ( const selector of this . #featureFlagSelectors) {
487+ const listOptions : ListConfigurationSettingsOptions = {
488+ keyFilter : `${ featureFlagPrefix } ${ selector . keyFilter } ` ,
489+ labelFilter : selector . labelFilter ,
490+ pageEtags : selector . pageEtags
491+ } ;
492+
493+ const pageIterator = listConfigurationSettingsWithTrace (
494+ this . #requestTraceOptions,
495+ client ,
496+ listOptions
497+ ) . byPage ( ) ;
498+
499+ for await ( const page of pageIterator ) {
500+ if ( page . _response . status === 200 ) { // created or changed
501+ return true ;
502+ }
451503 }
452504 }
453-
454- if ( needRefresh ) {
455- break ; // short-circuit if result from any of the selectors is changed
456- }
457- }
505+ return needRefresh ;
506+ } ;
507+
508+ let needRefresh : boolean ;
509+ needRefresh = await this . #executeWithFailoverPolicy ( funcToExecute ) ;
458510
459511 if ( needRefresh ) {
460512 try {
@@ -517,14 +569,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
517569 * Get a configuration setting by key and label. If the setting is not found, return undefine instead of throwing an error.
518570 */
519571 async #getConfigurationSetting( configurationSettingId : ConfigurationSettingId , customOptions ?: GetConfigurationSettingOptions ) : Promise < GetConfigurationSettingResponse | undefined > {
520- let response : GetConfigurationSettingResponse | undefined ;
521- try {
522- response = await getConfigurationSettingWithTrace (
572+ const funcToExecute = async ( client ) => {
573+ return getConfigurationSettingWithTrace (
523574 this . #requestTraceOptions,
524- this . # client,
575+ client ,
525576 configurationSettingId ,
526577 customOptions
527578 ) ;
579+ } ;
580+
581+ let response : GetConfigurationSettingResponse | undefined ;
582+ try {
583+ response = await this . #executeWithFailoverPolicy( funcToExecute ) ;
528584 } catch ( error ) {
529585 if ( error instanceof RestError && error . statusCode === 404 ) {
530586 response = undefined ;
@@ -578,3 +634,7 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
578634 return getValidSelectors ( selectors ) ;
579635 }
580636}
637+
638+ function isFailoverableError ( error : any ) : boolean {
639+ return ( error instanceof RestError ) && ( error . statusCode === 408 || error . statusCode === 429 || ( error . statusCode !== undefined && error . statusCode >= 500 ) ) ;
640+ }
0 commit comments