Skip to content

Commit 0b90940

Browse files
committed
Closes 1363
Add a local cache to AppConfigProvider to handle the scenario where the maxAge is expired, but the AppConfig session still has the latest configuration. In this case, AppConfig returns an empty value. We don't want to return empty values to our callers. This uses the same data structure we have in place to track NextPollConfigurationToken. This approach roughly the Python library's behavior.
1 parent 1168066 commit 0b90940

File tree

2 files changed

+63
-2
lines changed

2 files changed

+63
-2
lines changed

Diff for: packages/parameters/src/appconfig/AppConfigProvider.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
class AppConfigProvider extends BaseProvider {
1414
public client: AppConfigDataClient;
1515
protected configurationTokenStore: Map<string, string> = new Map();
16+
protected valueStore: Map<string, Uint8Array> = new Map();
1617
private application?: string;
1718
private environment: string;
1819

@@ -79,6 +80,10 @@ class AppConfigProvider extends BaseProvider {
7980
* The new AppConfig APIs require two API calls to return the configuration
8081
* First we start the session and after that we retrieve the configuration
8182
* We need to store { name: token } pairs to use in the next execution
83+
* We also need to store { name : value } pairs because AppConfig returns
84+
* an empty value if the session already has the latest configuration
85+
* but, we don't want to return an empty value to our callers.
86+
* {@link https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-retrieving-the-configuration.html}
8287
**/
8388

8489
if (!this.configurationTokenStore.has(name)) {
@@ -106,14 +111,25 @@ class AppConfigProvider extends BaseProvider {
106111
});
107112

108113
const response = await this.client.send(getConfigurationCommand);
109-
114+
110115
if (response.NextPollConfigurationToken) {
111116
this.configurationTokenStore.set(name, response.NextPollConfigurationToken);
112117
} else {
113118
this.configurationTokenStore.delete(name);
114119
}
115120

116-
return response.Configuration;
121+
/** When the response is not empty, stash the result locally before returning
122+
* See AppConfig docs:
123+
* {@link https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-retrieving-the-configuration.html}
124+
**/
125+
if (response.Configuration !== undefined && response.Configuration?.length > 0 ) {
126+
this.valueStore.set(name, response.Configuration);
127+
128+
return response.Configuration;
129+
}
130+
131+
// Otherwise, use a stashed value
132+
return this.valueStore.get(name);
117133
}
118134

119135
/**

Diff for: packages/parameters/tests/unit/AppConfigProvider.test.ts

+45
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,51 @@ describe('Class: AppConfigProvider', () => {
225225
// Act & Assess
226226
await expect(provider.get(name)).rejects.toThrow();
227227
});
228+
229+
test('when session returns an empty configuration on the second call, it returns the last value', async () => {
230+
231+
client.reset();
232+
233+
// Prepare
234+
const options: AppConfigProviderOptions = {
235+
application: 'MyApp',
236+
environment: 'MyAppProdEnv',
237+
};
238+
const provider = new AppConfigProvider(options);
239+
const name = 'MyAppFeatureFlag';
240+
241+
const fakeInitialToken = 'aW5pdGlhbFRva2Vu';
242+
const fakeNextToken1 = 'bmV4dFRva2Vu';
243+
const fakeNextToken2 = 'bmV4dFRva2Vq';
244+
const mockData = encoder.encode('myAppConfiguration');
245+
246+
client
247+
.on(StartConfigurationSessionCommand)
248+
.resolves({
249+
InitialConfigurationToken: fakeInitialToken,
250+
})
251+
.on(GetLatestConfigurationCommand)
252+
.resolvesOnce({
253+
Configuration: mockData,
254+
NextPollConfigurationToken: fakeNextToken1,
255+
})
256+
.resolvesOnce({
257+
Configuration: undefined,
258+
NextPollConfigurationToken: fakeNextToken2,
259+
});
260+
261+
// Act
262+
263+
// Load local cache
264+
const result1 = await provider.get(name, { forceFetch: true });
265+
266+
// Read from local cache, given empty response from service
267+
const result2 = await provider.get(name, { forceFetch: true });
268+
269+
// Assess
270+
expect(result1).toBe(mockData);
271+
expect(result2).toBe(mockData);
272+
});
228273
});
229274

230275
describe('Method: _getMultiple', () => {

0 commit comments

Comments
 (0)