Skip to content

fix(plugin): surface missing state fields on hook info records - #618

Open
wangyb-A wants to merge 4 commits into
mainfrom
plugin-hook-parity-fix
Open

fix(plugin): surface missing state fields on hook info records#618
wangyb-A wants to merge 4 commits into
mainfrom
plugin-hook-parity-fix

Conversation

@wangyb-A

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

Copy link
Copy Markdown
Contributor

Summary

Plugin hook info records dropped state that the Python and JS SDKs carry, so a Java plugin could not see per-operation replay status at the attempt and change hooks, nor operation state at the invocation hooks.

Bottom of a 2-PR stack. The conformance handlers that assert these field shapes — and the live suite results — are in the stacked follow-up. This PR is the SDK change only and is reviewable on its own.

What changed

Execution payload surface (JS parity)

  • InvocationInfo gains executionInput: the deserialized input the execution was started with — the same value the durable handler receives.
  • InvocationEndInfo gains executionInput (carried through from the start hook) and executionResult, the value the handler returned. executionResult is populated only when the invocation completed the execution successfully; null for PENDING/FAILED/RETRYING.
  • Both are typed Object, matching JS's unknown. Note Python instead exposes execution_result as the serialized string from the invocation output; Java follows the JS shape and reports the returned object.

Ordering change — please review. The execution input is now deserialized before onInvocationStart fires, because the hook must carry it. Deserialization can fail, so the failure path explicitly fires the start hook with a null executionInput before propagating: plugins must observe an invocation-start for every invocation, including one with a malformed input payload. A regression test covers this using a SerDes that rejects every deserialize.

Operation result (JS parity)

  • result added to the three operation-snapshot records: OperationInfo, OperationEndInfo and OperationChangeItemInfo.
  • It is the operation's checkpointed serialized result, exactly as the backend recorded it, typed String to match JS's result?: string. The serialized form is deliberate — the plugin boundary has no access to the caller's target type, so it cannot deserialize. Null when the operation produced none (still running, failed, or a type carrying none such as WAIT).
  • The payload lives on a different *Details member per operation type, so BaseDurableOperation.getResultPayload dispatches on operation type exactly as the existing getErrorObject does (STEP, CHAINED_INVOKE, CALLBACK, CONTEXT).

Attempt and change-item fields

  • UserFunctionStartInfo / UserFunctionEndInfo gain isReplay, the operation-level indicator for whether THIS operation was observed via checkpointed state. Distinct from the existing isReplayingChildren, which describes the child operations of a context body and does not substitute for it.
  • OperationChangeItemInfo was a reduced record. It now carries the full operation surface (status, attempt, isReplay, error), ordered to match OperationEndInfo, so an operation observed through a change delta exposes the same fields as through the per-operation hooks.

Invocation-info enrichment

  • InvocationInfo gains operations and updatedOperations.
  • InvocationEndInfo gains operations and executionStartTime. The latter was present on the start info but dropped from the end record, forcing plugins to correlate back to the start hook.
  • updatedOperations derives from the invocation input's UpdatedOperationIds intersected with the tracked operations, so it is empty on the first invocation and names the externally-completed operations on a replay.
  • The end-info snapshot is taken at end time, so unlike the start info it also includes operations created during the invocation.

Design notes for reviewers

Where the replay indicator comes from. The attempt hook needs the operation-level replay flag at its firing site in BaseDurableOperation.runUserFunction, but getOperation() delegates to getOperationAndUpdateReplayState, which flips REPLAY to EXECUTION mode as a side effect. Reading it from a plugin-hook site would mutate execution state. ExecutionManager instead snapshots the operation ids delivered in the initial state once, and wasObservedAtInvocationStart is a pure containment check. That yields one consistent definition of isReplay across the attempt, change and invocation hooks: the operation predates this invocation.

Map value type. Both invocation maps are keyed by operation id and valued with OperationChangeItemInfo, now the richest operation snapshot record the SDK has, so a single conversion path (PluginInfoConverter.toOperationItemMap) feeds the change hook and both invocation hooks. The record's name is a wart in this role; renaming it to something like OperationItemInfo is a reasonable follow-up, kept out of this change to limit blast radius.

No defaulting constructors. These are positional records, so the added components surface every constructor call site at compile time. I deliberately did not add convenience overloads: a defaulting constructor is exactly how a future internal call site would silently ship empty maps to plugins, which is the failure mode this fixes. Call sites in the OpenTelemetry plugin tests were updated mechanically. All touched APIs are @Deprecated preview, so the record-shape changes are sanctioned.

Out of scope

Testing

Full module build, unit tests and spotless:check: BUILD SUCCESS. SDK 1129 tests, integration 400, OpenTelemetry 165, examples 120 — all green. The first commit was additionally verified applied directly to main with no handler code present.

Adds six PluginIntegrationTest cases: execution input and result on success, no execution result when suspended, none when the execution fails, the start-hook-fires-on-malformed-input invariant, and operation-end reporting the serialized result on success versus no result plus an error on failure.

Known flaky test, not caused by this PR: InvocationOtelPluginTest.invocationEnd_closesNestedSpansChildFirst (a test added on main) failed 3 times across ~12 full-reactor runs on this branch, and 0 times across 8 runs on pristine main; it passes 6/6 in isolation on both. This PR changes no OpenTelemetry production code (git diff main HEAD -- otel-plugin/src/main is empty) and the span close order in endOpenSpansChildFirst is deterministic, so the correlation looks like timing sensitivity in the test rather than a behaviour change. Flagging rather than hiding it.

Conformance evidence lives in the stacked follow-up, where the handlers exist to assert these shapes: 21/21 covered plugin requirements pass, with 10-19, 10-21 and 10-22 flipping to green.

Refs: #604

Comment thread sdk/src/main/java/software/amazon/lambda/durable/plugin/InvocationInfo.java Outdated
Comment thread sdk/src/main/java/software/amazon/lambda/durable/plugin/PluginInfoConverter.java Outdated
Comment thread sdk/src/main/java/software/amazon/lambda/durable/execution/DurableExecutor.java Outdated
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

Claude AI review

No blocking findings. The diff is a mechanically consistent, compile-safe expansion of the preview (@Deprecated) plugin-hook record surface.

Verified:

  • Every positional constructor/factory call site for the changed records (InvocationInfo, InvocationEndInfo, UserFunctionStartInfo, UserFunctionEndInfo, OperationChangeItemInfo) and the changed PluginInfoConverter/ExecutionManager methods is updated in-diff across all modules; consumers use name-based accessors, so the OperationChangeItemInfo field reorder does not break any reader. Compilation is sound.
  • wasObservedAtInvocationStart is a pure Set.contains over initialOperationIds (snapshotted once at construction), so reading isReplay at the attempt/change/invocation hook sites does not mutate replay mode — a real hazard the design correctly avoids. The isReplay definition is consistent across all three hook families.
  • toOperationItemMap builds maps keyed by unique Operation::id with non-null values (no duplicate-key/NPE risk), and getUpdatedOperationsSnapshot filters nulls. Empty-not-null invariants hold; updatedOperations is empty on first invocation.
  • attempt extraction mirrors the pre-existing toOperationEndInfo exactly, so it introduces no regression. fireOnInvocationEnd runs while the ExecutionManager is still open.

Residual test risk (this PR only): The behavioral additions in this PR are effectively unverified here. The only test changes are mechanical constructor-arity updates to existing OTel/unit tests plus one isReplay assertion on toUserFunctionStartInfo. There is no unit test in this PR for: the new public PluginInfoConverter.toOperationItemMap; the new attempt/isReplay fields on OperationChangeItemInfo; toOperationChangeInfo's new replayedOperationIds argument; the new ExecutionManager methods (wasObservedAtInvocationStart, getInitialOperationIds, getOperationsSnapshot, getUpdatedOperationsSnapshot); or the populated operations/updatedOperations/executionStartTime on the invocation records. The PR defers these assertions to a stacked follow-up. Recommend adding SDK-module unit tests in this PR (e.g. in PluginInfoConverterTest/an ExecutionManager test) covering the isReplay/attempt population and the initial-vs-updated snapshot partitioning, so the new surface is guarded independently of the follow-up branch.

Note: the PR description references executionInput, executionResult, an operation result field, and a getResultPayload dispatcher that are not present in this diff; those were not reviewed as they are not part of these changes.

Reviewed commit 4f5639c25b35c389f90a7af14b147ae251b424e4. Workflow run

@wangyb-A
wangyb-A force-pushed the plugin-hook-parity-fix branch from 4f5639c to b42d0db Compare August 24, 2026 22:11
@wangyb-A
wangyb-A deployed to ai-pr-review August 24, 2026 22:11 — with GitHub Actions Active
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime August 24, 2026 22:11 — with GitHub Actions Inactive
@wangyb-A
wangyb-A had a problem deploying to ai-pr-review-runtime August 24, 2026 22:11 — with GitHub Actions Error
Comment thread sdk/src/main/java/software/amazon/lambda/durable/plugin/PluginInfoConverter.java Outdated
Comment thread sdk/src/main/java/software/amazon/lambda/durable/execution/DurableExecutor.java Outdated
@github-actions

This comment has been minimized.

@wangyb-A
wangyb-A deployed to ai-pr-review August 24, 2026 23:39 — with GitHub Actions Active
@wangyb-A
wangyb-A had a problem deploying to ai-pr-review-runtime August 24, 2026 23:40 — with GitHub Actions Error
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime August 24, 2026 23:40 — with GitHub Actions Inactive
Comment thread sdk/src/main/java/software/amazon/lambda/durable/plugin/PluginInfoConverter.java Outdated
@github-actions

This comment has been minimized.

Alex Wang added 2 commits August 24, 2026 16:43
Plugin hook infos dropped state that the Python and JS SDKs carry, so a
Java plugin could not see per-operation replay status at the attempt and
change hooks, nor operation state at the invocation hooks.

Attempt and change-item fields:

- UserFunctionStartInfo / UserFunctionEndInfo gain `isReplay`, the
  operation-level indicator for whether THIS operation was observed via
  checkpointed state. This is distinct from the existing
  `isReplayingChildren`, which describes the child operations of a
  context body and does not substitute for it.
- OperationChangeItemInfo was a reduced record; it now carries the full
  operation surface (`status`, `attempt`, `isReplay`, `error`), ordered to
  match OperationEndInfo, so an operation seen through a change delta
  exposes the same fields as through the per-operation hooks.

Invocation-info enrichment:

- InvocationInfo gains `operations` and `updatedOperations`.
- InvocationEndInfo gains `operations` and `executionStartTime`; the
  latter was present on the start info but dropped from the end record,
  forcing plugins to correlate back to the start hook.
- `updatedOperations` derives from the input's UpdatedOperationIds
  intersected with the tracked operations, so it is empty on the first
  invocation and names the externally-completed operations on a replay.
- The end-info snapshot is taken at end time, so unlike the start info it
  also includes operations created during the invocation.

ExecutionManager now snapshots the operation ids delivered in the initial
state and exposes non-mutating accessors for them. The attempt hook needs
the replay indicator at its firing site, but getOperation() delegates to
getOperationAndUpdateReplayState, which flips REPLAY to EXECUTION mode as
a side effect; reading it from a plugin-hook site would mutate execution
state. wasObservedAtInvocationStart is a pure containment check instead,
and gives one consistent definition of `isReplay` across the attempt,
change and invocation hooks.

Both invocation maps are keyed by operation id and valued with
OperationChangeItemInfo, now the richest operation snapshot record the
SDK has, so a single conversion path feeds the change hook and both
invocation hooks. The record name is a wart in this role; renaming it is
left as a follow-up.

These are positional records, so the added components surface every
constructor call site. No defaulting overloads were introduced: a
convenience constructor is exactly how a future internal call site would
silently ship empty maps to plugins, which is the failure mode this
change fixes. Call sites in the OpenTelemetry plugin tests were updated
mechanically.

Payload surfaces stay out of scope: no `result` and no execution
input/result on any info.

Verified with the full module build, unit tests and spotless:check. The
conformance handlers that assert these field shapes, and the live suite
results, are in the stacked follow-up PR.

Refs: #604
@wangyb-A
wangyb-A force-pushed the plugin-hook-parity-fix branch from 30b89d3 to ecc5856 Compare August 24, 2026 23:43
@wangyb-A
wangyb-A marked this pull request as ready for review August 25, 2026 00:20
@wangyb-A
wangyb-A requested a review from a team August 25, 2026 00:20
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime August 25, 2026 00:20 — with GitHub Actions Inactive
@wangyb-A
wangyb-A had a problem deploying to ai-pr-review-runtime August 25, 2026 00:25 — with GitHub Actions Error
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime August 25, 2026 00:25 — with GitHub Actions Inactive
Comment on lines +33 to +35
OperationStatus status,
Integer attempt,
boolean isReplay,

This comment was marked as outdated.

Comment thread sdk/src/main/java/software/amazon/lambda/durable/plugin/PluginInfoConverter.java Outdated
@github-actions

This comment has been minimized.

* @param identifier the operation identifier containing id, name, type, and subType
* @param parentId the parent operation ID (may be null)
* @param isReplay true if the user function is called during replay (context operations)
* @param isReplay true if this operation was already present in the checkpointed state when it started

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confusing. Can we find a better name and explain the behavior better?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only keep isReplay to match js

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface hasn't released for js yet. We can rename for both Java and JS if we have a better name

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can keep this name isReplay, since this apply to all operations, not limited to child context operation, do you have any suggession on this?

* @param parentId the parent operation ID (may be null)
* @param isReplay true if the user function is called during replay (context operations)
* @param isReplay true if this operation was already present in the checkpointed state when it started
* @param isReplayingChildren true if the child operations of this context body are replaying from checkpoints

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confusing. What's the difference from above? Can we find a better name and explain the behavior better?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only keep isReplay to match js

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the above. isReplayingChildren could be renamed.

Comment thread sdk/src/main/java/software/amazon/lambda/durable/plugin/PluginInfoConverter.java Outdated
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime August 25, 2026 22:05 — with GitHub Actions Inactive
@wangyb-A
wangyb-A had a problem deploying to ai-pr-review-runtime August 25, 2026 22:05 — with GitHub Actions Error
@github-actions

Copy link
Copy Markdown
Contributor

Codex AI review

Three public plugin API regressions need correction. Residual risk: runtime tests were not executed under the read-only review constraint.

Reviewed commit 726603271f06e136c1602bab0177420fef8cf6e6. Workflow run

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