Skip to content

Commit 022e40f

Browse files
committed
refactor: let ScaleError convert itself to batch item failures, add tests
This lets us move some logic from `processFleetResult`, simplifying it. We're additionally adding some bounds checking here, and testing it all.
1 parent 95eda18 commit 022e40f

File tree

3 files changed

+88
-6
lines changed

3 files changed

+88
-6
lines changed

lambdas/functions/control-plane/src/lambda.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,7 @@ export async function scaleUpHandler(event: SQSEvent, context: Context): Promise
6262
return { batchItemFailures };
6363
} catch (e) {
6464
if (e instanceof ScaleError) {
65-
for (let i = 0; i < e.failedInstanceCount; i++) {
66-
batchItemFailures.push({
67-
itemIdentifier: sqsMessages[i].messageId,
68-
});
69-
}
65+
batchItemFailures.push(...e.toBatchItemFailures(sqsMessages));
7066
logger.warn(`${e.detailedMessage} A retry will be attempted via SQS.`, { error: e });
7167
} else {
7268
logger.error(`Error processing batch (size: ${sqsMessages.length}): ${(e as Error).message}, ignoring batch`, {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { describe, expect, it } from 'vitest';
2+
import type { ActionRequestMessageSQS } from './scale-up';
3+
import ScaleError from './ScaleError';
4+
5+
describe('ScaleError', () => {
6+
describe('detailedMessage', () => {
7+
it('should format message for single instance failure', () => {
8+
const error = new ScaleError('Failed to create fleet', 1);
9+
10+
expect(error.detailedMessage).toBe('Failed to create fleet (Failed to create 1 instance)');
11+
});
12+
13+
it('should format message for multiple instance failures', () => {
14+
const error = new ScaleError('Failed to create fleet', 3);
15+
16+
expect(error.detailedMessage).toBe('Failed to create fleet (Failed to create 3 instances)');
17+
});
18+
});
19+
20+
describe('toBatchItemFailures', () => {
21+
const mockMessages: ActionRequestMessageSQS[] = [
22+
{ messageId: 'msg-1', id: 1, eventType: 'workflow_job' },
23+
{ messageId: 'msg-2', id: 2, eventType: 'workflow_job' },
24+
{ messageId: 'msg-3', id: 3, eventType: 'workflow_job' },
25+
{ messageId: 'msg-4', id: 4, eventType: 'workflow_job' },
26+
];
27+
28+
it.each([
29+
{ failedCount: 1, expected: [{ itemIdentifier: 'msg-1' }], description: 'default instance count' },
30+
{
31+
failedCount: 2,
32+
expected: [{ itemIdentifier: 'msg-1' }, { itemIdentifier: 'msg-2' }],
33+
description: 'less than message count',
34+
},
35+
{
36+
failedCount: 4,
37+
expected: [
38+
{ itemIdentifier: 'msg-1' },
39+
{ itemIdentifier: 'msg-2' },
40+
{ itemIdentifier: 'msg-3' },
41+
{ itemIdentifier: 'msg-4' },
42+
],
43+
description: 'equal to message count',
44+
},
45+
{
46+
failedCount: 10,
47+
expected: [
48+
{ itemIdentifier: 'msg-1' },
49+
{ itemIdentifier: 'msg-2' },
50+
{ itemIdentifier: 'msg-3' },
51+
{ itemIdentifier: 'msg-4' },
52+
],
53+
description: 'more than message count',
54+
},
55+
{ failedCount: 0, expected: [], description: 'zero failed instances' },
56+
{ failedCount: -1, expected: [], description: 'negative failed instances' },
57+
{ failedCount: -10, expected: [], description: 'large negative failed instances' },
58+
])('should handle $description (failedCount=$failedCount)', ({ failedCount, expected }) => {
59+
const error = new ScaleError('Test error', failedCount);
60+
const failures = error.toBatchItemFailures(mockMessages);
61+
62+
expect(failures).toEqual(expected);
63+
});
64+
65+
it('should handle empty message array', () => {
66+
const error = new ScaleError('Test error', 3);
67+
const failures = error.toBatchItemFailures([]);
68+
69+
expect(failures).toEqual([]);
70+
});
71+
});
72+
});

lambdas/functions/control-plane/src/scale-runners/ScaleError.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { SQSBatchItemFailure } from 'aws-lambda';
2+
import type { ActionRequestMessageSQS } from './scale-up';
3+
14
class ScaleError extends Error {
25
constructor(
36
message: string,
@@ -13,6 +16,17 @@ class ScaleError extends Error {
1316
public get detailedMessage(): string {
1417
return `${this.message} (Failed to create ${this.failedInstanceCount} instance${this.failedInstanceCount !== 1 ? 's' : ''})`;
1518
}
19+
20+
/**
21+
* Generate SQS batch item failures for the failed instances
22+
*/
23+
public toBatchItemFailures(messages: ActionRequestMessageSQS[]): SQSBatchItemFailure[] {
24+
// Ensure we don't retry negative counts or more messages than available
25+
const messagesToRetry = Math.max(0, Math.min(this.failedInstanceCount, messages.length));
26+
return messages.slice(0, messagesToRetry).map(({ messageId }) => ({
27+
itemIdentifier: messageId,
28+
}));
29+
}
1630
}
1731

18-
export default ScaleError;
32+
export default ScaleError;

0 commit comments

Comments
 (0)