Skip to content

Commit 6dedec4

Browse files
authored
[refactor] Refactor exceptions (#45)
* refactor exceptions * add more unit tests * add doc for exceptions * move all exception helper functions to ExceptionHelper * remove errorObject from operation exceptions * update method name for operationId
1 parent 30c07ff commit 6dedec4

32 files changed

Lines changed: 437 additions & 307 deletions

README.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -384,13 +384,22 @@ protected DurableConfig createConfiguration() {
384384

385385
The SDK throws specific exceptions to help you handle different failure scenarios:
386386

387-
| Exception | When Thrown | How to Handle |
388-
|-----------|-------------|---------------|
389-
| `StepFailedException` | Step exhausted all retry attempts | Catch to implement fallback logic or let execution fail |
390-
| `StepInterruptedException` | `AT_MOST_ONCE` step was interrupted before completion | Implement manual recovery (check if operation completed externally) |
391-
| `CallbackTimeoutException` | Callback exceeded its timeout duration | Implement fallback logic or escalation |
392-
| `CallbackFailedException` | External system sent an error response to the callback | Handle the error or propagate failure |
393-
| `NonDeterministicExecutionException` | Code changed between original execution and replay | Fix code to maintain determinism; don't change step order/names |
387+
```
388+
DurableExecutionException - General durable exception
389+
├── NonDeterministicExecutionException - Code changed between original execution and replay. Fix code to maintain determinism; don't change step order/names.
390+
├── SerDesException - Serialization and deserialization exception.
391+
└── DurableOperationException - General operation exception
392+
├── StepException - General Step exception
393+
│ ├── StepFailedException - Step exhausted all retry attempts.Catch to implement fallback logic or let execution fail.
394+
│ └── StepInterruptedException - `AT_MOST_ONCE` step was interrupted before completion. Implement manual recovery (check if operation completed externally)
395+
├── InvokeException - General chained invocation exception
396+
│ ├── InvokeFailedException - Chained invocation failed. Handle the error or propagate failure.
397+
│ ├── InvokeTimedoutException - Chained invocation timed out. Handle the error or propagate failure.
398+
│ └── InvokeStoppedException - Chained invocation stopped. Handle the error or propagate failure.
399+
└── CallbackException - General callback exception
400+
├── CallbackFailedException - External system sent an error response to the callback. Handle the error or propagate failure
401+
└── CallbackTimeoutException - Callback exceeded its timeout duration. Handle the error or propagate the failure
402+
```
394403

395404
```java
396405
try {

examples/src/main/java/com/amazonaws/lambda/durable/examples/ErrorHandlingExample.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ public String handleRequest(Object input, DurableContext context) {
8585
.semantics(StepSemantics.AT_MOST_ONCE_PER_RETRY)
8686
.build());
8787
} catch (StepInterruptedException e) {
88-
logger.warn("Payment step interrupted, checking external status: {}", e.getOperationId());
88+
logger.warn(
89+
"Payment step interrupted, checking external status: {}",
90+
e.getOperation().id());
8991
// In real code: check payment provider for transaction status
9092
// If payment went through, return success; otherwise, handle appropriately
9193
paymentResult = context.step("verify-payment-status", String.class, () -> "verified-payment");

sdk-integration-tests/src/test/java/com/amazonaws/lambda/durable/CallbackIntegrationTest.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
import static org.junit.jupiter.api.Assertions.*;
66

7-
import com.amazonaws.lambda.durable.exception.CallbackFailedException;
8-
import com.amazonaws.lambda.durable.exception.CallbackTimeoutException;
97
import com.amazonaws.lambda.durable.model.ExecutionStatus;
108
import com.amazonaws.lambda.durable.serde.JacksonSerDes;
119
import com.amazonaws.lambda.durable.serde.SerDes;
@@ -90,9 +88,8 @@ void callbackFailureFlow() {
9088
result = runner.run("test");
9189
assertEquals(ExecutionStatus.FAILED, result.getStatus());
9290
assertTrue(result.getError().isPresent());
93-
assertEquals(
94-
CallbackFailedException.class.getName(), result.getError().get().errorType());
95-
assertTrue(result.getError().get().errorMessage().contains("Rejected"));
91+
assertEquals("Rejected", result.getError().get().errorType());
92+
assertEquals("Request denied", result.getError().get().errorMessage());
9693
}
9794

9895
@Test
@@ -116,10 +113,9 @@ void callbackTimeoutFlow() {
116113
// Re-run - callback timed out, throws CallbackTimeoutException
117114
result = runner.run("test");
118115
assertEquals(ExecutionStatus.FAILED, result.getStatus());
119-
assertTrue(result.getError().isPresent());
116+
assertFalse(result.getError().isPresent());
120117
assertEquals(
121-
CallbackTimeoutException.class.getName(),
122-
result.getError().get().errorType());
118+
OperationStatus.TIMED_OUT, result.getFailedOperations().get(0).getStatus());
123119
}
124120

125121
@Test
@@ -258,8 +254,8 @@ void callbackFailedExceptionHandlesVariousErrorFormats() {
258254
var result = runner.run("test");
259255
assertEquals(ExecutionStatus.FAILED, result.getStatus());
260256
assertTrue(result.getError().isPresent());
261-
assertTrue(result.getError().get().errorMessage().contains("ValidationError"));
262-
assertTrue(result.getError().get().errorMessage().contains("Invalid input data"));
257+
assertEquals("ValidationError", result.getError().get().errorType());
258+
assertEquals("Invalid input data", result.getError().get().errorMessage());
263259
assertNotNull(result.getError().get().stackTrace());
264260
assertEquals(1, result.getError().get().stackTrace().size());
265261
}

sdk-integration-tests/src/test/java/com/amazonaws/lambda/durable/InvokeIntegrationTest.java

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package com.amazonaws.lambda.durable;
44

55
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertFalse;
67
import static org.junit.jupiter.api.Assertions.assertNull;
78

89
import com.amazonaws.lambda.durable.exception.InvokeFailedException;
@@ -93,8 +94,8 @@ void testInvokeWithFailedResults() {
9394
return new TestOutput(result);
9495
} catch (InvokeFailedException ex) {
9596
assertEquals("error output", ex.getMessage());
96-
assertEquals("error data", ex.getErrorData());
97-
assertEquals("error type", ex.getErrorType());
97+
assertEquals("error data", ex.getErrorObject().errorData());
98+
assertEquals("error type", ex.getErrorObject().errorType());
9899
throw ex;
99100
}
100101
});
@@ -114,9 +115,8 @@ void testInvokeWithFailedResults() {
114115
var output2 = runner.run(new TestInput("test"));
115116

116117
assertEquals(ExecutionStatus.FAILED, output2.getStatus());
117-
// todo: the error object should equal to the error object returned by chained invoke
118118
ErrorObject error = output2.getError().orElseThrow();
119-
assertEquals("com.amazonaws.lambda.durable.exception.InvokeFailedException", error.errorType());
119+
assertEquals("error type", error.errorType());
120120
assertEquals("error output", error.errorMessage());
121121
}
122122

@@ -128,8 +128,8 @@ void testInvokeWithStoppedResults() {
128128
return new TestOutput(result);
129129
} catch (InvokeFailedException ex) {
130130
assertEquals("error output", ex.getMessage());
131-
assertEquals("error data", ex.getErrorData());
132-
assertEquals("error type", ex.getErrorType());
131+
assertEquals("error data", ex.getErrorObject().errorData());
132+
assertEquals("error type", ex.getErrorObject().errorType());
133133
throw ex;
134134
}
135135
});
@@ -149,9 +149,8 @@ void testInvokeWithStoppedResults() {
149149
var output2 = runner.run(new TestInput("test"));
150150

151151
assertEquals(ExecutionStatus.FAILED, output2.getStatus());
152-
// todo: the error object should equal to the error object returned by chained invoke
153152
ErrorObject error = output2.getError().orElseThrow();
154-
assertEquals("com.amazonaws.lambda.durable.exception.InvokeStoppedException", error.errorType());
153+
assertEquals("error type", error.errorType());
155154
assertEquals("error output", error.errorMessage());
156155
}
157156

@@ -163,8 +162,8 @@ void testInvokeWithTimeoutResults() {
163162
return new TestOutput(result);
164163
} catch (InvokeFailedException ex) {
165164
assertNull(ex.getMessage());
166-
assertNull(ex.getErrorData());
167-
assertNull(ex.getErrorType());
165+
assertNull(ex.getErrorObject().errorData());
166+
assertNull(ex.getErrorObject().errorType());
168167
throw ex;
169168
}
170169
});
@@ -178,9 +177,6 @@ void testInvokeWithTimeoutResults() {
178177
var output2 = runner.run(new TestInput("test"));
179178

180179
assertEquals(ExecutionStatus.FAILED, output2.getStatus());
181-
// todo: the error object should equal to the error object returned by chained invoke
182-
ErrorObject error = output2.getError().orElseThrow();
183-
assertEquals("com.amazonaws.lambda.durable.exception.InvokeTimedOutException", error.errorType());
184-
assertNull(error.errorMessage());
180+
assertFalse(output2.getError().isPresent());
185181
}
186182
}

sdk-testing/src/main/java/com/amazonaws/lambda/durable/testing/TestResult.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
import java.util.List;
99
import java.util.Map;
1010
import java.util.Optional;
11+
import java.util.Set;
1112
import java.util.stream.Collectors;
1213
import software.amazon.awssdk.services.lambda.model.ErrorObject;
1314
import software.amazon.awssdk.services.lambda.model.Event;
1415
import software.amazon.awssdk.services.lambda.model.OperationStatus;
1516

1617
public class TestResult<O> {
18+
private static final Set<OperationStatus> FAIL_OPERATION_STATUS = Set.of(
19+
OperationStatus.FAILED, OperationStatus.CANCELLED, OperationStatus.TIMED_OUT, OperationStatus.STOPPED);
1720
private final ExecutionStatus status;
1821
private final String resultPayload;
1922
private final ErrorObject error;
@@ -90,7 +93,7 @@ public List<TestOperation> getSucceededOperations() {
9093

9194
public List<TestOperation> getFailedOperations() {
9295
return operations.stream()
93-
.filter(op -> op.getStatus() == OperationStatus.FAILED)
96+
.filter(op -> FAIL_OPERATION_STATUS.contains(op.getStatus()))
9497
.toList();
9598
}
9699
}

sdk/src/main/java/com/amazonaws/lambda/durable/DurableExecutor.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package com.amazonaws.lambda.durable;
44

5+
import com.amazonaws.lambda.durable.exception.DurableOperationException;
56
import com.amazonaws.lambda.durable.execution.ExecutionManager;
67
import com.amazonaws.lambda.durable.execution.SuspendExecutionException;
78
import com.amazonaws.lambda.durable.model.DurableExecutionInput;
89
import com.amazonaws.lambda.durable.model.DurableExecutionOutput;
910
import com.amazonaws.lambda.durable.serde.SerDes;
11+
import com.amazonaws.lambda.durable.util.ExceptionHelper;
1012
import com.amazonaws.services.lambda.runtime.Context;
1113
import com.amazonaws.services.lambda.runtime.RequestHandler;
1214
import java.nio.charset.StandardCharsets;
1315
import java.util.concurrent.CompletableFuture;
1416
import java.util.function.BiFunction;
1517
import org.slf4j.Logger;
1618
import org.slf4j.LoggerFactory;
19+
import software.amazon.awssdk.services.lambda.model.ErrorObject;
1720
import software.amazon.awssdk.services.lambda.model.Operation;
1821
import software.amazon.awssdk.services.lambda.model.OperationAction;
1922
import software.amazon.awssdk.services.lambda.model.OperationType;
@@ -121,13 +124,13 @@ public static <I, O> DurableExecutionOutput execute(
121124
logger.debug("Execution completed");
122125
return DurableExecutionOutput.success(outputPayload);
123126
} catch (Exception e) {
124-
Throwable cause = e.getCause() != null ? e.getCause() : e;
127+
Throwable cause = ExceptionHelper.unwrapCompletableFuture(e);
125128
if (cause instanceof SuspendExecutionException) {
126129
logger.debug("Execution suspended");
127130
return DurableExecutionOutput.pending();
128131
}
129132
logger.debug("Execution failed: {}", cause.getMessage());
130-
return DurableExecutionOutput.failure(cause, serDes);
133+
return DurableExecutionOutput.failure(buildErrorObject(cause, serDes));
131134
} finally {
132135
// We shutdown the execution to make sure remaining checkpoint calls in the queue are drained
133136
executionManager.shutdown();
@@ -138,6 +141,15 @@ public static <I, O> DurableExecutionOutput execute(
138141
}
139142
}
140143

144+
private static ErrorObject buildErrorObject(Throwable e, SerDes serDes) {
145+
// exceptions thrown from operations, e.g. Step
146+
if (e instanceof DurableOperationException) {
147+
return ((DurableOperationException) e).getErrorObject();
148+
}
149+
// exceptions thrown from non-operation code
150+
return ExceptionHelper.buildErrorObject(e, serDes);
151+
}
152+
141153
private static <I> I extractUserInput(Operation executionOp, SerDes serDes, Class<I> inputType) {
142154

143155
if (executionOp.executionDetails() == null) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.amazonaws.lambda.durable.exception;
4+
5+
import software.amazon.awssdk.services.lambda.model.Operation;
6+
7+
public class CallbackException extends DurableOperationException {
8+
private final String callbackId;
9+
10+
public CallbackException(Operation operation, String message) {
11+
super(operation, operation.callbackDetails().error(), message);
12+
this.callbackId = operation.callbackDetails().callbackId();
13+
}
14+
15+
public String getCallbackId() {
16+
return callbackId;
17+
}
18+
}

sdk/src/main/java/com/amazonaws/lambda/durable/exception/CallbackFailedException.java

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,12 @@
33
package com.amazonaws.lambda.durable.exception;
44

55
import software.amazon.awssdk.services.lambda.model.ErrorObject;
6+
import software.amazon.awssdk.services.lambda.model.Operation;
67

78
/** Exception thrown when a callback fails due to an error from the external system. */
8-
public class CallbackFailedException extends DurableExecutionException {
9-
public CallbackFailedException(ErrorObject error) {
10-
super(
11-
buildMessage(error),
12-
null,
13-
error.stackTrace() != null && !error.stackTrace().isEmpty()
14-
? deserializeStackTrace(error.stackTrace())
15-
: new StackTraceElement[0]);
16-
}
17-
18-
public CallbackFailedException(String message) {
19-
super(message);
9+
public class CallbackFailedException extends CallbackException {
10+
public CallbackFailedException(Operation operation) {
11+
super(operation, buildMessage(operation.callbackDetails().error()));
2012
}
2113

2214
private static String buildMessage(ErrorObject error) {

sdk/src/main/java/com/amazonaws/lambda/durable/exception/CallbackTimeoutException.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package com.amazonaws.lambda.durable.exception;
44

5+
import software.amazon.awssdk.services.lambda.model.Operation;
6+
57
/** Exception thrown when a callback times out. */
6-
public class CallbackTimeoutException extends DurableExecutionException {
7-
public CallbackTimeoutException(String callbackId) {
8-
super("Callback timed out: " + callbackId);
8+
public class CallbackTimeoutException extends CallbackException {
9+
public CallbackTimeoutException(String callbackId, Operation operation) {
10+
super(operation, "Callback timed out: " + callbackId);
911
}
1012
}

sdk/src/main/java/com/amazonaws/lambda/durable/exception/DurableExecutionException.java

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package com.amazonaws.lambda.durable.exception;
44

5-
import java.util.Arrays;
6-
import java.util.List;
7-
85
public class DurableExecutionException extends RuntimeException {
96
public DurableExecutionException(String message, Throwable cause, StackTraceElement[] stackTrace) {
107
super(message, cause);
@@ -20,26 +17,4 @@ public DurableExecutionException(String message, Throwable cause) {
2017
public DurableExecutionException(String message) {
2118
this(message, null, null);
2219
}
23-
24-
// StackTraceElement.toString() is implementation-dependent, so we'll define our
25-
// own format.
26-
public static List<String> serializeStackTrace(StackTraceElement[] stackTrace) {
27-
return Arrays.stream(stackTrace)
28-
.map((element) -> String.format(
29-
"%s|%s|%s|%d",
30-
element.getClassName(),
31-
element.getMethodName(),
32-
element.getFileName(),
33-
element.getLineNumber()))
34-
.toList();
35-
}
36-
37-
public static StackTraceElement[] deserializeStackTrace(List<String> stackTrace) {
38-
return stackTrace.stream()
39-
.map((s) -> {
40-
String[] tokens = s.split("\\|");
41-
return new StackTraceElement(tokens[0], tokens[1], tokens[2], Integer.parseInt(tokens[3]));
42-
})
43-
.toArray(StackTraceElement[]::new);
44-
}
4520
}

0 commit comments

Comments
 (0)