diff --git a/.github/scripts/verify_otel_api_compatibility.py b/.github/scripts/verify_otel_api_compatibility.py index b0e88c7aa..30a89ccca 100644 --- a/.github/scripts/verify_otel_api_compatibility.py +++ b/.github/scripts/verify_otel_api_compatibility.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -"""Exercise real released/candidate core and plugin artifacts with two visible OTel APIs. +"""Exercise the current 3.x core, plugin, and testing artifacts with two visible OTel APIs. Dependency resolution and every probe are required: a network, compilation, or case failure returns nonzero. No production dependency versions are modified. @@ -105,67 +105,59 @@ def run_matrix(args: argparse.Namespace) -> int: output.mkdir(parents=True, exist_ok=True) fixture = root / "otel-plugin/src/test/compatibility/b1" cp = resolve_classpaths(fixture, output, args.maven) - released_core = artifact(cp["1.66.0"], CORE, RELEASED_VERSION) - released_plugin = artifact(cp["1.66.0"], PLUGIN, RELEASED_VERSION) - new_core = args.new_core.resolve() if args.new_core else candidate_jar(root, "sdk", CORE) - new_plugin = args.new_plugin.resolve() if args.new_plugin else candidate_jar(root, "otel-plugin", PLUGIN) - for jar in (new_core, new_plugin): - if not jar.is_file(): - raise RuntimeError(f"Candidate artifact missing: {jar}") - candidate_inputs = {"core": jar_facts(new_core), "plugin": jar_facts(new_plugin)} - new_core = snapshot_candidate(new_core, output) - new_plugin = snapshot_candidate(new_plugin, output) + testing_name = CORE + "-testing" + inputs = { + "core": args.new_core.resolve() if args.new_core else candidate_jar(root, "sdk", CORE), + "plugin": args.new_plugin.resolve() if args.new_plugin else candidate_jar(root, "otel-plugin", PLUGIN), + "testing": args.new_testing.resolve() if args.new_testing else candidate_jar(root, "sdk-testing", testing_name), + } + candidate_inputs = {name: jar_facts(path) for name, path in inputs.items()} + selected = {name: snapshot_candidate(path, output) for name, path in inputs.items()} + excluded = {f"{name}-{RELEASED_VERSION}.jar" for name in (CORE, PLUGIN, testing_name)} + dependencies = {version: [path for path in paths if path.name not in excluded] + for version, paths in cp.items()} classes = output / "classes" classes.mkdir(exist_ok=True) - execute([args.javac, "--release", "17", "-classpath", os.pathsep.join(map(str, cp["1.66.0"])), + compile_cp = [*selected.values(), *dependencies["1.66.0"]] + execute([args.javac, "--release", "17", "-classpath", os.pathsep.join(map(str, compile_cp)), "-d", str(classes), str(fixture / "InstalledApiProbe.java")], output / "compile.log") services = classes / "META-INF/services" services.mkdir(parents=True, exist_ok=True) (services / "software.amazon.lambda.durable.plugin.DurableExecutionPluginProvider").write_text( PROBE + "$HealthyProvider\n") report: dict[str, object] = { - "released_core": jar_facts(released_core), "released_plugin": jar_facts(released_plugin), - "new_core": jar_facts(new_core), "new_plugin": jar_facts(new_plugin), + "contract": "Current 3.x factory API; cross-major core/plugin mixtures are unsupported.", "candidate_inputs": candidate_inputs, + "candidate_snapshots": {name: jar_facts(path) for name, path in selected.items()}, "cases": [], "agent_coverage": "This matrix is visible-API skew, not a deployed Java-agent test.", } cases: list[dict[str, object]] = report["cases"] # type: ignore[assignment] failures = 0 - pairs = {"old-old": (released_core, released_plugin), "new-old": (new_core, released_plugin), - "old-new": (released_core, new_plugin), "new-new": (new_core, new_plugin)} for version in API_VERSIONS: api = artifact(cp[version], "opentelemetry-api", version) context = artifact(cp[version], "opentelemetry-context", version) - dependencies = [p for p in cp[version] if p.name not in - (f"{CORE}-{RELEASED_VERSION}.jar", f"{PLUGIN}-{RELEASED_VERSION}.jar")] - for label, (core, plugin) in pairs.items(): - for view in ("otel-invocation", "otel-execution"): - name = f"{label}-api{version}-{view}" - negative = label == "old-old" and version == "1.49.0" - case: dict[str, object] = {"name": name, "expected_negative_control": negative, - "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()] - try: - log = output / f"{name}.log" - execute(command, log, env=probe_environment(view), timeout=90) - contents = log.read_text(errors="replace") - if "COMPAT_PASS " not in contents: - 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") - case["passed"] = True - except RuntimeError as error: - failures += 1 - case.update(passed=False, error=str(error)) - cases.append(case) - (output / "results.json").write_text(json.dumps(report, indent=2) + "\n") - print(f"{'PASS' if case['passed'] else 'FAIL'} {name}", flush=True) + for view in ("otel-invocation", "otel-execution"): + name = f"current3x-api{version}-{view}" + case: dict[str, object] = {"name": name, "api": jar_facts(api), "context": jar_facts(context)} + command = [args.java, "-cp", os.pathsep.join(map(str, [classes, *selected.values(), *dependencies[version]])), + PROBE, str(selected["core"]), str(selected["plugin"]), str(api), str(context), view, + str(version == "1.66.0").lower(), str(selected["testing"])] + try: + log = output / f"{name}.log" + execute(command, log, env=probe_environment(view), timeout=90) + if "COMPAT_PASS " not in log.read_text(errors="replace"): + raise RuntimeError("Probe did not report successful completion") + case["passed"] = True + except RuntimeError as error: + failures += 1 + case.update(passed=False, error=str(error)) + cases.append(case) + (output / "results.json").write_text(json.dumps(report, indent=2) + "\n") + print(f"{'PASS' if case['passed'] else 'FAIL'} {name}", flush=True) report["passed"] = failures == 0 report["failure_count"] = failures (output / "results.json").write_text(json.dumps(report, indent=2) + "\n") - print(f"Installed artifact matrix: {len(cases) - failures}/{len(cases)} passed; {output / 'results.json'}") + print(f"Current 3.x artifact matrix: {len(cases) - failures}/{len(cases)} passed; {output / 'results.json'}") return 1 if failures else 0 @@ -175,6 +167,7 @@ def main() -> int: parser.add_argument("--output", type=Path, default=Path("target/otel-api-compatibility")) parser.add_argument("--new-core", type=Path) parser.add_argument("--new-plugin", type=Path) + parser.add_argument("--new-testing", type=Path) parser.add_argument("--maven", default=shutil.which("mvn") or "mvn") parser.add_argument("--java", default=shutil.which("java") or "java") parser.add_argument("--javac", default=shutil.which("javac") or "javac") diff --git a/.github/workflows/conformance-tests.yml b/.github/workflows/conformance-tests.yml index c066bf277..357fa8f97 100644 --- a/.github/workflows/conformance-tests.yml +++ b/.github/workflows/conformance-tests.yml @@ -21,7 +21,7 @@ on: concurrency: # Runs share fixed per-suite stack names, so serialize the whole workflow - # (queue, don't cancel) to avoid concurrent CloudFormation updates on the + # with multiple pending runs to avoid concurrent CloudFormation updates on the # same stack -- mirrors e2e-tests.yml. group: conformance-tests cancel-in-progress: false diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 5dd246d57..36f1341e3 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -24,6 +24,7 @@ on: - 'pom.xml' concurrency: + # Shared stacks require serial deployment; preserve waiting runs from other PRs. group: e2e-tests cancel-in-progress: false queue: max @@ -149,6 +150,26 @@ jobs: if [[ "$invariant_violated" == true ]]; then exit 1 fi + - name: Collect Lambda errors after failed E2E tests + if: failure() && env.E2E_LOG_START_TIME_MS != '' + env: + E2E_STACK_NAME: Java${{ matrix.java }}-JavaSDKCloudBasedIntegrationTestStack + run: | + set -euo pipefail + log_groups=$(aws cloudformation list-stack-resources \ + --stack-name "$E2E_STACK_NAME" \ + --query "StackResourceSummaries[?ResourceType=='AWS::Logs::LogGroup'].PhysicalResourceId" \ + --output text) + for log_group in $log_groups; do + echo "::group::Lambda errors: $log_group" + aws logs filter-log-events \ + --log-group-name "$log_group" \ + --start-time "$E2E_LOG_START_TIME_MS" \ + --filter-pattern '%ERROR|Error|Exception|timed.out|Invalid.suspension|not.active|already.registered%' \ + --limit 100 --no-paginate \ + --query 'events[].message' --output text + echo "::endgroup::" + done - name: Publish test case summary if: always() env: diff --git a/.github/workflows/otel-conformance-tests.yml b/.github/workflows/otel-conformance-tests.yml index 452db089d..f91ad632c 100644 --- a/.github/workflows/otel-conformance-tests.yml +++ b/.github/workflows/otel-conformance-tests.yml @@ -68,14 +68,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@f5855f2d0f60be996973173cf479c3567f83e30f 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 || '98b802cdb172614f98e217f7464784d47b9bb484' }} 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 diff --git a/AGENTS.md b/AGENTS.md index 7894488db..6cc0114c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -335,6 +335,7 @@ Run `mvn spotless:apply` after Java changes. Then run the narrowest relevant tes - [Error Handling](docs/advanced/error-handling.md) - [Logging](docs/advanced/logging.md) - [Migration from 1.x to 2.x](docs/migration-1.x-to-2.x.md) +- [Migration from 2.x to 3.x](docs/migration-2.x-to-3.x.md) ### Official AWS SDKs diff --git a/README.md b/README.md index 8b3184c63..c7e14c3d3 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ See [Deploy Lambda durable functions with Infrastructure as Code](https://docs.a - [Error Handling](docs/advanced/error-handling.md) - SDK exceptions for handling failures - [Logging](docs/advanced/logging.md) - How to use DurableLogger - [Migrating from 1.x to 2.x](docs/migration-1.x-to-2.x.md) - Upgrade guide for breaking changes since `v1.2.1` +- [Migrating from 2.x to 3.x](docs/migration-2.x-to-3.x.md) - Upgrade guide for the factory-only, per-invocation plugin contract - [Release Process](RELEASE.md) - Prepare and publish Maven releases - [Testing](docs/advanced/testing.md) - Utilities for local development and cloud-based integration testing diff --git a/conformance-tests-otel/README.md b/conformance-tests-otel/README.md new file mode 100644 index 000000000..7f4c31312 --- /dev/null +++ b/conformance-tests-otel/README.md @@ -0,0 +1,53 @@ +# 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. | + +Cases 22 and 23 require a valid active `SpanContext` and create/end ordinary +`conformance.