Skip to content

Commit d9930ee

Browse files
committed
fix: convert InvocationCompletedDetails to unix milliseconds
SQLite storage was failing with "Object of type datetime is not JSON serializable" when attempting to save execution state after Lambda invocation. The root cause was InvocationCompletedDetails.to_dict() returning raw datetime objects instead of JSON-serializable integers. This fix adds to_json_dict() and from_json_dict() methods to InvocationCompletedDetails that convert datetime objects to/from Unix milliseconds using TimestampConverter, matching the pattern already used by the SDK's Operation class. Changes: - Add InvocationCompletedDetails.to_json_dict() for serialization - Add InvocationCompletedDetails.from_json_dict() for deserialization - Update Execution.to_json_dict() to call completion.to_json_dict() - Update Execution.from_json_dict() to call from_json_dict() The to_dict() method is preserved for internal use where datetime objects are needed, while to_json_dict() is used for storage and JSON serialization paths. Fixes execution persistence failures in SQLite and filesystem stores. closes #193
1 parent 65a4d59 commit d9930ee

6 files changed

Lines changed: 157 additions & 10 deletions

File tree

src/aws_durable_execution_sdk_python_testing/execution.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def to_json_dict(self) -> dict[str, Any]:
102102
"Operations": [op.to_json_dict() for op in self.operations],
103103
"Updates": [update.to_dict() for update in self.updates],
104104
"InvocationCompletions": [
105-
completion.to_dict() for completion in self.invocation_completions
105+
completion.to_json_dict() for completion in self.invocation_completions
106106
],
107107
"UsedTokens": list(self.used_tokens),
108108
"TokenSequence": self._token_sequence,
@@ -135,7 +135,7 @@ def from_json_dict(cls, data: dict[str, Any]) -> Execution:
135135
OperationUpdate.from_dict(update_data) for update_data in data["Updates"]
136136
]
137137
execution.invocation_completions = [
138-
InvocationCompletedDetails.from_dict(item)
138+
InvocationCompletedDetails.from_json_dict(item)
139139
for item in data.get("InvocationCompletions", [])
140140
]
141141
execution.used_tokens = set(data["UsedTokens"])

src/aws_durable_execution_sdk_python_testing/executor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def start_execution(
115115
execution = Execution.new(input=input)
116116
execution.start()
117117
self._store.save(execution)
118+
logger.debug("Created execution with ARN: %s", execution.durable_execution_arn)
118119

119120
completion_event = self._scheduler.create_event()
120121
self._completion_events[execution.durable_execution_arn] = completion_event

src/aws_durable_execution_sdk_python_testing/invoker.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

1717
from aws_durable_execution_sdk_python_testing.exceptions import (
1818
DurableFunctionsTestError,
19+
InvalidParameterValueException,
20+
ResourceNotFoundException,
1921
)
2022
from aws_durable_execution_sdk_python_testing.model import LambdaContext
2123

24+
2225
if TYPE_CHECKING:
2326
from collections.abc import Callable
2427

@@ -217,10 +220,6 @@ def invoke(
217220
InvalidParameterValueException: If parameters are invalid
218221
DurableFunctionsTestError: For other invocation failures
219222
"""
220-
from aws_durable_execution_sdk_python_testing.exceptions import (
221-
ResourceNotFoundException,
222-
InvalidParameterValueException,
223-
)
224223

225224
# Parameter validation
226225
if not function_name or not function_name.strip():

src/aws_durable_execution_sdk_python_testing/model.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33
from __future__ import annotations
44

55
import datetime
6+
import json
67
from dataclasses import dataclass, replace
78
from enum import Enum
89
from typing import Any
9-
import json
10-
11-
from dateutil.tz import UTC
1210

1311
from aws_durable_execution_sdk_python.execution import DurableExecutionInvocationOutput
1412

@@ -30,12 +28,14 @@
3028
OperationUpdate,
3129
StepDetails,
3230
StepOptions,
31+
TimestampConverter,
3332
WaitDetails,
3433
WaitOptions,
3534
)
3635
from aws_durable_execution_sdk_python.types import (
3736
LambdaContext as LambdaContextProtocol,
3837
)
38+
from dateutil.tz import UTC
3939

4040
from aws_durable_execution_sdk_python_testing.exceptions import (
4141
InvalidParameterValueException,
@@ -1239,13 +1239,30 @@ def from_dict(cls, data: dict) -> InvocationCompletedDetails:
12391239
request_id=data["RequestId"],
12401240
)
12411241

1242+
@classmethod
1243+
def from_json_dict(cls, data: dict) -> InvocationCompletedDetails:
1244+
"""Deserialize from JSON dict with Unix millisecond timestamps."""
1245+
return cls(
1246+
start_timestamp=TimestampConverter.from_unix_millis(data["StartTimestamp"]), # type: ignore[arg-type]
1247+
end_timestamp=TimestampConverter.from_unix_millis(data["EndTimestamp"]), # type: ignore[arg-type]
1248+
request_id=data["RequestId"],
1249+
)
1250+
12421251
def to_dict(self) -> dict[str, Any]:
12431252
return {
12441253
"StartTimestamp": self.start_timestamp,
12451254
"EndTimestamp": self.end_timestamp,
12461255
"RequestId": self.request_id,
12471256
}
12481257

1258+
def to_json_dict(self) -> dict[str, Any]:
1259+
"""Convert to JSON-serializable dict with Unix millisecond timestamps."""
1260+
return {
1261+
"StartTimestamp": TimestampConverter.to_unix_millis(self.start_timestamp),
1262+
"EndTimestamp": TimestampConverter.to_unix_millis(self.end_timestamp),
1263+
"RequestId": self.request_id,
1264+
}
1265+
12491266

12501267
# endregion event_structures
12511268

src/aws_durable_execution_sdk_python_testing/web/handlers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,16 +291,22 @@ def handle(self, parsed_route: Route, request: HTTPRequest) -> HTTPResponse: #
291291
Returns:
292292
HTTPResponse: The HTTP response to send to the client
293293
"""
294+
logger.debug("🌟 HANDLER: Received POST /start-durable-execution request")
294295
try:
295296
body_data: dict[str, Any] = self._parse_json_body(request)
297+
logger.debug("🌟 HANDLER: Parsed request body successfully")
296298

297299
start_input: StartDurableExecutionInput = (
298300
StartDurableExecutionInput.from_dict(body_data)
299301
)
302+
logger.debug(
303+
"🌟 HANDLER: Created StartDurableExecutionInput, calling executor.start_execution()"
304+
)
300305

301306
start_output: StartDurableExecutionOutput = self.executor.start_execution(
302307
start_input
303308
)
309+
logger.debug("🌟 HANDLER: executor.start_execution() returned successfully")
304310

305311
response_data: dict[str, Any] = start_output.to_dict()
306312

tests/model_test.py

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
import datetime
66

77
import pytest
8-
98
from aws_durable_execution_sdk_python.lambda_service import (
109
OperationStatus,
1110
OperationType,
1211
)
12+
1313
from aws_durable_execution_sdk_python_testing.exceptions import (
1414
InvalidParameterValueException,
1515
)
@@ -3564,3 +3564,127 @@ def test_events_to_operations_invalid_sub_type():
35643564
match=f"'{invalid_sub_type}' is not a valid OperationSubType",
35653565
):
35663566
events_to_operations([event])
3567+
3568+
3569+
def test_invocation_completed_details_to_json_dict():
3570+
"""Test InvocationCompletedDetails.to_json_dict() converts datetime to Unix milliseconds."""
3571+
from aws_durable_execution_sdk_python_testing.model import (
3572+
InvocationCompletedDetails,
3573+
)
3574+
3575+
start_time = datetime.datetime(2023, 1, 1, 0, 0, 0, 123456, tzinfo=datetime.UTC)
3576+
end_time = datetime.datetime(2023, 1, 1, 0, 1, 0, 456789, tzinfo=datetime.UTC)
3577+
3578+
details = InvocationCompletedDetails(
3579+
start_timestamp=start_time, end_timestamp=end_time, request_id="req-123"
3580+
)
3581+
3582+
json_dict = details.to_json_dict()
3583+
3584+
# Verify timestamps are converted to Unix milliseconds (integers)
3585+
assert json_dict["StartTimestamp"] == 1672531200123
3586+
assert json_dict["EndTimestamp"] == 1672531260456
3587+
assert json_dict["RequestId"] == "req-123"
3588+
3589+
# Verify all values are JSON-serializable
3590+
import json
3591+
3592+
json_str = json.dumps(json_dict)
3593+
assert json_str is not None
3594+
3595+
3596+
def test_invocation_completed_details_from_json_dict():
3597+
"""Test InvocationCompletedDetails.from_json_dict() converts Unix milliseconds to datetime."""
3598+
from aws_durable_execution_sdk_python_testing.model import (
3599+
InvocationCompletedDetails,
3600+
)
3601+
3602+
json_dict = {
3603+
"StartTimestamp": 1672531200123,
3604+
"EndTimestamp": 1672531260456,
3605+
"RequestId": "req-456",
3606+
}
3607+
3608+
details = InvocationCompletedDetails.from_json_dict(json_dict)
3609+
3610+
# Verify timestamps are converted to datetime objects
3611+
assert details.start_timestamp == datetime.datetime(
3612+
2023, 1, 1, 0, 0, 0, 123000, tzinfo=datetime.UTC
3613+
)
3614+
assert details.end_timestamp == datetime.datetime(
3615+
2023, 1, 1, 0, 1, 0, 456000, tzinfo=datetime.UTC
3616+
)
3617+
assert details.request_id == "req-456"
3618+
3619+
3620+
def test_invocation_completed_details_json_round_trip():
3621+
"""Test InvocationCompletedDetails to_json_dict/from_json_dict round-trip."""
3622+
from aws_durable_execution_sdk_python_testing.model import (
3623+
InvocationCompletedDetails,
3624+
)
3625+
3626+
original = InvocationCompletedDetails(
3627+
start_timestamp=datetime.datetime(
3628+
2023, 6, 15, 12, 30, 45, 678000, tzinfo=datetime.UTC
3629+
),
3630+
end_timestamp=datetime.datetime(
3631+
2023, 6, 15, 12, 31, 50, 123000, tzinfo=datetime.UTC
3632+
),
3633+
request_id="round-trip-test",
3634+
)
3635+
3636+
# Serialize to JSON dict
3637+
json_dict = original.to_json_dict()
3638+
3639+
# Deserialize back
3640+
restored = InvocationCompletedDetails.from_json_dict(json_dict)
3641+
3642+
# Verify round-trip preserves data
3643+
assert restored.start_timestamp == original.start_timestamp
3644+
assert restored.end_timestamp == original.end_timestamp
3645+
assert restored.request_id == original.request_id
3646+
3647+
3648+
def test_invocation_completed_details_to_dict_preserves_datetime():
3649+
"""Test InvocationCompletedDetails.to_dict() preserves datetime objects (not converted)."""
3650+
from aws_durable_execution_sdk_python_testing.model import (
3651+
InvocationCompletedDetails,
3652+
)
3653+
3654+
start_time = datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.UTC)
3655+
end_time = datetime.datetime(2023, 1, 1, 0, 1, 0, tzinfo=datetime.UTC)
3656+
3657+
details = InvocationCompletedDetails(
3658+
start_timestamp=start_time, end_timestamp=end_time, request_id="req-789"
3659+
)
3660+
3661+
regular_dict = details.to_dict()
3662+
3663+
# Verify to_dict() preserves datetime objects (not converted to Unix milliseconds)
3664+
assert regular_dict["StartTimestamp"] == start_time
3665+
assert regular_dict["EndTimestamp"] == end_time
3666+
assert isinstance(regular_dict["StartTimestamp"], datetime.datetime)
3667+
assert isinstance(regular_dict["EndTimestamp"], datetime.datetime)
3668+
3669+
3670+
def test_invocation_completed_details_from_json_dict_invalid_timestamp():
3671+
"""Test InvocationCompletedDetails.from_json_dict() raises error for invalid timestamps."""
3672+
from aws_durable_execution_sdk_python_testing.exceptions import (
3673+
InvalidParameterValueException,
3674+
)
3675+
from aws_durable_execution_sdk_python_testing.model import (
3676+
InvocationCompletedDetails,
3677+
)
3678+
3679+
# Test with invalid timestamp that would return None
3680+
json_dict = {
3681+
"StartTimestamp": None,
3682+
"EndTimestamp": 1672531260456,
3683+
"RequestId": "req-error",
3684+
}
3685+
3686+
with pytest.raises(
3687+
InvalidParameterValueException,
3688+
match="StartTimestamp and EndTimestamp must be valid",
3689+
):
3690+
InvocationCompletedDetails.from_json_dict(json_dict)

0 commit comments

Comments
 (0)