From 27e173c6ba7bc522af8c5db5494ff5ec996fa328 Mon Sep 17 00:00:00 2001 From: Frank Chen Date: Tue, 17 Mar 2026 16:43:08 -0700 Subject: [PATCH] move serdes out of DurableHandler --- .../amazon/lambda/durable/DurableConfig.java | 20 ++- .../amazon/lambda/durable/DurableHandler.java | 110 ++----------- .../serde/DurableInputOutputSerDes.java | 145 ++++++++++++++++++ .../lambda/durable/serde/JacksonSerDes.java | 9 +- .../lambda/durable/DurableHandlerTest.java | 117 +------------- .../durable/serde/AwsSdkV2ModuleTest.java | 9 +- .../serde/DurableInputOutputSerDesTest.java | 128 ++++++++++++++++ 7 files changed, 314 insertions(+), 224 deletions(-) create mode 100644 sdk/src/main/java/software/amazon/lambda/durable/serde/DurableInputOutputSerDes.java create mode 100644 sdk/src/test/java/software/amazon/lambda/durable/serde/DurableInputOutputSerDesTest.java diff --git a/sdk/src/main/java/software/amazon/lambda/durable/DurableConfig.java b/sdk/src/main/java/software/amazon/lambda/durable/DurableConfig.java index 664011cc..d30b6c47 100644 --- a/sdk/src/main/java/software/amazon/lambda/durable/DurableConfig.java +++ b/sdk/src/main/java/software/amazon/lambda/durable/DurableConfig.java @@ -92,6 +92,8 @@ private DurableConfig(Builder builder) { this.pollingStrategy = builder.pollingStrategy != null ? builder.pollingStrategy : PollingStrategies.Presets.DEFAULT; this.checkpointDelay = builder.checkpointDelay != null ? builder.checkpointDelay : Duration.ofSeconds(0); + + validateConfiguration(); } /** @@ -166,6 +168,18 @@ public Duration getCheckpointDelay() { return checkpointDelay; } + public void validateConfiguration() { + if (getDurableExecutionClient() == null) { + throw new IllegalStateException("DurableExecutionClient configuration failed"); + } + if (getSerDes() == null) { + throw new IllegalStateException("SerDes configuration failed"); + } + if (getExecutorService() == null) { + throw new IllegalStateException("ExecutorService configuration failed"); + } + } + /** * Creates a default DurableExecutionClient with production LambdaClient. Uses * EnvironmentVariableCredentialsProvider and region from AWS_REGION. If AWS_REGION is not set, defaults to @@ -317,7 +331,7 @@ public Builder withSerDes(SerDes serDes) { * will be created. * *

This executor is used exclusively for running user-defined operations. Internal SDK coordination (polling, - * checkpointing) uses the common ForkJoinPool and is not affected by this setting. + * checkpointing) uses the SDK InternalExecutor thread pool and is not affected by this setting. * * @param executorService Custom ExecutorService instance * @return This builder @@ -351,8 +365,8 @@ public Builder withPollingStrategy(PollingStrategy pollingStrategy) { } /** - * Sets how often the SDK checkpoints updates to backend. If not set, defaults to 0, which disables checkpoint - * batching. + * Sets how often the SDK checkpoints updates to backend. If not set, defaults to 0, SDK will checkpoint the + * updates as soon as possible. * * @param duration the checkpoint delay in Duration * @return This builder diff --git a/sdk/src/main/java/software/amazon/lambda/durable/DurableHandler.java b/sdk/src/main/java/software/amazon/lambda/durable/DurableHandler.java index 4f55a8ac..a9acc3b8 100644 --- a/sdk/src/main/java/software/amazon/lambda/durable/DurableHandler.java +++ b/sdk/src/main/java/software/amazon/lambda/durable/DurableHandler.java @@ -4,33 +4,14 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.ParameterizedType; -import java.time.Instant; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeFormatterBuilder; -import java.util.Date; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.lambda.durable.model.DurableExecutionInput; -import software.amazon.lambda.durable.serde.AwsSdkV2Module; +import software.amazon.lambda.durable.serde.DurableInputOutputSerDes; /** * Abstract base class for Lambda handlers that use durable execution. @@ -46,7 +27,7 @@ public abstract class DurableHandler implements RequestStreamHandler { private final TypeToken inputType; private final DurableConfig config; - private final ObjectMapper objectMapper = createObjectMapper(); // Internal ObjectMapper + private final DurableInputOutputSerDes serDes = new DurableInputOutputSerDes(); // Internal ObjectMapper private static final Logger logger = LoggerFactory.getLogger(DurableHandler.class); protected DurableHandler() { @@ -58,7 +39,6 @@ protected DurableHandler() { throw new IllegalArgumentException("Cannot determine input type parameter"); } this.config = createConfiguration(); - validateConfiguration(); } /** @@ -142,26 +122,22 @@ protected DurableConfig createConfiguration() { return DurableConfig.defaultConfig(); } - private void validateConfiguration() { - if (config.getDurableExecutionClient() == null) { - throw new IllegalStateException("DurableExecutionClient configuration failed"); - } - if (config.getSerDes() == null) { - throw new IllegalStateException("SerDes configuration failed"); - } - if (config.getExecutorService() == null) { - throw new IllegalStateException("ExecutorService configuration failed"); - } - } - + /** + * Reads the request, executes the durable function handler and writes the response + * + * @param inputStream the input stream + * @param outputStream the output stream + * @param context the Lambda context + * @throws IOException thrown when serialize/deserialize fails + */ @Override public final void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { var inputString = new String(inputStream.readAllBytes()); logger.debug("Raw input from durable handler: {}", inputString); - var input = this.objectMapper.readValue(inputString, DurableExecutionInput.class); + var input = serDes.deserialize(inputString, TypeToken.get(DurableExecutionInput.class)); var output = DurableExecutor.execute(input, context, inputType, this::handleRequest, config); - outputStream.write(objectMapper.writeValueAsBytes(output)); + outputStream.write(serDes.serialize(output).getBytes()); } /** @@ -172,66 +148,4 @@ public final void handleRequest(InputStream inputStream, OutputStream outputStre * @return Result */ public abstract O handleRequest(I input, DurableContext context); - - /** - * Creates ObjectMapper for DAR backend communication (internal use only). This is for INTERNAL use only - handles - * Lambda Durable Functions backend protocol. - * - *

Customer-facing serialization uses SerDes from DurableConfig. - * - * @return Configured ObjectMapper for durable backend communication - */ - public static ObjectMapper createObjectMapper() { - var dateModule = new SimpleModule(); - dateModule.addDeserializer(Date.class, new JsonDeserializer<>() { - @Override - public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - // Timestamp is a double value represent seconds since epoch. - var timestamp = jsonParser.getDoubleValue(); - // Date expects milliseconds since epoch, so multiply by 1000. - return new Date((long) (timestamp * 1000)); - } - }); - dateModule.addSerializer(Date.class, new JsonSerializer<>() { - @Override - public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) - throws IOException { - // Timestamp should be a double value representing seconds since epoch, so - // convert from milliseconds. - double timestamp = date.getTime() / 1000.0; - jsonGenerator.writeNumber(timestamp); - } - }); - - // Needed for deserialization of timestamps for some SDK v2 objects - dateModule.addDeserializer(Instant.class, new JsonDeserializer<>() { - private static final DateTimeFormatter TIMESTAMP_FORMATTER = new DateTimeFormatterBuilder() - .appendPattern("yyyy-MM-dd HH:mm:ss.SSSSSSXXX") - .toFormatter(); - - @Override - public Instant deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - if (jsonParser.hasToken(JsonToken.VALUE_NUMBER_INT)) { - return Instant.ofEpochMilli(jsonParser.getLongValue()); - } - var timestampStr = jsonParser.getValueAsString(); - return Instant.from(TIMESTAMP_FORMATTER.parse(timestampStr)); - } - }); - - return JsonMapper.builder() - .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - // Looks pretty, and probably needed for tests to be deterministic. - .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) - .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) - // Data passed over the wire from the backend is UpperCamelCase - .propertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE) - .addModule(new JavaTimeModule()) - .addModule(dateModule) - .addModule(new AwsSdkV2Module()) - .build(); - } } diff --git a/sdk/src/main/java/software/amazon/lambda/durable/serde/DurableInputOutputSerDes.java b/sdk/src/main/java/software/amazon/lambda/durable/serde/DurableInputOutputSerDes.java new file mode 100644 index 00000000..db89d693 --- /dev/null +++ b/sdk/src/main/java/software/amazon/lambda/durable/serde/DurableInputOutputSerDes.java @@ -0,0 +1,145 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.lambda.durable.serde; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; +import java.lang.reflect.Type; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import software.amazon.lambda.durable.TypeToken; +import software.amazon.lambda.durable.util.ExceptionHelper; + +/** + * Serializer/Deserializer for Durable Execution Input and Output objects. This is for INTERNAL use only - handles + * Lambda Durable Functions backend protocol. + * + *

Customer-facing serialization uses SerDes from DurableConfig. + */ +public class DurableInputOutputSerDes implements SerDes { + private final ObjectMapper objectMapper = createObjectMapper(); // Internal ObjectMapper + private final TypeFactory typeFactory = objectMapper.getTypeFactory(); + private final Map typeCache = new ConcurrentHashMap<>(); + + /** + * Creates ObjectMapper for DAR backend communication (internal use only). This is for INTERNAL use only - handles + * Lambda Durable Functions backend protocol. + * + *

Customer-facing serialization uses SerDes from DurableConfig. + * + * @return Configured ObjectMapper for durable backend communication + */ + static ObjectMapper createObjectMapper() { + var dateModule = new SimpleModule(); + dateModule.addDeserializer(Date.class, new JsonDeserializer<>() { + @Override + public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + // Timestamp is a double value represent seconds since epoch. + var timestamp = jsonParser.getDoubleValue(); + // Date expects milliseconds since epoch, so multiply by 1000. + return new Date((long) (timestamp * 1000)); + } + }); + dateModule.addSerializer(Date.class, new JsonSerializer<>() { + @Override + public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + // Timestamp should be a double value representing seconds since epoch, so + // convert from milliseconds. + double timestamp = date.getTime() / 1000.0; + jsonGenerator.writeNumber(timestamp); + } + }); + + // Needed for deserialization of timestamps for some SDK v2 objects + dateModule.addDeserializer(Instant.class, new JsonDeserializer<>() { + private static final DateTimeFormatter TIMESTAMP_FORMATTER = new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd HH:mm:ss.SSSSSSXXX") + .toFormatter(); + + @Override + public Instant deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + if (jsonParser.hasToken(JsonToken.VALUE_NUMBER_INT)) { + return Instant.ofEpochMilli(jsonParser.getLongValue()); + } + var timestampStr = jsonParser.getValueAsString(); + return Instant.from(TIMESTAMP_FORMATTER.parse(timestampStr)); + } + }); + + return JsonMapper.builder() + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + // Looks pretty, and probably needed for tests to be deterministic. + .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) + // Data passed over the wire from the backend is UpperCamelCase + .propertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE) + .addModule(new JavaTimeModule()) + .addModule(dateModule) + .addModule(new AwsSdkV2Module()) + .build(); + } + + /** + * Serializes an object to a JSON string. + * + * @param value the object to serialize + * @return the JSON string representation, or null if value is null + */ + @Override + public String serialize(Object value) { + if (value == null) { + return null; + } + try { + return objectMapper.writeValueAsString(value); + } catch (IOException e) { + ExceptionHelper.sneakyThrow(e); + return null; + } + } + + /** + * Deserializes a JSON string to DurableExecutionInput object + * + * @param data the JSON string to deserialize + * @param typeToken the type token of DurableExecutionInput + * @return the deserialized object, or null if data is null + */ + @Override + public T deserialize(String data, TypeToken typeToken) { + if (data == null) { + return null; + } + try { + JavaType javaType = typeCache.computeIfAbsent(typeToken.getType(), typeFactory::constructType); + return objectMapper.readValue(data, javaType); + } catch (IOException e) { + ExceptionHelper.sneakyThrow(e); + return null; + } + } +} diff --git a/sdk/src/main/java/software/amazon/lambda/durable/serde/JacksonSerDes.java b/sdk/src/main/java/software/amazon/lambda/durable/serde/JacksonSerDes.java index a11f3480..820cfd47 100644 --- a/sdk/src/main/java/software/amazon/lambda/durable/serde/JacksonSerDes.java +++ b/sdk/src/main/java/software/amazon/lambda/durable/serde/JacksonSerDes.java @@ -36,10 +36,15 @@ public class JacksonSerDes implements SerDes { /** Creates a new JacksonSerDes with default ObjectMapper configuration. */ public JacksonSerDes() { - this.mapper = new ObjectMapper() + this(new ObjectMapper() .registerModule(new JavaTimeModule()) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)); + } + + /** Creates a new JacksonSerDes with a custom ObjectMapper configuration. */ + public JacksonSerDes(ObjectMapper objectMapper) { + this.mapper = objectMapper; this.typeFactory = mapper.getTypeFactory(); this.typeCache = new ConcurrentHashMap<>(); } diff --git a/sdk/src/test/java/software/amazon/lambda/durable/DurableHandlerTest.java b/sdk/src/test/java/software/amazon/lambda/durable/DurableHandlerTest.java index 050e3a88..1f9f5de8 100644 --- a/sdk/src/test/java/software/amazon/lambda/durable/DurableHandlerTest.java +++ b/sdk/src/test/java/software/amazon/lambda/durable/DurableHandlerTest.java @@ -4,21 +4,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import software.amazon.awssdk.services.lambda.model.ErrorObject; import software.amazon.lambda.durable.client.DurableExecutionClient; -import software.amazon.lambda.durable.model.DurableExecutionInput; -import software.amazon.lambda.durable.model.DurableExecutionOutput; -import software.amazon.lambda.durable.model.ExecutionStatus; class DurableHandlerTest { @@ -28,97 +20,9 @@ class DurableHandlerTest { @Mock private DurableExecutionClient mockClient; - private ObjectMapper objectMapper; - @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - objectMapper = DurableHandler.createObjectMapper(); - } - - @Test - void testObjectMapperDeserializesDurableExecutionInput() throws IOException { - var json = """ - { - "DurableExecutionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-function", - "CheckpointToken": "token-123", - "InitialExecutionState": { - "Operations": [], - "NextMarker": null - } - } - """; - - var input = objectMapper.readValue(json, DurableExecutionInput.class); - - assertNotNull(input); - assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", input.durableExecutionArn()); - assertEquals("token-123", input.checkpointToken()); - assertNotNull(input.initialExecutionState()); - } - - @Test - void testObjectMapperSerializesSuccessOutput() throws IOException { - var output = DurableExecutionOutput.success("test-result"); - - var json = objectMapper.writeValueAsString(output); - - assertTrue(json.contains("\"Status\":\"SUCCEEDED\"")); - assertTrue(json.contains("\"Result\":\"test-result\"")); - assertTrue(json.contains("\"Error\":null")); - } - - @Test - void testObjectMapperSerializesPendingOutput() throws IOException { - var output = DurableExecutionOutput.pending(); - - var json = objectMapper.writeValueAsString(output); - - assertTrue(json.contains("\"Status\":\"PENDING\"")); - } - - @Test - void testObjectMapperSerializesFailureOutputWithErrorObject() throws IOException { - var output = DurableExecutionOutput.failure(ErrorObject.builder() - .errorType("myErrorType") - .errorMessage("myErrorMessage") - .errorData("myErrorData") - .stackTrace(List.of("s1", "s2")) - .build()); - - var json = objectMapper.writeValueAsString(output); - - assertTrue(json.contains("\"Status\":\"FAILED\"")); - assertTrue(json.contains("\"ErrorType\":\"myErrorType\"")); - assertTrue(json.contains("\"ErrorMessage\":\"myErrorMessage\"")); - assertTrue(json.contains("\"StackTrace\":[")); - assertTrue(json.contains("\"ErrorData\":\"myErrorData\"")); - } - - @Test - void testObjectMapperHandlesErrorObjectFromAwsSdk() throws IOException { - var errorObject = ErrorObject.builder() - .errorType("CustomError") - .errorMessage("Something went wrong") - .stackTrace(List.of("line1|method1|file1.java|10", "line2|method2|file2.java|20")) - .build(); - - var output = new DurableExecutionOutput(ExecutionStatus.FAILED, null, errorObject); - var json = objectMapper.writeValueAsString(output); - - // Verify serialization with custom ErrorObjectSerializer - assertTrue(json.contains("\"ErrorType\":\"CustomError\"")); - assertTrue(json.contains("\"ErrorMessage\":\"Something went wrong\"")); - assertTrue(json.contains("\"StackTrace\":[")); - assertTrue(json.contains("\"Status\":\"FAILED\"")); - - // Verify deserialization round-trip - var deserialized = objectMapper.readValue(json, DurableExecutionOutput.class); - assertEquals(ExecutionStatus.FAILED, deserialized.status()); - assertNotNull(deserialized.error()); - assertEquals("CustomError", deserialized.error().errorType()); - assertEquals("Something went wrong", deserialized.error().errorMessage()); - assertEquals(2, deserialized.error().stackTrace().size()); } @Test @@ -157,27 +61,8 @@ public Object handleRequest(Object input, DurableContext context) { } } - @Test - void testObjectMapperIgnoresUnknownProperties() throws IOException { - var json = """ - { - "Status": "SUCCEEDED", - "Result": "test", - "Error": null, - "UnknownProperty": "should be ignored" - } - """; - - // Should not fail on unknown properties - var output = objectMapper.readValue(json, DurableExecutionOutput.class); - - assertNotNull(output); - assertEquals(ExecutionStatus.SUCCEEDED, output.status()); - assertEquals("test", output.result()); - } - // Test handler implementation - private class TestDurableHandler extends DurableHandler { + private static class TestDurableHandler extends DurableHandler { @Override public String handleRequest(String input, DurableContext context) { return "processed: " + input; diff --git a/sdk/src/test/java/software/amazon/lambda/durable/serde/AwsSdkV2ModuleTest.java b/sdk/src/test/java/software/amazon/lambda/durable/serde/AwsSdkV2ModuleTest.java index 3aac30c1..8d036273 100644 --- a/sdk/src/test/java/software/amazon/lambda/durable/serde/AwsSdkV2ModuleTest.java +++ b/sdk/src/test/java/software/amazon/lambda/durable/serde/AwsSdkV2ModuleTest.java @@ -13,7 +13,6 @@ import software.amazon.awssdk.services.lambda.model.Operation; import software.amazon.awssdk.services.lambda.model.OperationStatus; import software.amazon.awssdk.services.lambda.model.OperationType; -import software.amazon.lambda.durable.DurableHandler; import software.amazon.lambda.durable.model.DurableExecutionInput; import software.amazon.lambda.durable.model.DurableExecutionOutput; import software.amazon.lambda.durable.model.ExecutionStatus; @@ -22,7 +21,7 @@ class AwsSdkV2ModuleTest { @Test void testDurableExecutionInputDeserializationIncludingSdkV2Operation() throws Exception { - ObjectMapper mapper = DurableHandler.createObjectMapper(); + ObjectMapper mapper = DurableInputOutputSerDes.createObjectMapper(); String json = """ { @@ -127,7 +126,7 @@ void testDurableExecutionInputDeserializationIncludingSdkV2Operation() throws Ex @Test void testErrorObjectSerializationAndDeserialization() throws Exception { - ObjectMapper mapper = DurableHandler.createObjectMapper(); + ObjectMapper mapper = DurableInputOutputSerDes.createObjectMapper(); // Create an ErrorObject using the builder var errorObject = ErrorObject.builder() @@ -167,7 +166,7 @@ void testErrorObjectSerializationAndDeserialization() throws Exception { @Test void testActualAWSLambdaPayload() throws Exception { - var mapper = DurableHandler.createObjectMapper(); + var mapper = DurableInputOutputSerDes.createObjectMapper(); var json = """ { "DurableExecutionArn": "c581e164-d7da-4108-8b35-109facaf1cc7", @@ -199,7 +198,7 @@ void testActualAWSLambdaPayload() throws Exception { @Test void testErrorObjectRoundTripWithNullFields() throws Exception { - ObjectMapper mapper = DurableHandler.createObjectMapper(); + ObjectMapper mapper = DurableInputOutputSerDes.createObjectMapper(); // Create an ErrorObject with minimal fields var errorObject = ErrorObject.builder() diff --git a/sdk/src/test/java/software/amazon/lambda/durable/serde/DurableInputOutputSerDesTest.java b/sdk/src/test/java/software/amazon/lambda/durable/serde/DurableInputOutputSerDesTest.java new file mode 100644 index 00000000..d64a8866 --- /dev/null +++ b/sdk/src/test/java/software/amazon/lambda/durable/serde/DurableInputOutputSerDesTest.java @@ -0,0 +1,128 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.lambda.durable.serde; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.lambda.model.ErrorObject; +import software.amazon.lambda.durable.TypeToken; +import software.amazon.lambda.durable.model.DurableExecutionInput; +import software.amazon.lambda.durable.model.DurableExecutionOutput; +import software.amazon.lambda.durable.model.ExecutionStatus; + +class DurableInputOutputSerDesTest { + + DurableInputOutputSerDes serDes; + + @BeforeEach + void setUp() { + serDes = new DurableInputOutputSerDes(); + } + + @Test + void testObjectMapperSerializesPendingOutput() { + var output = DurableExecutionOutput.pending(); + + var json = serDes.serialize(output); + + assertTrue(json.contains("\"Status\":\"PENDING\"")); + } + + @Test + void testObjectMapperDeserializesDurableExecutionInput() { + var json = """ + { + "DurableExecutionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-function", + "CheckpointToken": "token-123", + "InitialExecutionState": { + "Operations": [], + "NextMarker": null + } + } + """; + + var input = serDes.deserialize(json, TypeToken.get(DurableExecutionInput.class)); + + assertNotNull(input); + assertEquals("arn:aws:lambda:us-east-1:123456789012:function:my-function", input.durableExecutionArn()); + assertEquals("token-123", input.checkpointToken()); + assertNotNull(input.initialExecutionState()); + } + + @Test + void testObjectMapperSerializesSuccessOutput() { + var output = DurableExecutionOutput.success("test-result"); + + var json = serDes.serialize(output); + + assertTrue(json.contains("\"Status\":\"SUCCEEDED\"")); + assertTrue(json.contains("\"Result\":\"test-result\"")); + assertTrue(json.contains("\"Error\":null")); + } + + @Test + void testObjectMapperSerializesFailureOutputWithErrorObject() { + var output = DurableExecutionOutput.failure(ErrorObject.builder() + .errorType("myErrorType") + .errorMessage("myErrorMessage") + .errorData("myErrorData") + .stackTrace(List.of("s1", "s2")) + .build()); + + var json = serDes.serialize(output); + + assertTrue(json.contains("\"Status\":\"FAILED\"")); + assertTrue(json.contains("\"ErrorType\":\"myErrorType\"")); + assertTrue(json.contains("\"ErrorMessage\":\"myErrorMessage\"")); + assertTrue(json.contains("\"StackTrace\":[")); + assertTrue(json.contains("\"ErrorData\":\"myErrorData\"")); + } + + @Test + void testObjectMapperHandlesErrorObjectFromAwsSdk() { + var errorObject = ErrorObject.builder() + .errorType("CustomError") + .errorMessage("Something went wrong") + .stackTrace(List.of("line1|method1|file1.java|10", "line2|method2|file2.java|20")) + .build(); + + var output = new DurableExecutionOutput(ExecutionStatus.FAILED, null, errorObject); + var json = serDes.serialize(output); + + // Verify serialization with custom ErrorObjectSerializer + assertTrue(json.contains("\"ErrorType\":\"CustomError\"")); + assertTrue(json.contains("\"ErrorMessage\":\"Something went wrong\"")); + assertTrue(json.contains("\"StackTrace\":[")); + assertTrue(json.contains("\"Status\":\"FAILED\"")); + + // Verify deserialization round-trip + var deserialized = serDes.deserialize(json, TypeToken.get(DurableExecutionOutput.class)); + assertEquals(ExecutionStatus.FAILED, deserialized.status()); + assertNotNull(deserialized.error()); + assertEquals("CustomError", deserialized.error().errorType()); + assertEquals("Something went wrong", deserialized.error().errorMessage()); + assertEquals(2, deserialized.error().stackTrace().size()); + } + + @Test + void testObjectMapperIgnoresUnknownProperties() { + var json = """ + { + "Status": "SUCCEEDED", + "Result": "test", + "Error": null, + "UnknownProperty": "should be ignored" + } + """; + + // Should not fail on unknown properties + var output = serDes.deserialize(json, TypeToken.get(DurableExecutionOutput.class)); + + assertNotNull(output); + assertEquals(ExecutionStatus.SUCCEEDED, output.status()); + assertEquals("test", output.result()); + } +}