Skip to content

Commit 2315e4c

Browse files
Load balance support (#135)
* load balance support * improve test
1 parent 477f18d commit 2315e4c

File tree

7 files changed

+150
-7
lines changed

7 files changed

+150
-7
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
7575
#featureFlagRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
7676
#featureFlagRefreshTimer: RefreshTimer;
7777

78-
// selectors
78+
// Selectors
7979
#featureFlagSelectors: PagedSettingSelector[] = [];
8080

81+
// Load balancing
82+
#lastSuccessfulEndpoint: string = "";
83+
8184
constructor(
8285
clientManager: ConfigurationClientManager,
8386
options: AzureAppConfigurationOptions | undefined,
@@ -202,14 +205,29 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
202205
}
203206

204207
async #executeWithFailoverPolicy(funcToExecute: (client: AppConfigurationClient) => Promise<any>): Promise<any> {
205-
const clientWrappers = await this.#clientManager.getClients();
208+
let clientWrappers = await this.#clientManager.getClients();
209+
if (this.#options?.loadBalancingEnabled && this.#lastSuccessfulEndpoint !== "" && clientWrappers.length > 1) {
210+
let nextClientIndex = 0;
211+
// Iterate through clients to find the index of the client with the last successful endpoint
212+
for (const clientWrapper of clientWrappers) {
213+
nextClientIndex++;
214+
if (clientWrapper.endpoint === this.#lastSuccessfulEndpoint) {
215+
break;
216+
}
217+
}
218+
// If we found the last successful client, rotate the list so that the next client is at the beginning
219+
if (nextClientIndex < clientWrappers.length) {
220+
clientWrappers = [...clientWrappers.slice(nextClientIndex), ...clientWrappers.slice(0, nextClientIndex)];
221+
}
222+
}
206223

207224
let successful: boolean;
208225
for (const clientWrapper of clientWrappers) {
209226
successful = false;
210227
try {
211228
const result = await funcToExecute(clientWrapper.client);
212229
this.#isFailoverRequest = false;
230+
this.#lastSuccessfulEndpoint = clientWrapper.endpoint;
213231
successful = true;
214232
clientWrapper.updateBackoffStatus(successful);
215233
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,6 +44,9 @@ 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";
52+
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
@@ -20,7 +20,9 @@ import {
2020
RequestType,
2121
SERVICE_FABRIC_ENV_VAR,
2222
CORRELATION_CONTEXT_HEADER_NAME,
23-
FAILOVER_REQUEST_TAG
23+
FAILOVER_REQUEST_TAG,
24+
FEATURES_KEY,
25+
LOAD_BALANCE_CONFIGURED_TAG
2426
} from "./constants";
2527

2628
// Utils
@@ -84,6 +86,9 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt
8486
keyValues.set(REQUEST_TYPE_KEY, isInitialLoadCompleted ? RequestType.WATCH : RequestType.STARTUP);
8587
keyValues.set(HOST_TYPE_KEY, getHostType());
8688
keyValues.set(ENV_KEY, isDevEnvironment() ? DEV_ENV_VAL : undefined);
89+
if (options?.loadBalancingEnabled) {
90+
keyValues.set(FEATURES_KEY, LOAD_BALANCE_CONFIGURED_TAG);
91+
}
8792

8893
const tags: string[] = [];
8994
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/loadBalance.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import * as chai from "chai";
5+
import * as chaiAsPromised from "chai-as-promised";
6+
chai.use(chaiAsPromised);
7+
const expect = chai.expect;
8+
import { load } from "./exportedApi.js";
9+
import { restoreMocks, createMockedConnectionString, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js";
10+
import { AppConfigurationClient } from "@azure/app-configuration";
11+
import { ConfigurationClientWrapper } from "../src/ConfigurationClientWrapper.js";
12+
13+
const fakeEndpoint_1 = createMockedEndpoint("fake_1");
14+
const fakeEndpoint_2 = createMockedEndpoint("fake_2");
15+
const fakeClientWrapper_1 = new ConfigurationClientWrapper(fakeEndpoint_1, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint_1)));
16+
const fakeClientWrapper_2 = new ConfigurationClientWrapper(fakeEndpoint_2, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint_2)));
17+
const clientRequestCounter_1 = {count: 0};
18+
const clientRequestCounter_2 = {count: 0};
19+
20+
describe("load balance", function () {
21+
this.timeout(10000);
22+
23+
beforeEach(() => {
24+
});
25+
26+
afterEach(() => {
27+
restoreMocks();
28+
});
29+
30+
it("should load balance the request when loadBalancingEnabled", async () => {
31+
mockConfigurationManagerGetClients([fakeClientWrapper_1, fakeClientWrapper_2], false);
32+
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_1, clientRequestCounter_1);
33+
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_2, clientRequestCounter_2);
34+
35+
const connectionString = createMockedConnectionString();
36+
const settings = await load(connectionString, {
37+
loadBalancingEnabled: true,
38+
featureFlagOptions: {
39+
enabled: true,
40+
selectors: [{
41+
keyFilter: "*"
42+
}],
43+
refresh: {
44+
enabled: true,
45+
refreshIntervalInMs: 2000 // 2 seconds for quick test.
46+
}
47+
}
48+
});
49+
// one request for key values, one request for feature flags
50+
expect(clientRequestCounter_1.count).eq(1);
51+
expect(clientRequestCounter_2.count).eq(1);
52+
53+
await sleepInMs(2 * 1000 + 1);
54+
await settings.refresh();
55+
// refresh request for feature flags
56+
expect(clientRequestCounter_1.count).eq(2);
57+
expect(clientRequestCounter_2.count).eq(1);
58+
59+
await sleepInMs(2 * 1000 + 1);
60+
await settings.refresh();
61+
expect(clientRequestCounter_1.count).eq(2);
62+
expect(clientRequestCounter_2.count).eq(2);
63+
});
64+
65+
it("should not load balance the request when loadBalance disabled", async () => {
66+
clientRequestCounter_1.count = 0;
67+
clientRequestCounter_2.count = 0;
68+
mockConfigurationManagerGetClients([fakeClientWrapper_1, fakeClientWrapper_2], false);
69+
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_1, clientRequestCounter_1);
70+
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_2, clientRequestCounter_2);
71+
72+
const connectionString = createMockedConnectionString();
73+
// loadBalancingEnabled is default to false
74+
const settings = await load(connectionString, {
75+
featureFlagOptions: {
76+
enabled: true,
77+
selectors: [{
78+
keyFilter: "*"
79+
}],
80+
refresh: {
81+
enabled: true,
82+
refreshIntervalInMs: 2000 // 2 seconds for quick test.
83+
}
84+
}
85+
});
86+
// one request for key values, one request for feature flags
87+
expect(clientRequestCounter_1.count).eq(2);
88+
expect(clientRequestCounter_2.count).eq(0);
89+
90+
await sleepInMs(2 * 1000 + 1);
91+
await settings.refresh();
92+
// refresh request for feature flags
93+
expect(clientRequestCounter_1.count).eq(3);
94+
expect(clientRequestCounter_2.count).eq(0);
95+
});
96+
});

test/utils/testHelper.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,21 @@ function mockAppConfigurationClientListConfigurationSettings(...pages: Configura
100100
});
101101
}
102102

103-
function mockConfigurationManagerGetClients(isFailoverable: boolean, ...pages: ConfigurationSetting[][]) {
103+
function mockAppConfigurationClientLoadBalanceMode(clientWrapper: ConfigurationClientWrapper, countObject: { count: number }) {
104+
const emptyPages: ConfigurationSetting[][] = [];
105+
sinon.stub(clientWrapper.client, "listConfigurationSettings").callsFake((listOptions) => {
106+
countObject.count += 1;
107+
const kvs = _filterKVs(emptyPages.flat(), listOptions);
108+
return getMockedIterator(emptyPages, kvs, listOptions);
109+
});
110+
}
111+
112+
function mockConfigurationManagerGetClients(fakeClientWrappers: ConfigurationClientWrapper[], isFailoverable: boolean, ...pages: ConfigurationSetting[][]) {
104113
// Stub the getClients method on the class prototype
105114
sinon.stub(ConfigurationClientManager.prototype, "getClients").callsFake(async () => {
115+
if (fakeClientWrappers?.length > 0) {
116+
return fakeClientWrappers;
117+
}
106118
const clients: ConfigurationClientWrapper[] = [];
107119
const fakeEndpoint = createMockedEndpoint("fake");
108120
const fakeStaticClientWrapper = new ConfigurationClientWrapper(fakeEndpoint, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint)));
@@ -230,6 +242,7 @@ export {
230242
sinon,
231243
mockAppConfigurationClientListConfigurationSettings,
232244
mockAppConfigurationClientGetConfigurationSetting,
245+
mockAppConfigurationClientLoadBalanceMode,
233246
mockConfigurationManagerGetClients,
234247
mockSecretClientGetSecret,
235248
restoreMocks,

0 commit comments

Comments
 (0)