@@ -16,9 +16,11 @@ const ENDPOINT_KEY_NAME = "Endpoint";
1616const ID_KEY_NAME = "Id" ;
1717const SECRET_KEY_NAME = "Secret" ;
1818const TRUSTED_DOMAIN_LABELS = [ ".azconfig." , ".appconfig." ] ;
19- const FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL = 60 * 60 * 1000 ; // 1 hour in milliseconds
20- const MINIMAL_CLIENT_REFRESH_INTERVAL = 30 * 1000 ; // 30 seconds in milliseconds
21- const SRV_QUERY_TIMEOUT = 30 * 1000 ; // 30 seconds in milliseconds
19+ const FALLBACK_CLIENT_EXPIRE_INTERVAL = 60 * 60 * 1000 ; // 1 hour in milliseconds
20+ const MINIMAL_CLIENT_REFRESH_INTERVAL = 30_000 ; // 30 seconds in milliseconds
21+ const DNS_RESOLVER_TIMEOUT = 3_000 ; // 3 seconds in milliseconds, in most cases, dns resolution should be within 200 milliseconds
22+ const DNS_RESOLVER_TRIES = 2 ;
23+ const MAX_ALTNATIVE_SRV_COUNT = 10 ;
2224
2325export class ConfigurationClientManager {
2426 #isFailoverable: boolean ;
@@ -33,8 +35,8 @@ export class ConfigurationClientManager {
3335 #staticClients: ConfigurationClientWrapper [ ] ; // there should always be only one static client
3436 #dynamicClients: ConfigurationClientWrapper [ ] ;
3537 #replicaCount: number = 0 ;
36- #lastFallbackClientRefreshTime : number = 0 ;
37- #lastFallbackClientRefreshAttempt: number = 0 ;
38+ #lastFallbackClientUpdateTime : number = 0 ; // enforce to discover fallback client when it is expired
39+ #lastFallbackClientRefreshAttempt: number = 0 ; // avoid refreshing clients before the minimal refresh interval
3840
3941 constructor (
4042 connectionStringOrEndpoint ?: string | URL ,
@@ -85,10 +87,11 @@ export class ConfigurationClientManager {
8587 this . #isFailoverable = false ;
8688 return ;
8789 }
88- if ( this . #dns) {
90+ if ( this . #dns) { // dns module is already loaded
8991 return ;
9092 }
9193
94+ // We can only know whether dns module is available during runtime.
9295 try {
9396 this . #dns = await import ( "dns/promises" ) ;
9497 } catch ( error ) {
@@ -116,8 +119,7 @@ export class ConfigurationClientManager {
116119 ( ! this . #dynamicClients ||
117120 // All dynamic clients are in backoff means no client is available
118121 this . #dynamicClients. every ( client => currentTime < client . backoffEndTime ) ||
119- currentTime >= this . #lastFallbackClientRefreshTime + FALLBACK_CLIENT_REFRESH_EXPIRE_INTERVAL ) ) {
120- this . #lastFallbackClientRefreshAttempt = currentTime ;
122+ currentTime >= this . #lastFallbackClientUpdateTime + FALLBACK_CLIENT_EXPIRE_INTERVAL ) ) {
121123 await this . #discoverFallbackClients( this . endpoint . hostname ) ;
122124 return availableClients . concat ( this . #dynamicClients) ;
123125 }
@@ -135,27 +137,22 @@ export class ConfigurationClientManager {
135137 async refreshClients ( ) {
136138 const currentTime = Date . now ( ) ;
137139 if ( this . #isFailoverable &&
138- currentTime >= new Date ( this . #lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL ) . getTime ( ) ) {
139- this . #lastFallbackClientRefreshAttempt = currentTime ;
140+ currentTime >= this . #lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL ) {
140141 await this . #discoverFallbackClients( this . endpoint . hostname ) ;
141142 }
142143 }
143144
144145 async #discoverFallbackClients( host : string ) {
145- let result ;
146- let timeout ;
146+ this . #lastFallbackClientRefreshAttempt = Date . now ( ) ;
147+ let result : string [ ] ;
147148 try {
148- result = await Promise . race ( [
149- new Promise ( ( _ , reject ) => timeout = setTimeout ( ( ) => reject ( new Error ( "SRV record query timed out." ) ) , SRV_QUERY_TIMEOUT ) ) ,
150- this . #querySrvTargetHost( host )
151- ] ) ;
149+ result = await this . #querySrvTargetHost( host ) ;
152150 } catch ( error ) {
153- throw new Error ( `Failed to build fallback clients, ${ error . message } ` ) ;
154- } finally {
155- clearTimeout ( timeout ) ;
151+ console . warn ( `Failed to build fallback clients. ${ error . message } ` ) ;
152+ return ; // swallow the error when srv query fails
156153 }
157154
158- const srvTargetHosts = shuffleList ( result ) as string [ ] ;
155+ const srvTargetHosts = shuffleList ( result ) ;
159156 const newDynamicClients : ConfigurationClientWrapper [ ] = [ ] ;
160157 for ( const host of srvTargetHosts ) {
161158 if ( isValidEndpoint ( host , this . #validDomain) ) {
@@ -164,43 +161,36 @@ export class ConfigurationClientManager {
164161 continue ;
165162 }
166163 const client = this . #credential ?
167- new AppConfigurationClient ( targetEndpoint , this . #credential, this . #clientOptions) :
168- new AppConfigurationClient ( buildConnectionString ( targetEndpoint , this . #secret, this . #id) , this . #clientOptions) ;
164+ new AppConfigurationClient ( targetEndpoint , this . #credential, this . #clientOptions) :
165+ new AppConfigurationClient ( buildConnectionString ( targetEndpoint , this . #secret, this . #id) , this . #clientOptions) ;
169166 newDynamicClients . push ( new ConfigurationClientWrapper ( targetEndpoint , client ) ) ;
170167 }
171168 }
172169
173170 this . #dynamicClients = newDynamicClients ;
174- this . #lastFallbackClientRefreshTime = Date . now ( ) ;
171+ this . #lastFallbackClientUpdateTime = Date . now ( ) ;
175172 this . #replicaCount = this . #dynamicClients. length ;
176173 }
177174
178175 /**
179- * Query SRV records and return target hosts.
176+ * Queries SRV records for the given host and returns the target hosts.
180177 */
181178 async #querySrvTargetHost( host : string ) : Promise < string [ ] > {
182179 const results : string [ ] = [ ] ;
183180
184181 try {
185- // Look up SRV records for the origin host
186- const originRecords = await this . #dns. resolveSrv ( `${ TCP_ORIGIN_KEY_NAME } .${ host } ` ) ;
187- if ( originRecords . length === 0 ) {
188- return results ;
189- }
190-
191- // Add the first origin record to results
182+ // https://nodejs.org/api/dns.html#dnspromisesresolvesrvhostname
183+ const resolver = new this . #dns. Resolver ( { timeout : DNS_RESOLVER_TIMEOUT , tries : DNS_RESOLVER_TRIES } ) ;
184+ // On success, resolveSrv() returns an array of SrvRecord
185+ // On failure, resolveSrv() throws an error with code 'ENOTFOUND'.
186+ const originRecords = await resolver . resolveSrv ( `${ TCP_ORIGIN_KEY_NAME } .${ host } ` ) ; // look up SRV records for the origin host
192187 const originHost = originRecords [ 0 ] . name ;
193- results . push ( originHost ) ;
188+ results . push ( originHost ) ; // add the first origin record to results
194189
195- // Look up SRV records for alternate hosts
196190 let index = 0 ;
197- // eslint-disable-next-line no-constant-condition
198- while ( true ) {
199- const currentAlt = `${ ALT_KEY_NAME } ${ index } ` ;
200- const altRecords = await this . #dns. resolveSrv ( `${ currentAlt } .${ TCP_KEY_NAME } .${ originHost } ` ) ;
201- if ( altRecords . length === 0 ) {
202- break ; // No more alternate records, exit loop
203- }
191+ while ( index < MAX_ALTNATIVE_SRV_COUNT ) {
192+ const currentAlt = `${ ALT_KEY_NAME } ${ index } ` ; // look up SRV records for alternate hosts
193+ const altRecords = await resolver . resolveSrv ( `${ currentAlt } .${ TCP_KEY_NAME } .${ originHost } ` ) ;
204194
205195 altRecords . forEach ( record => {
206196 const altHost = record . name ;
@@ -212,7 +202,8 @@ export class ConfigurationClientManager {
212202 }
213203 } catch ( err ) {
214204 if ( err . code === "ENOTFOUND" ) {
215- return results ; // No more SRV records found, return results
205+ // No more SRV records found, return results.
206+ return results ;
216207 } else {
217208 throw new Error ( `Failed to lookup SRV records: ${ err . message } ` ) ;
218209 }
0 commit comments