aws-durable-execution-sdk-python-testing 1.2.0__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/routes.py +14 -6
- 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.0.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.0.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.0.dist-info/RECORD +0 -52
- {aws_durable_execution_sdk_python_testing-1.2.0.dist-info/licenses → aws_durable_execution_sdk_python_testing}/LICENSE +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.0.dist-info/licenses → aws_durable_execution_sdk_python_testing}/NOTICE +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.0.dist-info → aws_durable_execution_sdk_python_testing-1.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""The shared checkpoint write-transaction.
|
|
2
|
+
|
|
3
|
+
Both checkpoint entry points apply a batch of updates and compute the
|
|
4
|
+
delta of operations the handler has not yet seen. :class:`CheckpointCore`
|
|
5
|
+
holds that common logic so the two callers differ only in their gate,
|
|
6
|
+
locking, and response type.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
12
|
+
|
|
13
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.validators.checkpoint import (
|
|
14
|
+
CheckpointValidator,
|
|
15
|
+
)
|
|
16
|
+
from aws_durable_execution_sdk_python_testing.execution import (
|
|
17
|
+
CheckpointIdempotencyRecord,
|
|
18
|
+
OperationPaginatorState,
|
|
19
|
+
)
|
|
20
|
+
from aws_durable_execution_sdk_python_testing.token import CheckpointToken
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
|
|
25
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
26
|
+
Operation,
|
|
27
|
+
OperationUpdate,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.effects import (
|
|
31
|
+
CheckpointEffect,
|
|
32
|
+
)
|
|
33
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.transformer import (
|
|
34
|
+
CheckpointRequestDispatcher,
|
|
35
|
+
)
|
|
36
|
+
from aws_durable_execution_sdk_python_testing.execution import Execution
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CheckpointResult(NamedTuple):
|
|
40
|
+
"""Outcome of applying a checkpoint: the new token, the operations to
|
|
41
|
+
return to the handler this round, and the lifecycle effects raised.
|
|
42
|
+
|
|
43
|
+
``checkpoint_token`` is None when the execution is paused and the token
|
|
44
|
+
is withheld, telling the SDK this invocation may checkpoint no further.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
checkpoint_token: str | None
|
|
48
|
+
operations: list[Operation]
|
|
49
|
+
effects: list[CheckpointEffect]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CheckpointCore:
|
|
53
|
+
"""The checkpoint write-transaction shared by both entry points."""
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def match_cached(
|
|
57
|
+
execution: Execution,
|
|
58
|
+
checkpoint_token: str,
|
|
59
|
+
client_token: str | None,
|
|
60
|
+
) -> CheckpointIdempotencyRecord | None:
|
|
61
|
+
"""Return the cached idempotency record when the incoming
|
|
62
|
+
``(client_token, checkpoint_token)`` matches the last checkpoint,
|
|
63
|
+
else ``None``.
|
|
64
|
+
|
|
65
|
+
A missing ``client_token`` cannot replay: idempotency requires an
|
|
66
|
+
explicit client identifier. The caller builds its own response
|
|
67
|
+
type from the returned record.
|
|
68
|
+
"""
|
|
69
|
+
if not client_token:
|
|
70
|
+
return None
|
|
71
|
+
cached: CheckpointIdempotencyRecord | None = execution.last_checkpoint
|
|
72
|
+
if cached is None:
|
|
73
|
+
return None
|
|
74
|
+
if (
|
|
75
|
+
cached.client_token != client_token
|
|
76
|
+
or cached.inbound_checkpoint_token != checkpoint_token
|
|
77
|
+
):
|
|
78
|
+
return None
|
|
79
|
+
return cached
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def apply(
|
|
83
|
+
execution: Execution,
|
|
84
|
+
checkpoint_token: str,
|
|
85
|
+
updates: list[OperationUpdate],
|
|
86
|
+
client_token: str | None,
|
|
87
|
+
dispatcher: CheckpointRequestDispatcher,
|
|
88
|
+
now: datetime,
|
|
89
|
+
) -> CheckpointResult:
|
|
90
|
+
"""Apply ``updates`` to ``execution`` and compute the response delta.
|
|
91
|
+
|
|
92
|
+
Advances ``token_sequence`` exactly once, returns the full set of
|
|
93
|
+
operations the handler has not yet seen unless paused, advances
|
|
94
|
+
``handler_seen_seq`` only for returned operations, and records an
|
|
95
|
+
idempotency entry for a byte-identical replay of a retried call
|
|
96
|
+
only when returning a checkpoint token.
|
|
97
|
+
|
|
98
|
+
The caller is responsible for the invocation gate, locking,
|
|
99
|
+
persistence, and applying the returned effects.
|
|
100
|
+
|
|
101
|
+
``now`` is the checkpoint's single source of "now", resolved from
|
|
102
|
+
the execution's clock, so all timestamps stamped by this apply
|
|
103
|
+
advance with modeled time under a skip clock.
|
|
104
|
+
|
|
105
|
+
Before computing the delta, completes every wait and step retry
|
|
106
|
+
whose scheduled moment has passed, so those transitions are
|
|
107
|
+
delivered in this response and a suspended branch resumes within
|
|
108
|
+
the running invocation instead of waiting for the next one.
|
|
109
|
+
"""
|
|
110
|
+
effects: list[CheckpointEffect] = []
|
|
111
|
+
if updates:
|
|
112
|
+
CheckpointValidator.validate_input(updates, execution)
|
|
113
|
+
effects = dispatcher.apply_updates(
|
|
114
|
+
execution=execution,
|
|
115
|
+
updates=updates,
|
|
116
|
+
client_token=client_token,
|
|
117
|
+
touch=execution.touch_operation,
|
|
118
|
+
now=now,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
new_token_sequence: int = execution.advance_token_sequence()
|
|
122
|
+
|
|
123
|
+
# Pick up scheduler-due completions before pinning the paginator:
|
|
124
|
+
# each transition touches its operation, so it lands in this
|
|
125
|
+
# response's unseen delta and the handler sees it immediately.
|
|
126
|
+
execution.complete_due_operations(now)
|
|
127
|
+
|
|
128
|
+
paginator: OperationPaginatorState = OperationPaginatorState.pin(execution)
|
|
129
|
+
# The checkpoint response returns the full unseen delta in a single
|
|
130
|
+
# response. Advance handler_seen_seq to cover every returned op so
|
|
131
|
+
# the next delta carries only operations touched after this response.
|
|
132
|
+
# Paused checkpoints persist updates without delivering any state.
|
|
133
|
+
response_ops: list[Operation] = (
|
|
134
|
+
[] if execution.is_paused else paginator.unseen_operations()
|
|
135
|
+
)
|
|
136
|
+
if response_ops:
|
|
137
|
+
highest_delivered_seq: int = max(
|
|
138
|
+
execution.operation_last_touched_seq[op.operation_id]
|
|
139
|
+
for op in response_ops
|
|
140
|
+
)
|
|
141
|
+
paginator.advance_handler_seen(highest_delivered_seq)
|
|
142
|
+
|
|
143
|
+
new_token: str = CheckpointToken(
|
|
144
|
+
execution_arn=execution.durable_execution_arn,
|
|
145
|
+
token_sequence=new_token_sequence,
|
|
146
|
+
invocation_id=execution.current_invocation_id,
|
|
147
|
+
).to_str()
|
|
148
|
+
|
|
149
|
+
# A paused execution registers this checkpoint's updates but withholds
|
|
150
|
+
# the token: a response without one tells the SDK this invocation may
|
|
151
|
+
# checkpoint no further, so it reports PENDING at its next checkpoint
|
|
152
|
+
# rather than continuing, and owes a re-invoke once resumed.
|
|
153
|
+
#
|
|
154
|
+
# Checkpoints that were interrupted by a pause are not added to the
|
|
155
|
+
# idempotency record. A retry of this checkpoint will fail rather than be
|
|
156
|
+
# answered again.
|
|
157
|
+
|
|
158
|
+
outbound_token: str | None = new_token
|
|
159
|
+
if execution.is_paused:
|
|
160
|
+
outbound_token = None
|
|
161
|
+
execution.defer_invocation()
|
|
162
|
+
else:
|
|
163
|
+
execution.last_checkpoint = CheckpointIdempotencyRecord(
|
|
164
|
+
client_token=client_token or "",
|
|
165
|
+
inbound_checkpoint_token=checkpoint_token,
|
|
166
|
+
outbound_checkpoint_token=outbound_token,
|
|
167
|
+
operations=list(response_ops),
|
|
168
|
+
next_marker=None,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return CheckpointResult(outbound_token, response_ops, effects)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Lifecycle effects produced by applying checkpoint updates.
|
|
2
|
+
|
|
3
|
+
Applying a batch of operation updates can imply actions on the execution
|
|
4
|
+
beyond the state change itself: the execution completed, it failed, or a
|
|
5
|
+
callback was created and now needs a timeout. These are returned as data
|
|
6
|
+
so the caller can act on them after the write finishes, rather than
|
|
7
|
+
triggering them in the middle of it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
17
|
+
CallbackOptions,
|
|
18
|
+
ErrorObject,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from aws_durable_execution_sdk_python_testing.token import CallbackToken
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Completed:
|
|
26
|
+
"""The execution completed successfully with ``result``."""
|
|
27
|
+
|
|
28
|
+
execution_arn: str
|
|
29
|
+
result: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Failed:
|
|
34
|
+
"""The execution failed with ``error``."""
|
|
35
|
+
|
|
36
|
+
execution_arn: str
|
|
37
|
+
error: ErrorObject | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class CallbackCreated:
|
|
42
|
+
"""A callback was created and its timeout (if any) must be scheduled."""
|
|
43
|
+
|
|
44
|
+
execution_arn: str
|
|
45
|
+
operation_id: str
|
|
46
|
+
callback_options: CallbackOptions | None
|
|
47
|
+
callback_token: CallbackToken
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class ChainedInvokeStarted:
|
|
52
|
+
"""A chained invoke was accepted and its target must be dispatched."""
|
|
53
|
+
|
|
54
|
+
execution_arn: str
|
|
55
|
+
operation_id: str
|
|
56
|
+
function_name: str
|
|
57
|
+
tenant_id: str | None
|
|
58
|
+
payload: str | None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
CheckpointEffect = Completed | Failed | CallbackCreated | ChainedInvokeStarted
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""In-process checkpoint flow.
|
|
2
|
+
|
|
3
|
+
Orchestrates the full checkpoint request for in-process callers
|
|
4
|
+
(``InMemoryServiceClient``). Mirrors the flow inside
|
|
5
|
+
``Executor.checkpoint_execution`` used by the HTTP path, so both
|
|
6
|
+
entry points share identical delta + watermark semantics.
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
from __future__ import annotations
|
|
4
10
|
|
|
@@ -7,81 +13,128 @@ from typing import TYPE_CHECKING
|
|
|
7
13
|
from aws_durable_execution_sdk_python.lambda_service import (
|
|
8
14
|
CheckpointOutput,
|
|
9
15
|
CheckpointUpdatedExecutionState,
|
|
10
|
-
OperationUpdate,
|
|
11
16
|
StateOutput,
|
|
12
17
|
)
|
|
13
18
|
|
|
19
|
+
from aws_durable_execution_sdk_python_testing.checkpoint.core import CheckpointCore
|
|
14
20
|
from aws_durable_execution_sdk_python_testing.checkpoint.transformer import (
|
|
15
|
-
|
|
16
|
-
)
|
|
17
|
-
from aws_durable_execution_sdk_python_testing.checkpoint.validators.checkpoint import (
|
|
18
|
-
CheckpointValidator,
|
|
21
|
+
CheckpointRequestDispatcher,
|
|
19
22
|
)
|
|
23
|
+
from aws_durable_execution_sdk_python_testing.clock import RealClock
|
|
20
24
|
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
21
25
|
InvalidParameterValueException,
|
|
22
26
|
)
|
|
23
|
-
from aws_durable_execution_sdk_python_testing.observer import
|
|
27
|
+
from aws_durable_execution_sdk_python_testing.observer import apply_effects
|
|
24
28
|
from aws_durable_execution_sdk_python_testing.token import CheckpointToken
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
if TYPE_CHECKING:
|
|
32
|
+
from collections.abc import Callable
|
|
33
|
+
|
|
34
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
35
|
+
ErrorObject,
|
|
36
|
+
OperationUpdate,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from aws_durable_execution_sdk_python_testing.clock import Clock
|
|
28
40
|
from aws_durable_execution_sdk_python_testing.execution import Execution
|
|
41
|
+
from aws_durable_execution_sdk_python_testing.observer import ExecutionObserver
|
|
29
42
|
from aws_durable_execution_sdk_python_testing.scheduler import Scheduler
|
|
30
43
|
from aws_durable_execution_sdk_python_testing.stores.base import ExecutionStore
|
|
31
44
|
|
|
32
45
|
|
|
46
|
+
# Canonical invocation-page byte budget, shared with the Executor.
|
|
47
|
+
DEFAULT_MAX_INVOCATION_PAGE_BYTES = 5 * 1024 * 1024
|
|
48
|
+
|
|
49
|
+
|
|
33
50
|
class CheckpointProcessor:
|
|
34
|
-
"""
|
|
51
|
+
"""In-process checkpoint flow used by ``InMemoryServiceClient``.
|
|
35
52
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
self._transformer = OperationTransformer()
|
|
53
|
+
Uses the same :class:`CheckpointRequestDispatcher` + paginator
|
|
54
|
+
primitives as ``Executor.checkpoint_execution`` so observable
|
|
55
|
+
behaviour is identical across the two entry points.
|
|
56
|
+
"""
|
|
41
57
|
|
|
42
|
-
def
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
store: ExecutionStore,
|
|
61
|
+
scheduler: Scheduler, # noqa: ARG002 — kept for backward-compatible signature
|
|
62
|
+
clock: Clock | None = None,
|
|
63
|
+
):
|
|
64
|
+
self._store = store
|
|
65
|
+
self._observers: list[ExecutionObserver] = []
|
|
66
|
+
self._dispatcher = CheckpointRequestDispatcher()
|
|
67
|
+
# The runner's clock, shared with the Executor so stamping uses
|
|
68
|
+
# the same "now" as timer arming. Defaults to a real clock for
|
|
69
|
+
# standalone construction (e.g. direct unit tests).
|
|
70
|
+
self._clock: Clock = clock if clock is not None else RealClock()
|
|
71
|
+
|
|
72
|
+
def add_execution_observer(self, observer: ExecutionObserver) -> None:
|
|
43
73
|
"""Add observer for execution events."""
|
|
44
|
-
self.
|
|
74
|
+
self._observers.append(observer)
|
|
75
|
+
|
|
76
|
+
def set_chained_invoke_preflight(
|
|
77
|
+
self, preflight: Callable[[str], ErrorObject | None]
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Resolve chained-invoke targets at checkpoint time with ``preflight``.
|
|
80
|
+
|
|
81
|
+
A target it fails comes back FAILED in the checkpoint response
|
|
82
|
+
instead of being dispatched (see ``ChainedInvokeProcessor``).
|
|
83
|
+
"""
|
|
84
|
+
self._dispatcher = CheckpointRequestDispatcher(
|
|
85
|
+
chained_invoke_preflight=preflight
|
|
86
|
+
)
|
|
45
87
|
|
|
46
88
|
def process_checkpoint(
|
|
47
89
|
self,
|
|
48
90
|
checkpoint_token: str,
|
|
49
91
|
updates: list[OperationUpdate],
|
|
50
|
-
client_token: str | None,
|
|
92
|
+
client_token: str | None,
|
|
51
93
|
) -> CheckpointOutput:
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
"""Apply ``updates`` and return the delta since the handler's
|
|
95
|
+
last observation.
|
|
96
|
+
|
|
97
|
+
Advances ``token_sequence`` exactly once; bumps
|
|
98
|
+
``seq_counter`` once per accepted update via
|
|
99
|
+
``touch_operation``; advances ``handler_seen_seq`` only for
|
|
100
|
+
operations actually returned in the response.
|
|
101
|
+
"""
|
|
102
|
+
token = CheckpointToken.from_str(checkpoint_token)
|
|
55
103
|
execution: Execution = self._store.load(token.execution_arn)
|
|
56
104
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
105
|
+
# Idempotency first, before the token-sequence check.
|
|
106
|
+
cached = _maybe_replay_cached(execution, checkpoint_token, client_token)
|
|
107
|
+
if cached is not None:
|
|
108
|
+
return cached
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
execution.is_complete
|
|
112
|
+
or token.token_sequence != execution.token_sequence
|
|
113
|
+
or token.invocation_id != execution.current_invocation_id
|
|
114
|
+
):
|
|
115
|
+
msg = "Invalid checkpoint token"
|
|
61
116
|
raise InvalidParameterValueException(msg)
|
|
62
117
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
execution_arn=token.execution_arn,
|
|
118
|
+
now = self._clock.now()
|
|
119
|
+
result = CheckpointCore.apply(
|
|
120
|
+
execution,
|
|
121
|
+
checkpoint_token,
|
|
122
|
+
updates,
|
|
123
|
+
client_token,
|
|
124
|
+
self._dispatcher,
|
|
125
|
+
now,
|
|
72
126
|
)
|
|
73
127
|
|
|
74
|
-
# 5. Generate a new checkpoint token and save updated operations
|
|
75
|
-
new_checkpoint_token = execution.get_new_checkpoint_token()
|
|
76
|
-
execution.operations = updated_operations
|
|
77
|
-
execution.updates.extend(all_updates)
|
|
78
128
|
self._store.update(execution)
|
|
79
129
|
|
|
80
|
-
|
|
130
|
+
for observer in self._observers:
|
|
131
|
+
apply_effects(result.effects, observer)
|
|
132
|
+
|
|
81
133
|
return CheckpointOutput(
|
|
82
|
-
checkpoint_token=
|
|
134
|
+
checkpoint_token=result.checkpoint_token,
|
|
83
135
|
new_execution_state=CheckpointUpdatedExecutionState(
|
|
84
|
-
operations=
|
|
136
|
+
operations=result.operations,
|
|
137
|
+
next_marker=None,
|
|
85
138
|
),
|
|
86
139
|
)
|
|
87
140
|
|
|
@@ -91,11 +144,37 @@ class CheckpointProcessor:
|
|
|
91
144
|
next_marker: str, # noqa: ARG002
|
|
92
145
|
max_items: int = 1000, # noqa: ARG002
|
|
93
146
|
) -> StateOutput:
|
|
94
|
-
"""Get current execution state.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
147
|
+
"""Get current execution state.
|
|
148
|
+
|
|
149
|
+
Returns the full navigable operation list with a null marker.
|
|
150
|
+
Marker round-tripping and the invocation-state gate are
|
|
151
|
+
enforced on the HTTP path in :class:`Executor`.
|
|
152
|
+
"""
|
|
153
|
+
token = CheckpointToken.from_str(checkpoint_token)
|
|
154
|
+
execution = self._store.load(token.execution_arn)
|
|
99
155
|
return StateOutput(
|
|
100
|
-
operations=execution.get_navigable_operations(),
|
|
156
|
+
operations=execution.get_navigable_operations(),
|
|
157
|
+
next_marker=None,
|
|
101
158
|
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _maybe_replay_cached(
|
|
162
|
+
execution: Execution,
|
|
163
|
+
checkpoint_token: str,
|
|
164
|
+
client_token: str | None,
|
|
165
|
+
) -> CheckpointOutput | None:
|
|
166
|
+
"""Replay the cached response when the incoming
|
|
167
|
+
``(client_token, checkpoint_token)`` matches the last checkpoint.
|
|
168
|
+
|
|
169
|
+
Returns ``None`` when there is no matching cached record.
|
|
170
|
+
"""
|
|
171
|
+
cached = CheckpointCore.match_cached(execution, checkpoint_token, client_token)
|
|
172
|
+
if cached is None:
|
|
173
|
+
return None
|
|
174
|
+
return CheckpointOutput(
|
|
175
|
+
checkpoint_token=cached.outbound_checkpoint_token,
|
|
176
|
+
new_execution_state=CheckpointUpdatedExecutionState(
|
|
177
|
+
operations=list(cached.operations),
|
|
178
|
+
next_marker=cached.next_marker,
|
|
179
|
+
),
|
|
180
|
+
)
|
|
@@ -34,22 +34,42 @@ class OperationProcessor:
|
|
|
34
34
|
current_op: Operation | None,
|
|
35
35
|
notifier: ExecutionNotifier,
|
|
36
36
|
execution_arn: str,
|
|
37
|
+
now: datetime.datetime,
|
|
37
38
|
) -> Operation | None:
|
|
38
|
-
"""Process an operation update and return the transformed operation.
|
|
39
|
+
"""Process an operation update and return the transformed operation.
|
|
40
|
+
|
|
41
|
+
``now`` is the checkpoint's single source of "now", resolved from
|
|
42
|
+
the execution's clock so timestamps advance with modeled time
|
|
43
|
+
under a skip clock and match wall-clock under a real clock.
|
|
44
|
+
"""
|
|
39
45
|
raise NotImplementedError
|
|
40
46
|
|
|
47
|
+
def stored_update(
|
|
48
|
+
self,
|
|
49
|
+
update: OperationUpdate,
|
|
50
|
+
current_op: Operation | None, # noqa: ARG002
|
|
51
|
+
updated_op: Operation, # noqa: ARG002
|
|
52
|
+
) -> OperationUpdate:
|
|
53
|
+
"""The update to keep in the execution's record for history.
|
|
54
|
+
|
|
55
|
+
Most processors keep the update as sent. A processor overrides
|
|
56
|
+
this when the service keeps less than it was sent.
|
|
57
|
+
"""
|
|
58
|
+
return update
|
|
59
|
+
|
|
41
60
|
def _get_start_time(
|
|
42
|
-
self, current_operation: Operation | None
|
|
61
|
+
self, current_operation: Operation | None, now: datetime.datetime
|
|
43
62
|
) -> datetime.datetime | None:
|
|
44
63
|
start_time: datetime.datetime | None = (
|
|
45
|
-
current_operation.start_timestamp
|
|
46
|
-
if current_operation
|
|
47
|
-
else datetime.datetime.now(tz=datetime.UTC)
|
|
64
|
+
current_operation.start_timestamp if current_operation else now
|
|
48
65
|
)
|
|
49
66
|
return start_time
|
|
50
67
|
|
|
51
68
|
def _get_end_time(
|
|
52
|
-
self,
|
|
69
|
+
self,
|
|
70
|
+
current_operation: Operation | None,
|
|
71
|
+
status: OperationStatus,
|
|
72
|
+
now: datetime.datetime,
|
|
53
73
|
) -> datetime.datetime | None:
|
|
54
74
|
"""Get end timestamp for operation based on current state and status."""
|
|
55
75
|
if current_operation and current_operation.end_timestamp:
|
|
@@ -61,7 +81,7 @@ class OperationProcessor:
|
|
|
61
81
|
OperationStatus.TIMED_OUT,
|
|
62
82
|
OperationStatus.STOPPED,
|
|
63
83
|
}:
|
|
64
|
-
return
|
|
84
|
+
return now
|
|
65
85
|
return None
|
|
66
86
|
|
|
67
87
|
def _create_execution_details(
|
|
@@ -151,11 +171,14 @@ class OperationProcessor:
|
|
|
151
171
|
update: OperationUpdate,
|
|
152
172
|
current_operation: Operation | None,
|
|
153
173
|
status: OperationStatus,
|
|
174
|
+
now: datetime.datetime,
|
|
154
175
|
) -> Operation:
|
|
155
176
|
"""Transform OperationUpdate to Operation, always creating new Operation."""
|
|
156
|
-
start_time: datetime.datetime | None = self._get_start_time(
|
|
177
|
+
start_time: datetime.datetime | None = self._get_start_time(
|
|
178
|
+
current_operation, now
|
|
179
|
+
)
|
|
157
180
|
end_time: datetime.datetime | None = self._get_end_time(
|
|
158
|
-
current_operation, status
|
|
181
|
+
current_operation, status, now
|
|
159
182
|
)
|
|
160
183
|
|
|
161
184
|
execution_details = self._create_execution_details(update)
|
|
@@ -163,7 +186,7 @@ class OperationProcessor:
|
|
|
163
186
|
step_details = self._create_step_details(update, current_operation)
|
|
164
187
|
callback_details = self._create_callback_details(update)
|
|
165
188
|
invoke_details = self._create_invoke_details(update)
|
|
166
|
-
wait_details = self._create_wait_details(update, current_operation)
|
|
189
|
+
wait_details = self._create_wait_details(update, current_operation, now)
|
|
167
190
|
|
|
168
191
|
return Operation(
|
|
169
192
|
operation_id=update.operation_id,
|
|
@@ -183,7 +206,10 @@ class OperationProcessor:
|
|
|
183
206
|
)
|
|
184
207
|
|
|
185
208
|
def _create_wait_details(
|
|
186
|
-
self,
|
|
209
|
+
self,
|
|
210
|
+
update: OperationUpdate,
|
|
211
|
+
current_operation: Operation | None,
|
|
212
|
+
now: datetime.datetime,
|
|
187
213
|
) -> WaitDetails | None:
|
|
188
214
|
"""Create WaitDetails from OperationUpdate."""
|
|
189
215
|
if update.operation_type == OperationType.WAIT and update.wait_options:
|
|
@@ -192,8 +218,8 @@ class OperationProcessor:
|
|
|
192
218
|
current_operation.wait_details.scheduled_end_timestamp
|
|
193
219
|
)
|
|
194
220
|
else:
|
|
195
|
-
scheduled_end_timestamp =
|
|
196
|
-
|
|
197
|
-
)
|
|
221
|
+
scheduled_end_timestamp = now + timedelta(
|
|
222
|
+
seconds=update.wait_options.wait_seconds
|
|
223
|
+
)
|
|
198
224
|
return WaitDetails(scheduled_end_timestamp=scheduled_end_timestamp)
|
|
199
225
|
return None
|
|
@@ -35,6 +35,7 @@ class CallbackProcessor(OperationProcessor):
|
|
|
35
35
|
current_op: Operation | None,
|
|
36
36
|
notifier: ExecutionNotifier, # noqa: ARG002
|
|
37
37
|
execution_arn: str, # noqa: ARG002
|
|
38
|
+
now: datetime.datetime,
|
|
38
39
|
) -> Operation:
|
|
39
40
|
"""Process CALLBACK operation update with scheduler integration for activities."""
|
|
40
41
|
match update.action:
|
|
@@ -58,10 +59,12 @@ class CallbackProcessor(OperationProcessor):
|
|
|
58
59
|
|
|
59
60
|
status: OperationStatus = OperationStatus.STARTED
|
|
60
61
|
|
|
61
|
-
start_time: datetime.datetime | None = self._get_start_time(
|
|
62
|
+
start_time: datetime.datetime | None = self._get_start_time(
|
|
63
|
+
current_op, now
|
|
64
|
+
)
|
|
62
65
|
|
|
63
66
|
end_time: datetime.datetime | None = self._get_end_time(
|
|
64
|
-
current_op, status
|
|
67
|
+
current_op, status, now
|
|
65
68
|
)
|
|
66
69
|
|
|
67
70
|
operation: Operation = Operation(
|
|
@@ -20,6 +20,8 @@ from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
|
|
23
25
|
from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
|
|
24
26
|
|
|
25
27
|
|
|
@@ -32,6 +34,7 @@ class ContextProcessor(OperationProcessor):
|
|
|
32
34
|
current_op: Operation | None,
|
|
33
35
|
notifier: ExecutionNotifier, # noqa: ARG002
|
|
34
36
|
execution_arn: str, # noqa: ARG002
|
|
37
|
+
now: datetime,
|
|
35
38
|
) -> Operation:
|
|
36
39
|
"""Process CONTEXT operation update for context state transitions."""
|
|
37
40
|
match update.action:
|
|
@@ -41,18 +44,21 @@ class ContextProcessor(OperationProcessor):
|
|
|
41
44
|
update=update,
|
|
42
45
|
current_operation=current_op,
|
|
43
46
|
status=OperationStatus.STARTED,
|
|
47
|
+
now=now,
|
|
44
48
|
)
|
|
45
49
|
case OperationAction.SUCCEED:
|
|
46
50
|
return self._translate_update_to_operation(
|
|
47
51
|
update=update,
|
|
48
52
|
current_operation=current_op,
|
|
49
53
|
status=OperationStatus.SUCCEEDED,
|
|
54
|
+
now=now,
|
|
50
55
|
)
|
|
51
56
|
case OperationAction.FAIL:
|
|
52
57
|
return self._translate_update_to_operation(
|
|
53
58
|
update=update,
|
|
54
59
|
current_operation=current_op,
|
|
55
60
|
status=OperationStatus.FAILED,
|
|
61
|
+
now=now,
|
|
56
62
|
)
|
|
57
63
|
case _:
|
|
58
64
|
msg: str = "Invalid action for CONTEXT operation."
|
|
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
7
|
from aws_durable_execution_sdk_python.lambda_service import (
|
|
8
|
-
ErrorObject,
|
|
9
8
|
Operation,
|
|
10
9
|
OperationAction,
|
|
11
10
|
OperationUpdate,
|
|
@@ -17,6 +16,8 @@ from aws_durable_execution_sdk_python_testing.checkpoint.processors.base import
|
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
if TYPE_CHECKING:
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
|
|
20
21
|
from aws_durable_execution_sdk_python_testing.observer import ExecutionNotifier
|
|
21
22
|
|
|
22
23
|
|
|
@@ -29,6 +30,7 @@ class ExecutionProcessor(OperationProcessor):
|
|
|
29
30
|
current_op: Operation | None, # noqa: ARG002
|
|
30
31
|
notifier: ExecutionNotifier,
|
|
31
32
|
execution_arn: str,
|
|
33
|
+
now: datetime, # noqa: ARG002
|
|
32
34
|
) -> Operation | None:
|
|
33
35
|
"""Process EXECUTION operation update for workflow completion/failure."""
|
|
34
36
|
match update.action:
|
|
@@ -38,15 +40,8 @@ class ExecutionProcessor(OperationProcessor):
|
|
|
38
40
|
)
|
|
39
41
|
case _:
|
|
40
42
|
# intentional. actual service will fail any EXECUTION update that is not SUCCEED.
|
|
41
|
-
error = (
|
|
42
|
-
update.error
|
|
43
|
-
if update.error
|
|
44
|
-
else ErrorObject.from_message(
|
|
45
|
-
"There is no error details but EXECUTION checkpoint action is not SUCCEED."
|
|
46
|
-
)
|
|
47
|
-
)
|
|
48
43
|
# All EXECUTION failures go through normal fail path
|
|
49
44
|
# Timeout/Stop status is set by executor based on the operation that caused it
|
|
50
|
-
notifier.notify_failed(execution_arn=execution_arn, error=error)
|
|
45
|
+
notifier.notify_failed(execution_arn=execution_arn, error=update.error)
|
|
51
46
|
# TODO: Svc doesn't actually create checkpoint for EXECUTION. might have to for localrunner though.
|
|
52
47
|
return None
|