Skip to content

Commit afcb2d5

Browse files
committed
refactor: Address PR review feedback
- Move DurableApiErrorClassifier from exception to util package alongside ExceptionHelper - Use retryable flag from #350: classifyException now returns UnrecoverableDurableExecutionException on all paths with retryable=true for transient errors (429, 5xx, invalid checkpoint token) and retryable=false for permanent errors (KMS, 4xx) - Update all tests to assert isRetryable() flag
1 parent 5e9efda commit afcb2d5

3 files changed

Lines changed: 52 additions & 49 deletions

File tree

sdk/src/main/java/software/amazon/lambda/durable/execution/CheckpointManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
import software.amazon.awssdk.services.lambda.model.Operation;
2020
import software.amazon.awssdk.services.lambda.model.OperationUpdate;
2121
import software.amazon.lambda.durable.DurableConfig;
22-
import software.amazon.lambda.durable.exception.DurableApiErrorClassifier;
2322
import software.amazon.lambda.durable.retry.PollingStrategies;
2423
import software.amazon.lambda.durable.retry.PollingStrategy;
24+
import software.amazon.lambda.durable.util.DurableApiErrorClassifier;
2525

2626
/**
2727
* Package-private checkpoint manager for batching and queueing checkpoint API calls.

sdk/src/main/java/software/amazon/lambda/durable/exception/DurableApiErrorClassifier.java renamed to sdk/src/main/java/software/amazon/lambda/durable/util/DurableApiErrorClassifier.java

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
package software.amazon.lambda.durable.exception;
3+
package software.amazon.lambda.durable.util;
44

55
import java.util.Set;
66
import software.amazon.awssdk.awscore.exception.AwsServiceException;
77
import software.amazon.awssdk.services.lambda.model.ErrorObject;
8+
import software.amazon.lambda.durable.exception.UnrecoverableDurableExecutionException;
89

910
/**
10-
* Classifies AWS service exceptions from Durable Execution API calls as execution-level (non-retryable) or
11-
* invocation-level (retryable).
11+
* Classifies AWS service exceptions from Durable Execution API calls as non-retryable or retryable.
1212
*
13-
* <p>Execution-level errors throw {@link UnrecoverableDurableExecutionException} to terminate the execution
14-
* immediately. These represent permanent customer-side issues (e.g., KMS key misconfiguration) that will not
15-
* self-resolve on retry.
16-
*
17-
* <p>Invocation-level errors are allowed to propagate, crashing the current Lambda invocation so the backend can retry
18-
* with a fresh invocation.
13+
* <p>Returns {@link UnrecoverableDurableExecutionException} with {@code retryable=false} for non-retryable customer
14+
* errors (e.g., KMS key misconfiguration), or {@code retryable=true} for retryable errors (throttling, server errors,
15+
* stale checkpoint tokens).
1916
*
2017
* <p>To add a new non-retryable error, add its error code to {@link #NON_RETRYABLE_ERROR_CODES}.
2118
*/
@@ -58,28 +55,27 @@ private DurableApiErrorClassifier() {}
5855
/**
5956
* Classifies the given exception and returns the appropriate exception to throw.
6057
*
61-
* <p>Returns {@link UnrecoverableDurableExecutionException} for non-retryable customer errors, or the original
62-
* exception for retryable errors.
58+
* <p>Returns {@link UnrecoverableDurableExecutionException} with {@code retryable=false} for non-retryable customer
59+
* errors, or {@code retryable=true} for retryable errors.
6360
*
6461
* <p>Classification rules:
6562
*
6663
* <ul>
67-
* <li>Error code in {@link #NON_RETRYABLE_ERROR_CODES} → execution error (non-retryable)
68-
* <li>4xx + "Invalid Checkpoint Token" → invocation error (retryable, stale token resolves on retry)
69-
* <li>4xx (non-429) → execution error (non-retryable customer error)
70-
* <li>429, 5xx, unknown → invocation error (retryable)
64+
* <li>Error code in {@link #NON_RETRYABLE_ERROR_CODES} → non-retryable ({@code retryable=false})
65+
* <li>4xx + "Invalid Checkpoint Token" → retryable ({@code retryable=true}, stale token resolves on retry)
66+
* <li>4xx (non-429) → non-retryable ({@code retryable=false}, customer error)
67+
* <li>429, 5xx, unknown → retryable ({@code retryable=true})
7168
* </ul>
7269
*
7370
* @param e the AWS service exception from a Durable Execution API call
74-
* @return an {@link UnrecoverableDurableExecutionException} if non-retryable, or the original exception if
75-
* retryable
71+
* @return an {@link UnrecoverableDurableExecutionException} for all cases, with the retryable flag set accordingly
7672
*/
77-
public static RuntimeException classifyException(AwsServiceException e) {
73+
public static UnrecoverableDurableExecutionException classifyException(AwsServiceException e) {
7874
var errorCode = e.awsErrorDetails().errorCode();
7975

8076
// Non-retryable customer errors: execution is terminally broken (e.g., KMS key misconfiguration)
8177
if (NON_RETRYABLE_ERROR_CODES.contains(errorCode)) {
82-
return buildUnrecoverableDurableExecutionException(e);
78+
return buildUnrecoverableDurableExecutionException(e, false);
8379
}
8480

8581
var statusCode = e.awsErrorDetails().sdkHttpResponse().statusCode();
@@ -92,20 +88,22 @@ public static RuntimeException classifyException(AwsServiceException e) {
9288
if (INVALID_CHECKPOINT_TOKEN_ERROR_CODE.equals(errorCode)
9389
&& message != null
9490
&& message.startsWith(INVALID_CHECKPOINT_TOKEN_MESSAGE_PREFIX)) {
95-
return e;
91+
return buildUnrecoverableDurableExecutionException(e, true);
9692
}
97-
return buildUnrecoverableDurableExecutionException(e);
93+
return buildUnrecoverableDurableExecutionException(e, false);
9894
}
9995

10096
// 429 (throttling), 5xx (service errors), unknown — transient, retryable
101-
return e;
97+
return buildUnrecoverableDurableExecutionException(e, true);
10298
}
10399

104100
private static UnrecoverableDurableExecutionException buildUnrecoverableDurableExecutionException(
105-
AwsServiceException e) {
106-
return new UnrecoverableDurableExecutionException(ErrorObject.builder()
107-
.errorType(e.awsErrorDetails().errorCode())
108-
.errorMessage(e.getMessage())
109-
.build());
101+
AwsServiceException e, boolean retryable) {
102+
return new UnrecoverableDurableExecutionException(
103+
ErrorObject.builder()
104+
.errorType(e.awsErrorDetails().errorCode())
105+
.errorMessage(e.getMessage())
106+
.build(),
107+
retryable);
110108
}
111109
}

sdk/src/test/java/software/amazon/lambda/durable/exception/DurableApiErrorClassifierTest.java renamed to sdk/src/test/java/software/amazon/lambda/durable/util/DurableApiErrorClassifierTest.java

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3-
package software.amazon.lambda.durable.exception;
3+
package software.amazon.lambda.durable.util;
44

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

@@ -12,18 +12,19 @@
1212
import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
1313
import software.amazon.awssdk.awscore.exception.AwsServiceException;
1414
import software.amazon.awssdk.http.SdkHttpResponse;
15+
import software.amazon.lambda.durable.exception.UnrecoverableDurableExecutionException;
1516

1617
class DurableApiErrorClassifierTest {
1718

1819
@ParameterizedTest
1920
@MethodSource("kmsExceptions")
20-
void classifyException_kmsError_returnsUnrecoverableDurableExecutionException(String errorCode, String message) {
21+
void classifyException_kmsError_returnsNonRetryable(String errorCode, String message) {
2122
var error = awsError(502, errorCode, message);
2223
var result = DurableApiErrorClassifier.classifyException(error);
2324
assertInstanceOf(UnrecoverableDurableExecutionException.class, result);
24-
var unrecoverable = (UnrecoverableDurableExecutionException) result;
25-
assertEquals(errorCode, unrecoverable.getErrorObject().errorType());
26-
assertTrue(unrecoverable.getErrorObject().errorMessage().contains(message));
25+
assertFalse(result.isRetryable());
26+
assertEquals(errorCode, result.getErrorObject().errorType());
27+
assertTrue(result.getErrorObject().errorMessage().contains(message));
2728
}
2829

2930
static Stream<Arguments> kmsExceptions() {
@@ -48,58 +49,64 @@ static Stream<Arguments> kmsExceptions() {
4849
}
4950

5051
@Test
51-
void classifyException_clientError4xx_returnsUnrecoverableDurableExecutionException() {
52+
void classifyException_clientError4xx_returnsNonRetryable() {
5253
var error = awsError(403, "AccessDeniedException", "User is not authorized");
5354

5455
var result = DurableApiErrorClassifier.classifyException(error);
5556
assertInstanceOf(UnrecoverableDurableExecutionException.class, result);
57+
assertFalse(result.isRetryable());
5658
}
5759

5860
@Test
59-
void classifyException_invalidCheckpointToken_returnsOriginalException() {
61+
void classifyException_invalidCheckpointToken_returnsRetryable() {
6062
var error = awsError(400, "InvalidParameterValueException", "Invalid Checkpoint Token: token expired");
6163

6264
var result = DurableApiErrorClassifier.classifyException(error);
63-
assertSame(error, result);
65+
assertInstanceOf(UnrecoverableDurableExecutionException.class, result);
66+
assertTrue(result.isRetryable());
6467
}
6568

6669
@Test
67-
void classifyException_invalidParameterValueNonToken_returnsUnrecoverableDurableExecutionException() {
70+
void classifyException_invalidParameterValueNonToken_returnsNonRetryable() {
6871
var error = awsError(
6972
400,
7073
"InvalidParameterValueException",
7174
"The runtime parameter of python3.8 is no longer" + " supported for creating or updating functions.");
7275

7376
var result = DurableApiErrorClassifier.classifyException(error);
7477
assertInstanceOf(UnrecoverableDurableExecutionException.class, result);
78+
assertFalse(result.isRetryable());
7579
}
7680

7781
@Test
78-
void classifyException_throttled429_returnsOriginalException() {
82+
void classifyException_throttled429_returnsRetryable() {
7983
var error = awsError(429, "TooManyRequestsException", "Rate exceeded");
8084

8185
var result = DurableApiErrorClassifier.classifyException(error);
82-
assertSame(error, result);
86+
assertInstanceOf(UnrecoverableDurableExecutionException.class, result);
87+
assertTrue(result.isRetryable());
8388
}
8489

8590
@Test
86-
void classifyException_serverError500_returnsOriginalException() {
91+
void classifyException_serverError500_returnsRetryable() {
8792
var error = awsError(500, "ServiceException", "Service encountered an error");
8893

8994
var result = DurableApiErrorClassifier.classifyException(error);
90-
assertSame(error, result);
95+
assertInstanceOf(UnrecoverableDurableExecutionException.class, result);
96+
assertTrue(result.isRetryable());
9197
}
9298

9399
@Test
94-
void classifyException_nonMatchingErrorCode502_returnsOriginalException() {
100+
void classifyException_nonMatchingErrorCode502_returnsRetryable() {
95101
var error = awsError(502, "ServiceException", "Service unavailable");
96102

97103
var result = DurableApiErrorClassifier.classifyException(error);
98-
assertSame(error, result);
104+
assertInstanceOf(UnrecoverableDurableExecutionException.class, result);
105+
assertTrue(result.isRetryable());
99106
}
100107

101108
@Test
102-
void classifyException_errorDetailsPreserved() {
109+
void classifyException_nonRetryableError_preservesErrorDetails() {
103110
var error = awsError(
104111
502,
105112
"KMSAccessDeniedException",
@@ -109,11 +116,9 @@ void classifyException_errorDetailsPreserved() {
109116

110117
var result = DurableApiErrorClassifier.classifyException(error);
111118
assertInstanceOf(UnrecoverableDurableExecutionException.class, result);
112-
113-
var unrecoverable = (UnrecoverableDurableExecutionException) result;
114-
assertEquals("KMSAccessDeniedException", unrecoverable.getErrorObject().errorType());
115-
assertTrue(unrecoverable
116-
.getErrorObject()
119+
assertFalse(result.isRetryable());
120+
assertEquals("KMSAccessDeniedException", result.getErrorObject().errorType());
121+
assertTrue(result.getErrorObject()
117122
.errorMessage()
118123
.contains("Lambda was unable to decrypt the environment variables"));
119124
}

0 commit comments

Comments
 (0)