-
Notifications
You must be signed in to change notification settings - Fork 11
test(conformance): plugin hook-info field-shape handlers (10-19..10-23) #602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| package plugin; | ||
|
|
||
| import java.time.Duration; | ||
| import java.time.Instant; | ||
| import java.util.Locale; | ||
| import software.amazon.lambda.durable.DurableConfig; | ||
| import software.amazon.lambda.durable.DurableContext; | ||
| import software.amazon.lambda.durable.DurableHandler; | ||
| import software.amazon.lambda.durable.config.StepConfig; | ||
| import software.amazon.lambda.durable.plugin.DurableExecutionPlugin; | ||
| import software.amazon.lambda.durable.plugin.InvocationInfo; | ||
| import software.amazon.lambda.durable.plugin.UserFunctionEndInfo; | ||
| import software.amazon.lambda.durable.plugin.UserFunctionStartInfo; | ||
| import software.amazon.lambda.durable.retry.JitterStrategy; | ||
| import software.amazon.lambda.durable.retry.RetryStrategies; | ||
|
|
||
| /** | ||
| * 10-21: Attempt hook info field shape (CANONICAL DUMP). | ||
| * | ||
| * <p>A single step named {@code "flaky"} that throws on attempt 1 and succeeds on attempt 2 using the SDK's real | ||
| * exponential-backoff retry strategy (max attempts 3, ~1s delay), returning {@code "ok"}. The instrumentation plugin | ||
| * (filtering to step-type attempts) emits ONE single-line JSON record per per-attempt (user-function) hook event: a | ||
| * canonical dump of that hook's OWN info parameter, every exposed component mapped to its canonical camelCase name, | ||
| * null / unexposed fields OMITTED. | ||
| * | ||
| * <p>Java's {@link UserFunctionStartInfo} exposes id/name/type/subType/parentId/startTimestamp/isReplay/ | ||
| * isReplayingChildren/attempt (no endTimestamp/outcome/error at start). Java's {@link UserFunctionEndInfo} adds | ||
| * endTimestamp, the {@code succeeded} boolean (presented as the shared {@code outcome} SUCCEEDED/FAILED token) and | ||
| * {@code error}. {@code isReplay} is the operation-level replay indicator (this operation was present in the | ||
| * checkpointed state delivered at invocation start); {@code isReplayingChildren} is the distinct context-children | ||
| * indicator and is dumped unasserted here. | ||
| */ | ||
| @SuppressWarnings("deprecation") | ||
| public class PluginAttemptInfoShape extends DurableHandler<Object, String> { | ||
|
|
||
| @Override | ||
| protected DurableConfig createConfiguration() { | ||
| return DurableConfig.builder().withPlugins(new AttemptShapePlugin()).build(); | ||
| } | ||
|
|
||
| @Override | ||
| public String handleRequest(Object input, DurableContext context) { | ||
| return context.step( | ||
| "flaky", | ||
| String.class, | ||
| stepCtx -> { | ||
| // Fail on the first attempt, succeed on the second, using the SDK's built-in 1-based attempt | ||
| // number. | ||
| if (stepCtx.getAttempt() < 2) { | ||
| throw new RuntimeException("Attempt " + stepCtx.getAttempt() + " failed"); | ||
| } | ||
| return "ok"; | ||
| }, | ||
| StepConfig.builder() | ||
| .retryStrategy(RetryStrategies.exponentialBackoff( | ||
| 3, Duration.ofSeconds(1), Duration.ofSeconds(10), 1.0, JitterStrategy.NONE)) | ||
| .build()); | ||
| } | ||
|
|
||
| private static final class AttemptShapePlugin implements DurableExecutionPlugin { | ||
| private volatile String executionArn; | ||
|
|
||
| @Override | ||
| public void onInvocationStart(InvocationInfo info) { | ||
| this.executionArn = info.durableExecutionArn(); | ||
| } | ||
|
|
||
| @Override | ||
| public void onUserFunctionStart(UserFunctionStartInfo info) { | ||
| if (!PluginSupport.isStep(info.type()) || info.attempt() == null) { | ||
| return; | ||
| } | ||
| new Rec("attempt-start") | ||
| .str("id", info.id()) | ||
| .str("name", info.name()) | ||
| .str("type", Rec.upper(info.type())) | ||
| .str("subType", info.subType()) | ||
| .str("parentId", info.parentId()) | ||
| .num("attempt", info.attempt()) | ||
| .time("startTimestamp", info.startTimestamp()) | ||
| .bool("isReplay", info.isReplay()) | ||
| .bool("isReplayingChildren", info.isReplayingChildren()) | ||
| .emit(executionArn); | ||
| } | ||
|
|
||
| @Override | ||
| public void onUserFunctionEnd(UserFunctionEndInfo info) { | ||
| if (!PluginSupport.isStep(info.type()) || info.attempt() == null) { | ||
| return; | ||
| } | ||
| new Rec("attempt-end") | ||
| .str("id", info.id()) | ||
| .str("name", info.name()) | ||
| .str("type", Rec.upper(info.type())) | ||
| .str("subType", info.subType()) | ||
| .str("parentId", info.parentId()) | ||
| .num("attempt", info.attempt()) | ||
| .time("startTimestamp", info.startTimestamp()) | ||
| .time("endTimestamp", info.endTimestamp()) | ||
| .bool("isReplay", info.isReplay()) | ||
| .bool("isReplayingChildren", info.isReplayingChildren()) | ||
| .str("outcome", info.succeeded() ? "SUCCEEDED" : "FAILED") | ||
| .str("error", Rec.msg(info.error())) | ||
| .emit(executionArn); | ||
| } | ||
| } | ||
|
|
||
| /** Single-line JSON record builder: emits every provided key, skipping nulls, then stamps durableExecutionArn. */ | ||
| private static final class Rec { | ||
| private final StringBuilder sb = new StringBuilder("{"); | ||
|
|
||
| Rec(String hook) { | ||
| raw("plugin", "\"CONFPLUGIN\""); | ||
| raw("hook", "\"" + hook + "\""); | ||
| } | ||
|
|
||
| Rec str(String key, String value) { | ||
| if (value != null) { | ||
| raw(key, quote(value)); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
| Rec num(String key, Integer value) { | ||
| if (value != null) { | ||
| raw(key, value.toString()); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
| Rec bool(String key, boolean value) { | ||
| raw(key, value ? "true" : "false"); | ||
| return this; | ||
| } | ||
|
|
||
| Rec time(String key, Instant value) { | ||
| if (value != null) { | ||
| raw(key, quote(value.toString())); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
| private void raw(String key, String jsonValue) { | ||
| if (sb.length() > 1) { | ||
| sb.append(", "); | ||
| } | ||
| sb.append('"').append(key).append("\": ").append(jsonValue); | ||
| } | ||
|
|
||
| void emit(String executionArn) { | ||
| System.out.println(sb.append(PluginSupport.arnField(executionArn)).append('}')); | ||
| } | ||
|
|
||
| static String upper(String s) { | ||
| return s == null ? null : s.toUpperCase(Locale.ROOT); | ||
| } | ||
|
|
||
| static String msg(Throwable t) { | ||
| if (t == null) { | ||
| return null; | ||
| } | ||
| return t.getMessage() != null ? t.getMessage() : t.toString(); | ||
| } | ||
|
|
||
| static String quote(String s) { | ||
| StringBuilder b = new StringBuilder("\""); | ||
| for (int i = 0; i < s.length(); i++) { | ||
| char c = s.charAt(i); | ||
| switch (c) { | ||
| case '"': | ||
| b.append("\\\""); | ||
| break; | ||
| case '\\': | ||
| b.append("\\\\"); | ||
| break; | ||
| case '\n': | ||
| b.append("\\n"); | ||
| break; | ||
| case '\r': | ||
| b.append("\\r"); | ||
| break; | ||
| case '\t': | ||
| b.append("\\t"); | ||
| break; | ||
| default: | ||
| if (c < 0x20) { | ||
| b.append(String.format("\\u%04x", (int) c)); | ||
| } else { | ||
| b.append(c); | ||
| } | ||
| } | ||
| } | ||
| return b.append('"').toString(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,187 @@ | ||||||||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||||||||
| // SPDX-License-Identifier: Apache-2.0 | ||||||||
| package plugin; | ||||||||
|
|
||||||||
| import java.time.Duration; | ||||||||
| import java.time.Instant; | ||||||||
| import java.util.ArrayList; | ||||||||
| import java.util.List; | ||||||||
| import java.util.Locale; | ||||||||
| import software.amazon.lambda.durable.DurableConfig; | ||||||||
| import software.amazon.lambda.durable.DurableContext; | ||||||||
| import software.amazon.lambda.durable.DurableFuture; | ||||||||
| import software.amazon.lambda.durable.DurableHandler; | ||||||||
| import software.amazon.lambda.durable.ParallelDurableFuture; | ||||||||
| import software.amazon.lambda.durable.config.ParallelConfig; | ||||||||
| import software.amazon.lambda.durable.plugin.DurableExecutionPlugin; | ||||||||
| import software.amazon.lambda.durable.plugin.InvocationInfo; | ||||||||
| import software.amazon.lambda.durable.plugin.OperationInfo; | ||||||||
| import software.amazon.lambda.durable.plugin.UserFunctionStartInfo; | ||||||||
|
|
||||||||
| /** | ||||||||
| * 10-23: Context-typed hook info field shape (CANONICAL DUMP). | ||||||||
| * | ||||||||
| * <p>A parallel operation named {@code "ctx"} with max-concurrency 1 and two branches: branch A runs a step named | ||||||||
| * {@code "inner"} returning {@code "x"}, then a 2-second wait, and returns {@code "a-done"}; branch B returns | ||||||||
| * {@code "b-done"} directly. With max-concurrency 1 branch A runs live, suspends on the wait, and re-runs on the replay | ||||||||
| * (replaying its checkpointed children) before branch B runs live — so the children-replay indicator flips true on | ||||||||
| * branch A's second {@code fn-start}. | ||||||||
| * | ||||||||
| * <p>The instrumentation plugin (filtering to CONTEXT-type operations) emits ONE single-line JSON record per hook | ||||||||
| * event: a canonical camelCase dump of that hook's OWN info parameter, null / unset fields OMITTED. | ||||||||
| * | ||||||||
| * <p>Java's {@link OperationInfo} (operation-start) exposes id/name/type/subType/parentId/startTimestamp/endTimestamp/ | ||||||||
| * status/isReplay. Java's {@link UserFunctionStartInfo} (fn-start) exposes id/name/type/subType/parentId/ | ||||||||
| * startTimestamp/isReplayingChildren/attempt — for CONTEXT operations {@code attempt} is null and is omitted. Only | ||||||||
| * fn-start is probed; attempt-end hooks are out of scope for a suspending context run. | ||||||||
| */ | ||||||||
| @SuppressWarnings("deprecation") | ||||||||
| public class PluginContextInfoShape extends DurableHandler<Object, List<String>> { | ||||||||
|
|
||||||||
| @Override | ||||||||
| protected DurableConfig createConfiguration() { | ||||||||
| return DurableConfig.builder().withPlugins(new ContextShapePlugin()).build(); | ||||||||
| } | ||||||||
|
|
||||||||
| @Override | ||||||||
| public List<String> handleRequest(Object input, DurableContext context) { | ||||||||
| var config = ParallelConfig.builder().maxConcurrency(1).build(); | ||||||||
| var futures = new ArrayList<DurableFuture<String>>(); | ||||||||
| ParallelDurableFuture parallel = context.parallel("ctx", config); | ||||||||
| try (parallel) { | ||||||||
| futures.add(parallel.branch("branch-a", String.class, branch -> { | ||||||||
| branch.step("inner", String.class, stepCtx -> "x"); | ||||||||
| branch.wait(null, Duration.ofSeconds(2)); | ||||||||
| return "a-done"; | ||||||||
| })); | ||||||||
| futures.add(parallel.branch("branch-b", String.class, branch -> "b-done")); | ||||||||
| } | ||||||||
| return futures.stream().map(DurableFuture::get).toList(); | ||||||||
| } | ||||||||
|
|
||||||||
| private static final class ContextShapePlugin implements DurableExecutionPlugin { | ||||||||
| private volatile String executionArn; | ||||||||
|
|
||||||||
| @Override | ||||||||
| public void onInvocationStart(InvocationInfo info) { | ||||||||
| this.executionArn = info.durableExecutionArn(); | ||||||||
| } | ||||||||
|
|
||||||||
| @Override | ||||||||
| public void onOperationStart(OperationInfo info) { | ||||||||
| if (!PluginSupport.isContext(info.type())) { | ||||||||
| return; | ||||||||
| } | ||||||||
| new Rec("operation-start") | ||||||||
| .str("id", info.id()) | ||||||||
| .str("name", info.name()) | ||||||||
| .str("type", Rec.upper(info.type())) | ||||||||
| .str("subType", info.subType()) | ||||||||
| .str("parentId", info.parentId()) | ||||||||
| .str("status", Rec.upper(info.status())) | ||||||||
| .time("startTimestamp", info.startTimestamp()) | ||||||||
| .time("endTimestamp", info.endTimestamp()) | ||||||||
| .bool("isReplay", info.isReplay()) | ||||||||
| .emit(executionArn); | ||||||||
| } | ||||||||
|
|
||||||||
| @Override | ||||||||
| public void onUserFunctionStart(UserFunctionStartInfo info) { | ||||||||
| if (!PluginSupport.isContext(info.type())) { | ||||||||
| return; | ||||||||
| } | ||||||||
| new Rec("fn-start") | ||||||||
| .str("id", info.id()) | ||||||||
| .str("name", info.name()) | ||||||||
| .str("type", Rec.upper(info.type())) | ||||||||
| .str("subType", info.subType()) | ||||||||
| .str("parentId", info.parentId()) | ||||||||
| .num("attempt", info.attempt()) | ||||||||
| .time("startTimestamp", info.startTimestamp()) | ||||||||
| .bool("isReplayingChildren", info.isReplayingChildren()) | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Codex AI review [P2] Include the operation-level replay flag.
Suggested change
|
||||||||
| .emit(executionArn); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| /** Single-line JSON record builder: emits every provided key, skipping nulls, then stamps durableExecutionArn. */ | ||||||||
| private static final class Rec { | ||||||||
| private final StringBuilder sb = new StringBuilder("{"); | ||||||||
|
|
||||||||
| Rec(String hook) { | ||||||||
| raw("plugin", "\"CONFPLUGIN\""); | ||||||||
| raw("hook", "\"" + hook + "\""); | ||||||||
| } | ||||||||
|
|
||||||||
| Rec str(String key, String value) { | ||||||||
| if (value != null) { | ||||||||
| raw(key, quote(value)); | ||||||||
| } | ||||||||
| return this; | ||||||||
| } | ||||||||
|
|
||||||||
| Rec num(String key, Integer value) { | ||||||||
| if (value != null) { | ||||||||
| raw(key, value.toString()); | ||||||||
| } | ||||||||
| return this; | ||||||||
| } | ||||||||
|
|
||||||||
| Rec bool(String key, boolean value) { | ||||||||
| raw(key, value ? "true" : "false"); | ||||||||
| return this; | ||||||||
| } | ||||||||
|
|
||||||||
| Rec time(String key, Instant value) { | ||||||||
| if (value != null) { | ||||||||
| raw(key, quote(value.toString())); | ||||||||
| } | ||||||||
| return this; | ||||||||
| } | ||||||||
|
|
||||||||
| private void raw(String key, String jsonValue) { | ||||||||
| if (sb.length() > 1) { | ||||||||
| sb.append(", "); | ||||||||
| } | ||||||||
| sb.append('"').append(key).append("\": ").append(jsonValue); | ||||||||
| } | ||||||||
|
|
||||||||
| void emit(String executionArn) { | ||||||||
| System.out.println(sb.append(PluginSupport.arnField(executionArn)).append('}')); | ||||||||
| } | ||||||||
|
|
||||||||
| static String upper(String s) { | ||||||||
| return s == null ? null : s.toUpperCase(Locale.ROOT); | ||||||||
| } | ||||||||
|
|
||||||||
| static String quote(String s) { | ||||||||
| StringBuilder b = new StringBuilder("\""); | ||||||||
| for (int i = 0; i < s.length(); i++) { | ||||||||
| char c = s.charAt(i); | ||||||||
| switch (c) { | ||||||||
| case '"': | ||||||||
| b.append("\\\""); | ||||||||
| break; | ||||||||
| case '\\': | ||||||||
| b.append("\\\\"); | ||||||||
| break; | ||||||||
| case '\n': | ||||||||
| b.append("\\n"); | ||||||||
| break; | ||||||||
| case '\r': | ||||||||
| b.append("\\r"); | ||||||||
| break; | ||||||||
| case '\t': | ||||||||
| b.append("\\t"); | ||||||||
| break; | ||||||||
| default: | ||||||||
| if (c < 0x20) { | ||||||||
| b.append(String.format("\\u%04x", (int) c)); | ||||||||
| } else { | ||||||||
| b.append(c); | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
| return b.append('"').toString(); | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Claude AI review
Maintainability (low): The
Recsingle-line JSON builder — the constructor plusstr/num/bool/time/raw/emitand the staticupper/msg/quotemethods (~90 lines) — is duplicated verbatim in all five new handlers (PluginAttemptInfoShape,PluginContextInfoShape,PluginInvocationInfoShape,PluginOperationChangeShape,PluginOperationInfoShape), roughly 360 duplicated lines out of the PR's 982 additions.PluginSupportalready exists as the documented home for shared plugin-conformance helpers (it hostsarnField,isStep,isContext, etc.). Because every handler emits the same escaped JSON shape, any future fix to the JSON escaping or field formatting must be applied in five places, and the copies can silently diverge.Fix: Hoist
Rec(andupper/msg/quote) intoPluginSupport(or a new package-privatePluginJsonRecordclass) and have each handler construct the shared type, deleting the per-file copies.