Skip to content

Commit

Permalink
Making the poller restart the configuration session if there's an error.
Browse files Browse the repository at this point in the history
  • Loading branch information
tarehart committed Dec 11, 2023
1 parent cdfc2ae commit 96905f4
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 57 deletions.
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,51 @@ const poller = new Poller({
ConfigurationProfileIdentifier: 'Config1',
},
logger: console.log,
// Turn the config string into an object however you like,
// we'll cache the result (and also the raw string).
configParser: (s: string) => JSON.parse(s),
});

await poller.start();
try {
await poller.start();
} catch (e) {
// Handle any errors connecting to AppConfig
}
```

Fetch:

```typescript
const value = poller.getConfigurationString().latestValue;
// Instantly returns the cached configuration object that was
// polled in the background.
const configObject = poller.getConfigurationObject().latestValue;
```

## Error handling

A few things can go wrong when polling for AppConfig, such as:

- Permissions or networking error connecting to AWS.
- The config document could have been changed to something your configParser can't handle.

If there's an immediate connection problem during startup, and we're unable to retrieve the
configuration even once, we'll fail fast from the poller.start() function.

If we startup successfully, but some time later there are problems polling, we'll report
the error via the errorCausingStaleValue response attribute and continue polling in hopes
that the error resolves itself. You may wish to monitor or alarm on this situation because
your config object is potentially outdated.

```typescript
const { latestValue, errorCausingStaleValue } = poller.getConfigurationObject();
if (errorCausingStaleValue) {
// Log some metric
}
```

## License

Licensed under the MIT license. See the [LICENSE](https://github.com/tarehart/aws-appconfig-poller/blob/main/LICENSE) file for details.


[gha-badge]: https://github.com/tarehart/aws-appconfig-poller/actions/workflows/nodejs.yml/badge.svg
[gha-ci]: https://github.com/tarehart/aws-appconfig-poller/actions/workflows/nodejs.yml
157 changes: 140 additions & 17 deletions __tests__/poller.test.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,171 @@
import { Poller } from '../src/poller.js';
import { Poller, PollerConfig } from '../src/poller.js';
import {
AppConfigDataClient,
GetLatestConfigurationCommand,
StartConfigurationSessionCommand,
} from '@aws-sdk/client-appconfigdata';
import { mockClient } from 'aws-sdk-client-mock';
import { AwsError, mockClient } from 'aws-sdk-client-mock';
import { Uint8ArrayBlobAdapter } from '@smithy/util-stream';

const standardConfig: Omit<PollerConfig<string>, 'dataClient'> = {
sessionConfig: {
ApplicationIdentifier: 'MyApp',
EnvironmentIdentifier: 'Test',
ConfigurationProfileIdentifier: 'Config1',
},
configParser: (s: string) => s.substring(1),
pollIntervalSeconds: 1,
};

function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

describe('Poller', () => {
const appConfigClientMock = mockClient(AppConfigDataClient);

const configValue = 'Some Configuration';
let poller: Poller<string> | undefined;

appConfigClientMock.on(StartConfigurationSessionCommand).resolves({
InitialConfigurationToken: 'initialToken',
afterEach(() => {
poller.stop();
appConfigClientMock.reset();
});

appConfigClientMock.on(GetLatestConfigurationCommand).resolves({
Configuration: Uint8ArrayBlobAdapter.fromString(configValue),
it('Polls', async () => {
const configValue = 'Some Configuration';

appConfigClientMock.on(StartConfigurationSessionCommand).resolves({
InitialConfigurationToken: 'initialToken',
});

appConfigClientMock.on(GetLatestConfigurationCommand).resolves({
Configuration: Uint8ArrayBlobAdapter.fromString(configValue),
});

const dataClient = new AppConfigDataClient();

poller = new Poller({
dataClient: dataClient,
...standardConfig,
});

await poller.start();
const latest = poller.getConfigurationString();

expect(latest.latestValue).toEqual(configValue);
expect(latest.errorCausingStaleValue).toBeUndefined();
});

const dataClient = new AppConfigDataClient();
it('Bubbles up error on startup', async () => {
appConfigClientMock.on(StartConfigurationSessionCommand).rejects({
message: 'Failed to start session',
} as AwsError);

let poller: Poller<string> | undefined;
const dataClient = new AppConfigDataClient();

afterAll(() => {
poller.stop();
poller = new Poller({
dataClient: dataClient,
...standardConfig,
});

await expect(poller.start()).rejects.toThrow(Error);
});

it('Polls', async () => {
it('Bubbles up error if first getLatest fails', async () => {
appConfigClientMock.on(StartConfigurationSessionCommand).resolves({
InitialConfigurationToken: 'initialToken',
});

appConfigClientMock.on(GetLatestConfigurationCommand).rejects({
message: 'Failed to get latest',
});

const dataClient = new AppConfigDataClient();

poller = new Poller({
dataClient: dataClient,
sessionConfig: {
ApplicationIdentifier: 'MyApp',
EnvironmentIdentifier: 'Test',
ConfigurationProfileIdentifier: 'Config1',
...standardConfig,
});

await expect(poller.start()).rejects.toThrow(Error);
});

it('Continues polling if first getLatest string cannot be parsed', async () => {
appConfigClientMock.on(StartConfigurationSessionCommand).resolves({
InitialConfigurationToken: 'initialToken',
});

appConfigClientMock.on(GetLatestConfigurationCommand).resolves({
Configuration: Uint8ArrayBlobAdapter.fromString('abc'),
});

const dataClient = new AppConfigDataClient();

poller = new Poller({
dataClient: dataClient,
...standardConfig,
configParser(s): string {
throw new Error('bad string ' + s);
},
logger: console.log,
});

await poller.start();

const str = poller.getConfigurationString();
expect(str.latestValue).toBeDefined();

const obj = poller.getConfigurationObject();
expect(obj.latestValue).toBeUndefined();
expect(obj.errorCausingStaleValue).toBeDefined();
});

it('Attempts session restart if second fetch fails', async () => {
const configValue = 'worked once';
const configValue2 = 'worked again';

appConfigClientMock.on(StartConfigurationSessionCommand).resolves({
InitialConfigurationToken: 'initialToken',
});

appConfigClientMock
.on(GetLatestConfigurationCommand)
.resolvesOnce({
Configuration: Uint8ArrayBlobAdapter.fromString(configValue),
})
.rejectsOnce({
message: 'Failed to get latest',
})
.resolves({
Configuration: Uint8ArrayBlobAdapter.fromString(configValue2),
});

const dataClient = new AppConfigDataClient();

poller = new Poller({
dataClient: dataClient,
...standardConfig,
});

await poller.start();
const latest = poller.getConfigurationString();

expect(latest.latestValue).toEqual(configValue);
expect(latest.errorCausingStaleValue).toBeUndefined();

await wait(standardConfig.pollIntervalSeconds * 1000 + 100);

const updated = poller.getConfigurationObject();
expect(updated.errorCausingStaleValue).toBeDefined();
expect(updated.latestValue).toEqual(
standardConfig.configParser(configValue),
);

await wait(standardConfig.pollIntervalSeconds * 1000 + 100);

const updatedAgain = poller.getConfigurationObject();
expect(updatedAgain.errorCausingStaleValue).toBeUndefined();
expect(updatedAgain.latestValue).toEqual(
standardConfig.configParser(configValue2),
);
});
});
2 changes: 1 addition & 1 deletion examples/infinitePoller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const poller = new Poller<SampleFormat>({
EnvironmentIdentifier: 'Live',
ConfigurationProfileIdentifier: 'YamlTest',
},
configTransformer: (s): SampleFormat => parse(s),
configParser: (s): SampleFormat => parse(s),
logger: console.log,
pollIntervalSeconds: 60,
});
Expand Down
Loading

0 comments on commit 96905f4

Please sign in to comment.