@@ -19,7 +19,8 @@ import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from ".
1919import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js" ;
2020import { IKeyValueAdapter } from "./IKeyValueAdapter.js" ;
2121import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js" ;
22- import { DEFAULT_REFRESH_INTERVAL_IN_MS , MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions.js" ;
22+ import { DEFAULT_STARTUP_TIMEOUT_IN_MS } from "./StartupOptions.js" ;
23+ import { DEFAULT_REFRESH_INTERVAL_IN_MS , MIN_REFRESH_INTERVAL_IN_MS } from "./refresh/refreshOptions.js" ;
2324import { Disposable } from "./common/disposable.js" ;
2425import {
2526 FEATURE_FLAGS_KEY_NAME ,
@@ -52,6 +53,10 @@ import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOp
5253import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js" ;
5354import { KeyFilter , LabelFilter , SettingSelector } from "./types.js" ;
5455import { ConfigurationClientManager } from "./ConfigurationClientManager.js" ;
56+ import { getFixedBackoffDuration , getExponentialBackoffDuration } from "./common/backoffUtils.js" ;
57+ import { InvalidOperationError , ArgumentError , isFailoverableError , isInputError } from "./common/error.js" ;
58+
59+ const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000 ; // 5 seconds
5560
5661const MAX_TAG_FILTERS = 5 ;
5762
@@ -139,10 +144,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
139144 } else {
140145 for ( const setting of watchedSettings ) {
141146 if ( setting . key . includes ( "*" ) || setting . key . includes ( "," ) ) {
142- throw new Error ( "The characters '*' and ',' are not supported in key of watched settings." ) ;
147+ throw new ArgumentError ( "The characters '*' and ',' are not supported in key of watched settings." ) ;
143148 }
144149 if ( setting . label ?. includes ( "*" ) || setting . label ?. includes ( "," ) ) {
145- throw new Error ( "The characters '*' and ',' are not supported in label of watched settings." ) ;
150+ throw new ArgumentError ( "The characters '*' and ',' are not supported in label of watched settings." ) ;
146151 }
147152 this . #sentinels. push ( setting ) ;
148153 }
@@ -151,7 +156,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
151156 // custom refresh interval
152157 if ( refreshIntervalInMs !== undefined ) {
153158 if ( refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS ) {
154- throw new Error ( `The refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS } milliseconds.` ) ;
159+ throw new RangeError ( `The refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS } milliseconds.` ) ;
155160 } else {
156161 this . #kvRefreshInterval = refreshIntervalInMs ;
157162 }
@@ -169,7 +174,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
169174 // custom refresh interval
170175 if ( refreshIntervalInMs !== undefined ) {
171176 if ( refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS ) {
172- throw new Error ( `The feature flag refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS } milliseconds.` ) ;
177+ throw new RangeError ( `The feature flag refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS } milliseconds.` ) ;
173178 } else {
174179 this . #ffRefreshInterval = refreshIntervalInMs ;
175180 }
@@ -246,13 +251,40 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
246251 * Loads the configuration store for the first time.
247252 */
248253 async load ( ) {
249- await this . #inspectFmPackage( ) ;
250- await this . #loadSelectedAndWatchedKeyValues( ) ;
251- if ( this . #featureFlagEnabled) {
252- await this . #loadFeatureFlags( ) ;
254+ const startTimestamp = Date . now ( ) ;
255+ const startupTimeout : number = this . #options?. startupOptions ?. timeoutInMs ?? DEFAULT_STARTUP_TIMEOUT_IN_MS ;
256+ const abortController = new AbortController ( ) ;
257+ const abortSignal = abortController . signal ;
258+ let timeoutId ;
259+ try {
260+ // Promise.race will be settled when the first promise in the list is settled.
261+ // It will not cancel the remaining promises in the list.
262+ // To avoid memory leaks, we must ensure other promises will be eventually terminated.
263+ await Promise . race ( [
264+ this . #initializeWithRetryPolicy( abortSignal ) ,
265+ // this promise will be rejected after timeout
266+ new Promise ( ( _ , reject ) => {
267+ timeoutId = setTimeout ( ( ) => {
268+ abortController . abort ( ) ; // abort the initialization promise
269+ reject ( new Error ( "Load operation timed out." ) ) ;
270+ } ,
271+ startupTimeout ) ;
272+ } )
273+ ] ) ;
274+ } catch ( error ) {
275+ if ( ! isInputError ( error ) ) {
276+ const timeElapsed = Date . now ( ) - startTimestamp ;
277+ if ( timeElapsed < MIN_DELAY_FOR_UNHANDLED_FAILURE ) {
278+ // load() method is called in the application's startup code path.
279+ // Unhandled exceptions cause application crash which can result in crash loops as orchestrators attempt to restart the application.
280+ // Knowing the intended usage of the provider in startup code path, we mitigate back-to-back crash loops from overloading the server with requests by waiting a minimum time to propagate fatal errors.
281+ await new Promise ( resolve => setTimeout ( resolve , MIN_DELAY_FOR_UNHANDLED_FAILURE - timeElapsed ) ) ;
282+ }
283+ }
284+ throw new Error ( "Failed to load." , { cause : error } ) ;
285+ } finally {
286+ clearTimeout ( timeoutId ) ; // cancel the timeout promise
253287 }
254- // Mark all settings have loaded at startup.
255- this . #isInitialLoadCompleted = true ;
256288 }
257289
258290 /**
@@ -262,7 +294,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
262294 const separator = options ?. separator ?? "." ;
263295 const validSeparators = [ "." , "," , ";" , "-" , "_" , "__" , "/" , ":" ] ;
264296 if ( ! validSeparators . includes ( separator ) ) {
265- throw new Error ( `Invalid separator '${ separator } '. Supported values: ${ validSeparators . map ( s => `'${ s } '` ) . join ( ", " ) } .` ) ;
297+ throw new ArgumentError ( `Invalid separator '${ separator } '. Supported values: ${ validSeparators . map ( s => `'${ s } '` ) . join ( ", " ) } .` ) ;
266298 }
267299
268300 // construct hierarchical data object from map
@@ -275,22 +307,22 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
275307 const segment = segments [ i ] ;
276308 // undefined or empty string
277309 if ( ! segment ) {
278- throw new Error ( `invalid key: ${ key } `) ;
310+ throw new InvalidOperationError ( `Failed to construct configuration object: Invalid key: ${ key } `) ;
279311 }
280312 // create path if not exist
281313 if ( current [ segment ] === undefined ) {
282314 current [ segment ] = { } ;
283315 }
284316 // The path has been occupied by a non-object value, causing ambiguity.
285317 if ( typeof current [ segment ] !== "object" ) {
286- throw new Error ( `Ambiguity occurs when constructing configuration object from key '${ key } ', value '${ value } '. The path '${ segments . slice ( 0 , i + 1 ) . join ( separator ) } ' has been occupied.` ) ;
318+ throw new InvalidOperationError ( `Ambiguity occurs when constructing configuration object from key '${ key } ', value '${ value } '. The path '${ segments . slice ( 0 , i + 1 ) . join ( separator ) } ' has been occupied.` ) ;
287319 }
288320 current = current [ segment ] ;
289321 }
290322
291323 const lastSegment = segments [ segments . length - 1 ] ;
292324 if ( current [ lastSegment ] !== undefined ) {
293- throw new Error ( `Ambiguity occurs when constructing configuration object from key '${ key } ', value '${ value } '. The key should not be part of another key.` ) ;
325+ throw new InvalidOperationError ( `Ambiguity occurs when constructing configuration object from key '${ key } ', value '${ value } '. The key should not be part of another key.` ) ;
294326 }
295327 // set value to the last segment
296328 current [ lastSegment ] = value ;
@@ -303,7 +335,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
303335 */
304336 async refresh ( ) : Promise < void > {
305337 if ( ! this . #refreshEnabled && ! this . #featureFlagRefreshEnabled) {
306- throw new Error ( "Refresh is not enabled for key-values or feature flags." ) ;
338+ throw new InvalidOperationError ( "Refresh is not enabled for key-values or feature flags." ) ;
307339 }
308340
309341 if ( this . #refreshInProgress) {
@@ -322,7 +354,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
322354 */
323355 onRefresh ( listener : ( ) => any , thisArg ?: any ) : Disposable {
324356 if ( ! this . #refreshEnabled && ! this . #featureFlagRefreshEnabled) {
325- throw new Error ( "Refresh is not enabled for key-values or feature flags." ) ;
357+ throw new InvalidOperationError ( "Refresh is not enabled for key-values or feature flags." ) ;
326358 }
327359
328360 const boundedListener = listener . bind ( thisArg ) ;
@@ -337,6 +369,42 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
337369 return new Disposable ( remove ) ;
338370 }
339371
372+ /**
373+ * Initializes the configuration provider.
374+ */
375+ async #initializeWithRetryPolicy( abortSignal : AbortSignal ) : Promise < void > {
376+ if ( ! this . #isInitialLoadCompleted) {
377+ await this . #inspectFmPackage( ) ;
378+ const startTimestamp = Date . now ( ) ;
379+ let postAttempts = 0 ;
380+ do { // at least try to load once
381+ try {
382+ await this . #loadSelectedAndWatchedKeyValues( ) ;
383+ if ( this . #featureFlagEnabled) {
384+ await this . #loadFeatureFlags( ) ;
385+ }
386+ this . #isInitialLoadCompleted = true ;
387+ break ;
388+ } catch ( error ) {
389+ if ( isInputError ( error ) ) {
390+ throw error ;
391+ }
392+ if ( abortSignal . aborted ) {
393+ return ;
394+ }
395+ const timeElapsed = Date . now ( ) - startTimestamp ;
396+ let backoffDuration = getFixedBackoffDuration ( timeElapsed ) ;
397+ if ( backoffDuration === undefined ) {
398+ postAttempts += 1 ;
399+ backoffDuration = getExponentialBackoffDuration ( postAttempts ) ;
400+ }
401+ console . warn ( `Failed to load. Error message: ${ error . message } . Retrying in ${ backoffDuration } ms.` ) ;
402+ await new Promise ( resolve => setTimeout ( resolve , backoffDuration ) ) ;
403+ }
404+ } while ( ! abortSignal . aborted ) ;
405+ }
406+ }
407+
340408 /**
341409 * Inspects the feature management package version.
342410 */
@@ -471,7 +539,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
471539 this . #aiConfigurationTracing. reset ( ) ;
472540 }
473541
474- // process key-values, watched settings have higher priority
542+ // adapt configuration settings to key-values
475543 for ( const setting of loadedSettings ) {
476544 const [ key , value ] = await this . #processKeyValue( setting ) ;
477545 keyValues . push ( [ key , value ] ) ;
@@ -678,6 +746,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
678746 return response ;
679747 }
680748
749+ // Only operations related to Azure App Configuration should be executed with failover policy.
681750 async #executeWithFailoverPolicy( funcToExecute : ( client : AppConfigurationClient ) => Promise < any > ) : Promise < any > {
682751 let clientWrappers = await this . #clientManager. getClients ( ) ;
683752 if ( this . #options?. loadBalancingEnabled && this . #lastSuccessfulEndpoint !== "" && clientWrappers . length > 1 ) {
@@ -717,7 +786,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
717786 }
718787
719788 this . #clientManager. refreshClients ( ) ;
720- throw new Error ( "All clients failed to get configuration settings." ) ;
789+ throw new Error ( "All fallback clients failed to get configuration settings." ) ;
721790 }
722791
723792 async #processKeyValue( setting : ConfigurationSetting < string > ) : Promise < [ string , unknown ] > {
@@ -772,7 +841,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
772841 async #parseFeatureFlag( setting : ConfigurationSetting < string > ) : Promise < any > {
773842 const rawFlag = setting . value ;
774843 if ( rawFlag === undefined ) {
775- throw new Error ( "The value of configuration setting cannot be undefined." ) ;
844+ throw new ArgumentError ( "The value of configuration setting cannot be undefined." ) ;
776845 }
777846 const featureFlag = JSON . parse ( rawFlag ) ;
778847
@@ -839,17 +908,17 @@ function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector
839908 const selector = { ...selectorCandidate } ;
840909 if ( selector . snapshotName ) {
841910 if ( selector . keyFilter || selector . labelFilter || selector . tagFilters ) {
842- throw new Error ( "Key, label or tag filter should not be used for a snapshot." ) ;
911+ throw new ArgumentError ( "Key, label or tag filter should not be used for a snapshot." ) ;
843912 }
844913 } else {
845914 if ( ! selector . keyFilter && ( ! selector . tagFilters || selector . tagFilters . length === 0 ) ) {
846- throw new Error ( "Key filter cannot be null or empty." ) ;
915+ throw new ArgumentError ( "Key filter cannot be null or empty." ) ;
847916 }
848917 if ( ! selector . labelFilter ) {
849918 selector . labelFilter = LabelFilter . Null ;
850919 }
851920 if ( selector . labelFilter . includes ( "*" ) || selector . labelFilter . includes ( "," ) ) {
852- throw new Error ( "The characters '*' and ',' are not supported in label filters." ) ;
921+ throw new ArgumentError ( "The characters '*' and ',' are not supported in label filters." ) ;
853922 }
854923 if ( selector . tagFilters ) {
855924 validateTagFilters ( selector . tagFilters ) ;
@@ -906,9 +975,3 @@ function validateTagFilters(tagFilters: string[]): void {
906975 }
907976 }
908977}
909-
910- function isFailoverableError ( error : any ) : boolean {
911- // ENOTFOUND: DNS lookup failed, ENOENT: no such file or directory
912- return isRestError ( error ) && ( error . code === "ENOTFOUND" || error . code === "ENOENT" ||
913- ( error . statusCode !== undefined && ( error . statusCode === 401 || error . statusCode === 403 || error . statusCode === 408 || error . statusCode === 429 || error . statusCode >= 500 ) ) ) ;
914- }
0 commit comments