From ac609b84fee226275f066c598857eba93804ff58 Mon Sep 17 00:00:00 2001 From: Frank Chen Date: Mon, 6 Jul 2026 12:09:54 -0700 Subject: [PATCH 1/2] fix: fetch paginated cloud execution history --- .../examples-catalog.json | 11 + .../many_operations.py | 69 ++++++ .../template.yaml | 20 +- .../test_many_operations.py | 70 ++++++ .../runner.py | 27 ++- .../tests/runner_test.py | 214 +++++++++++++++++- 6 files changed, 404 insertions(+), 7 deletions(-) create mode 100644 packages/aws-durable-execution-sdk-python-examples/src/comprehensive_operations/many_operations.py create mode 100644 packages/aws-durable-execution-sdk-python-examples/test/comprehensive_operations/test_many_operations.py diff --git a/packages/aws-durable-execution-sdk-python-examples/examples-catalog.json b/packages/aws-durable-execution-sdk-python-examples/examples-catalog.json index 18a2083d..40090b5a 100644 --- a/packages/aws-durable-execution-sdk-python-examples/examples-catalog.json +++ b/packages/aws-durable-execution-sdk-python-examples/examples-catalog.json @@ -585,6 +585,17 @@ }, "path": "./src/comprehensive_operations/comprehensive_operations.py" }, + { + "name": "Many Operations", + "description": "Runs many durable steps and child contexts to exercise long execution history retrieval", + "handler": "many_operations.handler", + "integration": true, + "durableConfig": { + "RetentionPeriodInDays": 7, + "ExecutionTimeout": 300 + }, + "path": "./src/comprehensive_operations/many_operations.py" + }, { "name": "Create Callback Concurrency", "description": "Demonstrates multiple concurrent createCallback operations using context.parallel", diff --git a/packages/aws-durable-execution-sdk-python-examples/src/comprehensive_operations/many_operations.py b/packages/aws-durable-execution-sdk-python-examples/src/comprehensive_operations/many_operations.py new file mode 100644 index 00000000..f6b40b99 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-examples/src/comprehensive_operations/many_operations.py @@ -0,0 +1,69 @@ +"""Example with many durable step operations.""" + +from aws_durable_execution_sdk_python.context import ( + DurableContext, + durable_with_child_context, +) +from aws_durable_execution_sdk_python.execution import durable_execution + + +TOP_LEVEL_STEP_COUNT = 12 +CHILD_CONTEXT_COUNT = 4 +CHILD_STEPS_PER_CONTEXT = 2 + + +def make_step(value: int): + return lambda _: value + + +@durable_with_child_context +def child_group( + child_context: DurableContext, group_index: int, steps_per_context: int +) -> list[int]: + values = [] + for step_index in range(steps_per_context): + value = group_index * 100 + step_index + values.append( + child_context.step( + make_step(value), + name=f"child-{group_index:02d}-step-{step_index:02d}", + ) + ) + return values + + +@durable_execution +def handler(_event, context: DurableContext) -> dict[str, object]: + """Run many named step operations across parent and child contexts.""" + + step_results = [] + for index in range(TOP_LEVEL_STEP_COUNT): + step_results.append( + context.step( + make_step(index), + name=f"many-step-{index:03d}", + ) + ) + + child_results = [] + for group_index in range(CHILD_CONTEXT_COUNT): + child_results.append( + context.run_in_child_context( + child_group(group_index, CHILD_STEPS_PER_CONTEXT), + name=f"child-context-{group_index:02d}", + ) + ) + + child_values = [value for child_result in child_results for value in child_result] + + return { + "topLevelStepCount": TOP_LEVEL_STEP_COUNT, + "childContextCount": CHILD_CONTEXT_COUNT, + "childStepCount": CHILD_CONTEXT_COUNT * CHILD_STEPS_PER_CONTEXT, + "totalStepCount": TOP_LEVEL_STEP_COUNT + + CHILD_CONTEXT_COUNT * CHILD_STEPS_PER_CONTEXT, + "firstStep": step_results[0] if step_results else None, + "lastStep": step_results[-1] if step_results else None, + "topLevelStepTotal": sum(step_results), + "childStepTotal": sum(child_values), + } diff --git a/packages/aws-durable-execution-sdk-python-examples/template.yaml b/packages/aws-durable-execution-sdk-python-examples/template.yaml index fa1220a7..5ae82aa3 100644 --- a/packages/aws-durable-execution-sdk-python-examples/template.yaml +++ b/packages/aws-durable-execution-sdk-python-examples/template.yaml @@ -960,6 +960,24 @@ } } }, + "ManyOperations": { + "Type": "AWS::Serverless::Function", + "Properties": { + "CodeUri": "build/", + "Handler": "many_operations.handler", + "Description": "Runs many durable steps and child contexts to exercise long execution history retrieval", + "Role": { + "Fn::GetAtt": [ + "DurableFunctionRole", + "Arn" + ] + }, + "DurableConfig": { + "RetentionPeriodInDays": 7, + "ExecutionTimeout": 300 + } + } + }, "CallbackConcurrency": { "Type": "AWS::Serverless::Function", "Properties": { @@ -1193,4 +1211,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/aws-durable-execution-sdk-python-examples/test/comprehensive_operations/test_many_operations.py b/packages/aws-durable-execution-sdk-python-examples/test/comprehensive_operations/test_many_operations.py new file mode 100644 index 00000000..01308a79 --- /dev/null +++ b/packages/aws-durable-execution-sdk-python-examples/test/comprehensive_operations/test_many_operations.py @@ -0,0 +1,70 @@ +"""Tests for many_operations.""" + +import pytest +from aws_durable_execution_sdk_python.execution import InvocationStatus + +from src.comprehensive_operations import many_operations +from test.conftest import deserialize_operation_payload + + +TOP_LEVEL_STEP_COUNT = 12 +CHILD_CONTEXT_COUNT = 4 +CHILD_STEPS_PER_CONTEXT = 2 +TOTAL_STEP_COUNT = TOP_LEVEL_STEP_COUNT + CHILD_CONTEXT_COUNT * CHILD_STEPS_PER_CONTEXT + + +@pytest.mark.example +@pytest.mark.durable_execution( + handler=many_operations.handler, + lambda_function_name="Many Operations", +) +def test_fetches_many_steps_and_child_context_operations(durable_runner): + """Verify result history includes many steps and child context operations.""" + with durable_runner: + result = durable_runner.run(input=None, timeout=60) + + assert result.status is InvocationStatus.SUCCEEDED + + result_data = deserialize_operation_payload(result.result) + assert result_data == { + "topLevelStepCount": TOP_LEVEL_STEP_COUNT, + "childContextCount": CHILD_CONTEXT_COUNT, + "childStepCount": CHILD_CONTEXT_COUNT * CHILD_STEPS_PER_CONTEXT, + "totalStepCount": TOTAL_STEP_COUNT, + "firstStep": 0, + "lastStep": TOP_LEVEL_STEP_COUNT - 1, + "topLevelStepTotal": sum(range(TOP_LEVEL_STEP_COUNT)), + "childStepTotal": sum( + group_index * 100 + step_index + for group_index in range(CHILD_CONTEXT_COUNT) + for step_index in range(CHILD_STEPS_PER_CONTEXT) + ), + } + + all_ops = result.get_all_operations() + top_level_step_ops = [ + op + for op in all_ops + if op.operation_type.value == "STEP" + and op.name is not None + and op.name.startswith("many-step-") + ] + assert len(top_level_step_ops) == TOP_LEVEL_STEP_COUNT + + child_context_ops = [ + op + for op in all_ops + if op.operation_type.value == "CONTEXT" + and op.name is not None + and op.name.startswith("child-context-") + ] + assert len(child_context_ops) == CHILD_CONTEXT_COUNT + + child_step_ops = [ + op + for op in all_ops + if op.operation_type.value == "STEP" + and op.name is not None + and op.name.startswith("child-") + ] + assert len(child_step_ops) == CHILD_CONTEXT_COUNT * CHILD_STEPS_PER_CONTEXT diff --git a/packages/aws-durable-execution-sdk-python-testing/src/aws_durable_execution_sdk_python_testing/runner.py b/packages/aws-durable-execution-sdk-python-testing/src/aws_durable_execution_sdk_python_testing/runner.py index c263e8bc..2444dd13 100644 --- a/packages/aws-durable-execution-sdk-python-testing/src/aws_durable_execution_sdk_python_testing/runner.py +++ b/packages/aws-durable-execution-sdk-python-testing/src/aws_durable_execution_sdk_python_testing/runner.py @@ -1170,11 +1170,28 @@ def _fetch_execution_history( Raises: ClientError: If lambda client encounter error """ - history_dict = self.lambda_client.get_durable_execution_history( - DurableExecutionArn=execution_arn, - IncludeExecutionData=True, - ) - history_response = GetDurableExecutionHistoryResponse.from_dict(history_dict) + events: list[Event] = [] + next_marker: str | None = None + + while True: + request = { + "DurableExecutionArn": execution_arn, + "IncludeExecutionData": True, + } + if next_marker: + request["Marker"] = next_marker + + history_dict = self.lambda_client.get_durable_execution_history(**request) + history_response = GetDurableExecutionHistoryResponse.from_dict( + history_dict + ) + events.extend(history_response.events) + + next_marker = history_response.next_marker + if not next_marker: + break + + history_response = GetDurableExecutionHistoryResponse(events=events) logger.info("Retrieved %d events from history", len(history_response.events)) diff --git a/packages/aws-durable-execution-sdk-python-testing/tests/runner_test.py b/packages/aws-durable-execution-sdk-python-testing/tests/runner_test.py index b243ae96..730db044 100644 --- a/packages/aws-durable-execution-sdk-python-testing/tests/runner_test.py +++ b/packages/aws-durable-execution-sdk-python-testing/tests/runner_test.py @@ -2,7 +2,7 @@ import datetime import json -from unittest.mock import Mock, patch +from unittest.mock import Mock, call, patch import pytest from aws_durable_execution_sdk_python.execution import InvocationStatus @@ -45,6 +45,40 @@ ) +def _step_started_event(index: int) -> dict[str, object]: + return { + "EventType": "StepStarted", + "EventId": index, + "EventTimestamp": "2023-01-01T00:00:00Z", + "Id": f"long-step-{index}", + "Name": f"long-step-{index}", + } + + +def _callback_started_event(index: int) -> dict[str, object]: + return { + "EventType": "CallbackStarted", + "EventId": index, + "EventTimestamp": "2023-01-01T00:00:00Z", + "Id": "late-callback", + "Name": "late-callback", + "CallbackStartedDetails": {"CallbackId": "callback-late"}, + } + + +def _invocation_completed_event(index: int) -> dict[str, object]: + return { + "EventType": "InvocationCompleted", + "EventId": index, + "EventTimestamp": "2023-01-01T00:00:01Z", + "InvocationCompletedDetails": { + "StartTimestamp": "2023-01-01T00:00:00Z", + "EndTimestamp": "2023-01-01T00:00:01Z", + "RequestId": "request-1", + }, + } + + def test_operation_creation(): """Test basic Operation creation.""" op = Operation( @@ -1883,6 +1917,163 @@ def test_cloud_runner_wait_for_callback_waits_for_invocation(mock_boto3): assert mock_client.get_durable_execution_history.call_count == 2 +@patch("aws_durable_execution_sdk_python_testing.runner.boto3") +def test_cloud_runner_wait_for_callback_with_paginated_long_history(mock_boto3): + """Test cloud callback lookup follows history pagination.""" + from aws_durable_execution_sdk_python_testing.runner import ( + DurableFunctionCloudTestRunner, + ) + + mock_client = Mock() + mock_boto3.client.return_value = mock_client + + first_page = [_step_started_event(index) for index in range(1, 51)] + second_page = [_step_started_event(index) for index in range(51, 101)] + third_page = [ + *[_step_started_event(index) for index in range(101, 121)], + _callback_started_event(121), + _invocation_completed_event(122), + ] + mock_client.get_durable_execution_history.side_effect = [ + {"Events": first_page, "NextMarker": "page-2"}, + {"Events": second_page, "NextMarker": "page-3"}, + {"Events": third_page}, + ] + + runner = DurableFunctionCloudTestRunner( + function_name="test-function", poll_interval=0.01 + ) + callback_id = runner.wait_for_callback("test-arn", name="late-callback", timeout=10) + + assert callback_id == "callback-late" + assert mock_client.get_durable_execution_history.call_args_list == [ + call(DurableExecutionArn="test-arn", IncludeExecutionData=True), + call( + DurableExecutionArn="test-arn", + IncludeExecutionData=True, + Marker="page-2", + ), + call( + DurableExecutionArn="test-arn", + IncludeExecutionData=True, + Marker="page-3", + ), + ] + + +@patch("aws_durable_execution_sdk_python_testing.runner.boto3") +def test_cloud_runner_wait_for_result_with_paginated_child_context_history(mock_boto3): + """Test cloud result construction follows history pagination.""" + from aws_durable_execution_sdk_python_testing.runner import ( + DurableFunctionCloudTestRunner, + ) + + mock_client = Mock() + mock_boto3.client.return_value = mock_client + + mock_client.get_durable_execution.return_value = { + "DurableExecutionArn": "test-arn", + "DurableExecutionName": "test-execution", + "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:test", + "Status": "SUCCEEDED", + "StartTimestamp": "2023-01-01T00:00:00Z", + "EndTimestamp": "2023-01-01T00:01:00Z", + "Result": "test-result", + } + mock_client.get_durable_execution_history.side_effect = [ + { + "Events": [ + { + "EventType": "ExecutionStarted", + "EventId": 1, + "EventTimestamp": "2023-01-01T00:00:00Z", + "Id": "exec-1", + }, + { + "EventType": "StepStarted", + "EventId": 2, + "EventTimestamp": "2023-01-01T00:00:01Z", + "Id": "top-step-1", + "Name": "top-step", + }, + { + "EventType": "StepSucceeded", + "EventId": 3, + "EventTimestamp": "2023-01-01T00:00:02Z", + "Id": "top-step-1", + }, + ], + "NextMarker": "page-2", + }, + { + "Events": [ + { + "EventType": "ContextStarted", + "EventId": 4, + "EventTimestamp": "2023-01-01T00:00:03Z", + "Id": "child-context-1", + "Name": "child-context", + }, + { + "EventType": "StepStarted", + "EventId": 5, + "EventTimestamp": "2023-01-01T00:00:04Z", + "Id": "child-step-1", + "Name": "child-step", + "ParentId": "child-context-1", + }, + { + "EventType": "StepSucceeded", + "EventId": 6, + "EventTimestamp": "2023-01-01T00:00:05Z", + "Id": "child-step-1", + "ParentId": "child-context-1", + }, + ], + "NextMarker": "page-3", + }, + { + "Events": [ + { + "EventType": "ContextSucceeded", + "EventId": 7, + "EventTimestamp": "2023-01-01T00:00:06Z", + "Id": "child-context-1", + }, + { + "EventType": "ExecutionSucceeded", + "EventId": 8, + "EventTimestamp": "2023-01-01T00:00:07Z", + "Id": "exec-1", + }, + ] + }, + ] + + runner = DurableFunctionCloudTestRunner( + function_name="test-function", poll_interval=0.01 + ) + result = runner.wait_for_result("test-arn", timeout=10) + + assert result.status is InvocationStatus.SUCCEEDED + assert result.get_step("top-step").operation_id == "top-step-1" + child_context = result.get_context("child-context") + assert child_context.get_step("child-step").operation_id == "child-step-1" + assert mock_client.get_durable_execution_history.call_args_list == [ + call(DurableExecutionArn="test-arn", IncludeExecutionData=True), + call( + DurableExecutionArn="test-arn", + IncludeExecutionData=True, + Marker="page-2", + ), + call( + DurableExecutionArn="test-arn", + IncludeExecutionData=True, + Marker="page-3", + ), + ] + + @patch("aws_durable_execution_sdk_python_testing.runner.boto3") def test_cloud_runner_wait_for_callback_success_without_name(mock_boto3): """Test DurableFunctionCloudTestRunner.wait_for_callback success.""" @@ -1990,6 +2181,27 @@ def test_local_runner_wait_for_callback_all_done_without_name(mock_executor_clas runner.wait_for_callback("test-arn", timeout=2) +@patch("aws_durable_execution_sdk_python_testing.runner.Executor") +def test_local_runner_wait_for_callback_with_long_history(mock_executor_class): + """Test local callback lookup scans long histories.""" + handler = Mock() + mock_executor = Mock() + mock_executor_class.return_value = mock_executor + long_history = [ + *[_step_started_event(index) for index in range(1, 121)], + _callback_started_event(121), + _invocation_completed_event(122), + ] + mock_executor.get_execution_history.return_value = ( + GetDurableExecutionHistoryResponse.from_dict({"Events": long_history}) + ) + + runner = DurableFunctionTestRunner(handler) + callback_id = runner.wait_for_callback("test-arn", name="late-callback", timeout=2) + + assert callback_id == "callback-late" + + @patch("aws_durable_execution_sdk_python_testing.runner.Executor") def test_local_runner_wait_for_callback_with_exception(mock_executor_class): """Test DurableFunctionCloudTestRunner.wait_for_callback with exception""" From aeea072264d78dbc420e7621acf9402c9ec9fd53 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 6 Jul 2026 19:11:04 +0000 Subject: [PATCH 2/2] chore: update SAM template --- .../aws-durable-execution-sdk-python-examples/template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-durable-execution-sdk-python-examples/template.yaml b/packages/aws-durable-execution-sdk-python-examples/template.yaml index 5ae82aa3..9e5e2bf5 100644 --- a/packages/aws-durable-execution-sdk-python-examples/template.yaml +++ b/packages/aws-durable-execution-sdk-python-examples/template.yaml @@ -1211,4 +1211,4 @@ } } } -} +} \ No newline at end of file