Skip to content

Commit f88af6d

Browse files
committed
Updating the error handling behavior to emphasize resilience.
1 parent 96905f4 commit f88af6d

File tree

5 files changed

+93
-41
lines changed

5 files changed

+93
-41
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Initialize:
1212
const poller = new Poller({
1313
dataClient: dataClient,
1414
sessionConfig: {
15+
// This configuration will be specific to your app
1516
ApplicationIdentifier: 'MyApp',
1617
EnvironmentIdentifier: 'Test',
1718
ConfigurationProfileIdentifier: 'Config1',
@@ -22,19 +23,17 @@ const poller = new Poller({
2223
configParser: (s: string) => JSON.parse(s),
2324
});
2425

25-
try {
26-
await poller.start();
27-
} catch (e) {
28-
// Handle any errors connecting to AppConfig
29-
}
26+
// We avoid bubbling up exceptions, and keep trying in the background
27+
// even if we were not initially successful.
28+
const { isInitiallySuccessful, error } = await poller.start();
3029
```
3130

3231
Fetch:
3332

3433
```typescript
3534
// Instantly returns the cached configuration object that was
3635
// polled in the background.
37-
const configObject = poller.getConfigurationObject().latestValue;
36+
const { latestValue } = poller.getConfigurationObject();
3837
```
3938

4039
## Error handling
@@ -45,7 +44,8 @@ A few things can go wrong when polling for AppConfig, such as:
4544
- The config document could have been changed to something your configParser can't handle.
4645

4746
If there's an immediate connection problem during startup, and we're unable to retrieve the
48-
configuration even once, we'll fail fast from the poller.start() function.
47+
configuration even once, we'll report it in the response from poller.start(), and continue
48+
attempting to connect in the background.
4949

5050
If we startup successfully, but some time later there are problems polling, we'll report
5151
the error via the errorCausingStaleValue response attribute and continue polling in hopes

__tests__/poller.test.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,18 @@ describe('Poller', () => {
4949
...standardConfig,
5050
});
5151

52-
await poller.start();
52+
const { isInitiallySuccessful, error } = await poller.start();
53+
54+
expect(isInitiallySuccessful).toBeTruthy();
55+
expect(error).toBeUndefined();
56+
5357
const latest = poller.getConfigurationString();
5458

5559
expect(latest.latestValue).toEqual(configValue);
5660
expect(latest.errorCausingStaleValue).toBeUndefined();
5761
});
5862

59-
it('Bubbles up error on startup', async () => {
63+
it('Reports error if startConfigurationSession fails', async () => {
6064
appConfigClientMock.on(StartConfigurationSessionCommand).rejects({
6165
message: 'Failed to start session',
6266
} as AwsError);
@@ -68,10 +72,14 @@ describe('Poller', () => {
6872
...standardConfig,
6973
});
7074

71-
await expect(poller.start()).rejects.toThrow(Error);
75+
const { isInitiallySuccessful, error } = await poller.start();
76+
77+
expect(isInitiallySuccessful).toBeFalsy();
78+
expect(error).toBeDefined();
79+
expect(error.message).toBe('Failed to start session');
7280
});
7381

74-
it('Bubbles up error if first getLatest fails', async () => {
82+
it('Reports error if first getLatest fails', async () => {
7583
appConfigClientMock.on(StartConfigurationSessionCommand).resolves({
7684
InitialConfigurationToken: 'initialToken',
7785
});
@@ -87,7 +95,11 @@ describe('Poller', () => {
8795
...standardConfig,
8896
});
8997

90-
await expect(poller.start()).rejects.toThrow(Error);
98+
const { isInitiallySuccessful, error } = await poller.start();
99+
100+
expect(isInitiallySuccessful).toBeFalsy();
101+
expect(error).toBeDefined();
102+
expect(error.message).toBe('Failed to get latest');
91103
});
92104

93105
it('Continues polling if first getLatest string cannot be parsed', async () => {

examples/infinitePoller.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,19 @@ const poller = new Poller<SampleFormat>({
6262
pollIntervalSeconds: 60,
6363
});
6464

65-
await poller.start();
65+
const { isInitiallySuccessful, error } = await poller.start();
6666

67-
console.log('Starting at:', new Date());
67+
if (!isInitiallySuccessful) {
68+
poller.stop();
69+
throw new Error('Startup failed', { cause: error });
70+
}
71+
72+
console.log('Connection succeeded at:', new Date());
6873

6974
setInterval(() => {
7075
const obj = poller.getConfigurationObject();
7176
console.log('Current config entry', obj);
72-
}, 1000 * 60);
77+
}, 1000 * 5);
7378

7479
// This will run forever until you manually terminate it.
7580
// Normally you would call poller.stop() if you want the program to exit.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aws-appconfig-poller",
3-
"version": "0.0.2",
3+
"version": "0.0.3",
44
"description": "A wrapper around @aws-sdk/client-appconfigdata to provide background polling and caching.",
55
"repository": {
66
"type": "git",

src/poller.ts

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ export interface ConfigStore<T> {
2121
versionLabel?: string;
2222
}
2323

24+
export interface Outcome {
25+
isInitiallySuccessful: boolean;
26+
error?: Error;
27+
}
28+
2429
type PollingPhase = 'ready' | 'starting' | 'active' | 'stopped';
2530

26-
/**
27-
* Starts polling immediately upon construction.
28-
*/
2931
export class Poller<T> {
30-
private readonly DEFAULT_POLL_INTERVAL_SECONDS = 30;
32+
private readonly DEFAULT_POLL_INTERVAL_SECONDS = 60;
3133

3234
private readonly config: PollerConfig<T>;
3335

@@ -43,17 +45,33 @@ export class Poller<T> {
4345
constructor(config: PollerConfig<T>) {
4446
this.config = config;
4547

48+
const {
49+
pollIntervalSeconds,
50+
sessionConfig: { RequiredMinimumPollIntervalInSeconds: requiredMin },
51+
} = config;
52+
53+
if (
54+
pollIntervalSeconds &&
55+
requiredMin &&
56+
pollIntervalSeconds < requiredMin
57+
) {
58+
throw new Error(
59+
'Cannot configure a poll interval shorter than RequiredMinimumPollIntervalInSeconds',
60+
);
61+
}
62+
4663
this.configStringStore = {};
4764
this.configObjectStore = {};
4865
}
4966

50-
public async start(): Promise<void> {
67+
public async start(): Promise<Outcome> {
5168
if (this.pollingPhase != 'ready') {
5269
throw new Error('Can only call start() once for an instance of Poller!');
5370
}
5471
this.pollingPhase = 'starting';
55-
await this.startPolling();
72+
const result = await this.startPolling();
5673
this.pollingPhase = 'active';
74+
return result;
5775
}
5876

5977
public stop(): void {
@@ -94,25 +112,33 @@ export class Poller<T> {
94112
return this.configObjectStore;
95113
}
96114

97-
private async startPolling(): Promise<void> {
115+
private async startPolling(): Promise<Outcome> {
98116
const { dataClient, sessionConfig } = this.config;
99117

100118
const startCommand = new StartConfigurationSessionCommand(sessionConfig);
101-
const result = await dataClient.send(startCommand);
102119

103-
if (!result.InitialConfigurationToken) {
104-
throw new Error(
105-
'Missing configuration token from AppConfig StartConfigurationSession response',
106-
);
107-
}
120+
try {
121+
const result = await dataClient.send(startCommand);
108122

109-
this.configurationToken = result.InitialConfigurationToken;
123+
if (!result.InitialConfigurationToken) {
124+
throw new Error(
125+
'Missing configuration token from AppConfig StartConfigurationSession response',
126+
);
127+
}
128+
129+
this.configurationToken = result.InitialConfigurationToken;
110130

111-
await this.fetchLatestConfiguration();
131+
return await this.fetchLatestConfiguration();
132+
} catch (e) {
133+
return {
134+
isInitiallySuccessful: false,
135+
error: e,
136+
};
137+
}
112138
}
113139

114-
private async fetchLatestConfiguration(): Promise<void> {
115-
const { dataClient, logger } = this.config;
140+
private async fetchLatestConfiguration(): Promise<Outcome> {
141+
const { dataClient, logger, pollIntervalSeconds } = this.config;
116142

117143
const getCommand = new GetLatestConfigurationCommand({
118144
ConfigurationToken: this.configurationToken,
@@ -129,13 +155,9 @@ export class Poller<T> {
129155
this.configStringStore.errorCausingStaleValue = e;
130156
this.configObjectStore.errorCausingStaleValue = e;
131157

132-
if (this.pollingPhase === 'starting') {
133-
// If we're part of the initial startup sequence, fail fast.
134-
throw e;
135-
}
136-
137158
logger?.(
138-
'Values have gone stale, will wait and then start a new configuration session in response to error:',
159+
`Failed to get value from AppConfig during ${this.pollingPhase} phase!` +
160+
`Will wait ${pollIntervalSeconds}s and then start a new configuration session in response to error:`,
139161
e,
140162
);
141163

@@ -144,9 +166,12 @@ export class Poller<T> {
144166
'Starting new configuration session in hopes of recovering...',
145167
);
146168
this.startPolling();
147-
}, this.config.pollIntervalSeconds * 1000);
169+
}, pollIntervalSeconds * 1000);
148170

149-
return;
171+
return {
172+
isInitiallySuccessful: false,
173+
error: e,
174+
};
150175
}
151176

152177
const nextIntervalInSeconds = this.getNextIntervalInSeconds(
@@ -156,6 +181,10 @@ export class Poller<T> {
156181
this.timeoutHandle = setTimeout(() => {
157182
this.fetchLatestConfiguration();
158183
}, nextIntervalInSeconds * 1000);
184+
185+
return {
186+
isInitiallySuccessful: true,
187+
};
159188
}
160189

161190
private processGetResponse(
@@ -210,6 +239,12 @@ export class Poller<T> {
210239
}
211240

212241
private getNextIntervalInSeconds(awsSuggestedSeconds?: number): number {
242+
const { pollIntervalSeconds } = this.config;
243+
244+
if (awsSuggestedSeconds && pollIntervalSeconds) {
245+
return Math.max(awsSuggestedSeconds, pollIntervalSeconds);
246+
}
247+
213248
return (
214249
this.config.pollIntervalSeconds ||
215250
awsSuggestedSeconds ||

0 commit comments

Comments
 (0)