Skip to content

Commit 50607da

Browse files
optimization for afd watch scenarios (#275)
1 parent 950dd46 commit 50607da

File tree

6 files changed

+110
-36
lines changed

6 files changed

+110
-36
lines changed

package-lock.json

Lines changed: 4 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"playwright": "^1.55.0"
6767
},
6868
"dependencies": {
69-
"@azure/app-configuration": "^1.10.0",
69+
"@azure/app-configuration": "^1.11.0",
7070
"@azure/core-rest-pipeline": "^1.6.0",
7171
"@azure/identity": "^4.2.1",
7272
"@azure/keyvault-secrets": "^4.7.0",

src/appConfigurationImpl.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import { AzureKeyVaultKeyValueAdapter } from "./keyvault/keyVaultKeyValueAdapter
5858
import { RefreshTimer } from "./refresh/refreshTimer.js";
5959
import {
6060
RequestTracingOptions,
61+
checkConfigurationSettingsWithTrace,
6162
getConfigurationSettingWithTrace,
6263
listConfigurationSettingsWithTrace,
6364
getSnapshotWithTrace,
@@ -766,7 +767,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
766767
listOptions.pageEtags = pageWatchers.map(w => w.etag ?? "") ;
767768
}
768769

769-
const pageIterator = listConfigurationSettingsWithTrace(
770+
const pageIterator = checkConfigurationSettingsWithTrace(
770771
this.#requestTraceOptions,
771772
client,
772773
listOptions

src/requestTracing/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ConfigurationSettingId,
88
GetConfigurationSettingOptions,
99
ListConfigurationSettingsOptions,
10+
CheckConfigurationSettingsOptions,
1011
GetSnapshotOptions,
1112
ListConfigurationSettingsForSnapshotOptions
1213
} from "@azure/app-configuration";
@@ -69,6 +70,15 @@ export function listConfigurationSettingsWithTrace(
6970
return client.listConfigurationSettings(actualListOptions);
7071
}
7172

73+
export function checkConfigurationSettingsWithTrace(
74+
requestTracingOptions: RequestTracingOptions,
75+
client: AppConfigurationClient,
76+
checkOptions: CheckConfigurationSettingsOptions
77+
) {
78+
const actualCheckOptions = applyRequestTracing(requestTracingOptions, checkOptions);
79+
return client.checkConfigurationSettings(actualCheckOptions);
80+
}
81+
7282
export function getConfigurationSettingWithTrace(
7383
requestTracingOptions: RequestTracingOptions,
7484
client: AppConfigurationClient,

test/afd.test.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -134,30 +134,35 @@ describe("loadFromAzureFrontDoor", function() {
134134
const kv2_updated = createMockedKeyValue({ key: "app.key2", value: "value2-updated" });
135135
const kv3 = createMockedKeyValue({ key: "app.key3", value: "value3" });
136136

137-
const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
137+
const listStub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
138+
const checkStub = sinon.stub(AppConfigurationClient.prototype, "checkConfigurationSettings" as any);
138139

139-
stub.onCall(0).returns(getCachedIterator([
140+
// Initial load
141+
listStub.onCall(0).returns(getCachedIterator([
140142
{ items: [kv1, kv2], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } }
141143
]));
142144

143-
stub.onCall(1).returns(getCachedIterator([
145+
// 1st refresh: check (HEAD) detects change, then reload (GET)
146+
checkStub.onCall(0).returns(getCachedIterator([
144147
{ items: [kv1, kv2_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:01Z") } }
145148
]));
146-
stub.onCall(2).returns(getCachedIterator([
149+
listStub.onCall(1).returns(getCachedIterator([
147150
{ items: [kv1, kv2_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:01Z") } }
148151
]));
149152

150-
stub.onCall(3).returns(getCachedIterator([
153+
// 2nd refresh: check (HEAD) detects change, then reload (GET)
154+
checkStub.onCall(1).returns(getCachedIterator([
151155
{ items: [kv1], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:03Z") } }
152156
]));
153-
stub.onCall(4).returns(getCachedIterator([
157+
listStub.onCall(2).returns(getCachedIterator([
154158
{ items: [kv1], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:03Z") } }
155159
]));
156160

157-
stub.onCall(5).returns(getCachedIterator([
161+
// 3rd refresh: check (HEAD) detects change, then reload (GET)
162+
checkStub.onCall(2).returns(getCachedIterator([
158163
{ items: [kv1, kv3], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:05Z") } }
159164
]));
160-
stub.onCall(6).returns(getCachedIterator([
165+
listStub.onCall(3).returns(getCachedIterator([
161166
{ items: [kv1, kv3], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:05Z") } }
162167
]));
163168

@@ -192,19 +197,22 @@ describe("loadFromAzureFrontDoor", function() {
192197
const ff = createMockedFeatureFlag("Beta");
193198
const ff_updated = createMockedFeatureFlag("Beta", { enabled: false });
194199

195-
const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
200+
const listStub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
201+
const checkStub = sinon.stub(AppConfigurationClient.prototype, "checkConfigurationSettings" as any);
196202

197-
stub.onCall(0).returns(getCachedIterator([
203+
// Initial load: onCall(0) = default KV selector, onCall(1) = feature flags
204+
listStub.onCall(0).returns(getCachedIterator([
198205
{ items: [ff], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } }
199206
]));
200-
stub.onCall(1).returns(getCachedIterator([
207+
listStub.onCall(1).returns(getCachedIterator([
201208
{ items: [ff], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } }
202209
]));
203210

204-
stub.onCall(2).returns(getCachedIterator([
211+
// 1st refresh: check (HEAD) detects change, then reload (GET)
212+
checkStub.onCall(0).returns(getCachedIterator([
205213
{ items: [ff_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:03Z") } }
206214
]));
207-
stub.onCall(3).returns(getCachedIterator([
215+
listStub.onCall(2).returns(getCachedIterator([
208216
{ items: [ff_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:03Z") } }
209217
]));
210218

@@ -235,18 +243,23 @@ describe("loadFromAzureFrontDoor", function() {
235243
const kv1_stale = createMockedKeyValue({ key: "app.key1", value: "stale-value" });
236244
const kv1_new = createMockedKeyValue({ key: "app.key1", value: "new-value" });
237245

238-
const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
239-
stub.onCall(0).returns(getCachedIterator([
246+
const listStub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
247+
const checkStub = sinon.stub(AppConfigurationClient.prototype, "checkConfigurationSettings" as any);
248+
249+
// Initial load
250+
listStub.onCall(0).returns(getCachedIterator([
240251
{ items: [kv1], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:01Z") } }
241252
]));
242253

243-
stub.onCall(1).returns(getCachedIterator([
244-
{ items: [kv1_stale], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } } // stale response, should not trigger refresh
254+
// 1st refresh: check (HEAD) returns stale response, should not trigger refresh
255+
checkStub.onCall(0).returns(getCachedIterator([
256+
{ items: [kv1_stale], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } }
245257
]));
246-
stub.onCall(2).returns(getCachedIterator([
247-
{ items: [kv1_new], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:02Z") } } // new response, should trigger refresh
258+
// 2nd refresh: check (HEAD) detects change, then reload (GET)
259+
checkStub.onCall(1).returns(getCachedIterator([
260+
{ items: [kv1_new], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:02Z") } }
248261
]));
249-
stub.onCall(3).returns(getCachedIterator([
262+
listStub.onCall(1).returns(getCachedIterator([
250263
{ items: [kv1_new], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:02Z") } }
251264
]));
252265

test/utils/testHelper.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,48 @@ function getCachedIterator(pages: Array<{
150150
return iterator as any;
151151
}
152152

153+
function getMockedHeadIterator(pages: ConfigurationSetting[][], listOptions: any) {
154+
const mockIterator: AsyncIterableIterator<any> & { byPage(): AsyncIterableIterator<any> } = {
155+
[Symbol.asyncIterator](): AsyncIterableIterator<any> {
156+
return this;
157+
},
158+
next() {
159+
return Promise.resolve({ done: true, value: undefined });
160+
},
161+
byPage(): AsyncIterableIterator<any> {
162+
let remainingPages;
163+
const pageEtags = listOptions?.pageEtags ? [...listOptions.pageEtags] : undefined;
164+
return {
165+
[Symbol.asyncIterator](): AsyncIterableIterator<any> {
166+
remainingPages = [...pages];
167+
return this;
168+
},
169+
async next() {
170+
const pageItems = remainingPages.shift();
171+
const pageEtag = pageEtags?.shift();
172+
if (pageItems === undefined) {
173+
return { done: true, value: undefined };
174+
} else {
175+
const items = _filterKVs(pageItems ?? [], listOptions);
176+
const etag = await _sha256(JSON.stringify(items));
177+
const statusCode = pageEtag === etag ? 304 : 200;
178+
return {
179+
done: false,
180+
value: {
181+
items: [], // HEAD request returns no items
182+
etag,
183+
_response: { status: statusCode }
184+
}
185+
};
186+
}
187+
}
188+
};
189+
}
190+
};
191+
192+
return mockIterator as any;
193+
}
194+
153195
/**
154196
* Mocks the listConfigurationSettings method of AppConfigurationClient to return the provided pages of ConfigurationSetting.
155197
* E.g.
@@ -167,6 +209,14 @@ function mockAppConfigurationClientListConfigurationSettings(pages: Configuratio
167209
const kvs = _filterKVs(pages.flat(), listOptions);
168210
return getMockedIterator(pages, kvs, listOptions);
169211
});
212+
213+
sinon.stub(AppConfigurationClient.prototype, "checkConfigurationSettings").callsFake((listOptions) => {
214+
if (customCallback) {
215+
customCallback(listOptions);
216+
}
217+
218+
return getMockedHeadIterator(pages, listOptions);
219+
});
170220
}
171221

172222
function mockAppConfigurationClientLoadBalanceMode(pages: ConfigurationSetting[][], clientWrapper: ConfigurationClientWrapper, countObject: { count: number }) {
@@ -175,6 +225,10 @@ function mockAppConfigurationClientLoadBalanceMode(pages: ConfigurationSetting[]
175225
const kvs = _filterKVs(pages.flat(), listOptions);
176226
return getMockedIterator(pages, kvs, listOptions);
177227
});
228+
sinon.stub(clientWrapper.client, "checkConfigurationSettings").callsFake((listOptions) => {
229+
countObject.count += 1;
230+
return getMockedHeadIterator(pages, listOptions);
231+
});
178232
}
179233

180234
function mockConfigurationManagerGetClients(fakeClientWrappers: ConfigurationClientWrapper[], isFailoverable: boolean, ...pages: ConfigurationSetting[][]) {
@@ -189,6 +243,9 @@ function mockConfigurationManagerGetClients(fakeClientWrappers: ConfigurationCli
189243
sinon.stub(fakeStaticClientWrapper.client, "listConfigurationSettings").callsFake(() => {
190244
throw new RestError("Internal Server Error", { statusCode: 500 });
191245
});
246+
sinon.stub(fakeStaticClientWrapper.client, "checkConfigurationSettings").callsFake(() => {
247+
throw new RestError("Internal Server Error", { statusCode: 500 });
248+
});
192249
clients.push(fakeStaticClientWrapper);
193250

194251
if (!isFailoverable) {
@@ -202,6 +259,9 @@ function mockConfigurationManagerGetClients(fakeClientWrappers: ConfigurationCli
202259
const kvs = _filterKVs(pages.flat(), listOptions);
203260
return getMockedIterator(pages, kvs, listOptions);
204261
});
262+
sinon.stub(fakeDynamicClientWrapper.client, "checkConfigurationSettings").callsFake((listOptions) => {
263+
return getMockedHeadIterator(pages, listOptions);
264+
});
205265
return clients;
206266
});
207267
}

0 commit comments

Comments
 (0)