Skip to content

Commit aa18443

Browse files
authored
fix: align log metadata with other SDKs (#465)
1 parent 23eae96 commit aa18443

20 files changed

Lines changed: 432 additions & 222 deletions

File tree

docs/advanced/error-handling.md

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
The SDK throws specific exceptions to help you handle different failure scenarios:
44

55
```
6+
Error
7+
└── DurableExecutionError - Internal SDK control-flow/error base type
8+
└── SuspendExecutionException - Internal signal used by the SDK to suspend execution
9+
(for example during `wait()`, `waitForCallback()`, and
10+
`waitForCondition()`). User code should not catch it.
11+
612
RuntimeException
7-
├── SuspendExecutionException - Internal control-flow exception thrown by the SDK to suspend execution
8-
│ (e.g., during wait(), waitForCallback(), waitForCondition()).
9-
│ The SDK catches this internally — you will never see it unless you have
10-
│ a broad catch(Exception) block around durable operations. If caught
11-
│ accidentally, you MUST re-throw it so the SDK can suspend correctly.
12-
1313
└── DurableExecutionException - General durable exception
1414
├── SerDesException - Serialization and deserialization exception.
1515
├── UnrecoverableDurableExecutionException - Execution cannot be recovered. The durable execution will be immediately terminated.
@@ -52,16 +52,36 @@ try {
5252

5353
### Handling SuspendExecutionException
5454

55-
If you have a broad `catch (Exception e)` block around durable operations, you must re-throw `SuspendExecutionException` to let the SDK suspend correctly:
55+
`SuspendExecutionException` is an internal SDK control-flow signal. It extends `Error`, not `Exception`, so a
56+
normal `catch (Exception e)` block will not intercept it.
57+
58+
The real risk is code that catches `Throwable`, or code that explicitly catches `SuspendExecutionException`. In those
59+
cases, you must re-throw it immediately so the SDK can suspend the execution correctly.
5660

5761
```java
5862
try {
5963
ctx.step("work", String.class, stepCtx -> doWork());
6064
ctx.wait("pause", Duration.ofDays(1));
6165
ctx.step("more-work", String.class, stepCtx -> doMoreWork());
6266
} catch (SuspendExecutionException e) {
63-
throw e; // Always re-throw — lets the SDK suspend the execution
64-
} catch (Exception e) {
67+
throw e; // Always re-throw internal suspension signals
68+
} catch (Throwable t) {
69+
log.error("Unexpected throwable", t);
70+
throw t;
71+
}
72+
```
73+
74+
Avoid broad `catch (Throwable)` blocks around durable operations unless you have a strong reason to use them. Prefer
75+
catching specific application exceptions instead:
76+
77+
```java
78+
try {
79+
ctx.step("work", String.class, stepCtx -> doWork());
80+
ctx.wait("pause", Duration.ofDays(1));
81+
ctx.step("more-work", String.class, stepCtx -> doMoreWork());
82+
} catch (SuspendExecutionException e) {
83+
throw e; // Always re-throw internal suspension signals
84+
} catch (MyBusinessException e) {
6585
log.error("Operation failed", e);
6686
}
67-
```
87+
```

examples/src/main/java/software/amazon/lambda/durable/examples/general/LoggingExample.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// SPDX-License-Identifier: Apache-2.0
33
package software.amazon.lambda.durable.examples.general;
44

5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
57
import software.amazon.lambda.durable.DurableContext;
68
import software.amazon.lambda.durable.DurableHandler;
79
import software.amazon.lambda.durable.examples.types.GreetingRequest;
@@ -13,15 +15,16 @@
1315
* in log entries via MDC. By default, logs are suppressed during replay to avoid duplicates.
1416
*/
1517
public class LoggingExample extends DurableHandler<GreetingRequest, String> {
18+
Logger logger = LoggerFactory.getLogger(LoggingExample.class);
1619

1720
@Override
1821
public String handleRequest(GreetingRequest input, DurableContext context) {
1922
// Log at execution level (outside any step)
20-
context.getLogger().info("Processing greeting for: {}", input.getName());
23+
context.getLogger(logger).info("Processing greeting for: {}", input.getName());
2124

2225
// Step 1: Create greeting - logs inside step include operation context
2326
var greeting = context.step("create-greeting", String.class, ctx -> {
24-
ctx.getLogger().info("Creating greeting message");
27+
ctx.getLogger(logger).info("Creating greeting message");
2528
return "Hello, " + input.getName();
2629
});
2730

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,14 @@ void testWaitAtLeastInProcessExample() {
253253
assertTrue(asyncOp.getStepResult(String.class).contains("Processed: TestUser"));
254254
}
255255

256+
@Test
257+
void testLoggingExample() {
258+
var runner = CloudDurableTestRunner.create(
259+
arn("logging-example"), GreetingRequest.class, String.class, lambdaClient);
260+
var result = runner.run(new GreetingRequest("TestUser"));
261+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
262+
}
263+
256264
@Test
257265
void testGenericTypesExample() {
258266
var runner = CloudDurableTestRunner.create(

sdk/src/main/java/software/amazon/lambda/durable/DurableContext.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
import software.amazon.lambda.durable.model.WaitForConditionResult;
2323

2424
public interface DurableContext extends BaseContext {
25+
static DurableContext getCurrentContext() {
26+
return (DurableContext) BaseContext.getCurrentContext();
27+
}
28+
2529
/**
2630
* Executes a durable step with the given name and blocks until it completes.
2731
*

sdk/src/main/java/software/amazon/lambda/durable/ParallelDurableFuture.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import java.util.function.Function;
66
import software.amazon.lambda.durable.config.ParallelBranchConfig;
77
import software.amazon.lambda.durable.model.ParallelResult;
8+
import software.amazon.lambda.durable.model.SafeCloseable;
89

910
/** User-facing context for managing parallel branch execution within a durable function. */
10-
public interface ParallelDurableFuture extends AutoCloseable, DurableFuture<ParallelResult> {
11+
public interface ParallelDurableFuture extends SafeCloseable, DurableFuture<ParallelResult> {
1112

1213
/**
1314
* Registers and immediately starts a branch (respects maxConcurrency).
@@ -68,7 +69,4 @@ default <T> DurableFuture<T> branch(
6869
*/
6970
<T> DurableFuture<T> branch(
7071
String name, TypeToken<T> resultType, Function<DurableContext, T> func, ParallelBranchConfig config);
71-
72-
/** Calls {@link #get()} if not already called. Guarantees that the context is closed. */
73-
void close();
7472
}

sdk/src/main/java/software/amazon/lambda/durable/StepContext.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@
77
public interface StepContext extends BaseContext {
88
/** Returns the current retry attempt number (0-based). */
99
int getAttempt();
10+
11+
static StepContext getCurrentContext() {
12+
return (StepContext) BaseContext.getCurrentContext();
13+
}
1014
}

sdk/src/main/java/software/amazon/lambda/durable/context/BaseContext.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,36 @@
33
package software.amazon.lambda.durable.context;
44

55
import com.amazonaws.services.lambda.runtime.Context;
6+
import org.slf4j.Logger;
67
import software.amazon.lambda.durable.DurableConfig;
78
import software.amazon.lambda.durable.logging.DurableLogger;
89

9-
public interface BaseContext extends AutoCloseable {
10+
public interface BaseContext {
11+
ThreadLocal<BaseContext> CONTEXT = new ThreadLocal<>();
12+
13+
/**
14+
* Gets the current context (DurableContext or StepContext) for this thread.
15+
*
16+
* @return the current context or null if not set
17+
*/
18+
static BaseContext getCurrentContext() {
19+
return CONTEXT.get();
20+
}
1021
/**
1122
* Gets a logger with additional information of the current execution context.
1223
*
1324
* @return a DurableLogger instance
1425
*/
1526
DurableLogger getLogger();
1627

28+
/**
29+
* Gets a logger with additional information of the current execution context.
30+
*
31+
* @param delegate the logger to wrap
32+
* @return a DurableLogger instance
33+
*/
34+
DurableLogger getLogger(Logger delegate);
35+
1736
/**
1837
* Returns the AWS Lambda runtime context.
1938
*
@@ -46,7 +65,4 @@ public interface BaseContext extends AutoCloseable {
4665

4766
/** Returns whether this context is currently in replay mode. */
4867
boolean isReplaying();
49-
50-
/** Closes this context. */
51-
void close();
5268
}

sdk/src/main/java/software/amazon/lambda/durable/context/BaseContextImpl.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
package software.amazon.lambda.durable.context;
44

55
import com.amazonaws.services.lambda.runtime.Context;
6+
import org.slf4j.Logger;
67
import software.amazon.lambda.durable.DurableConfig;
78
import software.amazon.lambda.durable.execution.ExecutionManager;
89
import software.amazon.lambda.durable.execution.ThreadType;
10+
import software.amazon.lambda.durable.logging.DurableLogger;
911

10-
public abstract class BaseContextImpl implements AutoCloseable, BaseContext {
12+
public abstract class BaseContextImpl implements BaseContext {
1113
private final ExecutionManager executionManager;
1214
private final DurableConfig durableConfig;
1315
private final Context lambdaContext;
@@ -109,4 +111,18 @@ public boolean isReplaying() {
109111
public void setExecutionMode() {
110112
this.isReplaying = false;
111113
}
114+
115+
/** Returns a durable logger for this context. */
116+
public DurableLogger getLogger() {
117+
return DurableLogger.INSTANCE;
118+
}
119+
120+
/** Returns a durable logger for this context. */
121+
public DurableLogger getLogger(Logger delegate) {
122+
return new DurableLogger(delegate);
123+
}
124+
125+
public static void setCurrentContext(BaseContext context) {
126+
CONTEXT.set(context);
127+
}
112128
}

sdk/src/main/java/software/amazon/lambda/durable/context/DurableContextImpl.java

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import java.util.function.BiConsumer;
1111
import java.util.function.BiFunction;
1212
import java.util.function.Function;
13-
import org.slf4j.LoggerFactory;
1413
import software.amazon.lambda.durable.DurableCallbackFuture;
1514
import software.amazon.lambda.durable.DurableConfig;
1615
import software.amazon.lambda.durable.DurableContext;
@@ -32,7 +31,6 @@
3231
import software.amazon.lambda.durable.execution.OperationIdGenerator;
3332
import software.amazon.lambda.durable.execution.SuspendExecutionException;
3433
import software.amazon.lambda.durable.execution.ThreadType;
35-
import software.amazon.lambda.durable.logging.DurableLogger;
3634
import software.amazon.lambda.durable.model.MapResult;
3735
import software.amazon.lambda.durable.model.OperationIdentifier;
3836
import software.amazon.lambda.durable.model.OperationSubType;
@@ -62,7 +60,6 @@ public class DurableContextImpl extends BaseContextImpl implements DurableContex
6260
private final OperationIdGenerator operationIdGenerator;
6361
private final DurableContextImpl parentContext;
6462
private final boolean isVirtual;
65-
private volatile DurableLogger logger;
6663

6764
/** Shared initialization — sets all fields. */
6865
private DurableContextImpl(
@@ -430,30 +427,6 @@ private static <T> T executeRetryLoop(
430427
}
431428

432429
// =============== accessors ================
433-
@Override
434-
public DurableLogger getLogger() {
435-
// lazy initialize logger
436-
if (logger == null) {
437-
synchronized (this) {
438-
if (logger == null) {
439-
logger = new DurableLogger(LoggerFactory.getLogger(DurableContext.class), this);
440-
}
441-
}
442-
}
443-
return logger;
444-
}
445-
446-
/**
447-
* Clears the logger's thread properties. Called during context destruction to prevent memory leaks and ensure clean
448-
* state for subsequent executions.
449-
*/
450-
@Override
451-
public void close() {
452-
if (logger != null) {
453-
logger.close();
454-
}
455-
}
456-
457430
/**
458431
* Get the next operationId. Returns a globally unique operation ID by hashing a sequential operation counter. For
459432
* root contexts, the counter value is hashed directly (e.g. "1", "2", "3"). For child contexts, the values are

sdk/src/main/java/software/amazon/lambda/durable/context/StepContextImpl.java

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33
package software.amazon.lambda.durable.context;
44

55
import com.amazonaws.services.lambda.runtime.Context;
6-
import org.slf4j.LoggerFactory;
76
import software.amazon.lambda.durable.DurableConfig;
87
import software.amazon.lambda.durable.StepContext;
98
import software.amazon.lambda.durable.execution.ExecutionManager;
109
import software.amazon.lambda.durable.execution.ThreadType;
11-
import software.amazon.lambda.durable.logging.DurableLogger;
1210

1311
/**
1412
* Context available inside a step operation's user function.
@@ -17,7 +15,6 @@
1715
* {@link BaseContext} for thread lifecycle management.
1816
*/
1917
public class StepContextImpl extends BaseContextImpl implements StepContext {
20-
private volatile DurableLogger logger;
2118
private final int attempt;
2219

2320
/**
@@ -46,25 +43,4 @@ protected StepContextImpl(
4643
public int getAttempt() {
4744
return attempt;
4845
}
49-
50-
@Override
51-
public DurableLogger getLogger() {
52-
// lazy initialize logger
53-
if (logger == null) {
54-
synchronized (this) {
55-
if (logger == null) {
56-
logger = new DurableLogger(LoggerFactory.getLogger(StepContext.class), this);
57-
}
58-
}
59-
}
60-
return logger;
61-
}
62-
63-
/** Closes the logger for this context. */
64-
@Override
65-
public void close() {
66-
if (logger != null) {
67-
logger.close();
68-
}
69-
}
7046
}

0 commit comments

Comments
 (0)