Skip to content

Commit a3e19fc

Browse files
committed
add tests for virtual context
1 parent db51df4 commit a3e19fc

6 files changed

Lines changed: 263 additions & 0 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package software.amazon.lambda.durable.examples.child;
4+
5+
import java.time.Duration;
6+
import software.amazon.lambda.durable.DurableContext;
7+
import software.amazon.lambda.durable.DurableFuture;
8+
import software.amazon.lambda.durable.DurableHandler;
9+
import software.amazon.lambda.durable.config.RunInChildContextConfig;
10+
import software.amazon.lambda.durable.examples.types.GreetingRequest;
11+
12+
/**
13+
* Example demonstrating virtual child context workflows with the Durable Execution SDK.
14+
*
15+
* <p>This handler runs three concurrent child contexts using {@code runInChildContextAsync}:
16+
*
17+
* <ol>
18+
* <li><b>Order validation</b> — performs a step then suspends via {@code wait()} before completing
19+
* <li><b>Inventory check</b> — performs a step then suspends via {@code wait()} before completing
20+
* <li><b>Shipping estimate</b> — nests another child context inside it to demonstrate hierarchical contexts
21+
* </ol>
22+
*
23+
* <p>All three child contexts run concurrently. Results are collected with {@link DurableFuture#allOf} and combined
24+
* into a summary string.
25+
*/
26+
public class VirtualChildContextExample extends DurableHandler<GreetingRequest, String> {
27+
28+
@Override
29+
public String handleRequest(GreetingRequest input, DurableContext context) {
30+
var name = input.getName();
31+
context.getLogger().info("Starting child context workflow for {}", name);
32+
33+
// Child context 1: Order validation — step + wait + step
34+
var orderFuture = context.runInChildContextAsync(
35+
"order-validation",
36+
String.class,
37+
child -> {
38+
var prepared = child.step("prepare-order", String.class, stepCtx -> "Order for " + name);
39+
child.getLogger().info("Order prepared, waiting for validation");
40+
41+
child.wait("validation-delay", Duration.ofSeconds(5));
42+
43+
return child.step("validate-order", String.class, stepCtx -> prepared + " [validated]");
44+
},
45+
RunInChildContextConfig.builder().isVirtual(true).build());
46+
47+
// Child context 2: Inventory check — step + wait + step
48+
var inventoryFuture = context.runInChildContextAsync(
49+
"inventory-check",
50+
String.class,
51+
child -> {
52+
var stock = child.step("check-stock", String.class, stepCtx -> "Stock available for " + name);
53+
child.getLogger().info("Stock checked, waiting for confirmation");
54+
55+
child.wait("confirmation-delay", Duration.ofSeconds(3));
56+
57+
return child.step("confirm-inventory", String.class, stepCtx -> stock + " [confirmed]");
58+
},
59+
RunInChildContextConfig.builder().isVirtual(true).build());
60+
61+
// Child context 3: Shipping estimate — nests a child context inside it
62+
var shippingFuture = context.runInChildContextAsync(
63+
"shipping-estimate",
64+
String.class,
65+
child -> {
66+
var baseRate = child.step("calculate-base-rate", String.class, stepCtx -> "Base rate for " + name);
67+
68+
// Nested child context: calculate regional adjustment
69+
var adjustment = child.runInChildContext(
70+
"regional-adjustment",
71+
String.class,
72+
nested -> nested.step(
73+
"lookup-region", String.class, stepCtx -> baseRate + " + regional adjustment"));
74+
75+
return child.step("finalize-shipping", String.class, stepCtx -> adjustment + " [shipping ready]");
76+
},
77+
RunInChildContextConfig.builder().isVirtual(true).build());
78+
79+
// Collect all results using allOf
80+
context.getLogger().info("Waiting for all child contexts to complete");
81+
var results = DurableFuture.allOf(orderFuture, inventoryFuture, shippingFuture);
82+
83+
// Combine into summary
84+
var summary = String.join(" | ", results);
85+
context.getLogger().info("All child contexts complete: {}", summary);
86+
87+
return summary;
88+
}
89+
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,23 @@ void testChildContextExample() {
530530
assertNotNull(runner.getOperation("shipping-estimate"));
531531
}
532532

533+
@Test
534+
void testVirtualChildContextExample() {
535+
var runner = CloudDurableTestRunner.create(
536+
arn("virtual-child-context-example"), GreetingRequest.class, String.class, lambdaClient);
537+
var result = runner.run(new GreetingRequest("Alice"));
538+
539+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
540+
assertEquals(
541+
"Order for Alice [validated] | Stock available for Alice [confirmed] | Base rate for Alice + regional adjustment [shipping ready]",
542+
result.getResult());
543+
544+
// Verify step operations in child context were tracked
545+
assertNotNull(runner.getOperation("validate-order"));
546+
assertNotNull(runner.getOperation("confirm-inventory"));
547+
assertNotNull(runner.getOperation("finalize-shipping"));
548+
}
549+
533550
@ParameterizedTest
534551
@CsvSource({"100, 1000, 20", "500, 2000, 30", "1000, 3000, 50"})
535552
void testManyAsyncStepsExample(int steps, long maxExecutionTime, long maxReplayTime) {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package software.amazon.lambda.durable.examples.child;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
7+
import org.junit.jupiter.api.Test;
8+
import software.amazon.lambda.durable.examples.types.GreetingRequest;
9+
import software.amazon.lambda.durable.model.ExecutionStatus;
10+
import software.amazon.lambda.durable.testing.LocalDurableTestRunner;
11+
12+
class VirtualChildContextExampleTest {
13+
14+
@Test
15+
void testVirtualChildContextExampleRunsToCompletion() {
16+
var handler = new VirtualChildContextExample();
17+
var runner = LocalDurableTestRunner.create(GreetingRequest.class, handler);
18+
19+
var input = new GreetingRequest("Alice");
20+
var result = runner.runUntilComplete(input);
21+
22+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
23+
assertEquals(
24+
"Order for Alice [validated] | Stock available for Alice [confirmed] | Base rate for Alice + regional adjustment [shipping ready]",
25+
result.getResult(String.class));
26+
}
27+
28+
@Test
29+
void testVirtualChildContextExampleSuspendsOnFirstRun() {
30+
var handler = new VirtualChildContextExample();
31+
var runner = LocalDurableTestRunner.create(GreetingRequest.class, handler);
32+
33+
var input = new GreetingRequest("Bob");
34+
35+
// First run should suspend due to wait operations inside child contexts
36+
var result = runner.run(input);
37+
assertEquals(ExecutionStatus.PENDING, result.getStatus());
38+
}
39+
40+
@Test
41+
void testVirtualChildContextExampleReplay() {
42+
var handler = new VirtualChildContextExample();
43+
var runner = LocalDurableTestRunner.create(GreetingRequest.class, handler);
44+
45+
var input = new GreetingRequest("Alice");
46+
47+
// First full execution
48+
var result1 = runner.runUntilComplete(input);
49+
assertEquals(ExecutionStatus.SUCCEEDED, result1.getStatus());
50+
51+
// Replay — should return cached results
52+
var result2 = runner.run(input);
53+
assertEquals(ExecutionStatus.SUCCEEDED, result2.getStatus());
54+
assertEquals(result1.getResult(String.class), result2.getResult(String.class));
55+
}
56+
}

examples/template.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,23 @@ Resources:
296296
- lambda:GetDurableExecutionState
297297
Resource: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:child-context-example-${JavaVersion}-runtime"
298298

299+
VirtualChildContextExampleFunction:
300+
Type: AWS::Serverless::Function
301+
Properties:
302+
FunctionName: !Join
303+
- '-'
304+
- - 'virtual-child-context-example'
305+
- !Ref JavaVersion
306+
- runtime
307+
Handler: "software.amazon.lambda.durable.examples.child.VirtualChildContextExample"
308+
Policies:
309+
- Statement:
310+
- Effect: Allow
311+
Action:
312+
- lambda:CheckpointDurableExecutions
313+
- lambda:GetDurableExecutionState
314+
Resource: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:virtual-child-context-example-${JavaVersion}-runtime"
315+
299316
WaitAsyncExampleFunction:
300317
Type: AWS::Serverless::Function
301318
Properties:

sdk-integration-tests/src/test/java/software/amazon/lambda/durable/ChildContextIntegrationTest.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.concurrent.atomic.AtomicInteger;
99
import org.junit.jupiter.api.Test;
1010
import software.amazon.awssdk.services.lambda.model.OperationType;
11+
import software.amazon.lambda.durable.config.RunInChildContextConfig;
1112
import software.amazon.lambda.durable.model.ExecutionStatus;
1213
import software.amazon.lambda.durable.testing.LocalDurableTestRunner;
1314

@@ -42,6 +43,34 @@ void childContextResultSurvivesReplay() {
4243
assertEquals(1, childExecutionCount.get(), "Child function should not re-execute on replay");
4344
}
4445

46+
@Test
47+
void virtualChildContextResultSurvivesReplay() {
48+
var childExecutionCount = new AtomicInteger(0);
49+
50+
var runner = LocalDurableTestRunner.create(
51+
String.class,
52+
(input, ctx) -> ctx.runInChildContext(
53+
"compute",
54+
TypeToken.get(String.class),
55+
child -> {
56+
childExecutionCount.incrementAndGet();
57+
return child.step("work", String.class, stepCtx -> "result-" + input);
58+
},
59+
RunInChildContextConfig.builder().isVirtual(true).build()));
60+
61+
// First run - executes child context
62+
var result = runner.runUntilComplete("test");
63+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
64+
assertEquals("result-test", result.getResult(String.class));
65+
assertEquals(1, childExecutionCount.get());
66+
67+
// Second run - replays, should return cached result without re-executing
68+
result = runner.run("test");
69+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
70+
assertEquals("result-test", result.getResult(String.class));
71+
assertEquals(2, childExecutionCount.get(), "Child function should re-execute on replay");
72+
}
73+
4574
/**
4675
* A child context that fails with a reconstructable exception SHALL preserve the exception type, message, and error
4776
* details through the checkpoint-and-replay cycle.
@@ -72,6 +101,36 @@ void childContextExceptionPreservedOnReplay() {
72101
assertEquals(1, childExecutionCount.get(), "Child function should not re-execute on failed replay");
73102
}
74103

104+
@Test
105+
void virtualChildContextExceptionPreservedOnReplay() {
106+
var childExecutionCount = new AtomicInteger(0);
107+
108+
var runner = LocalDurableTestRunner.create(
109+
String.class,
110+
(input, ctx) -> ctx.runInChildContext(
111+
"failing",
112+
String.class,
113+
child -> {
114+
childExecutionCount.incrementAndGet();
115+
throw new IllegalArgumentException("bad input: " + input);
116+
},
117+
RunInChildContextConfig.builder().isVirtual(true).build()));
118+
119+
// First run - child context fails
120+
var result = runner.run("test");
121+
assertEquals(ExecutionStatus.FAILED, result.getStatus());
122+
assertEquals(1, childExecutionCount.get());
123+
124+
// Second run - replays, should throw same exception without re-executing
125+
result = runner.run("test");
126+
assertEquals(ExecutionStatus.FAILED, result.getStatus());
127+
assertTrue(result.getError().isPresent());
128+
var error = result.getError().get();
129+
assertEquals("java.lang.IllegalArgumentException", error.errorType());
130+
assertEquals("bad input: test", error.errorMessage());
131+
assertEquals(2, childExecutionCount.get(), "Child function should re-execute on failed replay");
132+
}
133+
75134
/** Operations checkpointed from within a child context SHALL have the child context's ID as their parentId. */
76135
@Test
77136
void operationsInChildContextHaveCorrectParentId() {

sdk/src/test/java/software/amazon/lambda/durable/operation/ChildContextOperationTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ private ChildContextOperation<String> createOperation(Function<DurableContext, S
6666
durableContext);
6767
}
6868

69+
private ChildContextOperation<String> createVirtualOperation(Function<DurableContext, String> func) {
70+
return new ChildContextOperation<>(
71+
OPERATION_IDENTIFIER,
72+
func,
73+
TypeToken.get(String.class),
74+
RunInChildContextConfig.builder().serDes(SERDES).isVirtual(true).build(),
75+
durableContext);
76+
}
77+
6978
private ChildContextOperation<String> createOperationWithParent(
7079
Function<DurableContext, String> func, ConcurrencyOperation<?> parent) {
7180
return new ChildContextOperation<>(
@@ -107,6 +116,22 @@ void replaySucceededReturnsCachedResult() {
107116
assertFalse(functionCalled.get(), "Function should not be called during SUCCEEDED replay");
108117
}
109118

119+
/** Virtual contexts are always executed, even during SUCCEEDED replay. */
120+
@Test
121+
void executeVirtualContext() {
122+
var functionCalled = new AtomicBoolean(false);
123+
var operation = createVirtualOperation(ctx -> {
124+
functionCalled.set(true);
125+
return "should-execute";
126+
});
127+
128+
operation.execute();
129+
var result = operation.get();
130+
131+
assertEquals("should-execute", result);
132+
assertTrue(functionCalled.get(), "Function should be called during SUCCEEDED replay");
133+
}
134+
110135
// ===== FAILED replay =====
111136

112137
/** FAILED replay throws the original exception without re-executing. */

0 commit comments

Comments
 (0)