Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,14 @@
import threading
from typing import Any

from aws_durable_execution_sdk_python.lambda_service import (
InvocationStatus,
OperationType,
)
from aws_durable_execution_sdk_python.plugin import (
DurableInstrumentationPlugin,
InvocationEndInfo,
InvocationStatus,
Comment thread
zhongkechen marked this conversation as resolved.
InvocationStartInfo,
OperationEndInfo,
OperationStartInfo,
OperationType,
UserFunctionEndInfo,
UserFunctionOutcome,
UserFunctionStartInfo,
Expand Down Expand Up @@ -521,7 +519,7 @@ def _operation_attributes(self, info: Any) -> dict[str, Any]:
# STEP user-function spans represent attempts, not durable operations.
if (
not (
isinstance(info, (UserFunctionStartInfo, UserFunctionEndInfo))
hasattr(info, "is_replay_children")
and info.operation_type is OperationType.STEP
)
and getattr(info, "status", None) is not None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@
import threading
from typing import Any

from aws_durable_execution_sdk_python.lambda_service import (
InvocationStatus,
OperationType,
)
from aws_durable_execution_sdk_python.plugin import (
DurableInstrumentationPlugin,
InvocationEndInfo,
InvocationStatus,
InvocationStartInfo,
OperationEndInfo,
OperationStartInfo,
OperationType,
UserFunctionEndInfo,
UserFunctionOutcome,
UserFunctionStartInfo,
Expand Down Expand Up @@ -634,7 +632,7 @@ def _extract_attributes(self, info: Any) -> _SpanAttributes:
"durable.execution.arn": self._execution_arn,
}

if isinstance(info, InvocationStartInfo):
if hasattr(info, "is_first_invocation"):
attributes["durable.invocation.first"] = info.is_first_invocation
if hasattr(info, "operation_id") and info.operation_id is not None:
attributes["durable.operation.id"] = info.operation_id
Expand All @@ -645,7 +643,7 @@ def _extract_attributes(self, info: Any) -> _SpanAttributes:
# STEP user-function spans represent attempts, not durable operations.
if (
not (
isinstance(info, (UserFunctionStartInfo, UserFunctionEndInfo))
hasattr(info, "is_replay_children")
and info.operation_type is OperationType.STEP
)
and hasattr(info, "status")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@
from __future__ import annotations

from datetime import UTC, datetime
from types import SimpleNamespace

import opentelemetry.context as otel_context
import pytest
from aws_durable_execution_sdk_python.lambda_service import (
ErrorObject,
InvocationStatus,
OperationStatus,
OperationSubType,
OperationType,
)
from aws_durable_execution_sdk_python.plugin import (
InvocationEndInfo,
InvocationStatus,
InvocationStartInfo,
OperationEndInfo,
OperationStartInfo,
OperationType,
UserFunctionEndInfo,
UserFunctionOutcome,
UserFunctionStartInfo,
Expand Down Expand Up @@ -96,6 +97,30 @@ def _invocation_end_info(
)


def test_operation_attributes_use_structural_user_function_marker():
plugin, _ = _create_plugin()

operation_attributes = plugin._operation_attributes(
SimpleNamespace(
operation_type=OperationType.STEP,
status=OperationStatus.STARTED,
)
)
assert (
operation_attributes["durable.operation.status"]
== OperationStatus.STARTED.value
)

user_function_attributes = plugin._operation_attributes(
SimpleNamespace(
operation_type=OperationType.STEP,
status=OperationStatus.STARTED,
is_replay_children=False,
)
)
assert "durable.operation.status" not in user_function_attributes


# ---------------------------------------------------------------------------
# derive_workflow_span_id
# ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
import opentelemetry.context as otel_context
import pytest
from aws_durable_execution_sdk_python.lambda_service import (
InvocationStatus,
OperationStatus,
OperationSubType,
OperationType,
)
from aws_durable_execution_sdk_python.plugin import (
InvocationEndInfo,
InvocationStatus,
InvocationStartInfo,
OperationEndInfo,
OperationStartInfo,
OperationType,
UserFunctionEndInfo,
UserFunctionOutcome,
UserFunctionStartInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import UTC, datetime
from types import SimpleNamespace

import opentelemetry.context as otel_context
import pytest
from aws_durable_execution_sdk_python.lambda_service import (
ErrorObject,
InvocationStatus,
OperationStatus,
OperationSubType,
OperationType,
)
from aws_durable_execution_sdk_python.plugin import (
InvocationEndInfo,
InvocationStatus,
InvocationStartInfo,
OperationEndInfo,
OperationStartInfo,
OperationType,
UserFunctionEndInfo,
UserFunctionOutcome,
UserFunctionStartInfo,
Expand Down Expand Up @@ -148,6 +149,35 @@ def _user_function_end_info(
)


def test_extract_attributes_uses_structural_event_attributes():
plugin, _ = _create_plugin()

invocation_attributes = plugin._extract_attributes(
SimpleNamespace(is_first_invocation=False)
)
assert invocation_attributes["durable.invocation.first"] is False

operation_attributes = plugin._extract_attributes(
SimpleNamespace(
operation_type=OperationType.STEP,
status=OperationStatus.STARTED,
)
)
assert (
operation_attributes["durable.operation.status"]
== OperationStatus.STARTED.value
)

user_function_attributes = plugin._extract_attributes(
SimpleNamespace(
operation_type=OperationType.STEP,
status=OperationStatus.STARTED,
is_replay_children=False,
)
)
assert "durable.operation.status" not in user_function_attributes


def test_invocation_start_and_end_emit_invocation_span():
"""Verify invocation lifecycle callbacks create and finish the root span."""
plugin, exporter = _create_plugin()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@
import opentelemetry.context as otel_context
import pytest
from aws_durable_execution_sdk_python.lambda_service import (
InvocationStatus,
OperationStatus,
OperationSubType,
OperationType,
)
from aws_durable_execution_sdk_python.plugin import (
InvocationEndInfo,
InvocationStatus,
InvocationStartInfo,
OperationEndInfo,
OperationStartInfo,
OperationType,
UserFunctionEndInfo,
UserFunctionOutcome,
UserFunctionStartInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

from aws_durable_execution_sdk_python.lambda_service import (
OperationStatus,
OperationType,
)
from aws_durable_execution_sdk_python.plugin import (
InvocationStartInfo,
OperationType,
UserFunctionStartInfo,
)
from opentelemetry.context import Context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,35 @@


PACKAGE_ROOT = Path(__file__).resolve().parents[1]
REPOSITORY_ROOT = PACKAGE_ROOT.parents[1]
CORE_DEPENDENCY = "aws-durable-execution-sdk-python>=1.8.0"


def _load_pyproject(path: Path) -> dict:
with path.open("rb") as pyproject:
return tomllib.load(pyproject)


def test_package_is_marked_production_stable() -> None:
with (PACKAGE_ROOT / "pyproject.toml").open("rb") as pyproject:
classifiers = tomllib.load(pyproject)["project"]["classifiers"]
classifiers = _load_pyproject(PACKAGE_ROOT / "pyproject.toml")["project"][
"classifiers"
]

assert "Development Status :: 5 - Production/Stable" in classifiers
assert "Development Status :: 4 - Beta" not in classifiers


def test_package_requires_compatible_core_sdk() -> None:
dependencies = _load_pyproject(PACKAGE_ROOT / "pyproject.toml")["project"][
"dependencies"
]

assert CORE_DEPENDENCY in dependencies


def test_pypi_compatibility_environment_uses_compatible_core_sdk() -> None:
dependencies = _load_pyproject(REPOSITORY_ROOT / "pyproject.toml")["tool"]["hatch"][
"envs"
]["test-pypi-otel"]["dependencies"]

assert CORE_DEPENDENCY in dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
)
from aws_durable_execution_sdk_python.lambda_service import (
OperationStatus,
OperationType,
)
from aws_durable_execution_sdk_python.plugin import (
DurableInstrumentationPlugin,
InvocationStartInfo,
OperationEndInfo,
OperationType,
)

from aws_durable_execution_sdk_python_testing.runner import (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
from aws_durable_execution_sdk_python.lambda_service import (
DurableExecutionInvocationOutput,
ErrorObject,
InvocationStatus,
InvocationStatus as ServiceInvocationStatus,
Operation,
OperationAction,
OperationStatus,
OperationSubType,
OperationType,
OperationType as ServiceOperationType,
OperationUpdate,
)
from aws_durable_execution_sdk_python.types import LambdaContext
Expand All @@ -31,6 +31,34 @@
DURABLE_INSTRUMENTATION_PLUGIN_API_VERSION = 1


class InvocationStatus(Enum):

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Codex AI review

[P1] Version this enum identity change as a new plugin API. Existing API-v1 providers remain accepted, but they compare hook values against lambda_service.InvocationStatus and OperationType; these new enum instances are unequal. The previous OTel release consequently misses terminal statuses, fails to export workflow spans, and raises from operation hooks. Either preserve the v1 enum identities or bump DURABLE_INSTRUMENTATION_PLUGIN_API_VERSION, update provider versions/dependency bounds, and reject older plugins. Add a previous-OTel/new-core compatibility test.

"""Invocation outcomes exposed to instrumentation plugins."""

SUCCEEDED = "SUCCEEDED"
FAILED = "FAILED"
PENDING = "PENDING"
RETRY = "RETRY"


class OperationType(Enum):
Comment thread
zhongkechen marked this conversation as resolved.
"""Durable operation categories exposed to instrumentation plugins."""

EXECUTION = "EXECUTION"
CONTEXT = "CONTEXT"
STEP = "STEP"
WAIT = "WAIT"
CALLBACK = "CALLBACK"
CHAINED_INVOKE = "CHAINED_INVOKE"


def _to_invocation_status(status: ServiceInvocationStatus) -> InvocationStatus:
return InvocationStatus(status.value)


def _to_operation_type(operation_type: ServiceOperationType) -> OperationType:
return OperationType(operation_type.value)


def _extract_result(operation: Operation) -> str | None:
if operation.step_details and operation.step_details.result is not None:
return operation.step_details.result
Expand Down Expand Up @@ -91,7 +119,7 @@ def from_operation(
) -> OperationInfo:
return OperationInfo(
operation_id=operation.operation_id,
operation_type=operation.operation_type,
operation_type=_to_operation_type(operation.operation_type),
sub_type=operation.sub_type,
name=operation.name,
parent_id=operation.parent_id,
Expand Down Expand Up @@ -338,7 +366,7 @@ def from_durable_execution_invocation_output(
if operations is not None
else invocation_start_info.operations
),
status=output.status,
status=_to_invocation_status(output.status),
error=output.error,
execution_result=output.result,
)
Expand Down Expand Up @@ -609,7 +637,7 @@ def on_user_function_start(
"""Execute any registered plugins for the operation when its user function starts to execute."""
start_info = UserFunctionStartInfo(
operation_id=operation_identifier.operation_id,
operation_type=operation_identifier.type,
operation_type=_to_operation_type(operation_identifier.type),
sub_type=operation_identifier.sub_type,
name=operation_identifier.name,
parent_id=operation_identifier.parent_id,
Expand Down Expand Up @@ -647,7 +675,7 @@ def on_operation_action(
self.execute_plugins(
OperationStartInfo(
operation_id=update.operation_id,
operation_type=update.operation_type,
operation_type=_to_operation_type(update.operation_type),
sub_type=update.sub_type,
name=update.name,
parent_id=update.parent_id,
Expand All @@ -665,7 +693,7 @@ def on_operation_replay(self, operation: Operation) -> None:

start_info = OperationStartInfo(
operation_id=operation.operation_id,
operation_type=operation.operation_type,
operation_type=_to_operation_type(operation.operation_type),
sub_type=operation.sub_type,
name=operation.name,
parent_id=operation.parent_id,
Expand All @@ -688,7 +716,7 @@ def on_child_context_end(
self.execute_plugins(
OperationEndInfo(
operation_id=operation_identifier.operation_id,
operation_type=operation_identifier.type,
operation_type=_to_operation_type(operation_identifier.type),
sub_type=operation_identifier.sub_type,
name=operation_identifier.name,
parent_id=operation_identifier.parent_id,
Expand Down Expand Up @@ -732,7 +760,7 @@ def on_operation_update(
self.execute_plugins(
OperationEndInfo(
operation_id=operation.operation_id,
operation_type=operation.operation_type,
operation_type=_to_operation_type(operation.operation_type),
sub_type=operation.sub_type,
name=operation.name,
parent_id=operation.parent_id,
Expand Down
Loading
Loading