Skip to content

Commit eec7114

Browse files
merge
2 parents 4f36a1c + b74e983 commit eec7114

13 files changed

+308
-55
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -64,27 +64,32 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
6464
#isFailoverRequest: boolean = false;
6565

6666
// Refresh
67-
#watchAll: boolean = false;
68-
#refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
67+
#refreshInProgress: boolean = false;
68+
6969
#onRefreshListeners: Array<() => any> = [];
7070
/**
7171
* Aka watched settings.
7272
*/
7373
#sentinels: ConfigurationSettingId[] = [];
74-
#refreshTimer: RefreshTimer;
74+
#watchAll: boolean = false;
75+
#kvRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
76+
#kvRefreshTimer: RefreshTimer;
7577

7678
// Feature flags
77-
#featureFlagRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
78-
#featureFlagRefreshTimer: RefreshTimer;
79+
#ffRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
80+
#ffRefreshTimer: RefreshTimer;
7981

8082
/**
81-
* selectors of key-values obtained from @see AzureAppConfigurationOptions.selectors
83+
* Selectors of key-values obtained from @see AzureAppConfigurationOptions.selectors
8284
*/
83-
#keyValueSelectors: PagedSettingSelector[] = [];
85+
#kvSelectors: PagedSettingSelector[] = [];
8486
/**
85-
* selectors of feature flags obtained from @see AzureAppConfigurationOptions.featureFlagOptions.selectors
87+
* Selectors of feature flags obtained from @see AzureAppConfigurationOptions.featureFlagOptions.selectors
8688
*/
87-
#featureFlagSelectors: PagedSettingSelector[] = [];
89+
#ffSelectors: PagedSettingSelector[] = [];
90+
91+
// Load balancing
92+
#lastSuccessfulEndpoint: string = "";
8893

8994
constructor(
9095
clientManager: ConfigurationClientManager,
@@ -123,18 +128,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
123128
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
124129
throw new Error(`The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
125130
} else {
126-
this.#refreshInterval = refreshIntervalInMs;
131+
this.#kvRefreshInterval = refreshIntervalInMs;
127132
}
128133
}
129-
this.#refreshTimer = new RefreshTimer(this.#refreshInterval);
134+
this.#kvRefreshTimer = new RefreshTimer(this.#kvRefreshInterval);
130135
}
131136

132-
this.#keyValueSelectors = getValidKeyValueSelectors(options?.selectors);
137+
this.#kvSelectors = getValidKeyValueSelectors(options?.selectors);
133138

134139
// feature flag options
135140
if (options?.featureFlagOptions?.enabled) {
136141
// validate feature flag selectors
137-
this.#featureFlagSelectors = getValidFeatureFlagSelectors(options.featureFlagOptions.selectors);
142+
this.#ffSelectors = getValidFeatureFlagSelectors(options.featureFlagOptions.selectors);
138143

139144
if (options.featureFlagOptions.refresh?.enabled) {
140145
const { refreshIntervalInMs } = options.featureFlagOptions.refresh;
@@ -143,11 +148,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
143148
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
144149
throw new Error(`The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
145150
} else {
146-
this.#featureFlagRefreshInterval = refreshIntervalInMs;
151+
this.#ffRefreshInterval = refreshIntervalInMs;
147152
}
148153
}
149154

150-
this.#featureFlagRefreshTimer = new RefreshTimer(this.#featureFlagRefreshInterval);
155+
this.#ffRefreshTimer = new RefreshTimer(this.#ffRefreshInterval);
151156
}
152157
}
153158

@@ -274,6 +279,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
274279
throw new Error("Refresh is not enabled for key-values or feature flags.");
275280
}
276281

282+
if (this.#refreshInProgress) {
283+
return;
284+
}
285+
this.#refreshInProgress = true;
286+
try {
287+
await this.#refreshTasks();
288+
} finally {
289+
this.#refreshInProgress = false;
290+
}
291+
}
292+
293+
async #refreshTasks(): Promise<void> {
277294
const refreshTasks: Promise<boolean>[] = [];
278295
if (this.#refreshEnabled) {
279296
refreshTasks.push(this.#refreshKeyValues());
@@ -331,7 +348,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
331348
* If false, loads key-value using the key-value selectors. Defaults to false.
332349
*/
333350
async #loadConfigurationSettings(loadFeatureFlag: boolean = false): Promise<ConfigurationSetting[]> {
334-
const selectors = loadFeatureFlag ? this.#featureFlagSelectors : this.#keyValueSelectors;
351+
const selectors = loadFeatureFlag ? this.#ffSelectors : this.#kvSelectors;
335352
const funcToExecute = async (client) => {
336353
const loadedSettings: ConfigurationSetting[] = [];
337354
// deep copy selectors to avoid modification if current client fails
@@ -363,9 +380,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
363380
}
364381

365382
if (loadFeatureFlag) {
366-
this.#featureFlagSelectors = selectorsToUpdate;
383+
this.#ffSelectors = selectorsToUpdate;
367384
} else {
368-
this.#keyValueSelectors = selectorsToUpdate;
385+
this.#kvSelectors = selectorsToUpdate;
369386
}
370387
return loadedSettings;
371388
};
@@ -449,14 +466,14 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
449466
*/
450467
async #refreshKeyValues(): Promise<boolean> {
451468
// if still within refresh interval/backoff, return
452-
if (!this.#refreshTimer.canRefresh()) {
469+
if (!this.#kvRefreshTimer.canRefresh()) {
453470
return Promise.resolve(false);
454471
}
455472

456473
// try refresh if any of watched settings is changed.
457474
let needRefresh = false;
458475
if (this.#watchAll) {
459-
needRefresh = await this.#checkConfigurationSettingsChange(this.#keyValueSelectors);
476+
needRefresh = await this.#checkConfigurationSettingsChange(this.#kvSelectors);
460477
}
461478
for (const sentinel of this.#sentinels.values()) {
462479
const response = await this.#getConfigurationSetting(sentinel, {
@@ -476,7 +493,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
476493
await this.#loadSelectedAndWatchedKeyValues();
477494
}
478495

479-
this.#refreshTimer.reset();
496+
this.#kvRefreshTimer.reset();
480497
return Promise.resolve(needRefresh);
481498
}
482499

@@ -486,16 +503,16 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
486503
*/
487504
async #refreshFeatureFlags(): Promise<boolean> {
488505
// if still within refresh interval/backoff, return
489-
if (!this.#featureFlagRefreshTimer.canRefresh()) {
506+
if (!this.#ffRefreshTimer.canRefresh()) {
490507
return Promise.resolve(false);
491508
}
492509

493-
const needRefresh = await this.#checkConfigurationSettingsChange(this.#featureFlagSelectors);
510+
const needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectors);
494511
if (needRefresh) {
495512
await this.#loadFeatureFlags();
496513
}
497514

498-
this.#featureFlagRefreshTimer.reset();
515+
this.#ffRefreshTimer.reset();
499516
return Promise.resolve(needRefresh);
500517
}
501518

@@ -569,14 +586,29 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
569586
}
570587

571588
async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise<any>): Promise<any> {
572-
const clientWrappers = await this.#clientManager.getClients();
589+
let clientWrappers = await this.#clientManager.getClients();
590+
if (this.#options?.loadBalancingEnabled && this.#lastSuccessfulEndpoint !== "" && clientWrappers.length > 1) {
591+
let nextClientIndex = 0;
592+
// Iterate through clients to find the index of the client with the last successful endpoint
593+
for (const clientWrapper of clientWrappers) {
594+
nextClientIndex++;
595+
if (clientWrapper.endpoint === this.#lastSuccessfulEndpoint) {
596+
break;
597+
}
598+
}
599+
// If we found the last successful client, rotate the list so that the next client is at the beginning
600+
if (nextClientIndex < clientWrappers.length) {
601+
clientWrappers = [...clientWrappers.slice(nextClientIndex), ...clientWrappers.slice(0, nextClientIndex)];
602+
}
603+
}
573604

574605
let successful: boolean;
575606
for (const clientWrapper of clientWrappers) {
576607
successful = false;
577608
try {
578609
const result = await funcToExecute(clientWrapper.client);
579610
this.#isFailoverRequest = false;
611+
this.#lastSuccessfulEndpoint = clientWrapper.endpoint;
580612
successful = true;
581613
clientWrapper.updateBackoffStatus(successful);
582614
return result;

src/AzureAppConfigurationOptions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,12 @@ export interface AzureAppConfigurationOptions {
5555
* If not specified, the default value is true.
5656
*/
5757
replicaDiscoveryEnabled?: boolean;
58+
59+
/**
60+
* Specifies whether to enable load balance or not.
61+
*
62+
* @remarks
63+
* If not specified, the default value is false.
64+
*/
65+
loadBalancingEnabled?: boolean;
5866
}

src/requestTracing/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ export enum RequestType {
4444
WATCH = "Watch"
4545
}
4646

47+
export const FEATURES_KEY = "Features";
48+
4749
// Tag names
4850
export const FAILOVER_REQUEST_TAG = "Failover";
4951
export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault";
5052
export const CDN_USED_TAG = "CDN";
53+
export const LOAD_BALANCE_CONFIGURED_TAG = "LB";

src/requestTracing/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import {
2121
RequestType,
2222
SERVICE_FABRIC_ENV_VAR,
2323
CORRELATION_CONTEXT_HEADER_NAME,
24-
FAILOVER_REQUEST_TAG
24+
FAILOVER_REQUEST_TAG,
25+
FEATURES_KEY,
26+
LOAD_BALANCE_CONFIGURED_TAG
2527
} from "./constants";
2628

2729
// Utils
@@ -87,6 +89,9 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt
8789
keyValues.set(REQUEST_TYPE_KEY, isInitialLoadCompleted ? RequestType.WATCH : RequestType.STARTUP);
8890
keyValues.set(HOST_TYPE_KEY, getHostType());
8991
keyValues.set(ENV_KEY, isDevEnvironment() ? DEV_ENV_VAL : undefined);
92+
if (options?.loadBalancingEnabled) {
93+
keyValues.set(FEATURES_KEY, LOAD_BALANCE_CONFIGURED_TAG);
94+
}
9095

9196
const tags: string[] = [];
9297
if (options?.keyVaultOptions) {

test/failover.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe("failover", function () {
3535

3636
it("should failover to replica and load key values from config store", async () => {
3737
const isFailoverable = true;
38-
mockConfigurationManagerGetClients(isFailoverable, mockedKVs);
38+
mockConfigurationManagerGetClients([], isFailoverable, mockedKVs);
3939

4040
const connectionString = createMockedConnectionString();
4141
// replicaDiscoveryEnabled is default to true
@@ -47,7 +47,7 @@ describe("failover", function () {
4747

4848
it("should failover to replica and load feature flags from config store", async () => {
4949
const isFailoverable = true;
50-
mockConfigurationManagerGetClients(isFailoverable, mockedFeatureFlags);
50+
mockConfigurationManagerGetClients([], isFailoverable, mockedFeatureFlags);
5151

5252
const connectionString = createMockedConnectionString();
5353
// replicaDiscoveryEnabled is default to true
@@ -66,7 +66,7 @@ describe("failover", function () {
6666

6767
it("should throw error when all clients failed", async () => {
6868
const isFailoverable = false;
69-
mockConfigurationManagerGetClients(isFailoverable);
69+
mockConfigurationManagerGetClients([], isFailoverable);
7070

7171
const connectionString = createMockedConnectionString();
7272
return expect(load(connectionString)).eventually.rejectedWith("All clients failed to get configuration settings.");

test/featureFlag.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ describe("feature flags", function () {
202202
this.timeout(10000);
203203

204204
before(() => {
205-
mockAppConfigurationClientListConfigurationSettings(mockedKVs);
205+
mockAppConfigurationClientListConfigurationSettings([mockedKVs]);
206206
});
207207

208208
after(() => {

test/json.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe("json", function () {
2020
});
2121

2222
it("should load and parse if content type is application/json", async () => {
23-
mockAppConfigurationClientListConfigurationSettings([jsonKeyValue]);
23+
mockAppConfigurationClientListConfigurationSettings([[jsonKeyValue]]);
2424

2525
const connectionString = createMockedConnectionString();
2626
const settings = await load(connectionString);
@@ -34,7 +34,7 @@ describe("json", function () {
3434
});
3535

3636
it("should not parse key-vault reference", async () => {
37-
mockAppConfigurationClientListConfigurationSettings([jsonKeyValue, keyVaultKeyValue]);
37+
mockAppConfigurationClientListConfigurationSettings([[jsonKeyValue, keyVaultKeyValue]]);
3838

3939
const connectionString = createMockedConnectionString();
4040
const settings = await load(connectionString, {
@@ -50,7 +50,7 @@ describe("json", function () {
5050
});
5151

5252
it("should parse different kinds of legal values", async () => {
53-
mockAppConfigurationClientListConfigurationSettings([
53+
mockAppConfigurationClientListConfigurationSettings([[
5454
/**
5555
* A JSON value MUST be an object, array, number, or string, false, null, true
5656
* See https://www.ietf.org/rfc/rfc4627.txt
@@ -69,7 +69,7 @@ describe("json", function () {
6969
createMockedJsonKeyValue("json.settings.emptyString", ""), // should fail JSON.parse and use string value as fallback
7070
createMockedJsonKeyValue("json.settings.illegalString", "[unclosed"), // should fail JSON.parse
7171

72-
]);
72+
]]);
7373
const connectionString = createMockedConnectionString();
7474
const settings = await load(connectionString);
7575
expect(settings).not.undefined;

test/keyvault.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const mockedData = [
1919
function mockAppConfigurationClient() {
2020
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2121
const kvs = mockedData.map(([key, vaultUri, _value]) => createMockedKeyVaultReference(key, vaultUri));
22-
mockAppConfigurationClientListConfigurationSettings(kvs);
22+
mockAppConfigurationClientListConfigurationSettings([kvs]);
2323
}
2424

2525
function mockNewlyCreatedKeyVaultSecretClients() {

test/load.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe("load", function () {
8080
this.timeout(10000);
8181

8282
before(() => {
83-
mockAppConfigurationClientListConfigurationSettings(mockedKVs);
83+
mockAppConfigurationClientListConfigurationSettings([mockedKVs]);
8484
});
8585

8686
after(() => {

0 commit comments

Comments
 (0)