Skip to content

Commit 326bf46

Browse files
Merge branch 'main' of https://github.com/Azure/AppConfiguration-JavaScriptProvider into zhiyuanliang/startup-timeout
2 parents 435ff08 + c79f4bd commit 326bf46

File tree

2 files changed

+37
-43
lines changed

2 files changed

+37
-43
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ This client library adds additional [functionality](https://learn.microsoft.com/
1414

1515
[Dynamic Configuration Tutorial](https://learn.microsoft.com/azure/azure-app-configuration/enable-dynamic-configuration-javascript): A tutorial about how to enable dynamic configuration in your JavaScript applications.
1616

17+
[Feature Overview](https://learn.microsoft.com/azure/azure-app-configuration/configuration-provider-overview#feature-development-status): This document provides a feature status overview.
18+
1719
[Feature Reference](https://learn.microsoft.com/azure/azure-app-configuration/reference-javascript-provider): This document provides a full feature rundown.
1820

1921
### Prerequisites

src/ConfigurationClientManager.ts

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ const ENDPOINT_KEY_NAME = "Endpoint";
2121
const ID_KEY_NAME = "Id";
2222
const SECRET_KEY_NAME = "Secret";
2323
const 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
2525
const 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

2830
export 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

Comments
 (0)