|
16 | 16 | from aws_durable_execution_sdk_python.exceptions import ( |
17 | 17 | BackgroundThreadError, |
18 | 18 | CallableRuntimeError, |
| 19 | + DurableApiErrorCategory, |
| 20 | + GetExecutionStateError, |
19 | 21 | OrphanedChildException, |
20 | 22 | ) |
21 | 23 | from aws_durable_execution_sdk_python.identifier import OperationIdentifier |
@@ -724,6 +726,88 @@ def mock_get_execution_state(durable_execution_arn, checkpoint_token, next_marke |
724 | 726 | assert operation.operation_id == expected_op.operation_id |
725 | 727 |
|
726 | 728 |
|
| 729 | +def test_fetch_paginated_operations_stores_partial_results_on_error(): |
| 730 | + """Test that operations from successful pages are stored even when a later page fails.""" |
| 731 | + mock_lambda_client = Mock(spec=LambdaClient) |
| 732 | + |
| 733 | + non_retryable_error = GetExecutionStateError( |
| 734 | + message="KMS access denied", |
| 735 | + error_category=DurableApiErrorCategory.EXECUTION, |
| 736 | + error={"Code": "KMSAccessDeniedException", "Message": "KMS access denied"}, |
| 737 | + response_metadata={"HTTPStatusCode": 502}, |
| 738 | + ) |
| 739 | + |
| 740 | + def mock_get_execution_state(durable_execution_arn, checkpoint_token, next_marker): |
| 741 | + if next_marker == "marker1": |
| 742 | + return StateOutput( |
| 743 | + operations=[ |
| 744 | + Operation( |
| 745 | + operation_id="1", |
| 746 | + operation_type=OperationType.STEP, |
| 747 | + status=OperationStatus.STARTED, |
| 748 | + ) |
| 749 | + ], |
| 750 | + next_marker="marker2", |
| 751 | + ) |
| 752 | + raise non_retryable_error |
| 753 | + |
| 754 | + mock_lambda_client.get_execution_state.side_effect = mock_get_execution_state |
| 755 | + |
| 756 | + state = ExecutionState( |
| 757 | + durable_execution_arn="test_arn", |
| 758 | + initial_checkpoint_token="token123", # noqa: S106 |
| 759 | + operations={}, |
| 760 | + service_client=mock_lambda_client, |
| 761 | + ) |
| 762 | + |
| 763 | + with pytest.raises(GetExecutionStateError): |
| 764 | + state.fetch_paginated_operations( |
| 765 | + initial_operations=[ |
| 766 | + Operation( |
| 767 | + operation_id="0", |
| 768 | + operation_type=OperationType.STEP, |
| 769 | + status=OperationStatus.STARTED, |
| 770 | + ) |
| 771 | + ], |
| 772 | + checkpoint_token="test_token", # noqa: S106 |
| 773 | + next_marker="marker1", |
| 774 | + ) |
| 775 | + |
| 776 | + # Initial operation + page 1 should be stored despite page 2 failing |
| 777 | + assert "0" in state.operations |
| 778 | + assert "1" in state.operations |
| 779 | + assert len(state.operations) == 2 |
| 780 | + |
| 781 | + |
| 782 | +def test_fetch_paginated_operations_logs_error(caplog): |
| 783 | + """Test that GetExecutionStateError is logged with structured extras.""" |
| 784 | + mock_lambda_client = Mock(spec=LambdaClient) |
| 785 | + |
| 786 | + error = GetExecutionStateError( |
| 787 | + message="Service error", |
| 788 | + error_category=DurableApiErrorCategory.INVOCATION, |
| 789 | + error={"Code": "ServiceException", "Message": "Service error"}, |
| 790 | + response_metadata={"HTTPStatusCode": 500}, |
| 791 | + ) |
| 792 | + mock_lambda_client.get_execution_state.side_effect = error |
| 793 | + |
| 794 | + state = ExecutionState( |
| 795 | + durable_execution_arn="test_arn", |
| 796 | + initial_checkpoint_token="token123", # noqa: S106 |
| 797 | + operations={}, |
| 798 | + service_client=mock_lambda_client, |
| 799 | + ) |
| 800 | + |
| 801 | + with pytest.raises(GetExecutionStateError): |
| 802 | + state.fetch_paginated_operations( |
| 803 | + initial_operations=[], |
| 804 | + checkpoint_token="test_token", # noqa: S106 |
| 805 | + next_marker="marker1", |
| 806 | + ) |
| 807 | + |
| 808 | + assert "Durable API error during state fetch." in caplog.text |
| 809 | + |
| 810 | + |
727 | 811 | # ============================================================================ |
728 | 812 | # Checkpoint Batching Tests |
729 | 813 | # ============================================================================ |
|
0 commit comments