Skip to content

Commit e31279a

Browse files
feat(batch): sequential async processing of records for BatchProcessor (#3109)
Co-authored-by: Andrea Amorosi <[email protected]>
1 parent 39c8608 commit e31279a

File tree

6 files changed

+533
-442
lines changed

6 files changed

+533
-442
lines changed

Diff for: docs/utilities/batch.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,10 @@ For such cases we recommend to use the `BatchProcessorSync` and `processPartialR
416416
*If your function is `async` returning a `Promise`, use `BatchProcessor` and `processPartialResponse`
417417
* If your function is not `async`, use `BatchProcessorSync` and `processPartialResponseSync`
418418

419-
The difference between the two processors in implementation is that `BatchProcessor` uses `Promise.all()` while `BatchProcessorSync` loops through each record to preserve the order.
419+
The difference between the two processors is in how they handle record processing:
420+
421+
* **`BatchProcessor`**: By default, it processes records in parallel using `Promise.all()`. However, it also offers an [option](#sequential-async-processing) to process records sequentially, preserving the order.
422+
* **`BatchProcessorSync`**: Always processes records sequentially, ensuring the order is preserved by looping through each record one by one.
420423

421424
???+ question "When is this useful?"
422425

@@ -477,6 +480,16 @@ Let's suppose you'd like to add a metric named `BatchRecordFailures` for each ba
477480
--8<-- "examples/snippets/batch/extendingFailure.ts"
478481
```
479482

483+
### Sequential async processing
484+
485+
By default, the `BatchProcessor` processes records in parallel using `Promise.all()`. However, if you need to preserve the order of records, you can set the `processInParallel` option to `false` to process records sequentially.
486+
487+
!!! important "If the `processInParallel` option is not provided, the `BatchProcessor` will process records in parallel."
488+
489+
```typescript hl_lines="8 17" title="Sequential async processing"
490+
--8<-- "examples/snippets/batch/sequentialAsyncProcessing.ts"
491+
```
492+
480493
### Create your own partial processor
481494

482495
You can create your own partial batch processor from scratch by inheriting the `BasePartialProcessor` class, and implementing the `prepare()`, `clean()`, `processRecord()` and `processRecordSync()` abstract methods.

Diff for: examples/snippets/batch/sequentialAsyncProcessing.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {
2+
BatchProcessor,
3+
EventType,
4+
processPartialResponse,
5+
} from '@aws-lambda-powertools/batch';
6+
import type { SQSHandler, SQSRecord } from 'aws-lambda';
7+
8+
const processor = new BatchProcessor(EventType.SQS);
9+
10+
const recordHandler = async (_record: SQSRecord): Promise<void> => {
11+
// Process the record
12+
};
13+
14+
export const handler: SQSHandler = async (event, context) =>
15+
processPartialResponse(event, recordHandler, processor, {
16+
context,
17+
processInParallel: false,
18+
});

Diff for: packages/batch/src/BasePartialProcessor.ts

+32-8
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ abstract class BasePartialProcessor {
9898
public abstract prepare(): void;
9999

100100
/**
101-
* Process all records with an asyncronous handler
101+
* Process all records with an asynchronous handler
102102
*
103103
* Once called, the processor will create an array of promises to process each record
104104
* and wait for all of them to settle before returning the results.
@@ -122,21 +122,21 @@ abstract class BasePartialProcessor {
122122
}
123123
this.prepare();
124124

125-
const processingPromises: Promise<SuccessResponse | FailureResponse>[] =
126-
this.records.map((record) => this.processRecord(record));
127-
128-
const processedRecords: (SuccessResponse | FailureResponse)[] =
129-
await Promise.all(processingPromises);
125+
// Default to `true` if `processInParallel` is not specified.
126+
const processInParallel = this.options?.processInParallel ?? true;
127+
const processedRecords = processInParallel
128+
? await this.#processRecordsInParallel()
129+
: await this.#processRecordsSequentially();
130130

131131
this.clean();
132132

133133
return processedRecords;
134134
}
135135

136136
/**
137-
* Process a record with an asyncronous handler
137+
* Process a record with an asynchronous handler
138138
*
139-
* An implementation of this method is required for asyncronous processors.
139+
* An implementation of this method is required for asynchronous processors.
140140
*
141141
* When implementing this method, you should at least call the successHandler method
142142
* when a record succeeds processing and the failureHandler method when a record
@@ -249,6 +249,30 @@ abstract class BasePartialProcessor {
249249

250250
return entry;
251251
}
252+
253+
/**
254+
* Processes records in parallel using `Promise.all`.
255+
*/
256+
async #processRecordsInParallel(): Promise<
257+
(SuccessResponse | FailureResponse)[]
258+
> {
259+
return Promise.all(
260+
this.records.map((record) => this.processRecord(record))
261+
);
262+
}
263+
264+
/**
265+
* Processes records sequentially, ensuring that each record is processed one after the other.
266+
*/
267+
async #processRecordsSequentially(): Promise<
268+
(SuccessResponse | FailureResponse)[]
269+
> {
270+
const processedRecords: (SuccessResponse | FailureResponse)[] = [];
271+
for (const record of this.records) {
272+
processedRecords.push(await this.processRecord(record));
273+
}
274+
return processedRecords;
275+
}
252276
}
253277

254278
export { BasePartialProcessor };

Diff for: packages/batch/src/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { SqsFifoPartialProcessor } from './SqsFifoPartialProcessor.js';
1414
* @property context The context object provided by the AWS Lambda runtime
1515
* @property skipGroupOnError The option to group on error during processing
1616
* @property throwOnFullBatchFailure The option to throw an error if the entire batch fails
17+
* @property processInParallel Indicates whether the records should be processed in parallel
1718
*/
1819
type BatchProcessingOptions<T = BasePartialBatchProcessor> = {
1920
/**
@@ -30,6 +31,12 @@ type BatchProcessingOptions<T = BasePartialBatchProcessor> = {
3031
* Set this to false to prevent throwing an error if the entire batch fails.
3132
*/
3233
throwOnFullBatchFailure?: boolean;
34+
/**
35+
* Indicates whether the records should be processed in parallel.
36+
* When set to `true`, the records will be processed in parallel using `Promise.all`.
37+
* When set to `false`, the records will be processed sequentially.
38+
*/
39+
processInParallel?: T extends SqsFifoPartialProcessor ? never : boolean;
3340
};
3441

3542
/**

0 commit comments

Comments
 (0)