Skip to content

Commit 5e26f44

Browse files
authored
fix(sdk): handle empty items in map/parallel (#403)
*Issue #, if available:* Closes #399 *Description of changes:* The SDK does not properly handle map/parallel with an empty list. It will just wait forever waiting for the next item. Adding logic to resolve immediately when the lists are empty, and adding integ/unit tests for this case. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 688a3c5 commit 5e26f44

File tree

8 files changed

+467
-31
lines changed

8 files changed

+467
-31
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
[
2+
{
3+
"EventType": "ExecutionStarted",
4+
"EventId": 1,
5+
"Id": "5d2291db-f359-4b1c-8ef2-b144a192b751",
6+
"EventTimestamp": "2025-12-19T23:26:24.653Z",
7+
"ExecutionStartedDetails": {
8+
"Input": {
9+
"Payload": "{}"
10+
}
11+
}
12+
},
13+
{
14+
"EventType": "ContextStarted",
15+
"SubType": "Map",
16+
"EventId": 2,
17+
"Id": "c4ca4238a0b92382",
18+
"Name": "empty-map",
19+
"EventTimestamp": "2025-12-19T23:26:24.659Z",
20+
"ContextStartedDetails": {}
21+
},
22+
{
23+
"EventType": "ContextSucceeded",
24+
"SubType": "Map",
25+
"EventId": 3,
26+
"Id": "c4ca4238a0b92382",
27+
"Name": "empty-map",
28+
"EventTimestamp": "2025-12-19T23:26:24.659Z",
29+
"ContextSucceededDetails": {
30+
"Result": {
31+
"Payload": "{\"all\":[],\"completionReason\":\"ALL_COMPLETED\"}"
32+
}
33+
}
34+
},
35+
{
36+
"EventType": "WaitStarted",
37+
"SubType": "Wait",
38+
"EventId": 4,
39+
"Id": "c81e728d9d4c2f63",
40+
"EventTimestamp": "2025-12-19T23:26:24.661Z",
41+
"WaitStartedDetails": {
42+
"Duration": 1,
43+
"ScheduledEndTimestamp": "2025-12-19T23:26:25.661Z"
44+
}
45+
},
46+
{
47+
"EventType": "InvocationCompleted",
48+
"EventId": 5,
49+
"EventTimestamp": "2025-12-19T23:26:24.713Z",
50+
"InvocationCompletedDetails": {
51+
"StartTimestamp": "2025-12-19T23:26:24.653Z",
52+
"EndTimestamp": "2025-12-19T23:26:24.713Z",
53+
"Error": {},
54+
"RequestId": "5711d3b6-2f02-4175-9791-8f98ff53e4f7"
55+
}
56+
},
57+
{
58+
"EventType": "WaitSucceeded",
59+
"SubType": "Wait",
60+
"EventId": 6,
61+
"Id": "c81e728d9d4c2f63",
62+
"EventTimestamp": "2025-12-19T23:26:25.660Z",
63+
"WaitSucceededDetails": {
64+
"Duration": 1
65+
}
66+
},
67+
{
68+
"EventType": "InvocationCompleted",
69+
"EventId": 7,
70+
"EventTimestamp": "2025-12-19T23:26:25.663Z",
71+
"InvocationCompletedDetails": {
72+
"StartTimestamp": "2025-12-19T23:26:25.661Z",
73+
"EndTimestamp": "2025-12-19T23:26:25.663Z",
74+
"Error": {},
75+
"RequestId": "fbb3da82-3269-4ad1-8fad-00be9a118a11"
76+
}
77+
},
78+
{
79+
"EventType": "ExecutionSucceeded",
80+
"EventId": 8,
81+
"Id": "5d2291db-f359-4b1c-8ef2-b144a192b751",
82+
"EventTimestamp": "2025-12-19T23:26:25.663Z",
83+
"ExecutionSucceededDetails": {
84+
"Result": {
85+
"Payload": "{\"results\":[],\"errors\":[],\"successCount\":0,\"failureCount\":0,\"totalCount\":0,\"status\":\"SUCCEEDED\",\"completionReason\":\"ALL_COMPLETED\"}"
86+
}
87+
}
88+
}
89+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { handler } from "./map-empty";
2+
import { createTests } from "../../../utils/test-helper";
3+
4+
createTests({
5+
handler,
6+
tests: (runner, { assertEventSignatures }) => {
7+
it("should execute successfully with empty map array and expected result structure", async () => {
8+
const execution = await runner.run();
9+
10+
const result = execution.getResult() as any;
11+
12+
expect(result).toBeDefined();
13+
expect(result.results).toEqual([]);
14+
expect(result.errors).toEqual([]);
15+
expect(result.successCount).toBe(0);
16+
expect(result.failureCount).toBe(0);
17+
expect(result.totalCount).toBe(0);
18+
expect(result.status).toBe("SUCCEEDED");
19+
expect(result.completionReason).toBe("ALL_COMPLETED");
20+
21+
// Verify the map operation exists but has no child operations
22+
const emptyMap = runner.getOperation("empty-map");
23+
expect(emptyMap.getChildOperations()).toHaveLength(0);
24+
25+
assertEventSignatures(execution);
26+
});
27+
},
28+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { withDurableExecution } from "@aws/durable-execution-sdk-js";
2+
import { ExampleConfig } from "../../../types";
3+
4+
export const config: ExampleConfig = {
5+
name: "Empty Map",
6+
description: "Running map with an empty array of items",
7+
};
8+
9+
export const handler = withDurableExecution(async (event, context) => {
10+
const result = await context.map("empty-map", [], (mapContext, item) => {
11+
return item;
12+
});
13+
14+
await context.wait({ seconds: 1 });
15+
16+
return {
17+
results: result.getResults(),
18+
errors: result.getErrors(),
19+
successCount: result.successCount,
20+
failureCount: result.failureCount,
21+
totalCount: result.totalCount,
22+
status: result.status,
23+
completionReason: result.completionReason,
24+
};
25+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
[
2+
{
3+
"EventType": "ExecutionStarted",
4+
"EventId": 1,
5+
"Id": "c7a78a8b-d203-4f2e-a702-efb0585ee131",
6+
"EventTimestamp": "2025-12-19T23:26:31.718Z",
7+
"ExecutionStartedDetails": {
8+
"Input": {
9+
"Payload": "{}"
10+
}
11+
}
12+
},
13+
{
14+
"EventType": "ContextStarted",
15+
"SubType": "Parallel",
16+
"EventId": 2,
17+
"Id": "c4ca4238a0b92382",
18+
"Name": "empty-parallel",
19+
"EventTimestamp": "2025-12-19T23:26:31.725Z",
20+
"ContextStartedDetails": {}
21+
},
22+
{
23+
"EventType": "ContextSucceeded",
24+
"SubType": "Parallel",
25+
"EventId": 3,
26+
"Id": "c4ca4238a0b92382",
27+
"Name": "empty-parallel",
28+
"EventTimestamp": "2025-12-19T23:26:31.725Z",
29+
"ContextSucceededDetails": {
30+
"Result": {
31+
"Payload": "{\"all\":[],\"completionReason\":\"ALL_COMPLETED\"}"
32+
}
33+
}
34+
},
35+
{
36+
"EventType": "WaitStarted",
37+
"SubType": "Wait",
38+
"EventId": 4,
39+
"Id": "c81e728d9d4c2f63",
40+
"EventTimestamp": "2025-12-19T23:26:31.728Z",
41+
"WaitStartedDetails": {
42+
"Duration": 1,
43+
"ScheduledEndTimestamp": "2025-12-19T23:26:32.728Z"
44+
}
45+
},
46+
{
47+
"EventType": "InvocationCompleted",
48+
"EventId": 5,
49+
"EventTimestamp": "2025-12-19T23:26:31.779Z",
50+
"InvocationCompletedDetails": {
51+
"StartTimestamp": "2025-12-19T23:26:31.718Z",
52+
"EndTimestamp": "2025-12-19T23:26:31.779Z",
53+
"Error": {},
54+
"RequestId": "c81908f2-5367-45d3-9266-7d4d15272780"
55+
}
56+
},
57+
{
58+
"EventType": "WaitSucceeded",
59+
"SubType": "Wait",
60+
"EventId": 6,
61+
"Id": "c81e728d9d4c2f63",
62+
"EventTimestamp": "2025-12-19T23:26:32.730Z",
63+
"WaitSucceededDetails": {
64+
"Duration": 1
65+
}
66+
},
67+
{
68+
"EventType": "InvocationCompleted",
69+
"EventId": 7,
70+
"EventTimestamp": "2025-12-19T23:26:32.732Z",
71+
"InvocationCompletedDetails": {
72+
"StartTimestamp": "2025-12-19T23:26:32.731Z",
73+
"EndTimestamp": "2025-12-19T23:26:32.732Z",
74+
"Error": {},
75+
"RequestId": "fbdbe427-9edc-4769-8a74-3b907d28a442"
76+
}
77+
},
78+
{
79+
"EventType": "ExecutionSucceeded",
80+
"EventId": 8,
81+
"Id": "c7a78a8b-d203-4f2e-a702-efb0585ee131",
82+
"EventTimestamp": "2025-12-19T23:26:32.732Z",
83+
"ExecutionSucceededDetails": {
84+
"Result": {
85+
"Payload": "{\"results\":[],\"errors\":[],\"successCount\":0,\"failureCount\":0,\"totalCount\":0,\"status\":\"SUCCEEDED\",\"completionReason\":\"ALL_COMPLETED\"}"
86+
}
87+
}
88+
}
89+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { handler } from "./parallel-empty";
2+
import { createTests } from "../../../utils/test-helper";
3+
4+
createTests({
5+
handler,
6+
tests: (runner, { assertEventSignatures }) => {
7+
it("should execute successfully with empty parallel array and expected result structure", async () => {
8+
const execution = await runner.run();
9+
10+
const result = execution.getResult() as any;
11+
12+
expect(result).toBeDefined();
13+
expect(result.results).toEqual([]);
14+
expect(result.errors).toEqual([]);
15+
expect(result.successCount).toBe(0);
16+
expect(result.failureCount).toBe(0);
17+
expect(result.totalCount).toBe(0);
18+
expect(result.status).toBe("SUCCEEDED");
19+
expect(result.completionReason).toBe("ALL_COMPLETED");
20+
21+
// Verify the parallel operation exists but has no child operations
22+
const parallelOp = runner.getOperation("empty-parallel");
23+
expect(parallelOp.getChildOperations()).toHaveLength(0);
24+
25+
assertEventSignatures(execution);
26+
});
27+
},
28+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {
2+
DurableContext,
3+
withDurableExecution,
4+
} from "@aws/durable-execution-sdk-js";
5+
import { ExampleConfig } from "../../../types";
6+
7+
export const config: ExampleConfig = {
8+
name: "Empty Parallel",
9+
description: "Running parallel with an empty array of operations",
10+
};
11+
12+
export const handler = withDurableExecution(
13+
async (event: any, context: DurableContext) => {
14+
const result = await context.parallel("empty-parallel", []);
15+
16+
await context.wait({ seconds: 1 });
17+
18+
return {
19+
results: result.getResults(),
20+
errors: result.getErrors(),
21+
successCount: result.successCount,
22+
failureCount: result.failureCount,
23+
totalCount: result.totalCount,
24+
status: result.status,
25+
completionReason: result.completionReason,
26+
};
27+
},
28+
);

0 commit comments

Comments
 (0)