Skip to content

Commit c4e6b19

Browse files
dreamorosierikayao93Alexander Schuerendependabot[bot]github-actions[bot]
authored
feat(batch): add batch processing utility (#1625)
* chore: init workspace * chore: init workspace * feat(batch): Implementation of base batch processing classes (#1588) * chore: init workspace * chore: init workspace * Initial base class implementation * Added BatchProcessor implementation, attempted fix for async * Added unit tests * Refactoring unit tests * Lint fix, updated docstrings * Added response and identifier typings * test(idempotency): improve integration tests for utility (#1591) * docs: new name * chore: rename e2e files * tests(idempotency): expand integration tests * chore(idempotency): remove unreachable code * Removed unnecessary type casting * Moved exports for handlers and factories * Updated imports, refactored randomization in factories * Refactored EventType to be const instead of enum * Refactored and added documentation for errors * Removed debugging line * chore(ci): add canary to layer deployment (#1593) * docs(idempotency): write utility docs (#1592) * docs: base docs * wip * chore: added paths to snippets tsconfig * chore: added page to docs menu * docs(idempotency): utility docs * highlights * chore: remove CDK mention * build(internal): bump semver from 5.7.1 to 5.7.2 (#1594) Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2. - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md) - [Commits](npm/node-semver@v5.7.1...v5.7.2) --- updated-dependencies: - dependency-name: semver dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(idempotency): mark the utility ready public beta (#1595) * chore(idempotency): mark utility as public beta * chore: manually increment version in commons * docs(internal): update AWS SDK links to new docs (#1597) * chore(maintenance): remove parameters utility from layer bundling and layers e2e tests (#1599) * remove parameter from e2e tests * remove parameters from canary stack as well * chore(release): v1.11.1 [skip ci] * fix canary deploy in ci with correct workspace name (#1601) * chore: update layer ARN on documentation --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: Andrea Amorosi <[email protected]> Co-authored-by: Alexander Schueren <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Release bot[bot] <[email protected]> * feat(batch): Batch processing wrapper function (#1605) * Refactored some types, added function wrapper and base test * Added record check and tests, renamed factories * Refactored type check logic in function * Refactor test to remove error ignore * feat(batch): Implement SQS FIFO processor class (#1606) * Added SQS FIFO processor and unit tests * Added docstring for pbatch processing function * feat(batch): Support for Lambda context access in batch processing (#1609) * Added types and parameter for lambda context, added unit tests * Refactor parameter checking * Added test for malformed context handling * docs: created utility docs * docs: fixed white spaces * feat(batch): add async processor (#1616) * feat(batch): add async processor * tests: improved unit tests * chore: removed docstring + edited test handler * chore: fix typos * docs: added README * chore: added package to beta release * chore: marked package as public * chore: added new batch page to docs * chore: added utility to lerna workspace * chore: added utility to main readme * chore: added utility to CI --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: Erika Yao <[email protected]> Co-authored-by: Alexander Schueren <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Release bot[bot] <[email protected]>
1 parent 0b575a1 commit c4e6b19

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+3367
-23
lines changed

Diff for: .github/scripts/release_patch_package_json.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ if (process.argv.length < 3) {
1818
const basePath = resolve(process.argv[2]);
1919
const packageJsonPath = join(basePath, 'package.json');
2020
const alphaPackages = [];
21-
const betaPackages = ['@aws-lambda-powertools/idempotency'];
21+
const betaPackages = [
22+
'@aws-lambda-powertools/idempotency',
23+
'@aws-lambda-powertools/batch',
24+
];
2225

2326
(() => {
2427
try {

Diff for: .github/workflows/reusable-run-linting-check-and-unit-tests.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ jobs:
2727
with:
2828
nodeVersion: ${{ matrix.version }}
2929
- name: Run linting
30-
run: npm run lint -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency
30+
run: npm run lint -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch
3131
- name: Run unit tests
32-
run: npm t -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency
32+
run: npm t -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch
3333
check-examples:
3434
runs-on: ubuntu-latest
3535
env:

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ You can use the library in both TypeScript and JavaScript code bases.
3636
* **[Metrics](https://docs.powertools.aws.dev/lambda-typescript/latest/core/metrics/)** - Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF)
3737
* **[Parameters](https://docs.powertools.aws.dev/lambda-typescript/latest/utilities/parameters/)** - High-level functions to retrieve one or more parameters from AWS SSM Parameter Store, AWS Secrets Manager, AWS AppConfig, and Amazon DynamoDB
3838
* **[Idempotency (beta)](https://docs.powertools.aws.dev/lambda-typescript/latest/utilities/idempotency/)** - Class method decorator, Middy middleware, and function wrapper to make your Lambda functions idempotent and prevent duplicate execution based on payload content
39+
* **[Batch Processing (beta)](https://docs.powertools.aws.dev/lambda-typescript/latest/utilities/batch/)** - Utility to handle partial failures when processing batches from Amazon SQS, Amazon Kinesis Data Streams, and Amazon DynamoDB Streams.
3940

4041
## Getting started
4142

Diff for: docs/snippets/batch/accessLambdaContext.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {
2+
BatchProcessor,
3+
EventType,
4+
processPartialResponse,
5+
} from '@aws-lambda-powertools/batch';
6+
import { Logger } from '@aws-lambda-powertools/logger';
7+
import type {
8+
SQSEvent,
9+
SQSRecord,
10+
Context,
11+
SQSBatchResponse,
12+
} from 'aws-lambda';
13+
14+
const processor = new BatchProcessor(EventType.SQS);
15+
const logger = new Logger();
16+
17+
const recordHandler = (record: SQSRecord, lambdaContext?: Context): void => {
18+
const payload = record.body;
19+
if (payload) {
20+
const item = JSON.parse(payload);
21+
logger.info('Processed item', { item });
22+
}
23+
if (lambdaContext) {
24+
logger.info('Remaining time', {
25+
time: lambdaContext.getRemainingTimeInMillis(),
26+
});
27+
}
28+
};
29+
30+
export const handler = async (
31+
event: SQSEvent,
32+
context: Context
33+
): Promise<SQSBatchResponse> => {
34+
return processPartialResponse(event, recordHandler, processor, {
35+
context,
36+
});
37+
};

Diff for: docs/snippets/batch/accessProcessedMessages.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { BatchProcessor, EventType } from '@aws-lambda-powertools/batch';
2+
import { Logger } from '@aws-lambda-powertools/logger';
3+
import type {
4+
SQSEvent,
5+
SQSRecord,
6+
Context,
7+
SQSBatchResponse,
8+
} from 'aws-lambda';
9+
10+
const processor = new BatchProcessor(EventType.SQS);
11+
const logger = new Logger();
12+
13+
const recordHandler = (record: SQSRecord): void => {
14+
const payload = record.body;
15+
if (payload) {
16+
const item = JSON.parse(payload);
17+
logger.info('Processed item', { item });
18+
}
19+
};
20+
21+
export const handler = async (
22+
event: SQSEvent,
23+
context: Context
24+
): Promise<SQSBatchResponse> => {
25+
const batch = event.Records;
26+
27+
processor.register(batch, recordHandler, { context });
28+
const processedMessages = processor.process();
29+
30+
for (const message of processedMessages) {
31+
const status: 'success' | 'fail' = message[0];
32+
const record = message[2];
33+
34+
logger.info('Processed record', { status, record });
35+
}
36+
37+
return processor.response();
38+
};

Diff for: docs/snippets/batch/customPartialProcessor.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { randomInt } from 'node:crypto';
2+
import {
3+
DynamoDBClient,
4+
BatchWriteItemCommand,
5+
} from '@aws-sdk/client-dynamodb';
6+
import { marshall } from '@aws-sdk/util-dynamodb';
7+
import {
8+
BasePartialProcessor,
9+
processPartialResponse,
10+
} from '@aws-lambda-powertools/batch';
11+
import type {
12+
SuccessResponse,
13+
FailureResponse,
14+
EventSourceType,
15+
} from '@aws-lambda-powertools/batch';
16+
import type { SQSEvent, Context, SQSBatchResponse } from 'aws-lambda';
17+
18+
const tableName = process.env.TABLE_NAME || 'table-not-found';
19+
20+
class MyPartialProcessor extends BasePartialProcessor {
21+
#tableName: string;
22+
#client?: DynamoDBClient;
23+
24+
public constructor(tableName: string) {
25+
super();
26+
this.#tableName = tableName;
27+
}
28+
29+
public async asyncProcessRecord(
30+
_record: EventSourceType
31+
): Promise<SuccessResponse | FailureResponse> {
32+
throw new Error('Not implemented');
33+
}
34+
35+
/**
36+
* It's called once, **after** processing the batch.
37+
*
38+
* Here we are writing all the processed messages to DynamoDB.
39+
*/
40+
public clean(): void {
41+
// We know that the client is defined because clean() is called after prepare()
42+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
43+
this.#client!.send(
44+
new BatchWriteItemCommand({
45+
RequestItems: {
46+
[this.#tableName]: this.successMessages.map((message) => ({
47+
PutRequest: {
48+
Item: marshall(message),
49+
},
50+
})),
51+
},
52+
})
53+
);
54+
}
55+
56+
/**
57+
* It's called once, **before** processing the batch.
58+
*
59+
* It initializes a new client and cleans up any existing data.
60+
*/
61+
public prepare(): void {
62+
this.#client = new DynamoDBClient({});
63+
this.successMessages = [];
64+
}
65+
66+
/**
67+
* It handles how your record is processed.
68+
*
69+
* Here we are keeping the status of each run, `this.handler` is
70+
* the function that is passed when calling `processor.register()`.
71+
*/
72+
public processRecord(
73+
record: EventSourceType
74+
): SuccessResponse | FailureResponse {
75+
try {
76+
const result = this.handler(record);
77+
78+
return this.successHandler(record, result);
79+
} catch (error) {
80+
return this.failureHandler(record, error as Error);
81+
}
82+
}
83+
}
84+
85+
const processor = new MyPartialProcessor(tableName);
86+
87+
const recordHandler = (): number => {
88+
return Math.floor(randomInt(1, 10));
89+
};
90+
91+
export const handler = async (
92+
event: SQSEvent,
93+
context: Context
94+
): Promise<SQSBatchResponse> => {
95+
return processPartialResponse(event, recordHandler, processor, {
96+
context,
97+
});
98+
};

Diff for: docs/snippets/batch/extendingFailure.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics';
2+
import {
3+
BatchProcessor,
4+
EventType,
5+
FailureResponse,
6+
EventSourceType,
7+
processPartialResponse,
8+
} from '@aws-lambda-powertools/batch';
9+
import { Logger } from '@aws-lambda-powertools/logger';
10+
import type {
11+
SQSEvent,
12+
SQSRecord,
13+
Context,
14+
SQSBatchResponse,
15+
} from 'aws-lambda';
16+
17+
class MyProcessor extends BatchProcessor {
18+
#metrics: Metrics;
19+
20+
public constructor(eventType: keyof typeof EventType) {
21+
super(eventType);
22+
this.#metrics = new Metrics({ namespace: 'test' });
23+
}
24+
25+
public failureHandler(
26+
record: EventSourceType,
27+
error: Error
28+
): FailureResponse {
29+
this.#metrics.addMetric('BatchRecordFailures', MetricUnits.Count, 1);
30+
31+
return super.failureHandler(record, error);
32+
}
33+
}
34+
35+
const processor = new MyProcessor(EventType.SQS);
36+
const logger = new Logger();
37+
38+
const recordHandler = (record: SQSRecord): void => {
39+
const payload = record.body;
40+
if (payload) {
41+
const item = JSON.parse(payload);
42+
logger.info('Processed item', { item });
43+
}
44+
};
45+
46+
export const handler = async (
47+
event: SQSEvent,
48+
context: Context
49+
): Promise<SQSBatchResponse> => {
50+
return processPartialResponse(event, recordHandler, processor, {
51+
context,
52+
});
53+
};

Diff for: docs/snippets/batch/gettingStartedAsync.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
AsyncBatchProcessor,
3+
EventType,
4+
asyncProcessPartialResponse,
5+
} from '@aws-lambda-powertools/batch';
6+
import axios from 'axios'; // axios is an external dependency
7+
import type {
8+
SQSEvent,
9+
SQSRecord,
10+
Context,
11+
SQSBatchResponse,
12+
} from 'aws-lambda';
13+
14+
const processor = new AsyncBatchProcessor(EventType.SQS);
15+
16+
const recordHandler = async (record: SQSRecord): Promise<number> => {
17+
const res = await axios.post('https://httpbin.org/anything', {
18+
message: record.body,
19+
});
20+
21+
return res.status;
22+
};
23+
24+
export const handler = async (
25+
event: SQSEvent,
26+
context: Context
27+
): Promise<SQSBatchResponse> => {
28+
return await asyncProcessPartialResponse(event, recordHandler, processor, {
29+
context,
30+
});
31+
};

Diff for: docs/snippets/batch/gettingStartedDynamoDBStreams.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {
2+
BatchProcessor,
3+
EventType,
4+
processPartialResponse,
5+
} from '@aws-lambda-powertools/batch';
6+
import { Logger } from '@aws-lambda-powertools/logger';
7+
import type {
8+
DynamoDBStreamEvent,
9+
DynamoDBRecord,
10+
Context,
11+
DynamoDBBatchResponse,
12+
} from 'aws-lambda';
13+
14+
const processor = new BatchProcessor(EventType.DynamoDBStreams);
15+
const logger = new Logger();
16+
17+
const recordHandler = (record: DynamoDBRecord): void => {
18+
if (record.dynamodb && record.dynamodb.NewImage) {
19+
logger.info('Processing record', { record: record.dynamodb.NewImage });
20+
const message = record.dynamodb.NewImage.Message.S;
21+
if (message) {
22+
const payload = JSON.parse(message);
23+
logger.info('Processed item', { item: payload });
24+
}
25+
}
26+
};
27+
28+
export const handler = async (
29+
event: DynamoDBStreamEvent,
30+
context: Context
31+
): Promise<DynamoDBBatchResponse> => {
32+
return processPartialResponse(event, recordHandler, processor, {
33+
context,
34+
});
35+
};

Diff for: docs/snippets/batch/gettingStartedKinesis.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
BatchProcessor,
3+
EventType,
4+
processPartialResponse,
5+
} from '@aws-lambda-powertools/batch';
6+
import { Logger } from '@aws-lambda-powertools/logger';
7+
import type {
8+
KinesisStreamEvent,
9+
KinesisStreamRecord,
10+
Context,
11+
KinesisStreamBatchResponse,
12+
} from 'aws-lambda';
13+
14+
const processor = new BatchProcessor(EventType.KinesisDataStreams);
15+
const logger = new Logger();
16+
17+
const recordHandler = (record: KinesisStreamRecord): void => {
18+
logger.info('Processing record', { record: record.kinesis.data });
19+
const payload = JSON.parse(record.kinesis.data);
20+
logger.info('Processed item', { item: payload });
21+
};
22+
23+
export const handler = async (
24+
event: KinesisStreamEvent,
25+
context: Context
26+
): Promise<KinesisStreamBatchResponse> => {
27+
return processPartialResponse(event, recordHandler, processor, {
28+
context,
29+
});
30+
};

Diff for: docs/snippets/batch/gettingStartedSQS.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {
2+
BatchProcessor,
3+
EventType,
4+
processPartialResponse,
5+
} from '@aws-lambda-powertools/batch';
6+
import { Logger } from '@aws-lambda-powertools/logger';
7+
import type {
8+
SQSEvent,
9+
SQSRecord,
10+
Context,
11+
SQSBatchResponse,
12+
} from 'aws-lambda';
13+
14+
const processor = new BatchProcessor(EventType.SQS);
15+
const logger = new Logger();
16+
17+
const recordHandler = (record: SQSRecord): void => {
18+
const payload = record.body;
19+
if (payload) {
20+
const item = JSON.parse(payload);
21+
logger.info('Processed item', { item });
22+
}
23+
};
24+
25+
export const handler = async (
26+
event: SQSEvent,
27+
context: Context
28+
): Promise<SQSBatchResponse> => {
29+
return processPartialResponse(event, recordHandler, processor, {
30+
context,
31+
});
32+
};
33+
export { processor };

0 commit comments

Comments
 (0)