Skip to content

Commit 7530e2b

Browse files
feat(plugin): Wire plugin hooks into SDK execution lifecycle (#422)
1 parent 56534df commit 7530e2b

21 files changed

Lines changed: 946 additions & 295 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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.general;
4+
5+
import software.amazon.lambda.durable.DurableConfig;
6+
import software.amazon.lambda.durable.DurableContext;
7+
import software.amazon.lambda.durable.DurableHandler;
8+
import software.amazon.lambda.durable.examples.types.GreetingRequest;
9+
import software.amazon.lambda.durable.plugin.*;
10+
11+
/**
12+
* Example demonstrating plugin instrumentation with the Durable Execution SDK.
13+
*
14+
* <p>This handler registers a simple logging plugin that prints lifecycle events to stdout (which appears in CloudWatch
15+
* Logs when deployed). Deploy this and check CloudWatch to verify all hooks fire at the right times.
16+
*
17+
* <p>Expected output for a successful run:
18+
*
19+
* <pre>
20+
* [PLUGIN] onInvocationStart: requestId=..., executionArn=..., firstInvocation=true
21+
* [PLUGIN] onOperationStart: name=create-greeting, type=STEP
22+
* [PLUGIN] onUserFunctionStart: name=create-greeting, attempt=1
23+
* [PLUGIN] onUserFunctionEnd: name=create-greeting, succeeded=true
24+
* [PLUGIN] onOperationEnd: name=create-greeting
25+
* [PLUGIN] onOperationStart: name=transform, type=STEP
26+
* [PLUGIN] onUserFunctionStart: name=transform, attempt=1
27+
* [PLUGIN] onUserFunctionEnd: name=transform, succeeded=true
28+
* [PLUGIN] onOperationEnd: name=transform
29+
* [PLUGIN] onInvocationEnd: status=SUCCEEDED
30+
* </pre>
31+
*/
32+
public class PluginExample extends DurableHandler<GreetingRequest, String> {
33+
34+
@Override
35+
protected DurableConfig createConfiguration() {
36+
return DurableConfig.builder().withPlugins(new LoggingPlugin()).build();
37+
}
38+
39+
@Override
40+
public String handleRequest(GreetingRequest input, DurableContext context) {
41+
context.getLogger().info("Starting plugin example for {}", input.getName());
42+
43+
var greeting = context.step("create-greeting", String.class, stepCtx -> "Hello, " + input.getName());
44+
45+
var result = context.step("transform", String.class, stepCtx -> greeting.toUpperCase() + "!");
46+
47+
context.getLogger().info("Plugin example complete: {}", result);
48+
return result;
49+
}
50+
51+
/** A simple plugin that logs all lifecycle events to stdout. In Lambda, stdout goes to CloudWatch Logs. */
52+
static class LoggingPlugin implements DurableExecutionPlugin {
53+
54+
@Override
55+
public void onInvocationStart(InvocationInfo info) {
56+
System.out.printf(
57+
"[PLUGIN] onInvocationStart: requestId=%s, executionArn=%s, firstInvocation=%s%n",
58+
info.requestId(), info.executionArn(), info.isFirstInvocation());
59+
}
60+
61+
@Override
62+
public void onInvocationEnd(InvocationEndInfo info) {
63+
System.out.printf(
64+
"[PLUGIN] onInvocationEnd: status=%s, error=%s%n",
65+
info.invocationStatus(),
66+
info.executionError() != null ? info.executionError().getMessage() : null);
67+
}
68+
69+
@Override
70+
public void onOperationStart(OperationInfo info) {
71+
System.out.printf(
72+
"[PLUGIN] onOperationStart: name=%s, type=%s, id=%s%n", info.name(), info.type(), info.id());
73+
}
74+
75+
@Override
76+
public void onOperationEnd(OperationEndInfo info) {
77+
System.out.printf(
78+
"[PLUGIN] onOperationEnd: name=%s, type=%s, error=%s%n",
79+
info.name(),
80+
info.type(),
81+
info.error() != null ? info.error().getMessage() : null);
82+
}
83+
84+
@Override
85+
public void onUserFunctionStart(UserFunctionStartInfo info) {
86+
System.out.printf(
87+
"[PLUGIN] onUserFunctionStart: name=%s, type=%s, attempt=%s, isReplayingChildren=%s%n",
88+
info.name(), info.type(), info.attempt(), info.isReplayingChildren());
89+
}
90+
91+
@Override
92+
public void onUserFunctionEnd(UserFunctionEndInfo info) {
93+
System.out.printf(
94+
"[PLUGIN] onUserFunctionEnd: name=%s, succeeded=%s, error=%s%n",
95+
info.name(),
96+
info.succeeded(),
97+
info.error() != null ? info.error().getMessage() : null);
98+
}
99+
}
100+
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,4 +833,18 @@ void testConcurrentWaitForConditionExample() {
833833
+ waitForConditionResult.getDuration().toSeconds() + "s, expected < 30s");
834834
}
835835
}
836+
837+
@Test
838+
void testPluginExample() {
839+
var runner =
840+
CloudDurableTestRunner.create(arn("plugin-example"), GreetingRequest.class, String.class, lambdaClient);
841+
var result = runner.run(new GreetingRequest("World"));
842+
843+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
844+
assertEquals("HELLO, WORLD!", result.getResult());
845+
846+
// Verify operations were tracked
847+
assertNotNull(runner.getOperation("create-greeting"));
848+
assertNotNull(runner.getOperation("transform"));
849+
}
836850
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.general;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertNotNull;
7+
8+
import org.junit.jupiter.api.Test;
9+
import software.amazon.lambda.durable.examples.types.GreetingRequest;
10+
import software.amazon.lambda.durable.model.ExecutionStatus;
11+
import software.amazon.lambda.durable.testing.LocalDurableTestRunner;
12+
13+
class PluginExampleTest {
14+
15+
@Test
16+
void testPluginExample_executesSuccessfully() {
17+
var handler = new PluginExample();
18+
var runner = LocalDurableTestRunner.create(GreetingRequest.class, handler);
19+
20+
var result = runner.run(new GreetingRequest("World"));
21+
22+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
23+
assertEquals("HELLO, WORLD!", result.getResult(String.class));
24+
25+
// Verify operations were tracked
26+
assertNotNull(result.getOperation("create-greeting"));
27+
assertNotNull(result.getOperation("transform"));
28+
}
29+
30+
@Test
31+
void testPluginExample_pluginHooksFire() {
32+
// This test verifies that the plugin hooks fire without error.
33+
// Check stdout/CloudWatch for [PLUGIN] log lines when deployed.
34+
var handler = new PluginExample();
35+
var runner = LocalDurableTestRunner.create(GreetingRequest.class, handler);
36+
37+
var result = runner.runUntilComplete(new GreetingRequest("Test"));
38+
39+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
40+
assertEquals("HELLO, TEST!", result.getResult(String.class));
41+
}
42+
}

examples/template.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,17 @@ Resources:
333333
Handler: "software.amazon.lambda.durable.examples.general.CustomPollingExample"
334334
Role: !Ref RoleArn
335335

336+
PluginExampleFunction:
337+
Type: AWS::Serverless::Function
338+
Properties:
339+
FunctionName: !Join
340+
- '-'
341+
- - 'plugin-example'
342+
- !Ref JavaVersion
343+
- runtime
344+
Handler: "software.amazon.lambda.durable.examples.general.PluginExample"
345+
Role: !Ref RoleArn
346+
336347
RetryInvokeExampleFunction:
337348
Type: AWS::Serverless::Function
338349
Properties:
@@ -531,6 +542,10 @@ Outputs:
531542
Description: Custom Polling Example Function ARN
532543
Value: !GetAtt CustomPollingExampleFunction.Arn
533544

545+
PluginExampleFunction:
546+
Description: Plugin Example Function ARN
547+
Value: !GetAtt PluginExampleFunction.Arn
548+
534549
RetryInvokeExampleFunction:
535550
Description: Retry Invoke Example Function ARN
536551
Value: !GetAtt RetryInvokeExampleFunction.Arn

0 commit comments

Comments
 (0)