Skip to content

feat: add custom extension operations API - #607

Open
zhongkechen wants to merge 40 commits into
mainfrom
codex/extension-operation-refactor
Open

feat: add custom extension operations API#607
zhongkechen wants to merge 40 commits into
mainfrom
codex/extension-operation-refactor

Conversation

@zhongkechen

@zhongkechen zhongkechen commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Issue Link, if available

Closes #571

Description

  • Add the public ExtensionContext and ExtensionOperation SPI for composing SDK-owned durable primitives with stable reservations, custom subtypes, stateful step replay, and configurable child contexts.
  • Route customer-facing operations through singular Durable*Operation facades and move backend engines into the primitive package.
  • Add operation-owned nested config types while preserving existing DurableContext, ParallelDurableFuture, and compatibility config APIs.
  • Implement map, parallel, callback, condition, and retry operations on the extension path while preserving checkpoint/replay, plugin, retry, and serialization behavior.
  • Add extension author documentation and ADR-006.
  • PR #611, which migrates the experimental DAG support to the extension SPI, is a concrete example of using this new SPI.

Demo/Screenshots

Not applicable. This change adds Java SDK APIs and internal architecture; no visual interface is changed.

Checklist

  • I have filled out every section of the PR template
  • I have thoroughly tested this change

Testing

Unit Tests

Yes. Added and updated unit coverage for extension reservations, stateful steps, child-context replay, operation facades/config conversion, primitive implementations, deterministic IDs, plugin metadata, and compatibility APIs.

Full SDK result: 1,202 tests passed.

Integration Tests

Yes. Added integration coverage for extension operations, static operation facades, extension concurrency, plugins, replay, callbacks, retries, map, and parallel behavior.

Full integration result: 413 tests passed.

Examples

No new example was required. Existing examples remain source-compatible and passed their test suite: 120 tests passed, with 34 cloud-only tests skipped by default.

Verification command:

mvn clean install

The OpenTelemetry module also passed 163 tests. Spotless and diff checks pass.

@zhongkechen
zhongkechen had a problem deploying to ai-pr-review-runtime August 11, 2026 00:52 — with GitHub Actions Failure
@zhongkechen
zhongkechen temporarily deployed to ai-pr-review-runtime August 11, 2026 00:52 — with GitHub Actions Inactive
@zhongkechen
zhongkechen temporarily deployed to ai-pr-review-runtime August 11, 2026 03:08 — with GitHub Actions Inactive
@zhongkechen
zhongkechen had a problem deploying to ai-pr-review-runtime August 11, 2026 03:08 — with GitHub Actions Failure
@github-actions

This comment has been minimized.

@zhongkechen
zhongkechen temporarily deployed to ai-pr-review-runtime August 11, 2026 22:13 — with GitHub Actions Inactive
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.lambda.durable.operation;
package software.amazon.lambda.durable.primitive;

This comment was marked as outdated.

/** Returns the operation type derived from the sub-type. */
public OperationType operationType() {
return subType.getOperationType();
public record OperationIdentifier(String operationId, String name, OperationType operationType, String subType) {

This comment was marked as outdated.

Comment on lines +50 to +52
if (!allocatedLocalIds.add(localOperationId)) {
throw new IllegalArgumentException("Local operation ID is already in use: " + localOperationId);
}

This comment was marked as outdated.

Comment on lines +66 to +67
if (delay.isNegative()) {
throw new IllegalArgumentException("delay cannot be negative");

This comment was marked as outdated.

validateSubType(subType);
Objects.requireNonNull(function, "function cannot be null");
Objects.requireNonNull(config, "config cannot be null");
claim();

This comment was marked as outdated.

*/
public class ErrorHandlingExample extends DurableHandler<Object, String> {

private static final Logger logger = LoggerFactory.getLogger(ErrorHandlingExample.class);

This comment was marked as outdated.

@github-actions

This comment has been minimized.

@zhongkechen
zhongkechen temporarily deployed to ai-pr-review-runtime August 12, 2026 03:02 — with GitHub Actions Inactive
@zhongkechen
zhongkechen had a problem deploying to ai-pr-review-runtime August 12, 2026 03:02 — with GitHub Actions Failure
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.lambda.durable.operation;
package software.amazon.lambda.durable.primitive;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex AI review

[P1] Retain compatibility classes at the old public FQCNs. This move, together with removal of the other public operation engine classes, makes existing clients fail to compile or link (NoClassDefFoundError) when upgrading to 2.1.1. Keep deprecated forwarding classes under software.amazon.lambda.durable.operation, or defer their removal to a major release.

/** Returns the operation type derived from the sub-type. */
public OperationType operationType() {
return subType.getOperationType();
public record OperationIdentifier(String operationId, String name, OperationType operationType, String subType) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex AI review

[P1] Preserve the existing constructor and accessor descriptors. The public record previously accepted (String, String, OperationSubType) and exposed OperationSubType subType(). Replacing both breaks source and binary compatibility. Retain the three-argument constructor and enum accessor, and expose custom subtype strings through a separately named accessor such as subTypeValue().

Comment on lines +315 to +316
Completion awaitCompletion(ExpectedCompletionStatus expectedCompletionStatus) {
while (true) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex AI review

[P2] Restore unrecoverable handling for coordinator faults. The former concurrency engine converted unexpected exceptions, including failures from custom shouldComplete callbacks, into IllegalDurableOperationException and terminated execution. Exceptions escaping this loop are now caught by ChildContextPrimitive, checkpointed as a recoverable failed map/parallel context, and can be caught by user code. Wrap this loop with the previous policy: rethrow suspension/unrecoverable signals and convert other failures to IllegalDurableOperationException; add an integration test for a throwing completion callback.

Comment on lines +49 to +51
var localOperationId = String.valueOf(operationCounter.incrementAndGet());
if (!allocatedLocalIds.add(localOperationId)) {
throw new IllegalArgumentException("Local operation ID is already in use: " + localOperationId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex AI review

[P2] Skip custom-claimed numeric IDs instead of failing. After reserving custom local ID "2", the counter is 1, so the next sequential reservation calculates "2" and throws. This contradicts the new documented contract that generated IDs skip claimed values and makes valid mixed custom/sequential workflows fail before creating an operation. Increment in a loop until allocatedLocalIds.add(...) succeeds, and update the collision test accordingly.

Comment on lines +105 to +106
var virtualEmptyMap = itemList.isEmpty() && !context.getDurableConfig().shouldCheckpointEmptyMap();
var parentConfig = parentContextConfig(mapConfig.serDes(), virtualEmptyMap).toBuilder()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex AI review

[P2] Preserve the replay-to-execution transition for non-checkpointed empty maps. Making this parent virtual bypasses BasePrimitive.execute()'s missing-operation lookup and setExecutionMode() call. On replay, the enclosing context therefore remains replaying after reaching the empty map, so isReplaying() is incorrect and subsequent durable logs can be suppressed. Keep the map uncheckpointed while explicitly performing the transition, and cover this with a replay/logging test.

@github-actions

Copy link
Copy Markdown

Codex AI review

Found five actionable issues affecting compatibility, replay semantics, and execution lifecycle. Tests were not run per review constraints.

Reviewed commit 6d2ff84ffea314da23b4ce06abb7c86e96a787de. 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.

[Feature]: Add a public API for custom extension operations

1 participant