diff --git a/.github/scripts/tests/test_opentelemetry_conformance_workflow.py b/.github/scripts/tests/test_opentelemetry_conformance_workflow.py index df97b780..7ff9abb4 100644 --- a/.github/scripts/tests/test_opentelemetry_conformance_workflow.py +++ b/.github/scripts/tests/test_opentelemetry_conformance_workflow.py @@ -4,7 +4,10 @@ WORKFLOW_PATH = ( Path(__file__).parents[2] / "workflows" / "opentelemetry-conformance-tests.yml" ) -ORCHESTRATOR_REVISION = "397d523d01bdf97ff8461ab749ddaa445bbf67ca" +ORCHESTRATOR_REVISION = "8de1bbfdccfeb36d5f9ccee99f21533f0bcb1d72" +EXAMPLES_DIR = ( + ".build/durable-sdk/packages/aws-durable-execution-sdk-python-conformance-tests-otel" +) def test_opentelemetry_conformance_caller_uses_current_workflow_contract() -> None: @@ -24,9 +27,8 @@ def test_opentelemetry_conformance_caller_uses_current_workflow_contract() -> No "sdk_repository: aws/aws-durable-execution-sdk-python", "sdk_ref: ${{ github.event.pull_request.head.sha || github.sha }}", "conformance_test_ref: ${{ inputs.conformance_test_ref || 'main' }}", - "checkout_sdk: false", - "packages/aws-durable-execution-conformance-tests-otel/" - "tests/test_python_examples.py", + "checkout_sdk: true", + f"examples_dir: {EXAMPLES_DIR}", "adot_release_repository: aws-observability/aws-otel-python-instrumentation", "collector_compatible_runtime: python3.13", "collector_otlp_endpoint: http://localhost:4318", @@ -48,3 +50,23 @@ def test_opentelemetry_conformance_caller_uses_current_workflow_contract() -> No "DATADOG_OTLP_HEADERS", ): assert f"{obsolete_secret_name}:" not in workflow + + +def test_opentelemetry_conformance_handlers_come_from_this_repository() -> None: + workflow = WORKFLOW_PATH.read_text() + + # The handlers live here now, so the conformance repository's bundled Python + # example project and its contract test no longer take part in the run. + assert "contract_test_command" not in workflow + assert "packages/aws-durable-execution-conformance-tests-otel/" not in workflow + + +def test_opentelemetry_conformance_runs_when_the_handlers_change() -> None: + workflow = WORKFLOW_PATH.read_text() + + trigger_path = ( + " - " + '"packages/aws-durable-execution-sdk-python-conformance-tests-otel/**"' + ) + # Once for pull_request, once for push. + assert workflow.count(trigger_path) == 2 diff --git a/.github/workflows/opentelemetry-conformance-tests.yml b/.github/workflows/opentelemetry-conformance-tests.yml index 449da071..1bfb2027 100644 --- a/.github/workflows/opentelemetry-conformance-tests.yml +++ b/.github/workflows/opentelemetry-conformance-tests.yml @@ -1,17 +1,25 @@ name: OpenTelemetry Conformance Tests +# Per-PR OpenTelemetry conformance run. Calls the shared orchestrator in +# aws-durable-execution-conformance-tests and points it at this repo's own handlers and +# templates (packages/aws-durable-execution-sdk-python-conformance-tests-otel/) through +# examples_dir, so the handlers run from the commit under test while the orchestration +# (backend matrix, ADOT layer, collector build, long-running cycle) stays centralized. + on: pull_request: branches: ["main"] paths: - "packages/aws-durable-execution-sdk-python/**" - "packages/aws-durable-execution-sdk-python-otel/**" + - "packages/aws-durable-execution-sdk-python-conformance-tests-otel/**" - ".github/workflows/opentelemetry-conformance-tests.yml" push: branches: ["main"] paths: - "packages/aws-durable-execution-sdk-python/**" - "packages/aws-durable-execution-sdk-python-otel/**" + - "packages/aws-durable-execution-sdk-python-conformance-tests-otel/**" - ".github/workflows/opentelemetry-conformance-tests.yml" workflow_dispatch: inputs: @@ -48,17 +56,19 @@ jobs: actions: write contents: read id-token: write - uses: aws/aws-durable-execution-conformance-tests/.github/workflows/opentelemetry-orchestrator.yml@397d523d01bdf97ff8461ab749ddaa445bbf67ca + uses: aws/aws-durable-execution-conformance-tests/.github/workflows/opentelemetry-orchestrator.yml@8de1bbfdccfeb36d5f9ccee99f21533f0bcb1d72 with: language: python resource_prefix: p sdk_repository: aws/aws-durable-execution-sdk-python sdk_ref: ${{ github.event.pull_request.head.sha || github.sha }} conformance_test_ref: ${{ inputs.conformance_test_ref || 'main' }} - checkout_sdk: false - contract_test_command: >- - hatch run test:all - packages/aws-durable-execution-conformance-tests-otel/tests/test_python_examples.py + # Check the SDK out so the handlers and templates below are on disk. The handlers + # themselves are installed from sdk_ref by src/requirements.txt during the SAM build. + checkout_sdk: true + # Path is relative to the conformance workspace, where the SDK is checked out at + # .build/durable-sdk. + examples_dir: .build/durable-sdk/packages/aws-durable-execution-sdk-python-conformance-tests-otel adot_release_repository: aws-observability/aws-otel-python-instrumentation collector_compatible_runtime: python3.13 collector_otlp_endpoint: http://localhost:4318 diff --git a/AGENTS.md b/AGENTS.md index b23abc1b..ad1b986b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,7 +58,7 @@ for worked examples. All changes MUST include related tests. At minimum, include **unit tests**. Include **e2e integration tests** (in the `tests/e2e/` directory) when the change affects cross-component behavior, public API surfaces, or end-to-end workflows. For isolated bug fixes where a unit test alone sufficiently covers the fix, integration tests are not required. -Do NOT add or modify **conformance tests** without coordinating with the team. Conformance test requirements and the runner live in a separate repository ([aws-durable-execution-conformance-tests](https://github.com/aws/aws-durable-execution-conformance-tests)). The Python handlers live in-repo under `packages/aws-durable-execution-sdk-python-conformance-tests/`, but new requirement IDs must first be registered upstream. If a change warrants a new conformance test, note it in the PR description or [open an issue](https://github.com/aws/aws-durable-execution-conformance-tests/issues/new?template=new_requirement.yml) in that repository. +Do NOT add or modify **conformance tests** without coordinating with the team. Conformance test requirements and the runner live in a separate repository ([aws-durable-execution-conformance-tests](https://github.com/aws/aws-durable-execution-conformance-tests)). If a change warrants a new conformance test, note it in the PR description or [open an issue](https://github.com/aws/aws-durable-execution-conformance-tests/issues/new?template=new_requirement.yml) in that repository. ## Working in this repository diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/.gitignore b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/.gitignore new file mode 100644 index 00000000..3229e7c0 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/.gitignore @@ -0,0 +1,8 @@ +build/ +dist/ +.aws-sam/ +history-*/ +report-*.xml +report-*.json +__pycache__/ +*.pyc diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/README.md b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/README.md new file mode 100644 index 00000000..5ff978c8 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/README.md @@ -0,0 +1,201 @@ +# Durable Execution Python SDK - OpenTelemetry Conformance Tests + +OpenTelemetry **conformance test handlers** for the Durable Execution Python SDK +and its OpenTelemetry plugin: + +- [`aws-durable-execution-sdk-python`](https://pypi.org/project/aws-durable-execution-sdk-python/) +- [`aws-durable-execution-sdk-python-otel`](https://pypi.org/project/aws-durable-execution-sdk-python-otel/) + +The handlers deploy as AWS Lambda functions and are exercised by the +language-agnostic OTel conformance runner in +[`aws/aws-durable-execution-conformance-tests`](https://github.com/aws/aws-durable-execution-conformance-tests), +which invokes each function, collects its spans from the configured backend, and +asserts they match the shared requirement specification. That repository owns the +runner, the requirement specifications, and the orchestration (backend matrix, +ADOT layer resolution, collector build, long-running cycle); this package owns +the Python handlers and SAM templates. + +This mirrors the non-OTel +[`aws-durable-execution-sdk-python-conformance-tests`](../aws-durable-execution-sdk-python-conformance-tests) +package: handlers live next to the SDK so a PR runs them against its own commit. + +## Layout + +``` +src/ + common.py # plugin selection and input validation + otel__.py # one module per invocation/execution scenario + otel_long_running__.py + Makefile # SAM makefile build for every function + requirements.txt # SDK + OTel plugin, resolved from PYTHON_SDK_REF +template.yaml # otel-invocation and otel-execution suites +template-long-running.yaml # otel-long-running suite +tests/ # contract tests for the templates and handlers +``` + +The 20 invocation and 20 execution requirements reuse the same scenario +handlers; the view is selected per function through the `OTEL_PLUGIN_MODE` +environment variable, which `common.otel_plugin()` reads to pick +`InvocationOtelPlugin` or `ExecutionOtelPlugin`. `template.yaml` deploys only the +view named by its `OtelSuite` parameter. + +## Scenarios + +| Requirement | Handler | Behavior | +|---|---|---| +| `otel-invocation-1` | `otel_1_success.handler` | Verifies every successful step and attempt span. | +| `otel-invocation-2` | `otel_2_wait_resume.handler` | Verifies every wait, resume, and post-resume step span. | +| `otel-invocation-3` | `otel_3_retry.handler` | Verifies failed and successful retry attempts across invocations. | +| `otel-invocation-4` | `otel_4_terminal_failure.handler` | Verifies complete telemetry for a terminal execution failure. | +| `otel-invocation-5` | `otel_5_child_context.handler` | Verifies every child-context and nested-step span. | +| `otel-invocation-6` | `otel_6_parallel.handler` | Verifies every parallel context, branch, step, and attempt span. | +| `otel-invocation-7` | `otel_7_map.handler` | Verifies every map context, iteration, step, and attempt span. | +| `otel-invocation-8` | `otel_8_handled_failure.handler` | Verifies complete failed-step and recovery telemetry. | +| `otel-invocation-9` | `otel_9_wait_for_condition.handler` | Verifies every condition polling attempt and continuation. | +| `otel-invocation-10` | `otel_10_wait_for_callback.handler` | Verifies callback context, callback, and submitter spans. | +| `otel-invocation-11` | `otel_11_chained_invoke.handler` | Verifies chained-invoke continuation spans. | +| `otel-invocation-12` | `otel_12_child_context_failure.handler` | Verifies a failed child-context span. | +| `otel-invocation-13` | `otel_13_parallel_failure.handler` | Verifies failed parallel-branch telemetry. | +| `otel-invocation-14` | `otel_14_map_failure.handler` | Verifies failed map-iteration telemetry. | +| `otel-invocation-15` | `otel_15_wait_interrupted.handler` | Verifies an interrupted wait when execution times out. | +| `otel-invocation-16` | `otel_16_wait_for_condition_failure.handler` | Verifies failed condition-check telemetry. | +| `otel-invocation-17` | `otel_17_wait_for_callback_failure.handler` | Verifies external callback-failure telemetry. | +| `otel-invocation-18` | `otel_18_chained_invoke_failure.handler` | Verifies failed chained-invoke telemetry. | +| `otel-invocation-19` | `otel_19_execution_failure.handler` | Verifies telemetry for a direct handler failure. | +| `otel-invocation-20` | `otel_20_virtual_context.handler` | Verifies a virtual child-context span without context checkpoints. | +| `otel-execution-1` | `otel_1_success.handler` | Verifies the execution-view workflow, step, and attempt hierarchy. | +| `otel-execution-2` | `otel_2_wait_resume.handler` | Verifies the execution view across a resumed invocation. | +| `otel-execution-3` | `otel_3_retry.handler` | Verifies the execution view across retry attempts. | +| `otel-execution-4` | `otel_4_terminal_failure.handler` | Verifies the failed workflow, step, and attempt hierarchy. | +| `otel-execution-5` | `otel_5_child_context.handler` | Verifies child-context and nested-step parentage. | +| `otel-execution-6` | `otel_6_parallel.handler` | Verifies parallel context, branch, step, and attempt parentage. | +| `otel-execution-7` | `otel_7_map.handler` | Verifies map context, iteration, step, and attempt parentage. | +| `otel-execution-8` | `otel_8_handled_failure.handler` | Verifies failed and recovery operations under a successful workflow. | +| `otel-execution-9` | `otel_9_wait_for_condition.handler` | Verifies condition polling attempts across invocations. | +| `otel-execution-10` | `otel_10_wait_for_callback.handler` | Verifies callback, submitter, and attempt parentage. | +| `otel-execution-11` | `otel_11_chained_invoke.handler` | Verifies source and target workflow roots for a chained invoke. | +| `otel-execution-12` | `otel_12_child_context_failure.handler` | Verifies a failed child context under a failed workflow. | +| `otel-execution-13` | `otel_13_parallel_failure.handler` | Verifies a failed parallel branch under its operation. | +| `otel-execution-14` | `otel_14_map_failure.handler` | Verifies a failed map iteration under its operation. | +| `otel-execution-15` | `otel_15_wait_interrupted.handler` | Verifies a pending invocation when workflow spans do not complete. | +| `otel-execution-16` | `otel_16_wait_for_condition_failure.handler` | Verifies a failed condition operation and attempt. | +| `otel-execution-17` | `otel_17_wait_for_callback_failure.handler` | Verifies failed callback telemetry under one workflow. | +| `otel-execution-18` | `otel_18_chained_invoke_failure.handler` | Verifies source and target failed workflow roots. | +| `otel-execution-19` | `otel_19_execution_failure.handler` | Verifies a failed invocation without a completed workflow. | +| `otel-execution-20` | `otel_20_virtual_context.handler` | Verifies a virtual child-context span under the workflow root. | +| `otel-long-running-1` | `otel_long_running_1_wait.handler` | Verifies wait and resume telemetry across a long durable suspension. | +| `otel-long-running-2` | `otel_long_running_2_retry.handler` | Verifies retry telemetry across a long durable backoff. | +| `otel-long-running-3` | `otel_long_running_3_callback.handler` | Verifies callback telemetry when completion arrives after a long delay. | +| `otel-long-running-4` | `otel_long_running_4_chained_invoke.handler` | Verifies chained-invoke telemetry while the target stays suspended. | + +The runner discovers each mapping from `TestingMetadata.TestDescription` on the +functions in the templates. + +## How a handler maps to a requirement + +```yaml +Otel1Success: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-1 + Properties: + CodeUri: src/ + Handler: otel_1_success.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-1" + Role: !Ref LambdaExecutionRoleArn +``` + +## Building + +`src/requirements.txt` installs both SDK packages from the single commit in +`PYTHON_SDK_REF`, so every function in a run uses the same core and plugin +revision. The explicit `src/Makefile` build avoids SAM's package metadata +inspection, which does not support Git monorepo subdirectory dependencies, and +resolves binary dependencies for Lambda's `manylinux2014_x86_64` platform when +building from macOS. + +```bash +cd packages/aws-durable-execution-sdk-python-conformance-tests-otel +export PYTHON_SDK_REF=$(git rev-parse HEAD) # must be pushed to the SDK remote +sam build --template-file template.yaml +``` + +## Running a suite + +Prerequisites: the AWS SAM CLI, AWS credentials for an account where Durable +Execution is available, and an execution role allowing Durable Execution, logs, +and X-Ray. + +```bash +pip install \ + aws-durable-execution-conformance-tests \ + aws-durable-execution-conformance-tests-otel + +durable-execution-conformance \ + --template packages/aws-durable-execution-sdk-python-conformance-tests-otel/template.yaml \ + --language python \ + --suite otel-invocation \ + --parameter-overrides \ + LambdaExecutionRoleArn=arn:aws:iam::123456789012:role/example \ + OtelSuite=otel-invocation \ + --otel-exporter adot \ + --otel-layer-arn "$ADOT_LAYER_ARN" \ + --otel-service-name durable-execution-conformance \ + --otel-backend xray +``` + +Set `ADOT_LAYER_ARN` to the current regional ARN from the +[ADOT Python release](https://github.com/aws-observability/aws-otel-python-instrumentation/releases/latest). +The runner supplies the remaining OTel SAM parameters. + +To assert against official OTLP payloads instead of X-Ray, the runner can target +a collector extension that writes OTLP objects to S3: + +```bash +durable-execution-conformance \ + --template packages/aws-durable-execution-sdk-python-conformance-tests-otel/template.yaml \ + --language python \ + --suite otel-invocation \ + --parameter-overrides \ + LambdaExecutionRoleArn=arn:aws:iam::123456789012:role/example \ + OtelSuite=otel-invocation \ + OtelCollectorLayerArn="$COLLECTOR_LAYER_ARN" \ + OtelCollectorBucket="$OTEL_S3_BUCKET" \ + OtelCollectorPrefix=traces \ + --otel-exporter community \ + --otel-endpoint http://localhost:4318 \ + --otel-backend collector \ + --otel-backend-endpoint "s3://$OTEL_S3_BUCKET/traces" +``` + +The collector layer is built by the conformance repository's +`collector/build-lambda-layer.sh` and packages its config at +`/opt/collector-config/config-s3.yaml`. The function role needs prefix-scoped S3 +write access; the runner identity needs list, read, and cleanup access. + +## Authoring a new scenario + +1. Find or add the requirement in the conformance repository under + `test-requirements//.yaml`. New requirement IDs must be registered + there first. +2. Add `src/otel__.py` exporting `handler`. Select the plugin with + `common.otel_plugin()` and guard the input with `common.require_scenario()`. + Use the SDK's real API; never hand-roll behavior to force an expected result. +3. Register the function in `template.yaml` (or `template-long-running.yaml`) + with `Handler: .handler` and `TestDescription: [""]`, and add a + `build-` target to `src/Makefile`. +4. Update `tests/test_otel_examples.py`, which pins the template-to-requirement + mapping. + +## CI + +`.github/workflows/opentelemetry-conformance-tests.yml` calls the shared +orchestrator in the conformance repository and points it at this package with +`examples_dir`, so orchestration stays centralized while the handlers run from +the commit under test. Pull requests run the invocation and execution suites plus +a short (60-second) long-running cycle; the full multi-hour long-running cycle is +driven by `workflow_dispatch` with `phase: launch` and `phase: check`. diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/pyproject.toml b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/pyproject.toml new file mode 100644 index 00000000..da9a788e --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "aws-durable-execution-sdk-python-conformance-tests-otel" +version = "0.0.0" +description = "OpenTelemetry conformance test handlers for the AWS Durable Execution SDK for Python, exercised by the aws-durable-execution-conformance-tests OTel suites." +requires-python = ">=3.11" +dependencies = [ + "aws-durable-execution-sdk-python==1.8.0", + "aws-durable-execution-sdk-python-otel==0.4.0", +] + +[tool.hatch.build.targets.wheel] +# The handlers are flat Lambda modules deployed by SAM from src/, not an +# importable package, so there is nothing to ship in a wheel. +bypass-selection = true + +[tool.ruff] +line-length = 88 +target-version = "py311" + +[tool.ruff.lint] +preview = true +select = ["E4", "E7", "E9", "F", "TID252"] # pycodestyle (E4/E7/E9) + Pyflakes + absolute imports + +[tool.ruff.lint.isort] +known-first-party = [ + "aws_durable_execution_sdk_python", + "aws_durable_execution_sdk_python_otel", +] +force-single-line = false +lines-after-imports = 2 + +[tool.ruff.lint.per-file-ignores] +"tests/**" = [ + "ARG001", + "ARG002", + "ARG005", + "E402", + "S101", + "PLR2004", + "SIM117", + "TRY301", +] diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/Makefile b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/Makefile new file mode 100644 index 00000000..320577e7 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/Makefile @@ -0,0 +1,53 @@ +.PHONY: build-Otel1Success build-Otel2WaitResume build-Otel3Retry build-Otel4TerminalFailure \ + build-Otel5ChildContext build-Otel6Parallel build-Otel7Map build-Otel8HandledFailure \ + build-Otel9WaitForCondition build-Otel10WaitForCallback build-Otel11ChainedInvoke \ + build-Otel11InvokeTarget build-Otel12ChildContextFailure build-Otel13ParallelFailure \ + build-Otel14MapFailure build-Otel15WaitInterrupted build-Otel16WaitForConditionFailure \ + build-Otel17WaitForCallbackFailure build-Otel18ChainedInvokeFailure \ + build-Otel18InvokeTarget build-Otel19ExecutionFailure build-Otel20VirtualContext \ + build-OtelExecution1Success \ + build-OtelExecution2WaitResume build-OtelExecution3Retry \ + build-OtelExecution4TerminalFailure build-OtelExecution5ChildContext \ + build-OtelExecution6Parallel build-OtelExecution7Map build-OtelExecution8HandledFailure \ + build-OtelExecution9WaitForCondition build-OtelExecution10WaitForCallback \ + build-OtelExecution11ChainedInvoke build-OtelExecution11InvokeTarget \ + build-OtelExecution12ChildContextFailure build-OtelExecution13ParallelFailure \ + build-OtelExecution14MapFailure build-OtelExecution15WaitInterrupted \ + build-OtelExecution16WaitForConditionFailure \ + build-OtelExecution17WaitForCallbackFailure build-OtelExecution18ChainedInvokeFailure \ + build-OtelExecution18InvokeTarget build-OtelExecution19ExecutionFailure \ + build-OtelExecution20VirtualContext \ + build-OtelLongRunning1Wait \ + build-OtelLongRunning2Retry build-OtelLongRunning3Callback \ + build-OtelLongRunning4ChainedInvoke build-OtelLongRunning4InvokeTarget + +build-Otel1Success build-Otel2WaitResume build-Otel3Retry build-Otel4TerminalFailure \ +build-Otel5ChildContext build-Otel6Parallel build-Otel7Map build-Otel8HandledFailure \ +build-Otel9WaitForCondition build-Otel10WaitForCallback build-Otel11ChainedInvoke \ +build-Otel11InvokeTarget build-Otel12ChildContextFailure build-Otel13ParallelFailure \ +build-Otel14MapFailure build-Otel15WaitInterrupted build-Otel16WaitForConditionFailure \ +build-Otel17WaitForCallbackFailure build-Otel18ChainedInvokeFailure \ +build-Otel18InvokeTarget build-Otel19ExecutionFailure build-Otel20VirtualContext \ +build-OtelExecution1Success \ +build-OtelExecution2WaitResume build-OtelExecution3Retry \ +build-OtelExecution4TerminalFailure build-OtelExecution5ChildContext \ +build-OtelExecution6Parallel build-OtelExecution7Map build-OtelExecution8HandledFailure \ +build-OtelExecution9WaitForCondition build-OtelExecution10WaitForCallback \ +build-OtelExecution11ChainedInvoke build-OtelExecution11InvokeTarget \ +build-OtelExecution12ChildContextFailure build-OtelExecution13ParallelFailure \ +build-OtelExecution14MapFailure build-OtelExecution15WaitInterrupted \ +build-OtelExecution16WaitForConditionFailure \ +build-OtelExecution17WaitForCallbackFailure build-OtelExecution18ChainedInvokeFailure \ +build-OtelExecution18InvokeTarget build-OtelExecution19ExecutionFailure \ +build-OtelExecution20VirtualContext \ +build-OtelLongRunning1Wait \ +build-OtelLongRunning2Retry build-OtelLongRunning3Callback \ +build-OtelLongRunning4ChainedInvoke build-OtelLongRunning4InvokeTarget: + python3 -m pip install --upgrade \ + --target "$(ARTIFACTS_DIR)" \ + --platform manylinux2014_x86_64 \ + --implementation cp \ + --python-version 3.13 \ + --only-binary=:all: \ + -r requirements.txt + cp ./*.py "$(ARTIFACTS_DIR)/" diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/common.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/common.py new file mode 100644 index 00000000..7c8b7c59 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/common.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Shared input validation for the Python OTel conformance examples.""" + +from __future__ import annotations + +import os +from collections.abc import Mapping +from typing import Any + +from aws_durable_execution_sdk_python.plugin import DurableInstrumentationPlugin +from aws_durable_execution_sdk_python_otel import ( + ExecutionOtelPlugin, + InvocationOtelPlugin, + OtelPluginConfig, +) + + +def otel_plugin() -> DurableInstrumentationPlugin: + """Select the telemetry view configured for this deployed function.""" + + if os.environ.get("OTEL_PLUGIN_MODE") == "execution": + return ExecutionOtelPlugin(OtelPluginConfig()) + return InvocationOtelPlugin(OtelPluginConfig()) + + +def require_scenario(event: Mapping[str, Any], expected: str) -> None: + """Reject an event that was routed to the wrong conformance handler.""" + + actual = event.get("scenario") + if actual != expected: + raise ValueError(f"Expected scenario {expected!r}, received {actual!r}") + + +def long_delay_seconds(event: Mapping[str, Any]) -> int: + """Read a workflow delay constrained to the suite's one-day limit.""" + + raw_delay = event.get("delay_seconds") + if isinstance(raw_delay, bool): + raise ValueError("delay_seconds must be an integer from 1 through 86400") + try: + delay = int(raw_delay) + except (TypeError, ValueError) as exc: + raise ValueError( + "delay_seconds must be an integer from 1 through 86400" + ) from exc + if delay < 1 or delay > 86400: + raise ValueError("delay_seconds must be an integer from 1 through 86400") + return delay diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_10_wait_for_callback.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_10_wait_for_callback.py new file mode 100644 index 00000000..3807d2ae --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_10_wait_for_callback.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Wait-for-callback scenario for OTel requirement otel-invocation-10.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import DurableContext, durable_execution +from aws_durable_execution_sdk_python.types import WaitForCallbackContext +from common import otel_plugin, require_scenario + + +def submit_callback( + _callback_id: str, + _context: WaitForCallbackContext, +) -> None: + return None + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> str: + require_scenario(event, "wait-for-callback") + return context.wait_for_callback( + submit_callback, + name="otel-callback", + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_11_chained_invoke.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_11_chained_invoke.py new file mode 100644 index 00000000..0a43b520 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_11_chained_invoke.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Chained-invoke scenario for OTel requirement otel-invocation-11.""" + +from __future__ import annotations + +import os +from typing import Any + +from aws_durable_execution_sdk_python import DurableContext, durable_execution +from common import otel_plugin, require_scenario + + +@durable_execution(plugins=[otel_plugin()]) +def handler( + event: dict[str, Any], + context: DurableContext, +) -> dict[str, Any]: + require_scenario(event, "chained-invoke") + return context.invoke( + function_name=os.environ["OTEL_INVOKE_TARGET_FUNCTION_NAME"], + payload=event, + name="otel-invoke", + ) + + +@durable_execution(plugins=[otel_plugin()]) +def target_handler( + event: dict[str, Any], + _context: DurableContext, +) -> dict[str, Any]: + return event diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_12_child_context_failure.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_12_child_context_failure.py new file mode 100644 index 00000000..2d06b9a3 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_12_child_context_failure.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Failed child-context scenario for OTel requirement otel-invocation-12.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + durable_execution, + durable_with_child_context, +) +from common import otel_plugin, require_scenario + + +@durable_with_child_context +def fail_child_context(_context: DurableContext) -> None: + raise RuntimeError("Intentional child-context failure") + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> None: + require_scenario(event, "child-context-failure") + context.run_in_child_context( + fail_child_context(), + name="otel-failed-child-context", + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_13_parallel_failure.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_13_parallel_failure.py new file mode 100644 index 00000000..c36fadf7 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_13_parallel_failure.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Failed parallel scenario for OTel requirement otel-invocation-13.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + durable_execution, + durable_parallel_branch, +) +from aws_durable_execution_sdk_python.config import ParallelConfig +from common import otel_plugin, require_scenario + + +@durable_parallel_branch(name="otel-failed-parallel-branch") +def fail_parallel_branch(_context: DurableContext) -> None: + raise RuntimeError("Intentional parallel branch failure") + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> None: + require_scenario(event, "parallel-failure") + result = context.parallel( + functions=[fail_parallel_branch()], + name="otel-failed-parallel", + config=ParallelConfig(max_concurrency=1), + ) + result.throw_if_error() diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_14_map_failure.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_14_map_failure.py new file mode 100644 index 00000000..b77b1ddd --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_14_map_failure.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Failed map scenario for OTel requirement otel-invocation-14.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +from aws_durable_execution_sdk_python import DurableContext, durable_execution +from aws_durable_execution_sdk_python.config import MapConfig +from common import otel_plugin, require_scenario + + +def fail_map_item( + _context: DurableContext, + _item: int, + _index: int, + _items: Sequence[int], +) -> None: + raise RuntimeError("Intentional map iteration failure") + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> None: + require_scenario(event, "map-failure") + result = context.map( + inputs=[1], + func=fail_map_item, + name="otel-failed-map", + config=MapConfig( + item_namer=lambda _item, index: f"otel-failed-map-iteration-{index}", + max_concurrency=1, + ), + ) + result.throw_if_error() diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_15_wait_interrupted.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_15_wait_interrupted.py new file mode 100644 index 00000000..f8c225dc --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_15_wait_interrupted.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Interrupted wait scenario for OTel requirement otel-invocation-15.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import DurableContext, durable_execution +from aws_durable_execution_sdk_python.config import Duration +from common import otel_plugin, require_scenario + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> None: + require_scenario(event, "wait-interrupted") + context.wait( + Duration.from_seconds(30), + name="otel-interrupted-wait", + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_16_wait_for_condition_failure.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_16_wait_for_condition_failure.py new file mode 100644 index 00000000..db60d3c8 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_16_wait_for_condition_failure.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Failed wait-for-condition scenario for OTel requirement otel-invocation-16.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import DurableContext, durable_execution +from aws_durable_execution_sdk_python.config import Duration +from aws_durable_execution_sdk_python.types import WaitForConditionCheckContext +from aws_durable_execution_sdk_python.waits import ( + WaitForConditionConfig, + WaitForConditionDecision, +) +from common import otel_plugin, require_scenario + + +def fail_condition_check( + _state: int, + _context: WaitForConditionCheckContext, +) -> int: + raise RuntimeError("Intentional condition check failure") + + +def continue_condition( + _state: int, + _attempt: int, +) -> WaitForConditionDecision: + return WaitForConditionDecision.continue_waiting(Duration.from_seconds(1)) + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> None: + require_scenario(event, "wait-for-condition-failure") + context.wait_for_condition( + check=fail_condition_check, + name="otel-failed-condition", + config=WaitForConditionConfig( + initial_state=0, + wait_strategy=continue_condition, + ), + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_17_wait_for_callback_failure.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_17_wait_for_callback_failure.py new file mode 100644 index 00000000..8aa55881 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_17_wait_for_callback_failure.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Failed wait-for-callback scenario for OTel requirement otel-invocation-17.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import DurableContext, durable_execution +from aws_durable_execution_sdk_python.types import WaitForCallbackContext +from common import otel_plugin, require_scenario + + +def submit_failed_callback( + _callback_id: str, + _context: WaitForCallbackContext, +) -> None: + return None + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> None: + require_scenario(event, "wait-for-callback-failure") + context.wait_for_callback( + submit_failed_callback, + name="otel-failed-callback", + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_18_chained_invoke_failure.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_18_chained_invoke_failure.py new file mode 100644 index 00000000..c8acdd28 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_18_chained_invoke_failure.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Failed chained-invoke scenario for OTel requirement otel-invocation-18.""" + +from __future__ import annotations + +import os +from typing import Any + +from aws_durable_execution_sdk_python import DurableContext, durable_execution +from common import otel_plugin, require_scenario + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> None: + require_scenario(event, "chained-invoke-failure") + context.invoke( + function_name=os.environ["OTEL_INVOKE_TARGET_FUNCTION_NAME"], + payload=event, + name="otel-failed-invoke", + ) + + +@durable_execution(plugins=[otel_plugin()]) +def target_handler( + _event: dict[str, Any], + _context: DurableContext, +) -> None: + raise RuntimeError("Intentional chained-invoke target failure") diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_19_execution_failure.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_19_execution_failure.py new file mode 100644 index 00000000..887a488c --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_19_execution_failure.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Failed execution scenario for OTel requirement otel-invocation-19.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import DurableContext, durable_execution +from common import otel_plugin, require_scenario + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], _context: DurableContext) -> None: + require_scenario(event, "execution-failure") + raise RuntimeError("Intentional execution failure") diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_1_success.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_1_success.py new file mode 100644 index 00000000..0409588f --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_1_success.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Successful execution shared by OTel view case 1.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, +) +from common import otel_plugin, require_scenario + + +@durable_step +def complete_successfully(_step_context: StepContext) -> str: + return "success" + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> str: + require_scenario(event, "success") + return context.step(complete_successfully(), name="otel-success") diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_20_virtual_context.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_20_virtual_context.py new file mode 100644 index 00000000..1f957bdb --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_20_virtual_context.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Virtual child-context scenario for OTel requirement otel-invocation-20.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + durable_execution, + durable_with_child_context, +) +from aws_durable_execution_sdk_python.config import ChildConfig +from common import otel_plugin, require_scenario + + +@durable_with_child_context +def run_virtual_context(_context: DurableContext) -> str: + return "virtual-complete" + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> str: + require_scenario(event, "virtual-context") + return context.run_in_child_context( + run_virtual_context(), + name="otel-virtual-context", + config=ChildConfig(is_virtual=True), + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_2_wait_resume.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_2_wait_resume.py new file mode 100644 index 00000000..21041a94 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_2_wait_resume.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Wait and resume scenario shared by OTel view case 2.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, +) +from aws_durable_execution_sdk_python.config import Duration +from common import otel_plugin, require_scenario + + +@durable_step +def complete_after_resume(_step_context: StepContext) -> str: + return "resumed" + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> str: + require_scenario(event, "wait-resume") + context.wait(Duration.from_seconds(1), name="otel-wait") + return context.step(complete_after_resume(), name="otel-after-resume") diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_3_retry.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_3_retry.py new file mode 100644 index 00000000..761f76b4 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_3_retry.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Retried operation scenario shared by OTel view case 3.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, +) +from aws_durable_execution_sdk_python.config import Duration, StepConfig +from aws_durable_execution_sdk_python.retries import ( + RetryStrategyConfig, + create_retry_strategy, +) +from common import otel_plugin, require_scenario + + +@durable_step +def succeed_on_retry(step_context: StepContext) -> str: + if step_context.attempt == 1: + raise RuntimeError("Intentional first-attempt failure") + return "retried" + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> str: + require_scenario(event, "retry") + retry_strategy = create_retry_strategy( + RetryStrategyConfig( + max_attempts=2, + initial_delay=Duration.from_seconds(1), + backoff_rate=1.0, + retryable_error_types=[RuntimeError], + ) + ) + return context.step( + succeed_on_retry(), + name="otel-retry", + config=StepConfig(retry_strategy=retry_strategy), + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_4_terminal_failure.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_4_terminal_failure.py new file mode 100644 index 00000000..5850757e --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_4_terminal_failure.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Terminal execution failure scenario for OTel requirement otel-invocation-4.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, +) +from aws_durable_execution_sdk_python.config import StepConfig +from aws_durable_execution_sdk_python.retries import ( + RetryStrategyConfig, + create_retry_strategy, +) +from common import otel_plugin, require_scenario + + +@durable_step +def fail_terminally(_step_context: StepContext) -> None: + raise RuntimeError("Intentional terminal failure") + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> None: + require_scenario(event, "terminal-failure") + retry_strategy = create_retry_strategy( + RetryStrategyConfig( + max_attempts=1, + retryable_error_types=[RuntimeError], + ) + ) + context.step( + fail_terminally(), + name="otel-terminal-failure", + config=StepConfig(retry_strategy=retry_strategy), + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_5_child_context.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_5_child_context.py new file mode 100644 index 00000000..b9d47534 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_5_child_context.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Child-context hierarchy scenario for OTel requirement otel-invocation-5.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, + durable_with_child_context, +) +from common import otel_plugin, require_scenario + + +@durable_step +def complete_child_step(_step_context: StepContext) -> str: + return "child-complete" + + +@durable_with_child_context +def run_child_workflow(context: DurableContext) -> str: + return context.step(complete_child_step(), name="otel-child-step") + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> str: + require_scenario(event, "child-context") + return context.run_in_child_context( + run_child_workflow(), + name="otel-child-context", + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_6_parallel.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_6_parallel.py new file mode 100644 index 00000000..e2ac6adb --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_6_parallel.py @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Parallel hierarchy scenario for OTel requirement otel-invocation-6.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_parallel_branch, + durable_step, +) +from aws_durable_execution_sdk_python.config import ParallelConfig +from common import otel_plugin, require_scenario + + +@durable_step +def complete_parallel_step(_step_context: StepContext, label: str) -> str: + return label + + +@durable_parallel_branch(name="otel-parallel-branch-a") +def run_parallel_branch_a(context: DurableContext) -> str: + return context.step( + complete_parallel_step("a"), + name="otel-parallel-step-a", + ) + + +@durable_parallel_branch(name="otel-parallel-branch-b") +def run_parallel_branch_b(context: DurableContext) -> str: + return context.step( + complete_parallel_step("b"), + name="otel-parallel-step-b", + ) + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> list[str]: + require_scenario(event, "parallel-hierarchy") + return context.parallel( + functions=[ + run_parallel_branch_a(), + run_parallel_branch_b(), + ], + name="otel-parallel", + config=ParallelConfig(max_concurrency=1), + ).get_results() diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_7_map.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_7_map.py new file mode 100644 index 00000000..b35ad6e7 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_7_map.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Map hierarchy scenario for OTel requirement otel-invocation-7.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, +) +from aws_durable_execution_sdk_python.config import MapConfig +from common import otel_plugin, require_scenario + + +@durable_step +def double_map_item(_step_context: StepContext, item: int) -> int: + return item * 2 + + +def process_map_item( + context: DurableContext, + item: int, + index: int, + _items: Sequence[int], +) -> int: + return context.step( + double_map_item(item), + name=f"otel-map-step-{index}", + ) + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> list[int]: + require_scenario(event, "map-hierarchy") + return context.map( + inputs=[1, 2], + func=process_map_item, + name="otel-map", + config=MapConfig( + item_namer=lambda _item, index: f"otel-map-iteration-{index}", + max_concurrency=1, + ), + ).get_results() diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_8_handled_failure.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_8_handled_failure.py new file mode 100644 index 00000000..44fe595d --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_8_handled_failure.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Handled step failure scenario for OTel requirement otel-invocation-8.""" + +from __future__ import annotations + +import contextlib +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + StepError, + durable_execution, + durable_step, +) +from aws_durable_execution_sdk_python.config import StepConfig +from aws_durable_execution_sdk_python.retries import ( + RetryStrategyConfig, + create_retry_strategy, +) +from common import otel_plugin, require_scenario + + +@durable_step +def fail_and_handle(_step_context: StepContext) -> None: + raise RuntimeError("Intentional handled failure") + + +@durable_step +def recover_after_failure(_step_context: StepContext) -> str: + return "recovered" + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> str: + require_scenario(event, "handled-failure") + retry_strategy = create_retry_strategy( + RetryStrategyConfig( + max_attempts=1, + retryable_error_types=[RuntimeError], + ) + ) + with contextlib.suppress(StepError): + context.step( + fail_and_handle(), + name="otel-handled-failure", + config=StepConfig(retry_strategy=retry_strategy), + ) + return context.step( + recover_after_failure(), + name="otel-recovery-step", + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_9_wait_for_condition.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_9_wait_for_condition.py new file mode 100644 index 00000000..5daf3b5e --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_9_wait_for_condition.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Wait-for-condition scenario for OTel requirement otel-invocation-9.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import DurableContext, durable_execution +from aws_durable_execution_sdk_python.config import Duration +from aws_durable_execution_sdk_python.types import WaitForConditionCheckContext +from aws_durable_execution_sdk_python.waits import ( + WaitForConditionConfig, + WaitForConditionDecision, +) +from common import otel_plugin, require_scenario + + +def increment_condition( + state: int, + _context: WaitForConditionCheckContext, +) -> int: + return state + 1 + + +def stop_after_second_attempt( + state: int, + _attempt: int, +) -> WaitForConditionDecision: + if state >= 2: + return WaitForConditionDecision.stop_polling() + return WaitForConditionDecision.continue_waiting(Duration.from_seconds(1)) + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> int: + require_scenario(event, "wait-for-condition") + return context.wait_for_condition( + check=increment_condition, + name="otel-condition", + config=WaitForConditionConfig( + initial_state=0, + wait_strategy=stop_after_second_attempt, + ), + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_1_wait.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_1_wait.py new file mode 100644 index 00000000..3278f3c5 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_1_wait.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Long durable wait for OTel requirement otel-long-running-1.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, +) +from aws_durable_execution_sdk_python.config import Duration +from common import long_delay_seconds, otel_plugin, require_scenario + + +@durable_step +def complete_after_long_wait(_step_context: StepContext) -> str: + return "resumed" + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> str: + require_scenario(event, "long-wait") + context.wait( + Duration.from_seconds(long_delay_seconds(event)), + name="otel-long-wait", + ) + return context.step( + complete_after_long_wait(), + name="otel-after-long-wait", + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_2_retry.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_2_retry.py new file mode 100644 index 00000000..58db3540 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_2_retry.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Long retry delay for OTel requirement otel-long-running-2.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, +) +from aws_durable_execution_sdk_python.config import Duration, JitterStrategy, StepConfig +from aws_durable_execution_sdk_python.retries import ( + RetryStrategyConfig, + create_retry_strategy, +) +from common import long_delay_seconds, otel_plugin, require_scenario + + +@durable_step +def succeed_after_long_retry(step_context: StepContext) -> str: + if step_context.attempt == 1: + raise RuntimeError("Intentional first-attempt failure") + return "retried" + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> str: + require_scenario(event, "long-retry") + retry_strategy = create_retry_strategy( + RetryStrategyConfig( + max_attempts=2, + initial_delay=Duration.from_seconds(long_delay_seconds(event)), + backoff_rate=1.0, + jitter_strategy=JitterStrategy.NONE, + retryable_error_types=[RuntimeError], + ) + ) + return context.step( + succeed_after_long_retry(), + name="otel-long-retry", + config=StepConfig(retry_strategy=retry_strategy), + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_3_callback.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_3_callback.py new file mode 100644 index 00000000..a4078f10 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_3_callback.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Delayed callback for OTel requirement otel-long-running-3.""" + +from __future__ import annotations + +from typing import Any + +from aws_durable_execution_sdk_python import DurableContext, durable_execution +from aws_durable_execution_sdk_python.types import WaitForCallbackContext +from common import long_delay_seconds, otel_plugin, require_scenario + + +def submit_callback( + _callback_id: str, + _context: WaitForCallbackContext, +) -> None: + return None + + +@durable_execution(plugins=[otel_plugin()]) +def handler(event: dict[str, Any], context: DurableContext) -> str: + require_scenario(event, "long-callback") + long_delay_seconds(event) + return context.wait_for_callback( + submit_callback, + name="otel-long-callback", + ) diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_4_chained_invoke.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_4_chained_invoke.py new file mode 100644 index 00000000..a490d44d --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/otel_long_running_4_chained_invoke.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Long chained invoke for OTel requirement otel-long-running-4.""" + +from __future__ import annotations + +import os +from typing import Any + +from aws_durable_execution_sdk_python import DurableContext, durable_execution +from aws_durable_execution_sdk_python.config import Duration +from common import long_delay_seconds, otel_plugin, require_scenario + + +@durable_execution(plugins=[otel_plugin()]) +def handler( + event: dict[str, Any], + context: DurableContext, +) -> dict[str, Any]: + require_scenario(event, "long-chained-invoke") + return context.invoke( + function_name=os.environ["OTEL_INVOKE_TARGET_FUNCTION_NAME"], + payload=event, + name="otel-long-invoke", + ) + + +@durable_execution(plugins=[otel_plugin()]) +def target_handler( + event: dict[str, Any], + context: DurableContext, +) -> dict[str, Any]: + require_scenario(event, "long-chained-invoke") + context.wait( + Duration.from_seconds(long_delay_seconds(event)), + name="otel-long-invoke-target-wait", + ) + return event diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/requirements.txt b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/requirements.txt new file mode 100644 index 00000000..69bf3928 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/src/requirements.txt @@ -0,0 +1,2 @@ +aws-durable-execution-sdk-python @ git+https://github.com/aws/aws-durable-execution-sdk-python.git@${PYTHON_SDK_REF}#subdirectory=packages/aws-durable-execution-sdk-python +aws-durable-execution-sdk-python-otel @ git+https://github.com/aws/aws-durable-execution-sdk-python.git@${PYTHON_SDK_REF}#subdirectory=packages/aws-durable-execution-sdk-python-otel diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/template-long-running.yaml b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/template-long-running.yaml new file mode 100644 index 00000000..41c88efd --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/template-long-running.yaml @@ -0,0 +1,118 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: Python SDK examples for long-running durable execution OTel conformance + +Parameters: + LambdaExecutionRoleArn: + Type: String + Description: Pre-created execution role for the test functions + OtelLayerArn: + Type: String + Description: AWS Distro for OpenTelemetry Python Lambda layer + OtelExecWrapper: + Type: String + Default: /opt/otel-instrument + Description: Lambda execution wrapper supplied by the exporter profile + OtelServiceName: + Type: String + Default: durable-execution-conformance + Description: OpenTelemetry resource service name + OtelView: + Type: String + Default: invocation + AllowedValues: + - invocation + - execution + Description: OpenTelemetry plugin view used by the long-running functions + OtelTracesExporter: + Type: String + Default: otlp + Description: OpenTelemetry traces exporter selected by the runner + +Globals: + Function: + Runtime: python3.13 + Architectures: + - x86_64 + Timeout: 60 + MemorySize: 256 + Tracing: Active + Layers: + - !Ref OtelLayerArn + DurableConfig: + ExecutionTimeout: 180000 + RetentionPeriodInDays: 3 + Environment: + Variables: + AWS_LAMBDA_EXEC_WRAPPER: !Ref OtelExecWrapper + OTEL_SERVICE_NAME: !Ref OtelServiceName + OTEL_PLUGIN_MODE: !Ref OtelView + OTEL_TRACES_EXPORTER: !Ref OtelTracesExporter + OTEL_PROPAGATORS: xray + OTEL_TRACES_SAMPLER: always_on + +Resources: + OtelLongRunning1Wait: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-long-running-1 + Properties: + CodeUri: src/ + Handler: otel_long_running_1_wait.handler + FunctionName: !Sub "${AWS::StackName}-otel-long-running-1" + Role: !Ref LambdaExecutionRoleArn + + OtelLongRunning2Retry: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-long-running-2 + Properties: + CodeUri: src/ + Handler: otel_long_running_2_retry.handler + FunctionName: !Sub "${AWS::StackName}-otel-long-running-2" + Role: !Ref LambdaExecutionRoleArn + + OtelLongRunning3Callback: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-long-running-3 + Properties: + CodeUri: src/ + Handler: otel_long_running_3_callback.handler + FunctionName: !Sub "${AWS::StackName}-otel-long-running-3" + Role: !Ref LambdaExecutionRoleArn + + OtelLongRunning4ChainedInvoke: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-long-running-4 + Properties: + CodeUri: src/ + Handler: otel_long_running_4_chained_invoke.handler + FunctionName: !Sub "${AWS::StackName}-otel-long-running-4" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_INVOKE_TARGET_FUNCTION_NAME: !Sub "${OtelLongRunning4InvokeTarget.Arn}:$LATEST" + + OtelLongRunning4InvokeTarget: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: makefile + Properties: + CodeUri: src/ + Handler: otel_long_running_4_chained_invoke.target_handler + FunctionName: !Sub "${AWS::StackName}-otel-long-running-4-target" + Role: !Ref LambdaExecutionRoleArn diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/template.yaml b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/template.yaml new file mode 100644 index 00000000..be70e122 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/template.yaml @@ -0,0 +1,783 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: Python SDK examples for the durable execution OTel conformance suite + +Parameters: + LambdaExecutionRoleArn: + Type: String + Description: Pre-created execution role for the test functions + OtelSuite: + Type: String + Default: all + AllowedValues: + - all + - otel-invocation + - otel-execution + Description: OpenTelemetry view whose functions should be deployed + OtelLayerArn: + Type: String + Description: Lambda layer used to export OpenTelemetry spans + OtelCollectorLayerArn: + Type: String + Default: "" + Description: Optional OpenTelemetry Collector Lambda extension layer + OtelCollectorBucket: + Type: String + Default: "" + Description: S3 bucket where the collector writes OTLP trace objects + OtelCollectorPrefix: + Type: String + Default: "" + Description: S3 key prefix where the collector writes OTLP trace objects + OtelExecWrapper: + Type: String + Default: /opt/otel-instrument + Description: Lambda execution wrapper supplied by the exporter profile + OtelServiceName: + Type: String + Default: durable-execution-conformance + Description: OpenTelemetry resource service name + OtelTracesExporter: + Type: String + Default: otlp + Description: OpenTelemetry traces exporter selected by the runner + OtelExporterEndpoint: + Type: String + Default: "" + Description: Non-secret OTLP endpoint used by community exporters + OtelExporterHeaders: + Type: String + Default: "" + NoEcho: true + Description: Secret OTLP exporter headers supplied by the runner + OtelSecretEnvironmentNames: + Type: String + Default: "" + Description: Comma-separated secret environment variable names + +Conditions: + HasOtelCollectorLayer: !Not [!Equals [!Ref OtelCollectorLayerArn, ""]] + DeployInvocationView: !Or + - !Equals [!Ref OtelSuite, all] + - !Equals [!Ref OtelSuite, otel-invocation] + DeployExecutionView: !Or + - !Equals [!Ref OtelSuite, all] + - !Equals [!Ref OtelSuite, otel-execution] + +Globals: + Function: + Runtime: python3.13 + Architectures: + - x86_64 + Timeout: 60 + MemorySize: 256 + Tracing: Active + Layers: !If + - HasOtelCollectorLayer + - [!Ref OtelLayerArn, !Ref OtelCollectorLayerArn] + - [!Ref OtelLayerArn] + DurableConfig: + ExecutionTimeout: 300 + RetentionPeriodInDays: 1 + Environment: + Variables: + AWS_LAMBDA_EXEC_WRAPPER: !Ref OtelExecWrapper + OTEL_SERVICE_NAME: !Ref OtelServiceName + OTEL_TRACES_EXPORTER: !Ref OtelTracesExporter + OTEL_EXPORTER_OTLP_ENDPOINT: !Ref OtelExporterEndpoint + OTEL_EXPORTER_OTLP_HEADERS: !Ref OtelExporterHeaders + OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf + OTEL_PLUGIN_MODE: invocation + OTEL_PROPAGATORS: xray + OTEL_S3_BUCKET: !Ref OtelCollectorBucket + OTEL_S3_PREFIX: !Ref OtelCollectorPrefix + OTEL_TRACES_SAMPLER: always_on + OPENTELEMETRY_COLLECTOR_CONFIG_URI: !If + - HasOtelCollectorLayer + - /opt/collector-config/config-s3.yaml + - !Ref "AWS::NoValue" + +Resources: + Otel1Success: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-1 + Properties: + CodeUri: src/ + Handler: otel_1_success.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-1" + Role: !Ref LambdaExecutionRoleArn + + Otel2WaitResume: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-2 + Properties: + CodeUri: src/ + Handler: otel_2_wait_resume.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-2" + Role: !Ref LambdaExecutionRoleArn + + Otel3Retry: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-3 + Properties: + CodeUri: src/ + Handler: otel_3_retry.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-3" + Role: !Ref LambdaExecutionRoleArn + + Otel4TerminalFailure: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-4 + Properties: + CodeUri: src/ + Handler: otel_4_terminal_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-4" + Role: !Ref LambdaExecutionRoleArn + + Otel5ChildContext: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-5 + Properties: + CodeUri: src/ + Handler: otel_5_child_context.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-5" + Role: !Ref LambdaExecutionRoleArn + + Otel6Parallel: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-6 + Properties: + CodeUri: src/ + Handler: otel_6_parallel.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-6" + Role: !Ref LambdaExecutionRoleArn + + Otel7Map: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-7 + Properties: + CodeUri: src/ + Handler: otel_7_map.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-7" + Role: !Ref LambdaExecutionRoleArn + + Otel8HandledFailure: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-8 + Properties: + CodeUri: src/ + Handler: otel_8_handled_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-8" + Role: !Ref LambdaExecutionRoleArn + + Otel9WaitForCondition: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-9 + Properties: + CodeUri: src/ + Handler: otel_9_wait_for_condition.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-9" + Role: !Ref LambdaExecutionRoleArn + + Otel10WaitForCallback: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-10 + Properties: + CodeUri: src/ + Handler: otel_10_wait_for_callback.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-10" + Role: !Ref LambdaExecutionRoleArn + + Otel11ChainedInvoke: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-11 + Properties: + CodeUri: src/ + Handler: otel_11_chained_invoke.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-11" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_INVOKE_TARGET_FUNCTION_NAME: !Sub "${Otel11InvokeTarget.Arn}:$LATEST" + + Otel11InvokeTarget: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + Properties: + CodeUri: src/ + Handler: otel_11_chained_invoke.target_handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-11-target" + Role: !Ref LambdaExecutionRoleArn + + Otel12ChildContextFailure: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-12 + Properties: + CodeUri: src/ + Handler: otel_12_child_context_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-12" + Role: !Ref LambdaExecutionRoleArn + + Otel13ParallelFailure: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-13 + Properties: + CodeUri: src/ + Handler: otel_13_parallel_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-13" + Role: !Ref LambdaExecutionRoleArn + + Otel14MapFailure: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-14 + Properties: + CodeUri: src/ + Handler: otel_14_map_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-14" + Role: !Ref LambdaExecutionRoleArn + + Otel15WaitInterrupted: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-15 + Properties: + CodeUri: src/ + Handler: otel_15_wait_interrupted.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-15" + Role: !Ref LambdaExecutionRoleArn + DurableConfig: + ExecutionTimeout: 15 + RetentionPeriodInDays: 1 + + Otel16WaitForConditionFailure: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-16 + Properties: + CodeUri: src/ + Handler: otel_16_wait_for_condition_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-16" + Role: !Ref LambdaExecutionRoleArn + + Otel17WaitForCallbackFailure: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-17 + Properties: + CodeUri: src/ + Handler: otel_17_wait_for_callback_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-17" + Role: !Ref LambdaExecutionRoleArn + + Otel18ChainedInvokeFailure: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-18 + Properties: + CodeUri: src/ + Handler: otel_18_chained_invoke_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-18" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_INVOKE_TARGET_FUNCTION_NAME: !Sub "${Otel18InvokeTarget.Arn}:$LATEST" + + Otel18InvokeTarget: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + Properties: + CodeUri: src/ + Handler: otel_18_chained_invoke_failure.target_handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-18-target" + Role: !Ref LambdaExecutionRoleArn + + Otel19ExecutionFailure: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-19 + Properties: + CodeUri: src/ + Handler: otel_19_execution_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-19" + Role: !Ref LambdaExecutionRoleArn + + Otel20VirtualContext: + Type: AWS::Serverless::Function + Condition: DeployInvocationView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-invocation-20 + Properties: + CodeUri: src/ + Handler: otel_20_virtual_context.handler + FunctionName: !Sub "${AWS::StackName}-otel-invocation-20" + Role: !Ref LambdaExecutionRoleArn + + OtelExecution1Success: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-1 + Properties: + CodeUri: src/ + Handler: otel_1_success.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-1" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution2WaitResume: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-2 + Properties: + CodeUri: src/ + Handler: otel_2_wait_resume.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-2" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution3Retry: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-3 + Properties: + CodeUri: src/ + Handler: otel_3_retry.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-3" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution4TerminalFailure: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-4 + Properties: + CodeUri: src/ + Handler: otel_4_terminal_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-4" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution5ChildContext: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-5 + Properties: + CodeUri: src/ + Handler: otel_5_child_context.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-5" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution6Parallel: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-6 + Properties: + CodeUri: src/ + Handler: otel_6_parallel.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-6" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution7Map: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-7 + Properties: + CodeUri: src/ + Handler: otel_7_map.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-7" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution8HandledFailure: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-8 + Properties: + CodeUri: src/ + Handler: otel_8_handled_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-8" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution9WaitForCondition: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-9 + Properties: + CodeUri: src/ + Handler: otel_9_wait_for_condition.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-9" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution10WaitForCallback: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-10 + Properties: + CodeUri: src/ + Handler: otel_10_wait_for_callback.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-10" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution11ChainedInvoke: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-11 + Properties: + CodeUri: src/ + Handler: otel_11_chained_invoke.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-11" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + OTEL_INVOKE_TARGET_FUNCTION_NAME: !Sub "${OtelExecution11InvokeTarget.Arn}:$LATEST" + + OtelExecution11InvokeTarget: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + Properties: + CodeUri: src/ + Handler: otel_11_chained_invoke.target_handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-11-target" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution12ChildContextFailure: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-12 + Properties: + CodeUri: src/ + Handler: otel_12_child_context_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-12" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution13ParallelFailure: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-13 + Properties: + CodeUri: src/ + Handler: otel_13_parallel_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-13" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution14MapFailure: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-14 + Properties: + CodeUri: src/ + Handler: otel_14_map_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-14" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution15WaitInterrupted: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-15 + Properties: + CodeUri: src/ + Handler: otel_15_wait_interrupted.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-15" + Role: !Ref LambdaExecutionRoleArn + DurableConfig: + ExecutionTimeout: 15 + RetentionPeriodInDays: 1 + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution16WaitForConditionFailure: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-16 + Properties: + CodeUri: src/ + Handler: otel_16_wait_for_condition_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-16" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution17WaitForCallbackFailure: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-17 + Properties: + CodeUri: src/ + Handler: otel_17_wait_for_callback_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-17" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution18ChainedInvokeFailure: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-18 + Properties: + CodeUri: src/ + Handler: otel_18_chained_invoke_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-18" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + OTEL_INVOKE_TARGET_FUNCTION_NAME: !Sub "${OtelExecution18InvokeTarget.Arn}:$LATEST" + + OtelExecution18InvokeTarget: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + Properties: + CodeUri: src/ + Handler: otel_18_chained_invoke_failure.target_handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-18-target" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution19ExecutionFailure: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-19 + Properties: + CodeUri: src/ + Handler: otel_19_execution_failure.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-19" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution + + OtelExecution20VirtualContext: + Type: AWS::Serverless::Function + Condition: DeployExecutionView + Metadata: + BuildMethod: makefile + TestingMetadata: + TestDescription: + - otel-execution-20 + Properties: + CodeUri: src/ + Handler: otel_20_virtual_context.handler + FunctionName: !Sub "${AWS::StackName}-otel-execution-20" + Role: !Ref LambdaExecutionRoleArn + Environment: + Variables: + OTEL_PLUGIN_MODE: execution diff --git a/packages/aws-durable-execution-sdk-python-conformance-tests-otel/tests/test_otel_examples.py b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/tests/test_otel_examples.py new file mode 100644 index 00000000..0e2984ea --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-conformance-tests-otel/tests/test_otel_examples.py @@ -0,0 +1,510 @@ +# SPDX-FileCopyrightText: 2026-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Contract tests for the Python OTel conformance handlers and templates. + +The conformance runner reads requirement mappings from +``TestingMetadata.TestDescription``, and SAM builds each function through +``src/Makefile``. A handler that is renamed, unregistered, or missing a build +target therefore only fails once a deployment runs in the cloud, so these tests +pin that wiring locally. +""" + +from __future__ import annotations + +import ast +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import pytest +import yaml + + +PKG_DIR = Path(__file__).resolve().parents[1] +SRC_DIR = PKG_DIR / "src" +TEMPLATE_PATH = PKG_DIR / "template.yaml" +LONG_RUNNING_TEMPLATE_PATH = PKG_DIR / "template-long-running.yaml" + +SDK_REPOSITORY_URL = "git+https://github.com/aws/aws-durable-execution-sdk-python.git" +COLLECTOR_CONFIG_URI = "/opt/collector-config/config-s3.yaml" + +EXPECTED_MAPPINGS: list[tuple[str, str]] = [ + ("Otel1Success", "otel-invocation-1"), + ("Otel2WaitResume", "otel-invocation-2"), + ("Otel3Retry", "otel-invocation-3"), + ("Otel4TerminalFailure", "otel-invocation-4"), + ("Otel5ChildContext", "otel-invocation-5"), + ("Otel6Parallel", "otel-invocation-6"), + ("Otel7Map", "otel-invocation-7"), + ("Otel8HandledFailure", "otel-invocation-8"), + ("Otel9WaitForCondition", "otel-invocation-9"), + ("Otel10WaitForCallback", "otel-invocation-10"), + ("Otel11ChainedInvoke", "otel-invocation-11"), + ("Otel12ChildContextFailure", "otel-invocation-12"), + ("Otel13ParallelFailure", "otel-invocation-13"), + ("Otel14MapFailure", "otel-invocation-14"), + ("Otel15WaitInterrupted", "otel-invocation-15"), + ("Otel16WaitForConditionFailure", "otel-invocation-16"), + ("Otel17WaitForCallbackFailure", "otel-invocation-17"), + ("Otel18ChainedInvokeFailure", "otel-invocation-18"), + ("Otel19ExecutionFailure", "otel-invocation-19"), + ("Otel20VirtualContext", "otel-invocation-20"), + ("OtelExecution1Success", "otel-execution-1"), + ("OtelExecution2WaitResume", "otel-execution-2"), + ("OtelExecution3Retry", "otel-execution-3"), + ("OtelExecution4TerminalFailure", "otel-execution-4"), + ("OtelExecution5ChildContext", "otel-execution-5"), + ("OtelExecution6Parallel", "otel-execution-6"), + ("OtelExecution7Map", "otel-execution-7"), + ("OtelExecution8HandledFailure", "otel-execution-8"), + ("OtelExecution9WaitForCondition", "otel-execution-9"), + ("OtelExecution10WaitForCallback", "otel-execution-10"), + ("OtelExecution11ChainedInvoke", "otel-execution-11"), + ("OtelExecution12ChildContextFailure", "otel-execution-12"), + ("OtelExecution13ParallelFailure", "otel-execution-13"), + ("OtelExecution14MapFailure", "otel-execution-14"), + ("OtelExecution15WaitInterrupted", "otel-execution-15"), + ("OtelExecution16WaitForConditionFailure", "otel-execution-16"), + ("OtelExecution17WaitForCallbackFailure", "otel-execution-17"), + ("OtelExecution18ChainedInvokeFailure", "otel-execution-18"), + ("OtelExecution19ExecutionFailure", "otel-execution-19"), + ("OtelExecution20VirtualContext", "otel-execution-20"), +] +EXPECTED_LONG_RUNNING_MAPPINGS: list[tuple[str, str]] = [ + ("OtelLongRunning1Wait", "otel-long-running-1"), + ("OtelLongRunning2Retry", "otel-long-running-2"), + ("OtelLongRunning3Callback", "otel-long-running-3"), + ("OtelLongRunning4ChainedInvoke", "otel-long-running-4"), +] +# Invoke targets carry no requirement of their own; the chained-invoke cases +# assert on the telemetry the target produces. +EXPECTED_TARGETS: dict[str, str] = { + "Otel11InvokeTarget": "otel-invocation-11-target", + "Otel18InvokeTarget": "otel-invocation-18-target", + "OtelExecution11InvokeTarget": "otel-execution-11-target", + "OtelExecution18InvokeTarget": "otel-execution-18-target", +} +EXPECTED_LONG_RUNNING_TARGETS: dict[str, str] = { + "OtelLongRunning4InvokeTarget": "otel-long-running-4-target", +} +CHAINED_INVOKE_PAIRS: list[tuple[Path, str, str]] = [ + (TEMPLATE_PATH, "Otel11ChainedInvoke", "Otel11InvokeTarget"), + (TEMPLATE_PATH, "Otel18ChainedInvokeFailure", "Otel18InvokeTarget"), + (TEMPLATE_PATH, "OtelExecution11ChainedInvoke", "OtelExecution11InvokeTarget"), + ( + TEMPLATE_PATH, + "OtelExecution18ChainedInvokeFailure", + "OtelExecution18InvokeTarget", + ), + ( + LONG_RUNNING_TEMPLATE_PATH, + "OtelLongRunning4ChainedInvoke", + "OtelLongRunning4InvokeTarget", + ), +] +REQUIRED_PARAMETERS: frozenset[str] = frozenset( + { + "LambdaExecutionRoleArn", + "OtelCollectorBucket", + "OtelCollectorLayerArn", + "OtelCollectorPrefix", + "OtelExecWrapper", + "OtelExporterEndpoint", + "OtelExporterHeaders", + "OtelLayerArn", + "OtelSecretEnvironmentNames", + "OtelServiceName", + "OtelSuite", + "OtelTracesExporter", + } +) +REQUIRED_LONG_RUNNING_PARAMETERS: frozenset[str] = frozenset( + { + "LambdaExecutionRoleArn", + "OtelExecWrapper", + "OtelLayerArn", + "OtelServiceName", + "OtelTracesExporter", + "OtelView", + } +) +EXPECTED_MODULES: frozenset[str] = frozenset( + { + "common", + "otel_1_success", + "otel_2_wait_resume", + "otel_3_retry", + "otel_4_terminal_failure", + "otel_5_child_context", + "otel_6_parallel", + "otel_7_map", + "otel_8_handled_failure", + "otel_9_wait_for_condition", + "otel_10_wait_for_callback", + "otel_11_chained_invoke", + "otel_12_child_context_failure", + "otel_13_parallel_failure", + "otel_14_map_failure", + "otel_15_wait_interrupted", + "otel_16_wait_for_condition_failure", + "otel_17_wait_for_callback_failure", + "otel_18_chained_invoke_failure", + "otel_19_execution_failure", + "otel_20_virtual_context", + "otel_long_running_1_wait", + "otel_long_running_2_retry", + "otel_long_running_3_callback", + "otel_long_running_4_chained_invoke", + } +) + + +@dataclass(frozen=True) +class CfnTag: + """A CloudFormation short-form intrinsic, such as ``!Ref OtelLayerArn``.""" + + tag: str + value: Any + + +def ref(name: str) -> CfnTag: + return CfnTag("!Ref", name) + + +def sub(value: str) -> CfnTag: + return CfnTag("!Sub", value) + + +class CfnLoader(yaml.SafeLoader): + """SafeLoader that wraps CloudFormation short-form tags instead of failing.""" + + +def _construct_cfn_tag(loader: CfnLoader, _suffix: str, node: yaml.Node) -> CfnTag: + value: Any + if isinstance(node, yaml.ScalarNode): + value = loader.construct_scalar(node) + elif isinstance(node, yaml.SequenceNode): + value = loader.construct_sequence(node, deep=True) + else: + value = loader.construct_mapping(node, deep=True) + return CfnTag(node.tag, value) + + +CfnLoader.add_multi_constructor("!", _construct_cfn_tag) + + +def load_template(path: Path) -> dict[str, Any]: + with path.open(encoding="utf-8") as stream: + template: dict[str, Any] = yaml.load(stream, Loader=CfnLoader) + return template + + +def requirement_mappings(template: dict[str, Any]) -> list[tuple[str, str]]: + """Return the (logical id, requirement id) pairs the runner discovers.""" + mappings: list[tuple[str, str]] = [] + for logical_id, resource in template["Resources"].items(): + descriptions: list[str] = resource.get("TestingMetadata", {}).get( + "TestDescription", [] + ) + mappings.extend((logical_id, description) for description in descriptions) + return mappings + + +def environment_variables(resource: dict[str, Any]) -> dict[str, Any]: + environment: dict[str, Any] = resource["Properties"].get("Environment", {}) + variables: dict[str, Any] = environment.get("Variables", {}) + return variables + + +@pytest.fixture(scope="module") +def template() -> dict[str, Any]: + return load_template(TEMPLATE_PATH) + + +@pytest.fixture(scope="module") +def long_running_template() -> dict[str, Any]: + return load_template(LONG_RUNNING_TEMPLATE_PATH) + + +@pytest.fixture(scope="module") +def makefile() -> str: + return (SRC_DIR / "Makefile").read_text(encoding="utf-8") + + +@pytest.mark.parametrize( + ("template_path", "expected_mappings"), + [ + (TEMPLATE_PATH, EXPECTED_MAPPINGS), + (LONG_RUNNING_TEMPLATE_PATH, EXPECTED_LONG_RUNNING_MAPPINGS), + ], + ids=["template", "template-long-running"], +) +def test_templates_map_every_otel_requirement( + template_path: Path, expected_mappings: list[tuple[str, str]] +) -> None: + assert requirement_mappings(load_template(template_path)) == expected_mappings + + +@pytest.mark.parametrize( + ("template_path", "expected_mappings", "expected_targets"), + [ + (TEMPLATE_PATH, EXPECTED_MAPPINGS, EXPECTED_TARGETS), + ( + LONG_RUNNING_TEMPLATE_PATH, + EXPECTED_LONG_RUNNING_MAPPINGS, + EXPECTED_LONG_RUNNING_TARGETS, + ), + ], + ids=["template", "template-long-running"], +) +def test_templates_declare_only_the_expected_functions( + template_path: Path, + expected_mappings: list[tuple[str, str]], + expected_targets: dict[str, str], +) -> None: + resources: dict[str, Any] = load_template(template_path)["Resources"] + expected_ids: set[str] = {logical_id for logical_id, _ in expected_mappings} | set( + expected_targets + ) + + assert set(resources) == expected_ids + for resource in resources.values(): + assert resource["Type"] == "AWS::Serverless::Function" + assert resource["Metadata"]["BuildMethod"] == "makefile" + assert resource["Properties"]["CodeUri"] == "src/" + assert resource["Properties"]["Role"] == ref("LambdaExecutionRoleArn") + + +@pytest.mark.parametrize( + ("template_path", "expected_mappings", "expected_targets"), + [ + (TEMPLATE_PATH, EXPECTED_MAPPINGS, EXPECTED_TARGETS), + ( + LONG_RUNNING_TEMPLATE_PATH, + EXPECTED_LONG_RUNNING_MAPPINGS, + EXPECTED_LONG_RUNNING_TARGETS, + ), + ], + ids=["template", "template-long-running"], +) +def test_function_names_identify_the_requirement( + template_path: Path, + expected_mappings: list[tuple[str, str]], + expected_targets: dict[str, str], +) -> None: + resources: dict[str, Any] = load_template(template_path)["Resources"] + expected_names: dict[str, str] = { + **dict(expected_mappings), + **expected_targets, + } + + for logical_id, suffix in expected_names.items(): + assert resources[logical_id]["Properties"]["FunctionName"] == sub( + f"${{AWS::StackName}}-{suffix}" + ) + + +@pytest.mark.parametrize( + ("template_path", "expected_mappings", "expected_targets"), + [ + (TEMPLATE_PATH, EXPECTED_MAPPINGS, EXPECTED_TARGETS), + ( + LONG_RUNNING_TEMPLATE_PATH, + EXPECTED_LONG_RUNNING_MAPPINGS, + EXPECTED_LONG_RUNNING_TARGETS, + ), + ], + ids=["template", "template-long-running"], +) +def test_makefile_builds_every_function( + makefile: str, + template_path: Path, + expected_mappings: list[tuple[str, str]], + expected_targets: dict[str, str], +) -> None: + logical_ids: list[str] = [logical_id for logical_id, _ in expected_mappings] + list( + expected_targets + ) + + for logical_id in logical_ids: + assert f"build-{logical_id}" in makefile + + +@pytest.mark.parametrize( + ("template_path", "required_parameters"), + [ + (TEMPLATE_PATH, REQUIRED_PARAMETERS), + (LONG_RUNNING_TEMPLATE_PATH, REQUIRED_LONG_RUNNING_PARAMETERS), + ], + ids=["template", "template-long-running"], +) +def test_templates_accept_the_runner_parameters( + template_path: Path, required_parameters: frozenset[str] +) -> None: + parameters: dict[str, Any] = load_template(template_path)["Parameters"] + + assert required_parameters <= set(parameters) + + +@pytest.mark.parametrize( + ("template_path", "expected_mappings"), + [ + (TEMPLATE_PATH, EXPECTED_MAPPINGS), + (LONG_RUNNING_TEMPLATE_PATH, EXPECTED_LONG_RUNNING_MAPPINGS), + ], + ids=["template", "template-long-running"], +) +def test_every_registered_handler_exists( + template_path: Path, + expected_mappings: list[tuple[str, str]], +) -> None: + resources: dict[str, Any] = load_template(template_path)["Resources"] + + assert expected_mappings # every template registers at least one requirement + for logical_id, resource in resources.items(): + handler: str = resource["Properties"]["Handler"] + module_name, _, function_name = handler.rpartition(".") + module_path: Path = SRC_DIR / f"{module_name}.py" + assert module_path.is_file(), f"{logical_id} references missing {module_path}" + tree: ast.Module = ast.parse( + module_path.read_text(encoding="utf-8"), filename=str(module_path) + ) + exported: set[str] = { + node.name for node in tree.body if isinstance(node, ast.FunctionDef) + } + assert function_name in exported, ( + f"{module_name} does not define {function_name}" + ) + + +def test_template_deploys_only_the_selected_view(template: dict[str, Any]) -> None: + assert template["Parameters"]["OtelSuite"]["AllowedValues"] == [ + "all", + "otel-invocation", + "otel-execution", + ] + assert {"DeployInvocationView", "DeployExecutionView"} <= set( + template["Conditions"] + ) + for logical_id, resource in template["Resources"].items(): + expected_condition: str = ( + "DeployExecutionView" + if logical_id.startswith("OtelExecution") + else "DeployInvocationView" + ) + assert resource["Condition"] == expected_condition + + +def test_execution_view_functions_override_the_plugin_mode( + template: dict[str, Any], +) -> None: + globals_variables: dict[str, Any] = template["Globals"]["Function"]["Environment"][ + "Variables" + ] + + assert globals_variables["OTEL_PLUGIN_MODE"] == "invocation" + for logical_id, resource in template["Resources"].items(): + variables: dict[str, Any] = environment_variables(resource) + if logical_id.startswith("OtelExecution"): + assert variables["OTEL_PLUGIN_MODE"] == "execution" + else: + assert "OTEL_PLUGIN_MODE" not in variables + + +def test_long_running_functions_take_the_plugin_mode_from_a_parameter( + long_running_template: dict[str, Any], +) -> None: + globals_variables: dict[str, Any] = long_running_template["Globals"]["Function"][ + "Environment" + ]["Variables"] + + assert globals_variables["OTEL_PLUGIN_MODE"] == ref("OtelView") + assert long_running_template["Parameters"]["OtelView"]["AllowedValues"] == [ + "invocation", + "execution", + ] + + +def test_interrupted_wait_functions_time_out_before_the_wait_ends( + template: dict[str, Any], +) -> None: + for logical_id in ("Otel15WaitInterrupted", "OtelExecution15WaitInterrupted"): + assert template["Resources"][logical_id]["Properties"]["DurableConfig"] == { + "ExecutionTimeout": 15, + "RetentionPeriodInDays": 1, + } + + +@pytest.mark.parametrize( + ("template_path", "source_id", "target_id"), + CHAINED_INVOKE_PAIRS, + ids=[source_id for _, source_id, _ in CHAINED_INVOKE_PAIRS], +) +def test_chained_invoke_functions_point_at_their_target( + template_path: Path, source_id: str, target_id: str +) -> None: + resources: dict[str, Any] = load_template(template_path)["Resources"] + variables: dict[str, Any] = environment_variables(resources[source_id]) + + assert variables["OTEL_INVOKE_TARGET_FUNCTION_NAME"] == sub( + f"${{{target_id}.Arn}}:$LATEST" + ) + + +def test_exporter_headers_are_hidden(template: dict[str, Any]) -> None: + assert template["Parameters"]["OtelExporterHeaders"]["NoEcho"] is True + + +def test_the_test_collector_is_optional(template: dict[str, Any]) -> None: + globals_variables: dict[str, Any] = template["Globals"]["Function"]["Environment"][ + "Variables" + ] + + assert template["Conditions"]["HasOtelCollectorLayer"] == CfnTag( + "!Not", [CfnTag("!Equals", [ref("OtelCollectorLayerArn"), ""])] + ) + assert globals_variables["OTEL_S3_BUCKET"] == ref("OtelCollectorBucket") + assert globals_variables["OTEL_S3_PREFIX"] == ref("OtelCollectorPrefix") + assert globals_variables["OPENTELEMETRY_COLLECTOR_CONFIG_URI"] == CfnTag( + "!If", + [ + "HasOtelCollectorLayer", + COLLECTOR_CONFIG_URI, + ref("AWS::NoValue"), + ], + ) + + +def test_handler_modules_are_exactly_the_expected_set() -> None: + modules: set[str] = {path.stem for path in SRC_DIR.glob("*.py")} + + assert modules == EXPECTED_MODULES + + +@pytest.mark.parametrize( + "handler_path", sorted(SRC_DIR.glob("*.py")), ids=lambda path: path.stem +) +def test_handler_modules_are_valid_python(handler_path: Path) -> None: + ast.parse(handler_path.read_text(encoding="utf-8"), filename=str(handler_path)) + + +def test_requirements_install_both_sdk_packages_from_one_commit() -> None: + requirements: str = (SRC_DIR / "requirements.txt").read_text(encoding="utf-8") + + for package in ( + "aws-durable-execution-sdk-python", + "aws-durable-execution-sdk-python-otel", + ): + assert ( + f"{package} @ {SDK_REPOSITORY_URL}@${{PYTHON_SDK_REF}}" + f"#subdirectory=packages/{package}" + ) in requirements + + +def test_common_selects_the_plugin_from_the_deployed_view() -> None: + common: str = (SRC_DIR / "common.py").read_text(encoding="utf-8") + + assert 'os.environ.get("OTEL_PLUGIN_MODE") == "execution"' in common + assert "ExecutionOtelPlugin(OtelPluginConfig())" in common + assert "InvocationOtelPlugin(OtelPluginConfig())" in common diff --git a/pyproject.toml b/pyproject.toml index 0d5b48ba..6bc86c8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ testpaths = [ "packages/aws-durable-execution-sdk-python-testing/tests", "packages/aws-durable-execution-sdk-python-examples/test", "packages/aws-durable-execution-sdk-python-conformance-tests/tests", + "packages/aws-durable-execution-sdk-python-conformance-tests-otel/tests", ] markers = [ # Used for test selection with -m example