diff --git a/examples/src/main/java/software/amazon/lambda/durable/examples/general/CustomConfigExample.java b/examples/src/main/java/software/amazon/lambda/durable/examples/general/CustomConfigExample.java index 28d248395..5bbd902b0 100644 --- a/examples/src/main/java/software/amazon/lambda/durable/examples/general/CustomConfigExample.java +++ b/examples/src/main/java/software/amazon/lambda/durable/examples/general/CustomConfigExample.java @@ -31,6 +31,7 @@ *
When enabled, the SDK validates serialized operation results and exceptions before checkpointing them. This
+ * catches incompatible SerDes behavior early at the cost of an extra deserialize pass. Defaults to true, and custom
+ * SerDes implementations are still expected to be round-trip safe even if this validation is disabled.
+ *
+ * @return true when round-trip serialization validation is enabled
+ */
+ public boolean shouldValidateSerializationRoundTrip() {
+ return validateSerializationRoundTrip;
+ }
+
/**
* Gets the plugin runner that dispatches lifecycle events to registered plugins.
*
@@ -293,6 +308,7 @@ public static final class Builder {
private LoggerConfig loggerConfig;
private PollingStrategy pollingStrategy;
private Duration checkpointDelay;
+ private boolean validateSerializationRoundTrip = true;
private List This validation is enabled by default. Disable it only to avoid the extra deserialize pass when the
+ * additional safety check is too expensive for your workload. Custom SerDes implementations are still expected
+ * to round-trip SDK-managed values correctly.
+ *
+ * @param validateSerializationRoundTrip true to validate serialized data with an immediate deserialize pass
+ * @return This builder
+ */
+ public Builder withSerializationRoundTripValidation(boolean validateSerializationRoundTrip) {
+ this.validateSerializationRoundTrip = validateSerializationRoundTrip;
+ return this;
+ }
+
/**
* Registers one or more plugins for lifecycle event instrumentation.
*
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 a31c94d00..6fceee077 100644
--- a/sdk/src/main/java/software/amazon/lambda/durable/DurableHandler.java
+++ b/sdk/src/main/java/software/amazon/lambda/durable/DurableHandler.java
@@ -117,6 +117,7 @@ public DurableConfig getConfiguration() {
* .withDurableExecutionClient(durableClient)
* .withSerDes(customSerDes) // Optional: custom SerDes for user data
* .withExecutorService(customExecutor) // Optional: custom thread pool
+ * .withSerializationRoundTripValidation(false) // Optional: skip extra validation deserialize pass
* .build();
* }
* }
diff --git a/sdk/src/main/java/software/amazon/lambda/durable/operation/ChildContextOperation.java b/sdk/src/main/java/software/amazon/lambda/durable/operation/ChildContextOperation.java
index acdda283b..e5f287fed 100644
--- a/sdk/src/main/java/software/amazon/lambda/durable/operation/ChildContextOperation.java
+++ b/sdk/src/main/java/software/amazon/lambda/durable/operation/ChildContextOperation.java
@@ -149,6 +149,8 @@ private void executeChildContext() {
}
private void handleChildContextSuccess(T result) {
+ var serialized = serializeResult(result);
+
if (replayChildren.get() || isVirtual || parentOperation != null && parentOperation.isOperationCompleted()) {
// Skip checkpointing if
// - parent ConcurrencyOperation has already completed, preventing race conditions where a child finishes
@@ -159,13 +161,11 @@ private void handleChildContextSuccess(T result) {
cachedOperationResult.set(DeserializedOperationResult.succeeded(result));
markAlreadyCompleted();
} else {
- checkpointSuccess(result);
+ checkpointSuccess(result, serialized);
}
}
- private void checkpointSuccess(T result) {
- var serialized = serializeResult(result);
-
+ private void checkpointSuccess(T result, String serialized) {
if (serialized == null || serialized.getBytes(StandardCharsets.UTF_8).length < LARGE_RESULT_THRESHOLD) {
sendOperationUpdate(
OperationUpdate.builder().action(OperationAction.SUCCEED).payload(serialized));
diff --git a/sdk/src/main/java/software/amazon/lambda/durable/operation/SerializableDurableOperation.java b/sdk/src/main/java/software/amazon/lambda/durable/operation/SerializableDurableOperation.java
index 6ccf24e0f..5eeae4f6b 100644
--- a/sdk/src/main/java/software/amazon/lambda/durable/operation/SerializableDurableOperation.java
+++ b/sdk/src/main/java/software/amazon/lambda/durable/operation/SerializableDurableOperation.java
@@ -101,7 +101,11 @@ protected T deserializeResult(String result) {
* @return the serialized string
*/
protected String serializeResult(T result) {
- return resultSerDes.serialize(result);
+ var serialized = resultSerDes.serialize(result);
+ if (shouldValidateSerializationRoundTrip()) {
+ deserializeResult(serialized);
+ }
+ return serialized;
}
/**
@@ -110,8 +114,18 @@ protected String serializeResult(T result) {
* @param throwable the exception to serialize
* @return the serialized error object
*/
+ @SuppressWarnings("ThrowableNotThrown")
protected ErrorObject serializeException(Throwable throwable) {
- return ExceptionHelper.buildErrorObject(throwable, resultSerDes);
+ var error = ExceptionHelper.buildErrorObject(throwable, resultSerDes);
+ if (shouldValidateSerializationRoundTrip()) {
+ deserializeException(error);
+ }
+ return error;
+ }
+
+ private boolean shouldValidateSerializationRoundTrip() {
+ var config = getContext().getDurableConfig();
+ return config == null || config.shouldValidateSerializationRoundTrip();
}
/**
diff --git a/sdk/src/test/java/software/amazon/lambda/durable/DurableConfigTest.java b/sdk/src/test/java/software/amazon/lambda/durable/DurableConfigTest.java
index 94d13f5ef..9f1dceddf 100644
--- a/sdk/src/test/java/software/amazon/lambda/durable/DurableConfigTest.java
+++ b/sdk/src/test/java/software/amazon/lambda/durable/DurableConfigTest.java
@@ -87,6 +87,24 @@ void testBuilder_WithCustomExecutorService() {
assertNotNull(config.getSerDes());
}
+ @Test
+ void testBuilder_SerializationRoundTripValidationDefaultsToTrue() {
+ var config =
+ DurableConfig.builder().withDurableExecutionClient(mockClient).build();
+
+ assertTrue(config.shouldValidateSerializationRoundTrip());
+ }
+
+ @Test
+ void testBuilder_WithSerializationRoundTripValidationDisabled() {
+ var config = DurableConfig.builder()
+ .withDurableExecutionClient(mockClient)
+ .withSerializationRoundTripValidation(false)
+ .build();
+
+ assertFalse(config.shouldValidateSerializationRoundTrip());
+ }
+
@Test
void testBuilder_WithAllCustomComponents() {
var config = DurableConfig.builder()
@@ -131,6 +149,7 @@ void testBuilder_FluentAPI() {
assertSame(builder, builder.withDurableExecutionClient(mockClient));
assertSame(builder, builder.withSerDes(mockSerDes));
assertSame(builder, builder.withExecutorService(mockExecutor));
+ assertSame(builder, builder.withSerializationRoundTripValidation(false));
}
@Test
diff --git a/sdk/src/test/java/software/amazon/lambda/durable/operation/ChildContextOperationTest.java b/sdk/src/test/java/software/amazon/lambda/durable/operation/ChildContextOperationTest.java
index ac56262a4..098ecb1f6 100644
--- a/sdk/src/test/java/software/amazon/lambda/durable/operation/ChildContextOperationTest.java
+++ b/sdk/src/test/java/software/amazon/lambda/durable/operation/ChildContextOperationTest.java
@@ -24,16 +24,30 @@
import software.amazon.lambda.durable.context.DurableContextImpl;
import software.amazon.lambda.durable.exception.ChildContextFailedException;
import software.amazon.lambda.durable.exception.NonDeterministicExecutionException;
+import software.amazon.lambda.durable.exception.SerDesException;
import software.amazon.lambda.durable.execution.ExecutionManager;
import software.amazon.lambda.durable.execution.ThreadContext;
import software.amazon.lambda.durable.execution.ThreadType;
import software.amazon.lambda.durable.model.OperationIdentifier;
import software.amazon.lambda.durable.model.OperationSubType;
import software.amazon.lambda.durable.serde.JacksonSerDes;
+import software.amazon.lambda.durable.serde.SerDes;
/** Unit tests for ChildContextOperation. */
class ChildContextOperationTest {
+ private static final class SerializationOnlySerDes implements SerDes {
+ @Override
+ public String serialize(Object value) {
+ return "\"serialized\"";
+ }
+
+ @Override
+ public