Skip to content

feat(plugin): surface execution input and result on invocation hooks - #622

Merged
wangyb-A merged 4 commits into
mainfrom
feat/plugin-invocation-input-result
Aug 13, 2026
Merged

feat(plugin): surface execution input and result on invocation hooks#622
wangyb-A merged 4 commits into
mainfrom
feat/plugin-invocation-input-result

Conversation

@wangyb-A

@wangyb-A wangyb-A commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Widens the plugin invocation hooks so instrumentation plugins can record execution
input/output:

  • InvocationInfo.executionInput — the deserialized handler input
  • InvocationEndInfo.executionInput — the same value, so end-only plugins need not cache it
  • InvocationEndInfo.executionResult — the value the handler returned, populated only when the
    invocation completed the execution successfully (null on FAILED / PENDING / RETRYING)

Motivation

The JS SDK already exposes this to plugins (InvocationBaseInfo.executionInput,
InvocationEndInfo.executionResult), and the Python SDK added the equivalent in
aws-durable-execution-sdk-python#616. Java is the only SDK where a plugin cannot see execution
I/O at all, which blocks the Workflow Insight plugin from emitting the input/output fields
its record schema defines — it currently omits them rather than fabricating them. This is the
one remaining hook-surface gap of the two found by cross-SDK conformance validation (the other,
per-operation result, is #596).

Compatibility

Both records keep a constructor at their previous arity, delegating with nulls:

new InvocationInfo(requestId, arn, isFirstInvocation, executionStartTime);              // still compiles
new InvocationEndInfo(requestId, arn, isFirstInvocation, status, executionError);       // still compiles

So existing callers — including otel-plugin's tests, which construct these directly — compile
unchanged; nothing outside sdk/ needed edits. Adding a record component alone would have been a
source-breaking change to an API that #620 just promoted to stable.

Cost and failure behavior

  • The plugin-facing deserialization is skipped entirely when no plugins are registered
    (PluginRunner.isEmpty()), so plugin-less executions pay nothing.
  • If the input cannot be deserialized, the hook value is null rather than propagating; the
    authoritative extraction still surfaces the error at its original point. This keeps hook
    ordering intact — plugins continue to receive onInvocationStart before any input-extraction
    failure, so a plugin keying state on start never sees an end without a start.

Preview marking

The new components are marked @Experimental, matching the convention #620 established when it
replaced the preview-API @Deprecated markers — the same idiom already used by
@Experimental Throwable executionError in this record.

Testing

  • PluginRunnerTest — 13 pass, including 4 new: the compatibility constructors leave the new
    components null, and the canonical constructors carry input/result
  • PluginIntegrationTest — 28 pass, including 3 new end-to-end through the real executor:
    input reaches both hooks and the result appears on success; result is null on failure and on
    suspension (PENDING)
  • Full reactor: sdk 397 tests, otel-plugin 165 tests, all green; mvn spotless:check clean

Adds InvocationInfo.executionInput (the deserialized handler input) plus
InvocationEndInfo.executionInput/executionResult (the value the handler
returned, populated only when the invocation completed the execution
successfully), so instrumentation plugins can record execution I/O.

Both records keep a constructor at their previous arity, so existing
callers compile unchanged. The plugin-facing deserialization is skipped
when no plugins are registered and yields null on failure, leaving the
authoritative input extraction to surface errors at its original point.

Mirrors the JS SDK's InvocationBaseInfo.executionInput /
InvocationEndInfo.executionResult and the Python SDK's
execution_input/execution_result (aws-durable-execution-sdk-python#616).
@wangyb-A
wangyb-A deployed to ai-pr-review August 12, 2026 20:24 — with GitHub Actions Active
@wangyb-A
wangyb-A had a problem deploying to ai-pr-review-runtime August 12, 2026 20:24 — with GitHub Actions Failure
@wangyb-A
wangyb-A had a problem deploying to ai-pr-review-runtime August 12, 2026 20:24 — with GitHub Actions Failure
Matches the convention established by #620, which replaced the
preview-API @deprecated markers with @experimental on the individual
record components (as on InvocationEndInfo.executionError).
@wangyb-A
wangyb-A deployed to ai-pr-review August 12, 2026 21:13 — with GitHub Actions Active
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime August 12, 2026 21:13 — with GitHub Actions Inactive
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime August 12, 2026 21:13 — with GitHub Actions Inactive
Comment thread sdk/src/main/java/software/amazon/lambda/durable/execution/DurableExecutor.java Outdated
@github-actions

This comment has been minimized.

…String

Review feedback:

- Deserialize the handler input once and hand the same instance to the
  plugin hooks and the handler. The previous plugin-only extraction
  deserialized a second time, doubling the cost, giving plugins a
  different object than the handler, and re-running side effects in a
  stateful custom SerDes. A deserialization failure is now captured and
  rethrown after onInvocationStart, so the start and end hooks stay
  paired as before.
- Override toString() on both invocation records to omit executionInput
  and executionResult. Records render every component, so plugins that
  log the info object whole would have begun emitting customer payloads
  (possibly secrets or personal data). Output is otherwise unchanged
  from before this branch; mirrors repr=False on the Python fields.
@wangyb-A
wangyb-A deployed to ai-pr-review August 12, 2026 21:31 — with GitHub Actions Active
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime August 12, 2026 21:31 — with GitHub Actions Inactive
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime August 12, 2026 21:31 — with GitHub Actions Inactive
Comment thread sdk/src/main/java/software/amazon/lambda/durable/execution/DurableExecutor.java Outdated
@github-actions

This comment has been minimized.

SerDes.deserialize declares no checked exceptions, so an implementation
may sneaky-throw one (as DurableInputOutputSerDes does for IOException).
A RuntimeException-only capture let such a failure bypass
onInvocationStart while the future's failure path still fired
onInvocationEnd(FAILED), leaving a plugin that keys state on the start
hook with an unpaired end. Capture Throwable and rethrow it unchanged
via ExceptionHelper.sneakyThrow after the start hook.

Adds a regression test with a SerDes that sneaky-throws IOException:
verified failing (start hook count 0) with the narrower capture.
@wangyb-A
wangyb-A deployed to ai-pr-review August 12, 2026 21:59 — with GitHub Actions Active
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime August 12, 2026 22:00 — with GitHub Actions Inactive
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime August 12, 2026 22:00 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown

Codex AI review

No actionable findings. Residual risk: the added integration tests do not cover successful replay or RETRYING hook payloads. Tests were not run per review constraints.

Reviewed commit 38dbb45d6c072402fa3d3fb39e1624a8d3f986f5. Workflow run

@wangyb-A
wangyb-A marked this pull request as ready for review August 12, 2026 23:38
@wangyb-A
wangyb-A requested a review from a team August 12, 2026 23:38
@github-actions

Copy link
Copy Markdown

Claude AI review

Review — no blocking findings

This PR widens the plugin invocation hooks to surface deserialized execution input (InvocationInfo.executionInput, InvocationEndInfo.executionInput) and the handler's return value (InvocationEndInfo.executionResult). The implementation is correct and well-covered.

Verified in detail:

  • Argument ordering across all four end branches in DurableExecutor (PENDING/RETRYING/FAILED/SUCCEEDED) matches the new canonical InvocationEndInfo(requestId, arn, isFirstInvocation, status, error, executionInput, executionResult) component order. executionResult is passed only on SUCCEEDED; null elsewhere.
  • Cross-thread publication via pluginExecutionInput (AtomicReference): set on the handler thread before any suspend point, read on the completing thread — visibility is guaranteed, and it is always set before any suspension/failure that could fire onInvocationEnd.
  • Hook pairing on deserialization failure: extraction failure is captured, onInvocationStart still fires (with null input), then the original Throwable is re-thrown unchanged via sneakyThrow, preserving its type/stack and FAILED classification. Input is deserialized exactly once and shared with the handler.
  • API compatibility: the previous-arity constructors are retained as delegating constructors; the only external caller (examples/.../PluginExample.java, otel-plugin tests) is unaffected.
  • PII protection: toString() overrides on both records exclude the new payload components, with unit-test coverage.

Residual test risk (not blocking):

  • The RETRYING branch (sdk/src/main/java/software/amazon/lambda/durable/execution/DurableExecutor.java:142) now also carries executionInput, but no end-to-end test exercises it; its logic is identical to the other branches (pluginExecutionInput.get()), so risk is low.
  • Deserialization now runs before onInvocationStart (previously after). A custom SerDes or any logging emitted during deserialization no longer runs inside a plugin's onInvocationStart ThreadLocal/MDC setup. This is inherent to passing deserialized input to the start hook and is an intentional, documented trade-off, but the ordering change is undocumented in the hook contract — worth a note if any plugin relies on that ordering.

Reviewed commit 38dbb45d6c072402fa3d3fb39e1624a8d3f986f5. Workflow run

@wangyb-A
wangyb-A requested a deployment to ai-pr-review-runtime August 12, 2026 23:44 — with GitHub Actions Waiting
@wangyb-A
wangyb-A requested a deployment to ai-pr-review-runtime August 12, 2026 23:44 — with GitHub Actions Waiting
@wangyb-A
wangyb-A merged commit 4c290bd into main Aug 13, 2026
34 of 36 checks passed
@wangyb-A
wangyb-A deleted the feat/plugin-invocation-input-result branch August 13, 2026 02:57
wangyb-A pushed a commit that referenced this pull request Aug 13, 2026
Resolves the OperationEndInfo conflict against the preview-marking change
in #620, which replaced the record-level '@deprecated This is a preview
API' javadoc with @experimental on the individual record components.

The new result component now carries @experimental, matching error on
the same record and executionInput/executionResult on the invocation
records added in #622.
wangyb-A pushed a commit that referenced this pull request Aug 13, 2026
…he old arity

Review feedback on the result accessor:

- Only a SUCCEEDED operation reports a result. A wait-for-condition is
  checkpointed as STEP and reuses stepDetails().result() to carry its
  intermediate check-loop state between attempts, so a failed one could
  surface that state as its result, contradicting the documented
  null-on-failure contract. Verified: without the guard the new test
  reports {"polls":2} for a FAILED operation.
- Override toString() to omit result. Records render every component, so
  plugins that log the info object whole would have begun emitting
  customer payloads (possibly secrets or personal data). Output is
  otherwise unchanged; matches the invocation records from #622.
- Add a constructor at the previous 11-argument arity delegating with a
  null result, so existing callers keep compiling and linking.
- Cover the CHAINED_INVOKE, CALLBACK, and CONTEXT extraction branches,
  which had no direct test.
wangyb-A pushed a commit that referenced this pull request Aug 14, 2026
* feat(plugin): Add result accessor to OperationEndInfo

* fix(plugin): guard result on success, keep it out of toString, keep the old arity

Review feedback on the result accessor:

- Only a SUCCEEDED operation reports a result. A wait-for-condition is
  checkpointed as STEP and reuses stepDetails().result() to carry its
  intermediate check-loop state between attempts, so a failed one could
  surface that state as its result, contradicting the documented
  null-on-failure contract. Verified: without the guard the new test
  reports {"polls":2} for a FAILED operation.
- Override toString() to omit result. Records render every component, so
  plugins that log the info object whole would have begun emitting
  customer payloads (possibly secrets or personal data). Output is
  otherwise unchanged; matches the invocation records from #622.
- Add a constructor at the previous 11-argument arity delegating with a
  null result, so existing callers keep compiling and linking.
- Cover the CHAINED_INVOKE, CALLBACK, and CONTEXT extraction branches,
  which had no direct test.

---------

Co-authored-by: Frank Chen <65260095+zhongkechen@users.noreply.github.com>
Co-authored-by: Alex Wang <wangyb@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants