Skip to content

Commit 766a1fd

Browse files
authored
allow generic types for execution input and output (#172)
1 parent 2bf3529 commit 766a1fd

17 files changed

Lines changed: 304 additions & 46 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package software.amazon.lambda.durable.examples;
4+
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import software.amazon.lambda.durable.DurableContext;
11+
import software.amazon.lambda.durable.DurableHandler;
12+
import software.amazon.lambda.durable.StepConfig;
13+
import software.amazon.lambda.durable.TypeToken;
14+
import software.amazon.lambda.durable.retry.RetryStrategies;
15+
16+
/**
17+
* Example demonstrating a durable Lambda function that uses generic types in input and output.
18+
*
19+
* <p>This example shows how to use TypeToken to work with generic types like List<String>, Map<String, List<String,
20+
* String>, and nested generics that cannot be represented by simple Class objects.
21+
*/
22+
public class GenericInputOutputExample
23+
extends DurableHandler<Map<String, String>, Map<String, Map<String, List<String>>>> {
24+
25+
private static final Logger logger = LoggerFactory.getLogger(GenericInputOutputExample.class);
26+
27+
@Override
28+
public Map<String, Map<String, List<String>>> handleRequest(Map<String, String> input, DurableContext context) {
29+
logger.info("Starting generic types example for user: {}", input.get("userId"));
30+
31+
// Fetch nested generic type with retry (Map<String, List<String>>)
32+
Map<String, List<String>> categories = context.step(
33+
"fetch-categories",
34+
new TypeToken<Map<String, List<String>>>() {},
35+
stepCtx -> {
36+
logger.info("Fetching category details");
37+
var result = new HashMap<String, List<String>>();
38+
result.put("electronics", List.of("laptop", "phone"));
39+
result.put("books", List.of("fiction"));
40+
result.put("clothing", List.of("shirt"));
41+
return result;
42+
},
43+
StepConfig.builder()
44+
.retryStrategy(RetryStrategies.Presets.DEFAULT)
45+
.build());
46+
logger.info("Fetched {} category details", categories.size());
47+
logger.info("Generic types example completed successfully");
48+
49+
// return a result of Map<String, Map<String, List<String>>>
50+
return new HashMap<>(Map.of("categories", categories));
51+
}
52+
}

examples/src/test/java/software/amazon/lambda/durable/examples/CloudBasedIntegrationTest.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
package software.amazon.lambda.durable.examples;
44

55
import static org.junit.jupiter.api.Assertions.*;
6+
import static software.amazon.lambda.durable.TypeToken.get;
67

8+
import java.util.HashMap;
9+
import java.util.List;
710
import java.util.Map;
811
import org.junit.jupiter.api.BeforeAll;
912
import org.junit.jupiter.api.Test;
@@ -13,6 +16,7 @@
1316
import software.amazon.awssdk.services.lambda.LambdaClient;
1417
import software.amazon.awssdk.services.lambda.model.OperationStatus;
1518
import software.amazon.awssdk.services.sts.StsClient;
19+
import software.amazon.lambda.durable.TypeToken;
1620
import software.amazon.lambda.durable.model.ExecutionStatus;
1721
import software.amazon.lambda.durable.testing.CloudDurableTestRunner;
1822

@@ -60,7 +64,8 @@ private static String arn(String functionName) {
6064

6165
@Test
6266
void testSimpleStepExample() {
63-
var runner = CloudDurableTestRunner.create(arn("simple-step-example"), Map.class, String.class);
67+
var runner = CloudDurableTestRunner.create(
68+
arn("simple-step-example"), new TypeToken<Map<String, String>>() {}, get(String.class));
6469
var result = runner.run(Map.of("message", "test"));
6570

6671
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
@@ -73,7 +78,8 @@ void testSimpleStepExample() {
7378

7479
@Test
7580
void testNoopExampleWithLargeInput() {
76-
var runner = CloudDurableTestRunner.create(arn("noop-example"), Map.class, String.class);
81+
var runner = CloudDurableTestRunner.create(
82+
arn("noop-example"), new TypeToken<Map<String, String>>() {}, get(String.class));
7783
// 6MB large input
7884
var largeInput = "A".repeat(1024 * 1024 * 6 - 12);
7985
var result = runner.run(Map.of("name", largeInput));
@@ -84,7 +90,8 @@ void testNoopExampleWithLargeInput() {
8490

8591
@Test
8692
void testSimpleInvokeExample() {
87-
var runner = CloudDurableTestRunner.create(arn("simple-invoke-example"), Map.class, String.class);
93+
var runner = CloudDurableTestRunner.create(
94+
arn("simple-invoke-example"), new TypeToken<Map<String, String>>() {}, get(String.class));
8895
var result = runner.run(Map.of("name", functionNameSuffix));
8996

9097
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
@@ -229,6 +236,31 @@ void testGenericTypesExample() {
229236
assertNotNull(runner.getOperation("fetch-categories"));
230237
}
231238

239+
@Test
240+
void testGenericInputOutputExample() {
241+
final TypeToken<Map<String, Map<String, List<String>>>> resultType = new TypeToken<>() {};
242+
final TypeToken<Map<String, String>> inputType = new TypeToken<>() {};
243+
244+
var runner = CloudDurableTestRunner.create(arn("generic-input-output-example"), inputType, resultType);
245+
var result = runner.run(new HashMap<>(Map.of("userId", "user123")));
246+
247+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
248+
249+
var output = result.getResult(resultType);
250+
assertNotNull(output);
251+
252+
// Verify categories nested map
253+
var categories = output.get("categories");
254+
assertNotNull(categories);
255+
assertEquals(3, categories.size());
256+
assertEquals(2, categories.get("electronics").size());
257+
assertTrue(categories.get("electronics").contains("laptop"));
258+
assertTrue(categories.get("electronics").contains("phone"));
259+
260+
// Verify operations were executed
261+
assertNotNull(runner.getOperation("fetch-categories"));
262+
}
263+
232264
@Test
233265
void testCustomConfigExample() {
234266
var runner = CloudDurableTestRunner.create(arn("custom-config-example"), String.class, String.class);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package software.amazon.lambda.durable.examples;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertNotNull;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
import org.junit.jupiter.api.Test;
13+
import software.amazon.lambda.durable.TypeToken;
14+
import software.amazon.lambda.durable.testing.LocalDurableTestRunner;
15+
16+
class GenericInputOutputExampleTest {
17+
18+
private static final TypeToken<Map<String, Map<String, List<String>>>> resultType = new TypeToken<>() {};
19+
private static final TypeToken<Map<String, String>> inputType = new TypeToken<>() {};
20+
21+
@Test
22+
void testGenericTypesExample() {
23+
var handler = new GenericInputOutputExample();
24+
var runner = LocalDurableTestRunner.create(inputType, handler);
25+
26+
var input = new HashMap<>(Map.of("userId", "user123"));
27+
var result = runner.run(input);
28+
29+
assertNotNull(result);
30+
var output = result.getResult(resultType);
31+
assertNotNull(output);
32+
33+
// Verify categories nested map
34+
var categories = output.get("categories");
35+
assertNotNull(categories);
36+
assertEquals(3, categories.size());
37+
assertEquals(2, categories.get("electronics").size());
38+
assertTrue(categories.get("electronics").contains("laptop"));
39+
assertTrue(categories.get("electronics").contains("phone"));
40+
assertEquals(1, categories.get("books").size());
41+
assertTrue(categories.get("books").contains("fiction"));
42+
}
43+
44+
@Test
45+
void testOperationTracking() {
46+
var handler = new GenericInputOutputExample();
47+
var runner = LocalDurableTestRunner.create(inputType, handler);
48+
49+
var input = new HashMap<>(Map.of("userId", "user123"));
50+
var result = runner.run(input);
51+
52+
// Verify all operations were executed
53+
var fetchCategories = result.getOperation("fetch-categories");
54+
assertNotNull(fetchCategories);
55+
assertEquals("fetch-categories", fetchCategories.getName());
56+
}
57+
}

examples/template.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,31 @@ Resources:
243243
DockerContext: ../
244244
DockerTag: durable-examples
245245

246+
GenericInputOutputExampleFunction:
247+
Type: AWS::Serverless::Function
248+
Properties:
249+
PackageType: Image
250+
FunctionName: !Join
251+
- ''
252+
- - 'generic-input-output-example'
253+
- !Ref FunctionNameSuffix
254+
ImageConfig:
255+
Command: ["software.amazon.lambda.durable.examples.GenericInputOutputExample::handleRequest"]
256+
DurableConfig:
257+
ExecutionTimeout: 300
258+
RetentionPeriodInDays: 7
259+
Policies:
260+
- Statement:
261+
- Effect: Allow
262+
Action:
263+
- lambda:CheckpointDurableExecutions
264+
- lambda:GetDurableExecutionState
265+
Resource: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:generic-input-output-example${FunctionNameSuffix}"
266+
Metadata:
267+
Dockerfile: !Ref DockerFile
268+
DockerContext: ../
269+
DockerTag: durable-examples
270+
246271
CustomConfigExampleFunction:
247272
Type: AWS::Serverless::Function
248273
Properties:
@@ -491,6 +516,14 @@ Outputs:
491516
Description: Generic Types Example Function Name
492517
Value: !Ref GenericTypesExampleFunction
493518

519+
GenericInputOutputExampleFunction:
520+
Description: Generic Input Output Example Function ARN
521+
Value: !GetAtt GenericInputOutputExampleFunction.Arn
522+
523+
GenericInputOutputExampleFunctionName:
524+
Description: Generic Input Output Example Function Name
525+
Value: !Ref GenericInputOutputExampleFunction
526+
494527
CustomConfigExampleFunction:
495528
Description: Custom Config Example Function ARN
496529
Value: !GetAtt CustomConfigExampleFunction.Arn

sdk-integration-tests/src/test/java/software/amazon/lambda/durable/DurableExecutionCheckpointTest.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ void testLargePayloadCheckpointing() {
4040
var largeString = "x".repeat(7 * 1024 * 1024); // 7MB string
4141

4242
var output = DurableExecutor.execute(
43-
input, null, String.class, (userInput, ctx) -> largeString, configWithMockClient(client));
43+
input,
44+
null,
45+
TypeToken.get(String.class),
46+
(userInput, ctx) -> largeString,
47+
configWithMockClient(client));
4448

4549
assertEquals(ExecutionStatus.SUCCEEDED, output.status());
4650
assertEquals("", output.result());
@@ -76,7 +80,11 @@ void testSmallPayloadNoExtraCheckpoint() {
7680
var smallResult = "Small result";
7781

7882
var output = DurableExecutor.execute(
79-
input, null, String.class, (userInput, ctx) -> smallResult, configWithMockClient(client));
83+
input,
84+
null,
85+
TypeToken.get(String.class),
86+
(userInput, ctx) -> smallResult,
87+
configWithMockClient(client));
8088

8189
assertEquals(ExecutionStatus.SUCCEEDED, output.status());
8290
assertNotNull(output.result());

sdk-testing/src/main/java/software/amazon/lambda/durable/testing/AsyncExecution.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import software.amazon.awssdk.services.lambda.model.EventType;
1212
import software.amazon.awssdk.services.lambda.model.GetDurableExecutionHistoryRequest;
1313
import software.amazon.awssdk.services.lambda.model.ResourceNotFoundException;
14+
import software.amazon.lambda.durable.TypeToken;
1415
import software.amazon.lambda.durable.model.ExecutionStatus;
1516

1617
/**
@@ -20,7 +21,7 @@
2021
public class AsyncExecution<O> {
2122
private final String executionArn;
2223
private final LambdaClient lambdaClient;
23-
private final Class<O> outputType;
24+
private final TypeToken<O> outputType;
2425
private final Duration pollInterval;
2526
private final Duration timeout;
2627
private final HistoryEventProcessor processor;
@@ -30,7 +31,7 @@ public class AsyncExecution<O> {
3031
public AsyncExecution(
3132
String executionArn,
3233
LambdaClient lambdaClient,
33-
Class<O> outputType,
34+
TypeToken<O> outputType,
3435
Duration pollInterval,
3536
Duration timeout) {
3637
this.executionArn = executionArn;

sdk-testing/src/main/java/software/amazon/lambda/durable/testing/CloudDurableTestRunner.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
import software.amazon.awssdk.services.lambda.LambdaClient;
99
import software.amazon.awssdk.services.lambda.model.InvocationType;
1010
import software.amazon.awssdk.services.lambda.model.InvokeRequest;
11+
import software.amazon.lambda.durable.TypeToken;
1112
import software.amazon.lambda.durable.serde.JacksonSerDes;
1213

1314
public class CloudDurableTestRunner<I, O> {
1415
private final String functionArn;
15-
private final Class<I> inputType;
16-
private final Class<O> outputType;
16+
private final TypeToken<I> inputType;
17+
private final TypeToken<O> outputType;
1718
private final LambdaClient lambdaClient;
1819
private final Duration pollInterval;
1920
private final Duration timeout;
@@ -23,8 +24,8 @@ public class CloudDurableTestRunner<I, O> {
2324

2425
private CloudDurableTestRunner(
2526
String functionArn,
26-
Class<I> inputType,
27-
Class<O> outputType,
27+
TypeToken<I> inputType,
28+
TypeToken<O> outputType,
2829
LambdaClient lambdaClient,
2930
Duration pollInterval,
3031
Duration timeout,
@@ -40,6 +41,11 @@ private CloudDurableTestRunner(
4041

4142
public static <I, O> CloudDurableTestRunner<I, O> create(
4243
String functionArn, Class<I> inputType, Class<O> outputType) {
44+
return create(functionArn, TypeToken.get(inputType), TypeToken.get(outputType));
45+
}
46+
47+
public static <I, O> CloudDurableTestRunner<I, O> create(
48+
String functionArn, TypeToken<I> inputType, TypeToken<O> outputType) {
4349
return new CloudDurableTestRunner<>(
4450
functionArn,
4551
inputType,
@@ -55,6 +61,11 @@ public static <I, O> CloudDurableTestRunner<I, O> create(
5561

5662
public static <I, O> CloudDurableTestRunner<I, O> create(
5763
String functionArn, Class<I> inputType, Class<O> outputType, LambdaClient lambdaClient) {
64+
return create(functionArn, TypeToken.get(inputType), TypeToken.get(outputType), lambdaClient);
65+
}
66+
67+
public static <I, O> CloudDurableTestRunner<I, O> create(
68+
String functionArn, TypeToken<I> inputType, TypeToken<O> outputType, LambdaClient lambdaClient) {
5869
return new CloudDurableTestRunner<>(
5970
functionArn,
6071
inputType,

sdk-testing/src/main/java/software/amazon/lambda/durable/testing/HistoryEventProcessor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
import software.amazon.awssdk.services.lambda.model.OperationType;
1616
import software.amazon.awssdk.services.lambda.model.StepDetails;
1717
import software.amazon.awssdk.services.lambda.model.WaitDetails;
18+
import software.amazon.lambda.durable.TypeToken;
1819
import software.amazon.lambda.durable.model.ExecutionStatus;
1920
import software.amazon.lambda.durable.serde.JacksonSerDes;
2021

2122
public class HistoryEventProcessor {
2223
private final JacksonSerDes serDes = new JacksonSerDes();
2324

24-
public <O> TestResult<O> processEvents(List<Event> events, Class<O> outputType) {
25+
public <O> TestResult<O> processEvents(List<Event> events, TypeToken<O> outputType) {
2526
var operations = new HashMap<String, Operation>();
2627
var operationEvents = new HashMap<String, List<Event>>();
2728
var status = ExecutionStatus.PENDING;

0 commit comments

Comments
 (0)