Skip to content

Commit f30d237

Browse files
Merge pull request #139 from Azure/zhiyuanliang/merge-main-to-preview
Merge main to preview
2 parents 2315e4c + 11e505c commit f30d237

File tree

8 files changed

+121
-22
lines changed

8 files changed

+121
-22
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
6363
#isFailoverRequest: boolean = false;
6464

6565
// Refresh
66+
#refreshInProgress: boolean = false;
67+
6668
#refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
6769
#onRefreshListeners: Array<() => any> = [];
6870
/**
@@ -436,6 +438,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
436438
throw new Error("Refresh is not enabled for key-values or feature flags.");
437439
}
438440

441+
if (this.#refreshInProgress) {
442+
return;
443+
}
444+
this.#refreshInProgress = true;
445+
try {
446+
await this.#refreshTasks();
447+
} finally {
448+
this.#refreshInProgress = false;
449+
}
450+
}
451+
452+
async #refreshTasks(): Promise<void> {
439453
const refreshTasks: Promise<boolean>[] = [];
440454
if (this.#refreshEnabled) {
441455
refreshTasks.push(this.#refreshKeyValues());

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(() => {

test/refresh.test.ts

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ function addSetting(key: string, value: any) {
2323
mockedKVs.push(createMockedKeyValue({ key, value }));
2424
}
2525

26+
let listKvRequestCount = 0;
27+
const listKvCallback = () => {
28+
listKvRequestCount++;
29+
};
30+
let getKvRequestCount = 0;
31+
const getKvCallback = () => {
32+
getKvRequestCount++;
33+
};
34+
2635
describe("dynamic refresh", function () {
2736
this.timeout(10000);
2837

@@ -32,12 +41,14 @@ describe("dynamic refresh", function () {
3241
{ value: "40", key: "app.settings.fontSize" },
3342
{ value: "30", key: "app.settings.fontSize", label: "prod" }
3443
].map(createMockedKeyValue);
35-
mockAppConfigurationClientListConfigurationSettings(mockedKVs);
36-
mockAppConfigurationClientGetConfigurationSetting(mockedKVs);
44+
mockAppConfigurationClientListConfigurationSettings([mockedKVs], listKvCallback);
45+
mockAppConfigurationClientGetConfigurationSetting(mockedKVs, getKvCallback);
3746
});
3847

3948
afterEach(() => {
4049
restoreMocks();
50+
listKvRequestCount = 0;
51+
getKvRequestCount = 0;
4152
});
4253

4354
it("should throw error when refresh is not enabled but refresh is called", async () => {
@@ -139,6 +150,8 @@ describe("dynamic refresh", function () {
139150
]
140151
}
141152
});
153+
expect(listKvRequestCount).eq(1);
154+
expect(getKvRequestCount).eq(0);
142155
expect(settings).not.undefined;
143156
expect(settings.get("app.settings.fontColor")).eq("red");
144157
expect(settings.get("app.settings.fontSize")).eq("40");
@@ -149,10 +162,14 @@ describe("dynamic refresh", function () {
149162
// within refreshInterval, should not really refresh
150163
await settings.refresh();
151164
expect(settings.get("app.settings.fontColor")).eq("red");
165+
expect(listKvRequestCount).eq(1); // no more request should be sent during the refresh interval
166+
expect(getKvRequestCount).eq(0); // no more request should be sent during the refresh interval
152167

153168
// after refreshInterval, should really refresh
154169
await sleepInMs(2 * 1000 + 1);
155170
await settings.refresh();
171+
expect(listKvRequestCount).eq(2);
172+
expect(getKvRequestCount).eq(1);
156173
expect(settings.get("app.settings.fontColor")).eq("blue");
157174
});
158175

@@ -167,18 +184,22 @@ describe("dynamic refresh", function () {
167184
]
168185
}
169186
});
187+
expect(listKvRequestCount).eq(1);
188+
expect(getKvRequestCount).eq(0);
170189
expect(settings).not.undefined;
171190
expect(settings.get("app.settings.fontColor")).eq("red");
172191
expect(settings.get("app.settings.fontSize")).eq("40");
173192

174193
// delete setting 'app.settings.fontColor'
175194
const newMockedKVs = mockedKVs.filter(elem => elem.key !== "app.settings.fontColor");
176195
restoreMocks();
177-
mockAppConfigurationClientListConfigurationSettings(newMockedKVs);
178-
mockAppConfigurationClientGetConfigurationSetting(newMockedKVs);
196+
mockAppConfigurationClientListConfigurationSettings([newMockedKVs], listKvCallback);
197+
mockAppConfigurationClientGetConfigurationSetting(newMockedKVs, getKvCallback);
179198

180199
await sleepInMs(2 * 1000 + 1);
181200
await settings.refresh();
201+
expect(listKvRequestCount).eq(2);
202+
expect(getKvRequestCount).eq(2); // one conditional request to detect change and one request as part of loading all kvs (because app.settings.fontColor doesn't exist in the response of listKv request)
182203
expect(settings.get("app.settings.fontColor")).eq(undefined);
183204
});
184205

@@ -193,13 +214,17 @@ describe("dynamic refresh", function () {
193214
]
194215
}
195216
});
217+
expect(listKvRequestCount).eq(1);
218+
expect(getKvRequestCount).eq(0);
196219
expect(settings).not.undefined;
197220
expect(settings.get("app.settings.fontColor")).eq("red");
198221
expect(settings.get("app.settings.fontSize")).eq("40");
199222

200223
updateSetting("app.settings.fontSize", "50"); // unwatched setting
201224
await sleepInMs(2 * 1000 + 1);
202225
await settings.refresh();
226+
expect(listKvRequestCount).eq(1);
227+
expect(getKvRequestCount).eq(1);
203228
expect(settings.get("app.settings.fontSize")).eq("40");
204229
});
205230

@@ -215,6 +240,8 @@ describe("dynamic refresh", function () {
215240
]
216241
}
217242
});
243+
expect(listKvRequestCount).eq(1);
244+
expect(getKvRequestCount).eq(0);
218245
expect(settings).not.undefined;
219246
expect(settings.get("app.settings.fontColor")).eq("red");
220247
expect(settings.get("app.settings.fontSize")).eq("40");
@@ -224,6 +251,8 @@ describe("dynamic refresh", function () {
224251
updateSetting("app.settings.fontSize", "50");
225252
await sleepInMs(2 * 1000 + 1);
226253
await settings.refresh();
254+
expect(listKvRequestCount).eq(2);
255+
expect(getKvRequestCount).eq(2); // two getKv request for two watched settings
227256
expect(settings.get("app.settings.fontSize")).eq("50");
228257
expect(settings.get("app.settings.bgColor")).eq("white");
229258
});
@@ -309,6 +338,8 @@ describe("dynamic refresh", function () {
309338
]
310339
}
311340
});
341+
expect(listKvRequestCount).eq(1);
342+
expect(getKvRequestCount).eq(1); // app.settings.bgColor doesn't exist in the response of listKv request, so an additional getKv request is made to get it.
312343
expect(settings).not.undefined;
313344
expect(settings.get("app.settings.fontColor")).eq("red");
314345
expect(settings.get("app.settings.fontSize")).eq("40");
@@ -317,10 +348,45 @@ describe("dynamic refresh", function () {
317348
updateSetting("app.settings.fontColor", "blue");
318349
await sleepInMs(2 * 1000 + 1);
319350
await settings.refresh();
351+
expect(listKvRequestCount).eq(1);
352+
expect(getKvRequestCount).eq(2);
320353
// should not refresh
321354
expect(settings.get("app.settings.fontColor")).eq("red");
322355
});
323356

357+
it("should not refresh any more when there is refresh in progress", async () => {
358+
const connectionString = createMockedConnectionString();
359+
const settings = await load(connectionString, {
360+
refreshOptions: {
361+
enabled: true,
362+
refreshIntervalInMs: 2000,
363+
watchedSettings: [
364+
{ key: "app.settings.fontColor" }
365+
]
366+
}
367+
});
368+
expect(listKvRequestCount).eq(1);
369+
expect(getKvRequestCount).eq(0);
370+
expect(settings).not.undefined;
371+
expect(settings.get("app.settings.fontColor")).eq("red");
372+
373+
// change setting
374+
updateSetting("app.settings.fontColor", "blue");
375+
376+
// after refreshInterval, should really refresh
377+
await sleepInMs(2 * 1000 + 1);
378+
for (let i = 0; i < 5; i++) { // in practice, refresh should not be used in this way
379+
settings.refresh(); // refresh "concurrently"
380+
}
381+
expect(listKvRequestCount).to.be.at.most(2);
382+
expect(getKvRequestCount).to.be.at.most(1);
383+
384+
await sleepInMs(1000); // wait for all 5 refresh attempts to finish
385+
386+
expect(listKvRequestCount).eq(2);
387+
expect(getKvRequestCount).eq(1);
388+
expect(settings.get("app.settings.fontColor")).eq("blue");
389+
});
324390
});
325391

326392
describe("dynamic refresh feature flags", function () {
@@ -331,14 +397,16 @@ describe("dynamic refresh feature flags", function () {
331397

332398
afterEach(() => {
333399
restoreMocks();
400+
listKvRequestCount = 0;
401+
getKvRequestCount = 0;
334402
});
335403

336404
it("should refresh feature flags when enabled", async () => {
337405
mockedKVs = [
338406
createMockedFeatureFlag("Beta", { enabled: true })
339407
];
340-
mockAppConfigurationClientListConfigurationSettings(mockedKVs);
341-
mockAppConfigurationClientGetConfigurationSetting(mockedKVs);
408+
mockAppConfigurationClientListConfigurationSettings([mockedKVs], listKvCallback);
409+
mockAppConfigurationClientGetConfigurationSetting(mockedKVs, getKvCallback);
342410

343411
const connectionString = createMockedConnectionString();
344412
const settings = await load(connectionString, {
@@ -353,6 +421,8 @@ describe("dynamic refresh feature flags", function () {
353421
}
354422
}
355423
});
424+
expect(listKvRequestCount).eq(2); // one listKv request for kvs and one listKv request for feature flags
425+
expect(getKvRequestCount).eq(0);
356426
expect(settings).not.undefined;
357427
expect(settings.get("feature_management")).not.undefined;
358428
expect(settings.get<any>("feature_management").feature_flags).not.undefined;
@@ -371,6 +441,8 @@ describe("dynamic refresh feature flags", function () {
371441

372442
await sleepInMs(2 * 1000 + 1);
373443
await settings.refresh();
444+
expect(listKvRequestCount).eq(4); // 2 + 2 more requests: one conditional request to detect change and one request to reload all feature flags
445+
expect(getKvRequestCount).eq(0);
374446

375447
expect(settings.get<any>("feature_management").feature_flags[0].id).eq("Beta");
376448
expect(settings.get<any>("feature_management").feature_flags[0].enabled).eq(false);
@@ -387,8 +459,8 @@ describe("dynamic refresh feature flags", function () {
387459
createMockedFeatureFlag("Beta_1", { enabled: true }),
388460
createMockedFeatureFlag("Beta_2", { enabled: true }),
389461
];
390-
mockAppConfigurationClientListConfigurationSettings(page1, page2);
391-
mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2]);
462+
mockAppConfigurationClientListConfigurationSettings([page1, page2], listKvCallback);
463+
mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2], getKvCallback);
392464

393465
const connectionString = createMockedConnectionString();
394466
const settings = await load(connectionString, {
@@ -403,6 +475,8 @@ describe("dynamic refresh feature flags", function () {
403475
}
404476
}
405477
});
478+
expect(listKvRequestCount).eq(2);
479+
expect(getKvRequestCount).eq(0);
406480

407481
let refreshSuccessfulCount = 0;
408482
settings.onRefresh(() => {
@@ -411,16 +485,20 @@ describe("dynamic refresh feature flags", function () {
411485

412486
await sleepInMs(2 * 1000 + 1);
413487
await settings.refresh();
488+
expect(listKvRequestCount).eq(3); // one conditional request to detect change
489+
expect(getKvRequestCount).eq(0);
414490
expect(refreshSuccessfulCount).eq(0); // no change in feature flags, because page etags are the same.
415491

416492
// change feature flag Beta_1 to false
417493
page2[0] = createMockedFeatureFlag("Beta_1", { enabled: false });
418494
restoreMocks();
419-
mockAppConfigurationClientListConfigurationSettings(page1, page2);
420-
mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2]);
495+
mockAppConfigurationClientListConfigurationSettings([page1, page2], listKvCallback);
496+
mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2], getKvCallback);
421497

422498
await sleepInMs(2 * 1000 + 1);
423499
await settings.refresh();
500+
expect(listKvRequestCount).eq(5); // 3 + 2 more requests: one conditional request to detect change and one request to reload all feature flags
501+
expect(getKvRequestCount).eq(0);
424502
expect(refreshSuccessfulCount).eq(1); // change in feature flags, because page etags are different.
425503
});
426504
});

test/requestTracing.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,10 @@ describe("request tracing", function () {
115115
});
116116

117117
it("should have request type in correlation-context header when refresh is enabled", async () => {
118-
mockAppConfigurationClientListConfigurationSettings([{
118+
mockAppConfigurationClientListConfigurationSettings([[{
119119
key: "app.settings.fontColor",
120120
value: "red"
121-
}].map(createMockedKeyValue));
121+
}].map(createMockedKeyValue)]);
122122

123123
const settings = await load(createMockedConnectionString(fakeEndpoint), {
124124
clientOptions,

test/utils/testHelper.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,16 @@ function getMockedIterator(pages: ConfigurationSetting[][], kvs: ConfigurationSe
8888
* Mocks the listConfigurationSettings method of AppConfigurationClient to return the provided pages of ConfigurationSetting.
8989
* E.g.
9090
* - mockAppConfigurationClientListConfigurationSettings([item1, item2, item3]) // single page
91-
* - mockAppConfigurationClientListConfigurationSettings([item1, item2], [item3], [item4]) // multiple pages
9291
*
9392
* @param pages List of pages, each page is a list of ConfigurationSetting
9493
*/
95-
function mockAppConfigurationClientListConfigurationSettings(...pages: ConfigurationSetting[][]) {
94+
function mockAppConfigurationClientListConfigurationSettings(pages: ConfigurationSetting[][], customCallback?: (listOptions) => any) {
9695

9796
sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings").callsFake((listOptions) => {
97+
if (customCallback) {
98+
customCallback(listOptions);
99+
}
100+
98101
const kvs = _filterKVs(pages.flat(), listOptions);
99102
return getMockedIterator(pages, kvs, listOptions);
100103
});
@@ -138,8 +141,12 @@ function mockConfigurationManagerGetClients(fakeClientWrappers: ConfigurationCli
138141
});
139142
}
140143

141-
function mockAppConfigurationClientGetConfigurationSetting(kvList) {
144+
function mockAppConfigurationClientGetConfigurationSetting(kvList, customCallback?: (options) => any) {
142145
sinon.stub(AppConfigurationClient.prototype, "getConfigurationSetting").callsFake((settingId, options) => {
146+
if (customCallback) {
147+
customCallback(options);
148+
}
149+
143150
const found = kvList.find(elem => elem.key === settingId.key && elem.label === settingId.label);
144151
if (found) {
145152
if (options?.onlyIfChanged && settingId.etag === found.etag) {

0 commit comments

Comments
 (0)