aws-durable-execution-sdk-python-testing 1.2.1__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aws_durable_execution_sdk_python_testing/__about__.py +1 -1
- aws_durable_execution_sdk_python_testing/checkpoint/core.py +171 -0
- aws_durable_execution_sdk_python_testing/checkpoint/effects.py +61 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processor.py +124 -45
- aws_durable_execution_sdk_python_testing/checkpoint/processors/base.py +40 -14
- aws_durable_execution_sdk_python_testing/checkpoint/processors/callback.py +5 -2
- aws_durable_execution_sdk_python_testing/checkpoint/processors/context.py +6 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/execution.py +4 -9
- aws_durable_execution_sdk_python_testing/checkpoint/processors/invoke.py +139 -0
- aws_durable_execution_sdk_python_testing/checkpoint/processors/step.py +15 -21
- aws_durable_execution_sdk_python_testing/checkpoint/processors/wait.py +10 -16
- aws_durable_execution_sdk_python_testing/checkpoint/transformer.py +145 -48
- aws_durable_execution_sdk_python_testing/checkpoint/validators/checkpoint.py +65 -8
- aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/invoke.py +79 -18
- aws_durable_execution_sdk_python_testing/child_dispatcher.py +828 -0
- aws_durable_execution_sdk_python_testing/cli.py +48 -1
- aws_durable_execution_sdk_python_testing/client.py +21 -10
- aws_durable_execution_sdk_python_testing/clock.py +69 -0
- aws_durable_execution_sdk_python_testing/exceptions.py +14 -0
- aws_durable_execution_sdk_python_testing/execution.py +583 -49
- aws_durable_execution_sdk_python_testing/executor.py +1813 -349
- aws_durable_execution_sdk_python_testing/invoker.py +356 -85
- aws_durable_execution_sdk_python_testing/model.py +81 -73
- aws_durable_execution_sdk_python_testing/observer.py +100 -88
- aws_durable_execution_sdk_python_testing/runner.py +407 -30
- aws_durable_execution_sdk_python_testing/scheduler.py +38 -13
- aws_durable_execution_sdk_python_testing/stores/filesystem.py +10 -2
- aws_durable_execution_sdk_python_testing/stores/sqlite.py +18 -6
- aws_durable_execution_sdk_python_testing/threads.py +157 -0
- aws_durable_execution_sdk_python_testing/token.py +11 -2
- aws_durable_execution_sdk_python_testing/web/handlers.py +33 -7
- aws_durable_execution_sdk_python_testing/web/serialization.py +1 -2
- aws_durable_execution_sdk_python_testing/web/server.py +1 -1
- aws_durable_execution_sdk_python_testing/worker/__init__.py +9 -0
- aws_durable_execution_sdk_python_testing/worker/checkpoint_tasks.py +119 -0
- aws_durable_execution_sdk_python_testing/worker/execution_worker.py +117 -0
- aws_durable_execution_sdk_python_testing/worker/lane.py +118 -0
- aws_durable_execution_sdk_python_testing/worker/registry.py +124 -0
- aws_durable_execution_sdk_python_testing/worker/status.py +27 -0
- aws_durable_execution_sdk_python_testing/worker/task.py +57 -0
- {aws_durable_execution_sdk_python_testing-1.2.1.dist-info → aws_durable_execution_sdk_python_testing-1.3.0.dist-info}/METADATA +50 -58
- aws_durable_execution_sdk_python_testing-1.3.0.dist-info/RECORD +64 -0
- {aws_durable_execution_sdk_python_testing-1.2.1.dist-info → aws_durable_execution_sdk_python_testing-1.3.0.dist-info}/WHEEL +1 -1
- aws_durable_execution_sdk_python_testing/checkpoint/validators/transitions.py +0 -66
- aws_durable_execution_sdk_python_testing-1.2.1.dist-info/RECORD +0 -52
- {aws_durable_execution_sdk_python_testing-1.2.1.dist-info/licenses → aws_durable_execution_sdk_python_testing}/LICENSE +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1.dist-info/licenses → aws_durable_execution_sdk_python_testing}/NOTICE +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1.dist-info → aws_durable_execution_sdk_python_testing-1.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
6
|
+
|
|
7
|
+
from aws_durable_execution_sdk_python_testing.clock import real_now
|
|
6
8
|
import json
|
|
7
9
|
from dataclasses import dataclass, replace
|
|
8
10
|
from enum import Enum
|
|
@@ -35,8 +37,6 @@ from aws_durable_execution_sdk_python.lambda_service import (
|
|
|
35
37
|
from aws_durable_execution_sdk_python.types import (
|
|
36
38
|
LambdaContext as LambdaContextProtocol,
|
|
37
39
|
)
|
|
38
|
-
from dateutil.tz import UTC
|
|
39
|
-
|
|
40
40
|
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
41
41
|
InvalidParameterValueException,
|
|
42
42
|
)
|
|
@@ -279,6 +279,26 @@ class GetDurableExecutionResponse:
|
|
|
279
279
|
return result
|
|
280
280
|
|
|
281
281
|
|
|
282
|
+
LATEST_PUBLISHED_QUALIFIER = "$LATEST.PUBLISHED"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def executed_version(qualifier: str | None) -> str:
|
|
286
|
+
"""The function version an invocation runs, as Lambda reports it.
|
|
287
|
+
|
|
288
|
+
The service records the version Lambda executed: a numeric qualifier
|
|
289
|
+
names it, ``$LATEST.PUBLISHED`` is reported as itself, and an alias
|
|
290
|
+
resolves to the version it points at. The runner keeps no published
|
|
291
|
+
versions or aliases. So a numeric qualifier and ``$LATEST.PUBLISHED``
|
|
292
|
+
are reported as given, and no qualifier, ``$LATEST`` or an alias is
|
|
293
|
+
reported as ``$LATEST``.
|
|
294
|
+
"""
|
|
295
|
+
if qualifier is not None and (
|
|
296
|
+
qualifier.isdigit() or qualifier == LATEST_PUBLISHED_QUALIFIER
|
|
297
|
+
):
|
|
298
|
+
return qualifier
|
|
299
|
+
return "$LATEST"
|
|
300
|
+
|
|
301
|
+
|
|
282
302
|
@dataclass(frozen=True)
|
|
283
303
|
class Execution:
|
|
284
304
|
"""Execution summary structure from Smithy model."""
|
|
@@ -317,18 +337,22 @@ class Execution:
|
|
|
317
337
|
return result
|
|
318
338
|
|
|
319
339
|
@classmethod
|
|
320
|
-
def from_execution(cls, execution, status: str) -> Execution:
|
|
321
|
-
"""Create ExecutionSummary from Execution object.
|
|
340
|
+
def from_execution(cls, execution, status: str, default_region: str) -> Execution:
|
|
341
|
+
"""Create ExecutionSummary from Execution object.
|
|
342
|
+
|
|
343
|
+
``default_region`` serves an execution stored before the runner
|
|
344
|
+
recorded regions; every other execution reports its own.
|
|
345
|
+
"""
|
|
322
346
|
|
|
323
347
|
execution_op = execution.get_operation_execution_started()
|
|
324
348
|
return cls(
|
|
325
349
|
durable_execution_arn=execution.durable_execution_arn,
|
|
326
350
|
durable_execution_name=execution.start_input.execution_name,
|
|
327
|
-
function_arn=
|
|
351
|
+
function_arn=execution.function_arn(default_region),
|
|
328
352
|
status=status,
|
|
329
353
|
start_timestamp=execution_op.start_timestamp
|
|
330
354
|
if execution_op.start_timestamp
|
|
331
|
-
else
|
|
355
|
+
else real_now(),
|
|
332
356
|
end_timestamp=execution_op.end_timestamp
|
|
333
357
|
if execution_op.end_timestamp
|
|
334
358
|
else None,
|
|
@@ -1004,46 +1028,39 @@ class StepFailedDetails:
|
|
|
1004
1028
|
|
|
1005
1029
|
|
|
1006
1030
|
@dataclass(frozen=True)
|
|
1007
|
-
class
|
|
1008
|
-
"""Chained
|
|
1031
|
+
class ChainedInvokeStartedDetails:
|
|
1032
|
+
"""Chained invoke started event details.
|
|
1033
|
+
|
|
1034
|
+
ExecutedVersion is omitted: the local runner performs no function
|
|
1035
|
+
version resolution, so it has no resolved version to report.
|
|
1036
|
+
"""
|
|
1009
1037
|
|
|
1010
|
-
input: EventInput | None = None
|
|
1011
1038
|
function_name: str | None = None
|
|
1039
|
+
tenant_id: str | None = None
|
|
1040
|
+
input: EventInput | None = None
|
|
1041
|
+
durable_execution_arn: str | None = None
|
|
1012
1042
|
|
|
1013
1043
|
@classmethod
|
|
1014
|
-
def from_dict(cls, data: dict) ->
|
|
1044
|
+
def from_dict(cls, data: dict) -> ChainedInvokeStartedDetails:
|
|
1015
1045
|
input_data = None
|
|
1016
1046
|
if input_dict := data.get("Input"):
|
|
1017
1047
|
input_data = EventInput.from_dict(input_dict)
|
|
1018
1048
|
|
|
1019
1049
|
return cls(
|
|
1020
|
-
input=input_data,
|
|
1021
1050
|
function_name=data.get("FunctionName"),
|
|
1051
|
+
tenant_id=data.get("TenantId"),
|
|
1052
|
+
input=input_data,
|
|
1053
|
+
durable_execution_arn=data.get("DurableExecutionArn"),
|
|
1022
1054
|
)
|
|
1023
1055
|
|
|
1024
1056
|
def to_dict(self) -> dict[str, Any]:
|
|
1025
1057
|
result: dict[str, Any] = {}
|
|
1026
|
-
if self.input is not None:
|
|
1027
|
-
result["Input"] = self.input.to_dict()
|
|
1028
1058
|
if self.function_name is not None:
|
|
1029
1059
|
result["FunctionName"] = self.function_name
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
class ChainedInvokeStartedDetails:
|
|
1035
|
-
"""Chained invoke started event details."""
|
|
1036
|
-
|
|
1037
|
-
durable_execution_arn: str | None = None
|
|
1038
|
-
|
|
1039
|
-
@classmethod
|
|
1040
|
-
def from_dict(cls, data: dict) -> ChainedInvokeStartedDetails:
|
|
1041
|
-
return cls(
|
|
1042
|
-
durable_execution_arn=data.get("DurableExecutionArn"),
|
|
1043
|
-
)
|
|
1044
|
-
|
|
1045
|
-
def to_dict(self) -> dict[str, Any]:
|
|
1046
|
-
result: dict[str, Any] = {}
|
|
1060
|
+
if self.tenant_id is not None:
|
|
1061
|
+
result["TenantId"] = self.tenant_id
|
|
1062
|
+
if self.input is not None:
|
|
1063
|
+
result["Input"] = self.input.to_dict()
|
|
1047
1064
|
if self.durable_execution_arn is not None:
|
|
1048
1065
|
result["DurableExecutionArn"] = self.durable_execution_arn
|
|
1049
1066
|
return result
|
|
@@ -1288,6 +1305,7 @@ class EventCreationContext:
|
|
|
1288
1305
|
durable_execution_invocation_output: DurableExecutionInvocationOutput | None = None
|
|
1289
1306
|
operation_update: OperationUpdate | None = None
|
|
1290
1307
|
include_execution_data: bool = False # noqa: FBT001, FBT002
|
|
1308
|
+
child_execution_arn: str | None = None
|
|
1291
1309
|
|
|
1292
1310
|
@classmethod
|
|
1293
1311
|
def create(
|
|
@@ -1335,7 +1353,7 @@ class EventCreationContext:
|
|
|
1335
1353
|
return (
|
|
1336
1354
|
self.operation.start_timestamp
|
|
1337
1355
|
if self.operation.start_timestamp is not None
|
|
1338
|
-
else
|
|
1356
|
+
else real_now()
|
|
1339
1357
|
)
|
|
1340
1358
|
|
|
1341
1359
|
@property
|
|
@@ -1343,7 +1361,7 @@ class EventCreationContext:
|
|
|
1343
1361
|
return (
|
|
1344
1362
|
self.operation.end_timestamp
|
|
1345
1363
|
if self.operation.end_timestamp is not None
|
|
1346
|
-
else
|
|
1364
|
+
else real_now()
|
|
1347
1365
|
)
|
|
1348
1366
|
|
|
1349
1367
|
|
|
@@ -1373,7 +1391,6 @@ class Event:
|
|
|
1373
1391
|
step_started_details: StepStartedDetails | None = None
|
|
1374
1392
|
step_succeeded_details: StepSucceededDetails | None = None
|
|
1375
1393
|
step_failed_details: StepFailedDetails | None = None
|
|
1376
|
-
chained_invoke_pending_details: ChainedInvokePendingDetails | None = None
|
|
1377
1394
|
chained_invoke_started_details: ChainedInvokeStartedDetails | None = None
|
|
1378
1395
|
chained_invoke_succeeded_details: ChainedInvokeSucceededDetails | None = None
|
|
1379
1396
|
chained_invoke_failed_details: ChainedInvokeFailedDetails | None = None
|
|
@@ -1448,12 +1465,6 @@ class Event:
|
|
|
1448
1465
|
if details_data := data.get("StepFailedDetails"):
|
|
1449
1466
|
step_failed_details = StepFailedDetails.from_dict(details_data)
|
|
1450
1467
|
|
|
1451
|
-
chained_invoke_pending_details = None
|
|
1452
|
-
if details_data := data.get("ChainedInvokePendingDetails"):
|
|
1453
|
-
chained_invoke_pending_details = ChainedInvokePendingDetails.from_dict(
|
|
1454
|
-
details_data
|
|
1455
|
-
)
|
|
1456
|
-
|
|
1457
1468
|
chained_invoke_started_details = None
|
|
1458
1469
|
if details_data := data.get("ChainedInvokeStartedDetails"):
|
|
1459
1470
|
chained_invoke_started_details = ChainedInvokeStartedDetails.from_dict(
|
|
@@ -1530,7 +1541,6 @@ class Event:
|
|
|
1530
1541
|
step_started_details=step_started_details,
|
|
1531
1542
|
step_succeeded_details=step_succeeded_details,
|
|
1532
1543
|
step_failed_details=step_failed_details,
|
|
1533
|
-
chained_invoke_pending_details=chained_invoke_pending_details,
|
|
1534
1544
|
chained_invoke_started_details=chained_invoke_started_details,
|
|
1535
1545
|
chained_invoke_succeeded_details=chained_invoke_succeeded_details,
|
|
1536
1546
|
chained_invoke_failed_details=chained_invoke_failed_details,
|
|
@@ -1589,10 +1599,6 @@ class Event:
|
|
|
1589
1599
|
result["StepSucceededDetails"] = self.step_succeeded_details.to_dict()
|
|
1590
1600
|
if self.step_failed_details is not None:
|
|
1591
1601
|
result["StepFailedDetails"] = self.step_failed_details.to_dict()
|
|
1592
|
-
if self.chained_invoke_pending_details is not None:
|
|
1593
|
-
result["ChainedInvokePendingDetails"] = (
|
|
1594
|
-
self.chained_invoke_pending_details.to_dict()
|
|
1595
|
-
)
|
|
1596
1602
|
if self.chained_invoke_started_details is not None:
|
|
1597
1603
|
result["ChainedInvokeStartedDetails"] = (
|
|
1598
1604
|
self.chained_invoke_started_details.to_dict()
|
|
@@ -2010,31 +2016,20 @@ class Event:
|
|
|
2010
2016
|
# endregion step
|
|
2011
2017
|
|
|
2012
2018
|
# region chained_invoke
|
|
2013
|
-
@classmethod
|
|
2014
|
-
def create_chained_invoke_event_pending(
|
|
2015
|
-
cls, context: EventCreationContext
|
|
2016
|
-
) -> Event:
|
|
2017
|
-
input: EventInput = EventInput.from_start_durable_execution_input(
|
|
2018
|
-
context.start_durable_execution_input, context.include_execution_data
|
|
2019
|
-
)
|
|
2020
|
-
return cls(
|
|
2021
|
-
event_type=EventType.CHAINED_INVOKE_STARTED.value,
|
|
2022
|
-
event_timestamp=context.start_timestamp,
|
|
2023
|
-
sub_type=context.sub_type,
|
|
2024
|
-
event_id=context.event_id,
|
|
2025
|
-
operation_id=context.operation.operation_id,
|
|
2026
|
-
name=context.operation.name,
|
|
2027
|
-
parent_id=context.operation.parent_id,
|
|
2028
|
-
chained_invoke_pending_details=ChainedInvokePendingDetails(
|
|
2029
|
-
input=input,
|
|
2030
|
-
function_name=context.start_durable_execution_input.function_name,
|
|
2031
|
-
),
|
|
2032
|
-
)
|
|
2033
|
-
|
|
2034
2019
|
@classmethod
|
|
2035
2020
|
def create_chained_invoke_event_started(
|
|
2036
2021
|
cls, context: EventCreationContext
|
|
2037
2022
|
) -> Event:
|
|
2023
|
+
update: OperationUpdate | None = context.operation_update
|
|
2024
|
+
function_name: str | None = None
|
|
2025
|
+
payload: str | None = None
|
|
2026
|
+
if update is not None:
|
|
2027
|
+
payload = update.payload
|
|
2028
|
+
if update.chained_invoke_options is not None:
|
|
2029
|
+
function_name = update.chained_invoke_options.function_name
|
|
2030
|
+
# The API shape has TenantId here; the service's history does not
|
|
2031
|
+
# return it for this event. The runner matches the service, so a
|
|
2032
|
+
# history assertion that passes locally passes against it.
|
|
2038
2033
|
return cls(
|
|
2039
2034
|
event_type=EventType.CHAINED_INVOKE_STARTED.value,
|
|
2040
2035
|
event_timestamp=context.start_timestamp,
|
|
@@ -2044,7 +2039,16 @@ class Event:
|
|
|
2044
2039
|
name=context.operation.name,
|
|
2045
2040
|
parent_id=context.operation.parent_id,
|
|
2046
2041
|
chained_invoke_started_details=ChainedInvokeStartedDetails(
|
|
2047
|
-
|
|
2042
|
+
function_name=function_name,
|
|
2043
|
+
# No stored input, as for a target the runner could not
|
|
2044
|
+
# resolve, means no Input on the event, as at the service.
|
|
2045
|
+
input=EventInput(
|
|
2046
|
+
payload=payload if context.include_execution_data else None,
|
|
2047
|
+
truncated=not context.include_execution_data,
|
|
2048
|
+
)
|
|
2049
|
+
if payload is not None
|
|
2050
|
+
else None,
|
|
2051
|
+
durable_execution_arn=context.child_execution_arn,
|
|
2048
2052
|
),
|
|
2049
2053
|
)
|
|
2050
2054
|
|
|
@@ -2157,7 +2161,7 @@ class Event:
|
|
|
2157
2161
|
"""Create chained invoke event based on action."""
|
|
2158
2162
|
match context.operation.status:
|
|
2159
2163
|
case OperationStatus.PENDING:
|
|
2160
|
-
return cls.
|
|
2164
|
+
return cls.create_chained_invoke_event_started(context)
|
|
2161
2165
|
case OperationStatus.STARTED:
|
|
2162
2166
|
return cls.create_chained_invoke_event_started(context)
|
|
2163
2167
|
case OperationStatus.SUCCEEDED:
|
|
@@ -2357,7 +2361,6 @@ class Event:
|
|
|
2357
2361
|
step_started_details=event.step_started_details,
|
|
2358
2362
|
step_succeeded_details=event.step_succeeded_details,
|
|
2359
2363
|
step_failed_details=event.step_failed_details,
|
|
2360
|
-
chained_invoke_pending_details=event.chained_invoke_pending_details,
|
|
2361
2364
|
chained_invoke_started_details=event.chained_invoke_started_details,
|
|
2362
2365
|
chained_invoke_succeeded_details=event.chained_invoke_succeeded_details,
|
|
2363
2366
|
chained_invoke_failed_details=event.chained_invoke_failed_details,
|
|
@@ -2664,7 +2667,7 @@ def events_to_operations(events: list[Event]) -> list[Operation]:
|
|
|
2664
2667
|
name=event.name,
|
|
2665
2668
|
parent_id=event.parent_id,
|
|
2666
2669
|
sub_type=sub_type,
|
|
2667
|
-
start_timestamp=
|
|
2670
|
+
start_timestamp=event.event_timestamp,
|
|
2668
2671
|
)
|
|
2669
2672
|
|
|
2670
2673
|
# Merge with previous operation if it exists
|
|
@@ -3219,9 +3222,12 @@ class CheckpointDurableExecutionRequest:
|
|
|
3219
3222
|
|
|
3220
3223
|
@dataclass(frozen=True)
|
|
3221
3224
|
class CheckpointDurableExecutionResponse:
|
|
3222
|
-
"""Response from checkpointing a durable execution.
|
|
3225
|
+
"""Response from checkpointing a durable execution.
|
|
3223
3226
|
|
|
3224
|
-
checkpoint_token
|
|
3227
|
+
``checkpoint_token`` is None when this invocation may checkpoint no further
|
|
3228
|
+
"""
|
|
3229
|
+
|
|
3230
|
+
checkpoint_token: str | None
|
|
3225
3231
|
new_execution_state: CheckpointUpdatedExecutionState | None = None
|
|
3226
3232
|
|
|
3227
3233
|
@classmethod
|
|
@@ -3231,12 +3237,14 @@ class CheckpointDurableExecutionResponse:
|
|
|
3231
3237
|
new_execution_state = CheckpointUpdatedExecutionState.from_dict(state_data)
|
|
3232
3238
|
|
|
3233
3239
|
return cls(
|
|
3234
|
-
checkpoint_token=data
|
|
3240
|
+
checkpoint_token=data.get("CheckpointToken"),
|
|
3235
3241
|
new_execution_state=new_execution_state,
|
|
3236
3242
|
)
|
|
3237
3243
|
|
|
3238
3244
|
def to_dict(self) -> dict[str, Any]:
|
|
3239
|
-
result: dict[str, Any] = {
|
|
3245
|
+
result: dict[str, Any] = {}
|
|
3246
|
+
if self.checkpoint_token is not None:
|
|
3247
|
+
result["CheckpointToken"] = self.checkpoint_token
|
|
3240
3248
|
if self.new_execution_state is not None:
|
|
3241
3249
|
result["NewExecutionState"] = self.new_execution_state.to_dict()
|
|
3242
3250
|
return result
|
|
@@ -1,31 +1,48 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Execution lifecycle effects: collection and application.
|
|
2
|
+
|
|
3
|
+
While a batch of checkpoint updates is applied, the per-type processors
|
|
4
|
+
record lifecycle effects (completion, failure, callback creation) on an
|
|
5
|
+
:class:`ExecutionNotifier`. The checkpoint orchestrator then hands the
|
|
6
|
+
collected effects to :func:`apply_effects`, which drives them onto an
|
|
7
|
+
:class:`ExecutionObserver` after the write completes. Collecting first
|
|
8
|
+
and applying afterwards keeps effect handling out of the write itself.
|
|
9
|
+
"""
|
|
2
10
|
|
|
3
11
|
from __future__ import annotations
|
|
4
12
|
|
|
5
|
-
import threading
|
|
6
13
|
from abc import ABC, abstractmethod
|
|
7
14
|
from typing import TYPE_CHECKING
|
|
8
15
|
|
|
9
|
-
from aws_durable_execution_sdk_python_testing.
|
|
16
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.effects import (
|
|
17
|
+
CallbackCreated,
|
|
18
|
+
ChainedInvokeStarted,
|
|
19
|
+
Completed,
|
|
20
|
+
Failed,
|
|
21
|
+
)
|
|
10
22
|
|
|
11
23
|
if TYPE_CHECKING:
|
|
12
|
-
from collections.abc import
|
|
24
|
+
from collections.abc import Iterable
|
|
13
25
|
|
|
14
26
|
from aws_durable_execution_sdk_python.lambda_service import (
|
|
15
|
-
ErrorObject,
|
|
16
27
|
CallbackOptions,
|
|
28
|
+
ErrorObject,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.effects import (
|
|
32
|
+
CheckpointEffect,
|
|
17
33
|
)
|
|
34
|
+
from aws_durable_execution_sdk_python_testing.token import CallbackToken
|
|
18
35
|
|
|
19
36
|
|
|
20
37
|
class ExecutionObserver(ABC):
|
|
21
|
-
"""
|
|
38
|
+
"""Receives execution lifecycle events."""
|
|
22
39
|
|
|
23
40
|
@abstractmethod
|
|
24
41
|
def on_completed(self, execution_arn: str, result: str | None = None) -> None:
|
|
25
42
|
"""Called when execution completes successfully."""
|
|
26
43
|
|
|
27
44
|
@abstractmethod
|
|
28
|
-
def on_failed(self, execution_arn: str, error: ErrorObject) -> None:
|
|
45
|
+
def on_failed(self, execution_arn: str, error: ErrorObject | None) -> None:
|
|
29
46
|
"""Called when execution fails."""
|
|
30
47
|
|
|
31
48
|
@abstractmethod
|
|
@@ -36,18 +53,6 @@ class ExecutionObserver(ABC):
|
|
|
36
53
|
def on_stopped(self, execution_arn: str, error: ErrorObject) -> None:
|
|
37
54
|
"""Called when execution is stopped."""
|
|
38
55
|
|
|
39
|
-
@abstractmethod
|
|
40
|
-
def on_wait_timer_scheduled(
|
|
41
|
-
self, execution_arn: str, operation_id: str, delay: float
|
|
42
|
-
) -> None:
|
|
43
|
-
"""Called when wait timer scheduled."""
|
|
44
|
-
|
|
45
|
-
@abstractmethod
|
|
46
|
-
def on_step_retry_scheduled(
|
|
47
|
-
self, execution_arn: str, operation_id: str, delay: float
|
|
48
|
-
) -> None:
|
|
49
|
-
"""Called when step retry scheduled."""
|
|
50
|
-
|
|
51
56
|
@abstractmethod
|
|
52
57
|
def on_callback_created(
|
|
53
58
|
self,
|
|
@@ -56,74 +61,37 @@ class ExecutionObserver(ABC):
|
|
|
56
61
|
callback_options: CallbackOptions | None,
|
|
57
62
|
callback_token: CallbackToken,
|
|
58
63
|
) -> None:
|
|
59
|
-
"""Called when callback is created."""
|
|
60
|
-
|
|
64
|
+
"""Called when a callback is created."""
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def on_chained_invoke_started(
|
|
68
|
+
self,
|
|
69
|
+
execution_arn: str,
|
|
70
|
+
operation_id: str,
|
|
71
|
+
function_name: str,
|
|
72
|
+
tenant_id: str | None,
|
|
73
|
+
payload: str | None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Called when a chained invoke is accepted and must be dispatched."""
|
|
64
76
|
|
|
65
|
-
def __init__(self) -> None:
|
|
66
|
-
self._observers: list[ExecutionObserver] = []
|
|
67
|
-
self._lock = threading.RLock()
|
|
68
|
-
|
|
69
|
-
def add_observer(self, observer: ExecutionObserver) -> None:
|
|
70
|
-
"""Add an observer to be notified of execution events."""
|
|
71
|
-
with self._lock:
|
|
72
|
-
self._observers.append(observer)
|
|
73
|
-
|
|
74
|
-
def _notify_observers(self, method: Callable, *args, **kwargs) -> None:
|
|
75
|
-
"""Notify all observers by calling the specified method."""
|
|
76
|
-
with self._lock:
|
|
77
|
-
observers = self._observers.copy()
|
|
78
|
-
for observer in observers:
|
|
79
|
-
getattr(observer, method.__name__)(*args, **kwargs)
|
|
80
|
-
|
|
81
|
-
# region event emitters
|
|
82
|
-
def notify_completed(self, execution_arn: str, result: str | None = None) -> None:
|
|
83
|
-
"""Notify observers about execution completion."""
|
|
84
|
-
self._notify_observers(
|
|
85
|
-
ExecutionObserver.on_completed, execution_arn=execution_arn, result=result
|
|
86
|
-
)
|
|
87
77
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
self._notify_observers(
|
|
91
|
-
ExecutionObserver.on_failed, execution_arn=execution_arn, error=error
|
|
92
|
-
)
|
|
78
|
+
class ExecutionNotifier:
|
|
79
|
+
"""Collects lifecycle effects raised while applying checkpoint updates.
|
|
93
80
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
ExecutionObserver.on_timed_out, execution_arn=execution_arn, error=error
|
|
98
|
-
)
|
|
81
|
+
Processors record an effect here instead of acting on it; the
|
|
82
|
+
orchestrator reads :attr:`effects` once the updates are applied.
|
|
83
|
+
"""
|
|
99
84
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
self._notify_observers(
|
|
103
|
-
ExecutionObserver.on_stopped, execution_arn=execution_arn, error=error
|
|
104
|
-
)
|
|
85
|
+
def __init__(self) -> None:
|
|
86
|
+
self.effects: list[CheckpointEffect] = []
|
|
105
87
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"""Notify observers about wait timer scheduling."""
|
|
110
|
-
self._notify_observers(
|
|
111
|
-
ExecutionObserver.on_wait_timer_scheduled,
|
|
112
|
-
execution_arn=execution_arn,
|
|
113
|
-
operation_id=operation_id,
|
|
114
|
-
delay=delay,
|
|
115
|
-
)
|
|
88
|
+
def notify_completed(self, execution_arn: str, result: str | None = None) -> None:
|
|
89
|
+
"""Record that the execution completed successfully."""
|
|
90
|
+
self.effects.append(Completed(execution_arn=execution_arn, result=result))
|
|
116
91
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"""Notify observers about step retry scheduling."""
|
|
121
|
-
self._notify_observers(
|
|
122
|
-
ExecutionObserver.on_step_retry_scheduled,
|
|
123
|
-
execution_arn=execution_arn,
|
|
124
|
-
operation_id=operation_id,
|
|
125
|
-
delay=delay,
|
|
126
|
-
)
|
|
92
|
+
def notify_failed(self, execution_arn: str, error: ErrorObject | None) -> None:
|
|
93
|
+
"""Record that the execution failed."""
|
|
94
|
+
self.effects.append(Failed(execution_arn=execution_arn, error=error))
|
|
127
95
|
|
|
128
96
|
def notify_callback_created(
|
|
129
97
|
self,
|
|
@@ -132,13 +100,57 @@ class ExecutionNotifier:
|
|
|
132
100
|
callback_options: CallbackOptions | None,
|
|
133
101
|
callback_token: CallbackToken,
|
|
134
102
|
) -> None:
|
|
135
|
-
"""
|
|
136
|
-
self.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
103
|
+
"""Record that a callback was created."""
|
|
104
|
+
self.effects.append(
|
|
105
|
+
CallbackCreated(
|
|
106
|
+
execution_arn=execution_arn,
|
|
107
|
+
operation_id=operation_id,
|
|
108
|
+
callback_options=callback_options,
|
|
109
|
+
callback_token=callback_token,
|
|
110
|
+
)
|
|
142
111
|
)
|
|
143
112
|
|
|
144
|
-
|
|
113
|
+
def notify_chained_invoke_started(
|
|
114
|
+
self,
|
|
115
|
+
execution_arn: str,
|
|
116
|
+
operation_id: str,
|
|
117
|
+
function_name: str,
|
|
118
|
+
tenant_id: str | None,
|
|
119
|
+
payload: str | None,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Record that a chained invoke was accepted."""
|
|
122
|
+
self.effects.append(
|
|
123
|
+
ChainedInvokeStarted(
|
|
124
|
+
execution_arn=execution_arn,
|
|
125
|
+
operation_id=operation_id,
|
|
126
|
+
function_name=function_name,
|
|
127
|
+
tenant_id=tenant_id,
|
|
128
|
+
payload=payload,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def apply_effects(
|
|
134
|
+
effects: Iterable[CheckpointEffect], observer: ExecutionObserver
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Drive each collected effect onto ``observer``."""
|
|
137
|
+
for effect in effects:
|
|
138
|
+
if isinstance(effect, Completed):
|
|
139
|
+
observer.on_completed(effect.execution_arn, effect.result)
|
|
140
|
+
elif isinstance(effect, Failed):
|
|
141
|
+
observer.on_failed(effect.execution_arn, effect.error)
|
|
142
|
+
elif isinstance(effect, CallbackCreated):
|
|
143
|
+
observer.on_callback_created(
|
|
144
|
+
effect.execution_arn,
|
|
145
|
+
effect.operation_id,
|
|
146
|
+
effect.callback_options,
|
|
147
|
+
effect.callback_token,
|
|
148
|
+
)
|
|
149
|
+
elif isinstance(effect, ChainedInvokeStarted):
|
|
150
|
+
observer.on_chained_invoke_started(
|
|
151
|
+
effect.execution_arn,
|
|
152
|
+
effect.operation_id,
|
|
153
|
+
effect.function_name,
|
|
154
|
+
effect.tenant_id,
|
|
155
|
+
effect.payload,
|
|
156
|
+
)
|