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.
Files changed (48) hide show
  1. aws_durable_execution_sdk_python_testing/__about__.py +1 -1
  2. aws_durable_execution_sdk_python_testing/checkpoint/core.py +171 -0
  3. aws_durable_execution_sdk_python_testing/checkpoint/effects.py +61 -0
  4. aws_durable_execution_sdk_python_testing/checkpoint/processor.py +124 -45
  5. aws_durable_execution_sdk_python_testing/checkpoint/processors/base.py +40 -14
  6. aws_durable_execution_sdk_python_testing/checkpoint/processors/callback.py +5 -2
  7. aws_durable_execution_sdk_python_testing/checkpoint/processors/context.py +6 -0
  8. aws_durable_execution_sdk_python_testing/checkpoint/processors/execution.py +4 -9
  9. aws_durable_execution_sdk_python_testing/checkpoint/processors/invoke.py +139 -0
  10. aws_durable_execution_sdk_python_testing/checkpoint/processors/step.py +15 -21
  11. aws_durable_execution_sdk_python_testing/checkpoint/processors/wait.py +10 -16
  12. aws_durable_execution_sdk_python_testing/checkpoint/transformer.py +145 -48
  13. aws_durable_execution_sdk_python_testing/checkpoint/validators/checkpoint.py +65 -8
  14. aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/invoke.py +79 -18
  15. aws_durable_execution_sdk_python_testing/child_dispatcher.py +828 -0
  16. aws_durable_execution_sdk_python_testing/cli.py +48 -1
  17. aws_durable_execution_sdk_python_testing/client.py +21 -10
  18. aws_durable_execution_sdk_python_testing/clock.py +69 -0
  19. aws_durable_execution_sdk_python_testing/exceptions.py +14 -0
  20. aws_durable_execution_sdk_python_testing/execution.py +583 -49
  21. aws_durable_execution_sdk_python_testing/executor.py +1813 -349
  22. aws_durable_execution_sdk_python_testing/invoker.py +356 -85
  23. aws_durable_execution_sdk_python_testing/model.py +81 -73
  24. aws_durable_execution_sdk_python_testing/observer.py +100 -88
  25. aws_durable_execution_sdk_python_testing/runner.py +407 -30
  26. aws_durable_execution_sdk_python_testing/scheduler.py +38 -13
  27. aws_durable_execution_sdk_python_testing/stores/filesystem.py +10 -2
  28. aws_durable_execution_sdk_python_testing/stores/sqlite.py +18 -6
  29. aws_durable_execution_sdk_python_testing/threads.py +157 -0
  30. aws_durable_execution_sdk_python_testing/token.py +11 -2
  31. aws_durable_execution_sdk_python_testing/web/handlers.py +33 -7
  32. aws_durable_execution_sdk_python_testing/web/serialization.py +1 -2
  33. aws_durable_execution_sdk_python_testing/web/server.py +1 -1
  34. aws_durable_execution_sdk_python_testing/worker/__init__.py +9 -0
  35. aws_durable_execution_sdk_python_testing/worker/checkpoint_tasks.py +119 -0
  36. aws_durable_execution_sdk_python_testing/worker/execution_worker.py +117 -0
  37. aws_durable_execution_sdk_python_testing/worker/lane.py +118 -0
  38. aws_durable_execution_sdk_python_testing/worker/registry.py +124 -0
  39. aws_durable_execution_sdk_python_testing/worker/status.py +27 -0
  40. aws_durable_execution_sdk_python_testing/worker/task.py +57 -0
  41. {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
  42. aws_durable_execution_sdk_python_testing-1.3.0.dist-info/RECORD +64 -0
  43. {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
  44. aws_durable_execution_sdk_python_testing/checkpoint/validators/transitions.py +0 -66
  45. aws_durable_execution_sdk_python_testing-1.2.1.dist-info/RECORD +0 -52
  46. {aws_durable_execution_sdk_python_testing-1.2.1.dist-info/licenses → aws_durable_execution_sdk_python_testing}/LICENSE +0 -0
  47. {aws_durable_execution_sdk_python_testing-1.2.1.dist-info/licenses → aws_durable_execution_sdk_python_testing}/NOTICE +0 -0
  48. {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=f"arn:aws:lambda:us-east-1:123456789012:function:{execution.start_input.function_name}",
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 datetime.datetime.now(datetime.UTC),
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 ChainedInvokePendingDetails:
1008
- """Chained Invoke Pending event details."""
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) -> ChainedInvokePendingDetails:
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
- return result
1031
-
1032
-
1033
- @dataclass(frozen=True)
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 datetime.datetime.now(UTC)
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 datetime.datetime.now(UTC)
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
- durable_execution_arn=context.durable_execution_arn
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.create_chained_invoke_event_pending(context)
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=datetime.datetime.now(tz=datetime.timezone.utc),
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: str
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["CheckpointToken"],
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] = {"CheckpointToken": self.checkpoint_token}
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
- """Checkpoint processors can notify the Execution of notable event state changes. Observer pattern."""
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.token import CallbackToken
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 Callable
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
- """Observer for execution lifecycle events."""
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
- class ExecutionNotifier:
63
- """Notifies observers about execution events. Thread-safe."""
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
- def notify_failed(self, execution_arn: str, error: ErrorObject) -> None:
89
- """Notify observers about execution failure."""
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
- def notify_timed_out(self, execution_arn: str, error: ErrorObject) -> None:
95
- """Notify observers about execution timeout."""
96
- self._notify_observers(
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 notify_stopped(self, execution_arn: str, error: ErrorObject) -> None:
101
- """Notify observers about execution being stopped."""
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 notify_wait_timer_scheduled(
107
- self, execution_arn: str, operation_id: str, delay: float
108
- ) -> None:
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 notify_step_retry_scheduled(
118
- self, execution_arn: str, operation_id: str, delay: float
119
- ) -> None:
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
- """Notify observers about callback creation."""
136
- self._notify_observers(
137
- ExecutionObserver.on_callback_created,
138
- execution_arn=execution_arn,
139
- operation_id=operation_id,
140
- callback_options=callback_options,
141
- callback_token=callback_token,
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
- # endregion event emitters
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
+ )