@@ -21,14 +21,15 @@ const ENDPOINT_KEY_NAME = "Endpoint";
2121const ID_KEY_NAME = "Id" ;
2222const SECRET_KEY_NAME = "Secret" ;
2323const TRUSTED_DOMAIN_LABELS = [ ".azconfig." , ".appconfig." ] ;
24- const FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL = 60 * 60 * 1000 ; // 1 hour in milliseconds
24+ const FALLBACK_CLIENT_EXPIRE_INTERVAL = 60 * 60 * 1000 ; // 1 hour in milliseconds
2525const MINIMAL_CLIENT_REFRESH_INTERVAL = 30_000 ; // 30 seconds in milliseconds
26- const SRV_QUERY_TIMEOUT = 30_000 ; // 30 seconds in milliseconds
26+ const DNS_RESOLVER_TIMEOUT = 3_000 ; // 3 seconds in milliseconds, in most cases, dns resolution should be within 200 milliseconds
27+ const DNS_RESOLVER_TRIES = 2 ;
28+ const MAX_ALTNATIVE_SRV_COUNT = 10 ;
2729
2830export class ConfigurationClientManager {
2931 #isFailoverable: boolean ;
3032 #dns: any ;
31- endpoint : URL ;
3233 #secret : string ;
3334 #id : string ;
3435 #credential: TokenCredential ;
@@ -38,8 +39,11 @@ export class ConfigurationClientManager {
3839 #staticClients: ConfigurationClientWrapper [ ] ; // there should always be only one static client
3940 #dynamicClients: ConfigurationClientWrapper [ ] ;
4041 #replicaCount: number = 0 ;
41- #lastFallbackClientRefreshTime: number = 0 ;
42- #lastFallbackClientRefreshAttempt: number = 0 ;
42+ #lastFallbackClientUpdateTime: number = 0 ; // enforce to discover fallback client when it is expired
43+ #lastFallbackClientRefreshAttempt: number = 0 ; // avoid refreshing clients before the minimal refresh interval
44+
45+ // This property is public to allow recording the last successful endpoint for failover.
46+ endpoint : URL ;
4347
4448 constructor (
4549 connectionStringOrEndpoint ?: string | URL ,
@@ -90,10 +94,11 @@ export class ConfigurationClientManager {
9094 this . #isFailoverable = false ;
9195 return ;
9296 }
93- if ( this . #dns) {
97+ if ( this . #dns) { // dns module is already loaded
9498 return ;
9599 }
96100
101+ // We can only know whether dns module is available during runtime.
97102 try {
98103 this . #dns = await import ( "dns/promises" ) ;
99104 } catch ( error ) {
@@ -121,8 +126,7 @@ export class ConfigurationClientManager {
121126 ( ! this . #dynamicClients ||
122127 // All dynamic clients are in backoff means no client is available
123128 this . #dynamicClients. every ( client => currentTime < client . backoffEndTime ) ||
124- currentTime >= this . #lastFallbackClientRefreshTime + FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL ) ) {
125- this . #lastFallbackClientRefreshAttempt = currentTime ;
129+ currentTime >= this . #lastFallbackClientUpdateTime + FALLBACK_CLIENT_EXPIRE_INTERVAL ) ) {
126130 await this . #discoverFallbackClients( this . endpoint . hostname ) ;
127131 return availableClients . concat ( this . #dynamicClients) ;
128132 }
@@ -140,28 +144,22 @@ export class ConfigurationClientManager {
140144 async refreshClients ( ) {
141145 const currentTime = Date . now ( ) ;
142146 if ( this . #isFailoverable &&
143- currentTime >= new Date ( this . #lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL ) . getTime ( ) ) {
144- this . #lastFallbackClientRefreshAttempt = currentTime ;
147+ currentTime >= this . #lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL ) {
145148 await this . #discoverFallbackClients( this . endpoint . hostname ) ;
146149 }
147150 }
148151
149152 async #discoverFallbackClients( host : string ) {
150- let result ;
151- let timer ;
153+ this . #lastFallbackClientRefreshAttempt = Date . now ( ) ;
154+ let result : string [ ] ;
152155 try {
153- result = await Promise . race ( [
154- new Promise ( ( _ , reject ) =>
155- timer = setTimeout ( ( ) => reject ( new Error ( "SRV record query timed out." ) ) , SRV_QUERY_TIMEOUT ) ) ,
156- this . #querySrvTargetHost( host )
157- ] ) ;
156+ result = await this . #querySrvTargetHost( host ) ;
158157 } catch ( error ) {
159- throw new Error ( `Failed to build fallback clients: ${ error . message } ` ) ;
160- } finally {
161- clearTimeout ( timer ) ;
158+ console . warn ( `Failed to build fallback clients. ${ error . message } ` ) ;
159+ return ; // swallow the error when srv query fails
162160 }
163161
164- const srvTargetHosts = shuffleList ( result ) as string [ ] ;
162+ const srvTargetHosts = shuffleList ( result ) ;
165163 const newDynamicClients : ConfigurationClientWrapper [ ] = [ ] ;
166164 for ( const host of srvTargetHosts ) {
167165 if ( isValidEndpoint ( host , this . #validDomain) ) {
@@ -170,43 +168,36 @@ export class ConfigurationClientManager {
170168 continue ;
171169 }
172170 const client = this . #credential ?
173- new AppConfigurationClient ( targetEndpoint , this . #credential, this . #clientOptions) :
174- new AppConfigurationClient ( buildConnectionString ( targetEndpoint , this . #secret, this . #id) , this . #clientOptions) ;
171+ new AppConfigurationClient ( targetEndpoint , this . #credential, this . #clientOptions) :
172+ new AppConfigurationClient ( buildConnectionString ( targetEndpoint , this . #secret, this . #id) , this . #clientOptions) ;
175173 newDynamicClients . push ( new ConfigurationClientWrapper ( targetEndpoint , client ) ) ;
176174 }
177175 }
178176
179177 this . #dynamicClients = newDynamicClients ;
180- this . #lastFallbackClientRefreshTime = Date . now ( ) ;
178+ this . #lastFallbackClientUpdateTime = Date . now ( ) ;
181179 this . #replicaCount = this . #dynamicClients. length ;
182180 }
183181
184182 /**
185- * Query SRV records and return target hosts.
183+ * Queries SRV records for the given host and returns the target hosts.
186184 */
187185 async #querySrvTargetHost( host : string ) : Promise < string [ ] > {
188186 const results : string [ ] = [ ] ;
189187
190188 try {
191- // Look up SRV records for the origin host
192- const originRecords = await this . #dns. resolveSrv ( `${ TCP_ORIGIN_KEY_NAME } .${ host } ` ) ;
193- if ( originRecords . length === 0 ) {
194- return results ;
195- }
196-
197- // Add the first origin record to results
189+ // https://nodejs.org/api/dns.html#dnspromisesresolvesrvhostname
190+ const resolver = new this . #dns. Resolver ( { timeout : DNS_RESOLVER_TIMEOUT , tries : DNS_RESOLVER_TRIES } ) ;
191+ // On success, resolveSrv() returns an array of SrvRecord
192+ // On failure, resolveSrv() throws an error with code 'ENOTFOUND'.
193+ const originRecords = await resolver . resolveSrv ( `${ TCP_ORIGIN_KEY_NAME } .${ host } ` ) ; // look up SRV records for the origin host
198194 const originHost = originRecords [ 0 ] . name ;
199- results . push ( originHost ) ;
195+ results . push ( originHost ) ; // add the first origin record to results
200196
201- // Look up SRV records for alternate hosts
202197 let index = 0 ;
203- // eslint-disable-next-line no-constant-condition
204- while ( true ) {
205- const currentAlt = `${ ALT_KEY_NAME } ${ index } ` ;
206- const altRecords = await this . #dns. resolveSrv ( `${ currentAlt } .${ TCP_KEY_NAME } .${ originHost } ` ) ;
207- if ( altRecords . length === 0 ) {
208- break ; // No more alternate records, exit loop
209- }
198+ while ( index < MAX_ALTNATIVE_SRV_COUNT ) {
199+ const currentAlt = `${ ALT_KEY_NAME } ${ index } ` ; // look up SRV records for alternate hosts
200+ const altRecords = await resolver . resolveSrv ( `${ currentAlt } .${ TCP_KEY_NAME } .${ originHost } ` ) ;
210201
211202 altRecords . forEach ( record => {
212203 const altHost = record . name ;
@@ -218,7 +209,8 @@ export class ConfigurationClientManager {
218209 }
219210 } catch ( err ) {
220211 if ( err . code === "ENOTFOUND" ) {
221- return results ; // No more SRV records found, return results
212+ // No more SRV records found, return results.
213+ return results ;
222214 } else {
223215 throw new Error ( `Failed to lookup SRV records: ${ err . message } ` ) ;
224216 }
@@ -293,7 +285,7 @@ function getValidUrl(endpoint: string): URL {
293285 return new URL ( endpoint ) ;
294286 } catch ( error ) {
295287 if ( error . code === "ERR_INVALID_URL" ) {
296- throw new Error ( "Invalid endpoint URL." , { cause : error } ) ;
288+ throw new RangeError ( "Invalid endpoint URL." , { cause : error } ) ;
297289 } else {
298290 throw error ;
299291 }
0 commit comments