Skip to content

Commit d09c718

Browse files
MDavidson17blacha
andauthored
feat: EAI_AGAIN middleware TDE-1114 (#949)
#### Motivation Retry workflows that fail due to EAI_AGAIN errors. #### Modification Added an SDK middleware to try 3 times when receiving an `EAI_AGAIN` error. #### Checklist _If not applicable, provide explanation of why._ - [x] Tests updated - [ ] Docs updated -n/a - [x] Issue linked in Title --------- Co-authored-by: Blayne Chard <[email protected]>
1 parent 74b0907 commit d09c718

File tree

2 files changed

+93
-3
lines changed

2 files changed

+93
-3
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import assert from 'node:assert';
2+
import { beforeEach, describe, it } from 'node:test';
3+
4+
import { BuildHandler, MetadataBearer } from '@smithy/types';
5+
6+
import { eaiAgainBuilder } from '../fs.register.js';
7+
8+
let callCount = 0;
9+
10+
function fakeNextBuilder(failCount: number): BuildHandler<object, MetadataBearer> {
11+
const fakeNext: BuildHandler<object, MetadataBearer> = () => {
12+
// fail a specified number of times and then succeed
13+
callCount += 1;
14+
if (callCount < 1 + failCount) {
15+
return Promise.reject({ code: 'EAI_AGAIN', hostname: 'nz-imagery.s3.ap-southeast-2.amazonaws.com' });
16+
} else {
17+
return Promise.resolve({ output: { $metadata: {} }, response: {} });
18+
}
19+
};
20+
return fakeNext;
21+
}
22+
23+
describe('eai_againRetryMiddleware', () => {
24+
beforeEach(() => {
25+
callCount = 0;
26+
});
27+
it('should run next once if it succeeds', () => {
28+
const fakeNext = fakeNextBuilder(0);
29+
eaiAgainBuilder(() => 0)(fakeNext, {})({ input: {}, request: {} });
30+
assert.equal(callCount, 1);
31+
});
32+
33+
it('should try three times when getting EAI_AGAIN errors', async () => {
34+
const fakeNext = fakeNextBuilder(2);
35+
await eaiAgainBuilder(() => 0)(fakeNext, {})({ input: {}, request: {} });
36+
assert.equal(callCount, 3);
37+
});
38+
39+
it('should throw error if fails with unknown error type', () => {
40+
const fakeNext: BuildHandler<object, MetadataBearer> = () => {
41+
return Promise.reject({ message: 'ERROR MESSAGE' });
42+
};
43+
assert.rejects(eaiAgainBuilder(() => 0)(fakeNext, {})({ input: {}, request: {} }), {
44+
message: 'ERROR MESSAGE',
45+
});
46+
});
47+
48+
it('should throw error if next fails with EAI_AGAIN three times', () => {
49+
const fakeNext = fakeNextBuilder(3);
50+
assert.rejects(eaiAgainBuilder(() => 0)(fakeNext, {})({ input: {}, request: {} }), {
51+
message: 'EAI_AGAIN maximum tries (3) exceeded',
52+
});
53+
});
54+
});

src/fs.register.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import { setTimeout } from 'node:timers/promises';
2+
13
import { S3Client } from '@aws-sdk/client-s3';
24
import { FileSystem } from '@chunkd/core';
35
import { fsa } from '@chunkd/fs';
4-
import { AwsCredentialConfig } from '@chunkd/source-aws';
5-
import { FsAwsS3 } from '@chunkd/source-aws';
6+
import { AwsCredentialConfig, FsAwsS3 } from '@chunkd/source-aws';
67
import { FsAwsS3V3, S3LikeV3 } from '@chunkd/source-aws-v3';
7-
import { FinalizeRequestMiddleware, MetadataBearer } from '@smithy/types';
8+
import { BuildMiddleware, FinalizeRequestMiddleware, MetadataBearer } from '@smithy/types';
89

910
import { logger } from './log.js';
1011

@@ -31,8 +32,43 @@ export const fqdn: FinalizeRequestMiddleware<object, MetadataBearer> = (next) =>
3132
};
3233
};
3334

35+
/**
36+
* AWS SDK middleware logic to try 3 times if receiving an EAI_AGAIN error
37+
*/
38+
export function eaiAgainBuilder(timeout: (attempt: number) => number): BuildMiddleware<object, MetadataBearer> {
39+
const eaiAgain: BuildMiddleware<object, MetadataBearer> = (next) => {
40+
const maxTries = 3;
41+
let totalDelay = 0;
42+
return async (args) => {
43+
for (let attempt = 1; attempt <= maxTries; attempt++) {
44+
try {
45+
return await next(args);
46+
} catch (error) {
47+
if (error && typeof error === 'object' && 'code' in error && 'hostname' in error) {
48+
if (error.code !== 'EAI_AGAIN') {
49+
throw error;
50+
}
51+
const delay = timeout(attempt);
52+
totalDelay += delay;
53+
logger.warn({ host: error.hostname, attempt, delay, totalDelay }, `eai_again:retry`);
54+
await setTimeout(timeout(attempt));
55+
} else {
56+
throw error;
57+
}
58+
}
59+
}
60+
throw new Error(`EAI_AGAIN maximum tries (${maxTries}) exceeded`);
61+
};
62+
};
63+
return eaiAgain;
64+
}
65+
3466
const client = new S3Client();
3567
export const s3Fs = new FsAwsS3V3(client);
68+
client.middlewareStack.add(
69+
eaiAgainBuilder((attempt: number) => 100 + attempt * 1000),
70+
{ name: 'EAI_AGAIN', step: 'build' },
71+
);
3672
client.middlewareStack.add(fqdn, { name: 'FQDN', step: 'finalizeRequest' });
3773

3874
FsAwsS3.MaxListCount = 1000;

0 commit comments

Comments
 (0)