Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
8ebe7a7
test(otel): add replay callback-context and invocation-retry conformance
Oct 2, 2026
078ce3b
ci(otel): pin shared conformance cases 21 through 24
Oct 2, 2026
527d8be
fix(otel): scope root handler instrumentation to its execution
Oct 3, 2026
06727e7
fix(otel): finalize handler scopes before invocation response
Oct 3, 2026
94cb134
test(otel): integrate validated handler-scope review corrections
Oct 3, 2026
ba840d2
fix(plugin): preserve wrapped fatal handler-scope failures
Oct 3, 2026
b4e6899
test(otel): integrate wrapped-fatal scope correction
Oct 3, 2026
24d53b4
ci(otel): pin shared conformance revision R4
Oct 3, 2026
6269b00
fix(ci): keep PR review intake read-only
Oct 3, 2026
3e76962
fix(execution): bound handler scope cleanup and preserve control flow
Oct 3, 2026
95ed86d
test(otel): integrate bounded handler scope cleanup
Oct 3, 2026
be3e03c
merge(ci): retain read-only review intake for conformance
Oct 3, 2026
8132a08
test: verify earlier context cleanup after a scope fatal
Oct 3, 2026
b911c25
chore: preserve published handler scope correction ancestry
Oct 3, 2026
42dc7e1
fix: preserve plugin ABI and observe scope fatals after shutdown
Oct 3, 2026
f4fce66
chore: preserve published explicit handler scope dependency
Oct 3, 2026
5b2d85f
fix: isolate handler scope openers from legacy subclass methods
Oct 3, 2026
200affd
chore: preserve published SDK-owned scope opener ancestry
Oct 3, 2026
aa073e4
Merge branch 'main' into test/otel-conformance-21-24
zhongkechen Oct 5, 2026
a422c3f
fix: isolate handler scope errors and preserve thread MDC
zhongkechen Oct 6, 2026
406bdc9
test: pin final validated OTel conformance runner
zhongkechen Oct 6, 2026
e99d69a
fix: preserve finalized invocation outcome on late scope fatal
zhongkechen Oct 6, 2026
a1159f3
fix: preserve invocation end completion-thread dispatch
zhongkechen Oct 6, 2026
2797274
ci: queue serial conformance runs without replacing pending work
zhongkechen Oct 6, 2026
fe77dcd
Merge main for CodeBuild CI and isolated OTel test environment
zhongkechen Oct 6, 2026
69f3eea
Merge main after factory migration revert
Oct 6, 2026
c00b542
Run OTel conformance on CodeBuild with a Java 21 guard
Oct 7, 2026
e84609c
Select installed Corretto 21 before the CodeBuild Java 17 default
Oct 7, 2026
5e171cb
test: return one checkpoint state per operation in unit mocks
zhongkechen Oct 7, 2026
301fafa
ci: preserve pending OTel suites with the shared 24-case workflow
Oct 7, 2026
e152629
fix: preserve end-hook completion ownership and MDC lifetime
Oct 7, 2026
2412f4a
fix: settle handler future when MDC capture fails
Oct 7, 2026
edf5053
fix: wake invocation caller on reported scope fatal
zhongkechen Oct 7, 2026
01fbdd7
Merge main OpenTelemetry API compatibility fix
Oct 7, 2026
e58f437
Merge main exclusive OpenTelemetry view validation
Oct 7, 2026
3e3eb38
fix: preserve execution outcomes across end MDC failures
Oct 7, 2026
6691a55
fix: accept concrete handler scope return types
Oct 7, 2026
0632f62
fix: contain malformed handler scope diagnostics
Oct 7, 2026
641458a
Merge branch 'main' into test/otel-conformance-21-24
zhongkechen Oct 7, 2026
a709ee0
refactor: finalize invocation hooks on the handler thread
zhongkechen Oct 7, 2026
dceb158
fix(plugin): enforce invocation lifecycle boundaries
Oct 7, 2026
2cc0737
fix: propagate fatal MDC initialization failures before startup
Oct 8, 2026
0501307
test: add matched OTel empty failure and external completion replay c…
Oct 8, 2026
445b60e
fix: preserve invocation end failures through scope cleanup
Oct 8, 2026
4f421de
test: require external completion on its actual resume invocation
Oct 8, 2026
33f3c35
test: assert stored parallel tolerance outcome across racing branches
Oct 8, 2026
64f582a
fix: report absent plugin details for an empty operation error
Oct 8, 2026
f2f71d6
test: pin portable callback completion requirements
Oct 8, 2026
7ce0242
test: pin final callback phase and replay conformance checks
Oct 8, 2026
14db8ae
fix: preserve delivery and settled invocation outcomes
Oct 8, 2026
4cc0f35
fix: retain ready condition work across checkpoint responses
Oct 8, 2026
1735ff8
fix: inspect MDC initialization causes without cycling
Oct 8, 2026
587edf7
fix: preserve terminal wait-for-condition outcomes on replay
Oct 8, 2026
880c672
fix: resume condition polling outside the checkpoint batcher
Oct 8, 2026
639241b
fix: yield READY checks to queued user operations
Oct 8, 2026
00c1271
fix: consume durable sampling intent before processor callbacks
Oct 8, 2026
7adb5a6
fix: drain owned condition continuations before manager shutdown
Oct 8, 2026
2df3d68
fix: retain live operation parents for native span clocks
Oct 8, 2026
eb77e3d
fix: preserve preparation failures and isolate sampling carriers
Oct 8, 2026
75df3be
fix: preserve foreign sampling metadata and execution isolation
Oct 8, 2026
6c59016
fix: select manager control flow before waking handlers
Oct 8, 2026
40597b3
fix: retain visible replacement sampler root policy
Oct 8, 2026
b882ea7
fix: propagate fatal checkpoint continuation failures
Oct 8, 2026
18d4e3c
fix: surface owned continuation failures and post-End fatals
Oct 8, 2026
a14e96e
fix: preserve retry and MDC boundaries in conformance integration
Oct 8, 2026
96356e4
fix: coordinate step retries outside the checkpoint batcher
Oct 9, 2026
a43deba
fix: retain fatal cleanup and preparation error diagnostics
Oct 9, 2026
e287325
fix: select continuation failures atomically with close
Oct 9, 2026
f6d7344
ci: use validated conformance runner for 26-case catalog
Oct 9, 2026
c59039a
fix: prune completed polls before checkpoint batching
Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/scripts/verify_otel_api_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,14 @@ def run_matrix(args: argparse.Namespace) -> int:
for view in ("otel-invocation", "otel-execution"):
name = f"{label}-api{version}-{view}"
negative = label == "old-old" and version == "1.49.0"
incompatible_core = label == "old-new"
case: dict[str, object] = {"name": name, "expected_negative_control": negative,
"expected_core_rejection": incompatible_core,
"api": jar_facts(api), "context": jar_facts(context)}
command = [args.java, "-cp", os.pathsep.join(map(str, [classes, core, plugin, *dependencies])),
PROBE, str(core), str(plugin), str(api), str(context), view,
str(negative).lower(), str(version == "1.66.0").lower()]
str(negative).lower(), str(version == "1.66.0").lower(),
str(incompatible_core).lower()]
try:
log = output / f"{name}.log"
execute(command, log, env=probe_environment(view), timeout=90)
Expand All @@ -155,6 +158,8 @@ def run_matrix(args: argparse.Namespace) -> int:
raise RuntimeError("Probe did not report successful completion")
if negative and "NEGATIVE_CONTROL_REPRODUCED" not in contents:
raise RuntimeError("Released negative control did not reproduce the reported failure")
if incompatible_core and "CORE_LIFECYCLE_REJECTION_CONFIRMED" not in contents:
raise RuntimeError("Old core must reject the new plugin before invocation startup")
case["passed"] = True
except RuntimeError as error:
failures += 1
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/otel-conformance-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ on:
conformance_test_ref:
description: Optional conformance test commit SHA or branch name
required: false
default: "75987d46a915bc37409eed3ea9c3617a924c9756"
type: string

permissions: {}
Expand All @@ -68,14 +69,14 @@ jobs:
actions: write
contents: read
id-token: write
uses: aws/aws-durable-execution-conformance-tests/.github/workflows/opentelemetry-orchestrator.yml@a66037abbbfa55fde97f714e30f0bc262edefd63
uses: aws/aws-durable-execution-conformance-tests/.github/workflows/opentelemetry-orchestrator.yml@bdb4f1cd0f9252c1aaa978bb8b341b71f2b9d9dc
with:
runs_on: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && 'ubuntu-latest' || format('codebuild-github-actions-runner-{0}-{1}', github.run_id, github.run_attempt) }}
language: java
resource_prefix: j
sdk_repository: aws/aws-durable-execution-sdk-java
sdk_ref: ${{ github.event.pull_request.head.sha || github.sha }}
conformance_test_ref: ${{ inputs.conformance_test_ref || '02d6dca971a38c13d94d6233d12f687e55b2a572' }}
conformance_test_ref: ${{ inputs.conformance_test_ref || '75987d46a915bc37409eed3ea9c3617a924c9756' }}
checkout_sdk: true
# Build the handlers from this repo's checked-out module instead of the conformance repo's
# bundled examples/java. Path is relative to the conformance workspace where the SDK is
Expand Down
74 changes: 74 additions & 0 deletions conformance-tests-otel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Java OpenTelemetry conformance handlers

The shared conformance repository owns the requirements and validators. This
module supplies public-API handlers and SAM resources for both tracing views.
Existing cases 1–20 and their resources are unchanged by the additions below.

| Case | Scenario | Behavior |
| --- | --- | --- |
| 21 | `completed-step-replay` | Complete a step, suspend on a one-second durable wait, then complete another step. The first step body is skipped on replay. |
| 22 | `user-function-context` | Probe handler entry/restoration/resume, step, child, concurrent parallel branches and map iterations, and their nested steps. |
| 23 | `callback-function-context` | Probe step retry attempts, condition checks, callback submission, a wrapped retry helper's body and strategy, and a virtual child after the asynchronous work finishes. |
| 24 | `invocation-retry-status` | Throw the public retryable execution exception after a checkpointed step, then recover on replay. |
| 25 | `wait-for-callback-failure` | Reuse case 17’s public handler; the driver fails its callback without Error only after invocation completion. The shared validator checks the observed empty error payload and leaf status. |
| 26 | `external-callback-completion-replay` | Observe a root callback completion, checkpoint its result, then revisit it through two later callback barriers without duplicate terminal export. |

Cases 22 and 23 require a valid active `SpanContext` and create/end ordinary
`conformance.<label>` user spans with `conformance.callback=<label>`. They do not
set parents, attach contexts, copy the execution ARN onto user spans, or create
SDK spans. The shared validator determines the canonical trace from SDK spans
and checks the actual user-span parents.

Root handler probes preserve a valid same-trace ambient Lambda parent. Otherwise
the plugin activates its canonical Invocation/Workflow context on the handler
thread and restores it in the same-thread end hook. The plugin requires the
core's 2.2.2 lifecycle capability; older cores are rejected during configuration.
Nested callbacks retain their named context or attempt parents. Execution-view
context placeholders can be valid without recording, so probes do not require
`isRecording()`.

Case 23 uses the public step attempt number and checkpointed condition state;
it has no mutable warm-container markers. Its helper and virtual child run
after callback completion, so normal suspension/replay does not repeat those
probes. Only the intentional helper failure is caught. The wrapped helper's
strategy executes inside its established context lifecycle; this does not
establish a parent contract for every other policy callback.

Serialization, map item naming, completion policies, standalone step retry or
condition wait strategies, and callbacks invoked before tracing setup are not
claimed to share the stable user-function hook boundary. Plugin hooks,
extractors, and samplers cannot be required to have made their future SDK span
current. Arbitrary user-created threads are also outside these cases.

Case 24 captures `isReplaying()` at handler entry before consuming the completed
step. Java's public `UnrecoverableDurableExecutionException(ErrorObject, true)`
requests invocation retry; the handler does not inject plugin hooks or
checkpoint messages. Its expected invocation statuses are `RETRYING` then
`SUCCEEDED`. Recovery may repeat telemetry delivery, so this case imposes no
exact operation-export count.

The module builds against the SDK selected by `JAVA_SDK_VERSION` or the
`durable.sdk.version` Maven property. Run matched shared requirements when
validating new cases; a workflow pinned to an older requirement set does not
validate the newly added cases. The configured OTel conformance workflow owns
deployment and backend validation.

Case 26 creates `otel-external-target` directly on the root context and reads its
stored result on every replay. The `otel-external-target-observed` step returns
that result unchanged and runs only on the first completion. Two later
`waitForCallback` barriers force two more handler replays. The shared driver sends
each callback success only after its creating invocation has completed, producing
four invocations and the final result `target/one/two`. Lossless S3 telemetry must
show one terminal export for each callback; the first target completion precedes
the observation-step attempt. This is SDK cloud behavior coverage, not a claim
that a deployed Lambda exercises the JS local-runner wrapper.

Cases 25–26 use matched shared requirements and workflow pin
`bdb4f1cd0f9252c1aaa978bb8b341b71f2b9d9dc`. The shared cloud checks establish the
service error-payload representation and callback phase gating; local runner
checks do not establish those service facts.

For case 25, Java reports no plugin error details for an entirely empty error
container. The persisted container, callback failure, and caller error stay
unchanged. Any present type, message, data, or stack-trace field remains an error
detail, including an empty string or an explicitly supplied empty stack list.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.lambda.durable.conformance.otel;

import java.time.Duration;
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;

/** Completed-step replay scenario for OTel requirement 21 in both views. */
public final class Otel21CompletedStepReplay extends OtelConformanceHandler<String> {

@Override
public String handleRequest(Map<String, Object> event, DurableContext context) {
requireScenario(event, "completed-step-replay");
var before = context.step("otel-before-wait", String.class, step -> "before");
context.wait("otel-replay-wait", Duration.ofSeconds(1));
var after = context.step("otel-after-wait", String.class, step -> "after");
return before + "-" + after;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.lambda.durable.conformance.otel;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.config.MapConfig;
import software.amazon.lambda.durable.config.ParallelConfig;

/** Active user-function context scenario for OTel requirement 22 in both views. */
public final class Otel22UserFunctionContext extends OtelConformanceHandler<String> {

@Override
public String handleRequest(Map<String, Object> event, DurableContext context) {
requireScenario(event, "user-function-context");
probe("handler");
context.step("otel-context-step", String.class, step -> {
probe("step");
return "step";
});
runChild(context);
runParallel(context);
runMap(context);
probe("handler-restored");
context.wait("otel-context-resume", Duration.ofSeconds(1));
probe("handler-after-resume");
return "context-complete";
}

private static void runChild(DurableContext context) {
context.runInChildContext("otel-context-child", String.class, child -> {
probe("child");
var result = child.step("otel-context-child-step", String.class, step -> {
probe("child-step");
return "child";
});
probe("child-restored");
return result;
});
}

private static void runMap(DurableContext context) {
context.map(
"otel-context-map",
List.of(0, 1),
Integer.class,
(item, index, iteration) -> {
probe("map-" + index);
return iteration.step("otel-context-map-step-" + index, Integer.class, step -> {
probe("map-step-" + index);
return item;
});
},
MapConfig.builder()
.maxConcurrency(2)
.itemNamer((item, index) -> "otel-context-iteration-" + index)
.build());
}

private static void runParallel(DurableContext context) {
var parallel = context.parallel(
"otel-context-parallel",
ParallelConfig.builder().maxConcurrency(2).build());
try (parallel) {
parallel.branch("otel-context-branch-a", String.class, branch -> {
probe("parallel-a");
return branch.step("otel-context-branch-step-a", String.class, step -> {
probe("parallel-step-a");
return "a";
});
});
parallel.branch("otel-context-branch-b", String.class, branch -> {
probe("parallel-b");
return branch.step("otel-context-branch-step-b", String.class, step -> {
probe("parallel-step-b");
return "b";
});
});
}
}

private static void probe(String label) {
if (!Span.current().getSpanContext().isValid()) {
throw new IllegalStateException("No active span context for " + label);
}
var span = GlobalOpenTelemetry.getTracer("durable-conformance")
.spanBuilder("conformance." + label)
.setAttribute("conformance.callback", label)
.startSpan();
span.end();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.lambda.durable.conformance.otel;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import java.time.Duration;
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.config.RunInChildContextConfig;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.config.WaitForConditionConfig;
import software.amazon.lambda.durable.config.WithRetryConfig;
import software.amazon.lambda.durable.exception.DurableOperationException;
import software.amazon.lambda.durable.model.WaitForConditionResult;
import software.amazon.lambda.durable.retry.RetryDecision;
import software.amazon.lambda.durable.retry.RetryStrategies;
import software.amazon.lambda.durable.retry.WaitStrategies;

/** Retry, polling, submitter, and helper callback contexts for OTel requirement 23. */
public final class Otel23CallbackFunctionContext extends OtelConformanceHandler<String> {
private static final String HELPER_FAILURE = "intentional-helper-failure";

@Override
public String handleRequest(Map<String, Object> event, DurableContext context) {
requireScenario(event, "callback-function-context");
runRetryStep(context);
runCondition(context);
context.waitForCallback(
"otel-context-callback", String.class, (callbackId, step) -> probe("callback-submitter"));
runFailedHelper(context);
context.runInChildContext(
"otel-context-virtual",
String.class,
child -> {
probe("virtual-child");
return "virtual";
},
RunInChildContextConfig.builder().isVirtual(true).build());
return "callback-context-complete";
}

private static void runRetryStep(DurableContext context) {
context.step(
"otel-context-retry-step",
String.class,
step -> {
var attempt = step.getAttempt();
probe("retry-attempt-" + attempt);
if (attempt == 1) throw new IllegalStateException("intentional-step-retry");
return "retried";
},
StepConfig.builder()
.retryStrategy(RetryStrategies.fixedDelay(2, Duration.ofSeconds(1)))
.build());
}

private static void runCondition(DurableContext context) {
context.waitForCondition(
"otel-context-condition",
Integer.class,
(state, step) -> {
var next = state + 1;
probe("condition-check-" + next);
return next == 2
? WaitForConditionResult.stopPolling(next)
: WaitForConditionResult.continuePolling(next);
},
WaitForConditionConfig.<Integer>builder()
.initialState(0)
.waitStrategy(WaitStrategies.fixedDelay(2, Duration.ofSeconds(1)))
.build());
}

private static void runFailedHelper(DurableContext context) {
try {
context.withRetry(
"otel-context-with-retry",
(attempt, child) -> {
probe("with-retry-body");
throw new IllegalStateException(HELPER_FAILURE);
},
WithRetryConfig.builder()
.wrapInChildContext(true)
.retryStrategy((error, attempt) -> {
probe("with-retry-strategy");
return RetryDecision.fail();
})
.build());
} catch (RuntimeException error) {
var message =
error instanceof DurableOperationException operationError && operationError.getErrorObject() != null
? operationError.getErrorObject().errorMessage()
: error.getMessage();
if (!HELPER_FAILURE.equals(message)) throw error;
}
}

private static void probe(String label) {
if (!Span.current().getSpanContext().isValid()) {
throw new IllegalStateException("No active span context for " + label);
}
var span = GlobalOpenTelemetry.getTracer("durable-conformance")
.spanBuilder("conformance." + label)
.setAttribute("conformance.callback", label)
.startSpan();
span.end();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.lambda.durable.conformance.otel;

import java.util.Map;
import software.amazon.awssdk.services.lambda.model.ErrorObject;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.exception.UnrecoverableDurableExecutionException;

/** Public invocation-retry signal and checkpoint recovery for OTel requirement 24. */
public final class Otel24InvocationRetryStatus extends OtelConformanceHandler<String> {

@Override
public String handleRequest(Map<String, Object> event, DurableContext context) {
requireScenario(event, "invocation-retry-status");
var replayingAtEntry = context.isReplaying();
context.step("otel-before-invocation-retry", String.class, step -> "saved");
if (!replayingAtEntry) {
throw new UnrecoverableDurableExecutionException(
ErrorObject.builder()
.errorType("ConformanceInvocationRetry")
.errorMessage("intentional-invocation-retry")
.build(),
true);
}
return "retry-complete";
}
}
Loading
Loading