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
|
@@ -19,13 +19,13 @@ from dataclasses import dataclass
|
|
|
19
19
|
from typing import Any
|
|
20
20
|
from urllib.parse import urljoin
|
|
21
21
|
|
|
22
|
-
import aws_durable_execution_sdk_python
|
|
23
22
|
import boto3 # type: ignore
|
|
24
23
|
from urllib.error import HTTPError, URLError
|
|
25
24
|
from urllib.request import Request, urlopen
|
|
26
25
|
|
|
27
26
|
from botocore.exceptions import ConnectionError # type: ignore
|
|
28
27
|
|
|
28
|
+
from aws_durable_execution_sdk_python_testing.child_dispatcher import FunctionConfigs
|
|
29
29
|
from aws_durable_execution_sdk_python_testing.exceptions import (
|
|
30
30
|
DurableFunctionsLocalRunnerError,
|
|
31
31
|
DurableFunctionsTestError,
|
|
@@ -41,6 +41,14 @@ from aws_durable_execution_sdk_python_testing.web.server import WebServiceConfig
|
|
|
41
41
|
logger = logging.getLogger(__name__)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def _function_configs(value: str) -> FunctionConfigs:
|
|
45
|
+
"""argparse type for ``--function-configs``: parse at the boundary."""
|
|
46
|
+
try:
|
|
47
|
+
return FunctionConfigs.from_value(value)
|
|
48
|
+
except (ValueError, OSError) as exc:
|
|
49
|
+
raise argparse.ArgumentTypeError(str(exc)) from exc
|
|
50
|
+
|
|
51
|
+
|
|
44
52
|
@dataclass(frozen=True)
|
|
45
53
|
class CliConfig:
|
|
46
54
|
"""Configuration for the CLI application with environment variable support."""
|
|
@@ -210,6 +218,42 @@ class CliApp:
|
|
|
210
218
|
default=self.config.store_path,
|
|
211
219
|
help=f"Path for filesystem store (default: {self.config.store_path or '.durable_executions'}, env: AWS_DEX_STORE_PATH)",
|
|
212
220
|
)
|
|
221
|
+
start_server_parser.add_argument(
|
|
222
|
+
"--execution-timeout",
|
|
223
|
+
type=int,
|
|
224
|
+
default=300,
|
|
225
|
+
help="Default execution timeout in seconds (default: 300)",
|
|
226
|
+
)
|
|
227
|
+
start_server_parser.add_argument(
|
|
228
|
+
"--invocation-timeout",
|
|
229
|
+
type=int,
|
|
230
|
+
default=900,
|
|
231
|
+
help=(
|
|
232
|
+
"Per-invocation timeout in seconds, simulates Lambda Timeout "
|
|
233
|
+
"(default: 900). Also bounds how long the runner waits on any "
|
|
234
|
+
"Invoke it sends, with 60 seconds of headroom"
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
start_server_parser.add_argument(
|
|
238
|
+
"--skip-time",
|
|
239
|
+
action=argparse.BooleanOptionalAction,
|
|
240
|
+
default=False,
|
|
241
|
+
help="Skip durable timer wall-clock waits; history keeps real modeled durations. Default is real timing (--no-skip-time); pass --skip-time to opt in",
|
|
242
|
+
)
|
|
243
|
+
start_server_parser.add_argument(
|
|
244
|
+
"--function-configs",
|
|
245
|
+
default=None,
|
|
246
|
+
type=_function_configs,
|
|
247
|
+
help=(
|
|
248
|
+
"The Lambda functions a durable function may invoke: a JSON "
|
|
249
|
+
"object mapping each function name to its configuration in "
|
|
250
|
+
"Lambda's own shape, or file://<path> to a file holding it. "
|
|
251
|
+
"A durable function carries a DurableConfig, e.g. "
|
|
252
|
+
'{"ProcessPayment": {"DurableConfig": {"ExecutionTimeout": 60, '
|
|
253
|
+
'"RetentionPeriodInDays": 7}}, "LookupPrice": {}}. Required for '
|
|
254
|
+
"chained invokes; without it every chained invoke fails"
|
|
255
|
+
),
|
|
256
|
+
)
|
|
213
257
|
start_server_parser.set_defaults(func=self.start_server_command)
|
|
214
258
|
|
|
215
259
|
def _create_invoke_parser(self, subparsers) -> None:
|
|
@@ -282,6 +326,9 @@ class CliApp:
|
|
|
282
326
|
local_runner_mode=args.local_runner_mode,
|
|
283
327
|
store_type=StoreType(args.store_type),
|
|
284
328
|
store_path=args.store_path,
|
|
329
|
+
invocation_timeout_seconds=args.invocation_timeout,
|
|
330
|
+
skip_time=args.skip_time,
|
|
331
|
+
function_configs=args.function_configs,
|
|
285
332
|
)
|
|
286
333
|
|
|
287
334
|
logger.info(
|
|
@@ -9,42 +9,53 @@ from aws_durable_execution_sdk_python.lambda_service import (
|
|
|
9
9
|
StateOutput,
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
+
from aws_durable_execution_sdk_python_testing.clock import real_now
|
|
12
13
|
from aws_durable_execution_sdk_python_testing.checkpoint.processor import (
|
|
13
14
|
CheckpointProcessor,
|
|
14
15
|
)
|
|
16
|
+
from aws_durable_execution_sdk_python_testing.worker.checkpoint_tasks import (
|
|
17
|
+
CheckpointTask,
|
|
18
|
+
GetStateTask,
|
|
19
|
+
)
|
|
20
|
+
from aws_durable_execution_sdk_python_testing.worker.registry import ExecutionRegistry
|
|
15
21
|
|
|
16
22
|
|
|
17
23
|
class InMemoryServiceClient(DurableServiceClient):
|
|
18
24
|
"""An in-memory service client, that can replace the boto lambda service client."""
|
|
19
25
|
|
|
20
|
-
def __init__(
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
checkpoint_processor: CheckpointProcessor,
|
|
29
|
+
registry: ExecutionRegistry,
|
|
30
|
+
):
|
|
21
31
|
self._checkpoint_processor: CheckpointProcessor = checkpoint_processor
|
|
32
|
+
self._registry: ExecutionRegistry = registry
|
|
22
33
|
|
|
23
34
|
def checkpoint(
|
|
24
35
|
self,
|
|
25
|
-
durable_execution_arn: str,
|
|
36
|
+
durable_execution_arn: str,
|
|
26
37
|
checkpoint_token: str,
|
|
27
38
|
updates: list[OperationUpdate],
|
|
28
39
|
client_token: str | None,
|
|
29
40
|
) -> CheckpointOutput:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
checkpoint_token, updates, client_token
|
|
41
|
+
task = CheckpointTask(
|
|
42
|
+
self._checkpoint_processor, checkpoint_token, updates, client_token
|
|
33
43
|
)
|
|
44
|
+
return self._registry.submit(durable_execution_arn, task).result()
|
|
34
45
|
|
|
35
46
|
def get_execution_state(
|
|
36
47
|
self,
|
|
37
|
-
durable_execution_arn: str,
|
|
48
|
+
durable_execution_arn: str,
|
|
38
49
|
checkpoint_token: str,
|
|
39
50
|
next_marker: str,
|
|
40
51
|
max_items: int = 1000,
|
|
41
52
|
) -> StateOutput:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
checkpoint_token, next_marker, max_items
|
|
53
|
+
task = GetStateTask(
|
|
54
|
+
self._checkpoint_processor, checkpoint_token, next_marker, max_items
|
|
45
55
|
)
|
|
56
|
+
return self._registry.submit(durable_execution_arn, task).result()
|
|
46
57
|
|
|
47
58
|
def stop(self, execution_arn: str, payload: bytes | None) -> datetime.datetime: # noqa: ARG002
|
|
48
59
|
# TODO: implement
|
|
49
60
|
# Return current time for in-memory testing
|
|
50
|
-
return
|
|
61
|
+
return real_now()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Clock sources for scheduler-driven durable timers.
|
|
2
|
+
|
|
3
|
+
A clock decides how long the local runner actually waits before a
|
|
4
|
+
scheduled wake fires, and supplies the "now" used to stamp operation
|
|
5
|
+
and invocation timestamps. All time reads in the runner go through one
|
|
6
|
+
clock so history events share a single, totally ordered clock domain.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def real_now() -> datetime.datetime:
|
|
17
|
+
"""Return the current wall-clock time in UTC."""
|
|
18
|
+
return datetime.datetime.now(datetime.UTC) # noqa: TID251
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Clock(Protocol):
|
|
22
|
+
"""Time source used to arm timers and stamp timestamps."""
|
|
23
|
+
|
|
24
|
+
def now(self) -> datetime.datetime:
|
|
25
|
+
"""Return the current time on this clock."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def arm(self, wake: datetime.datetime) -> float:
|
|
29
|
+
"""Return the real seconds to wait before firing at ``wake``."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RealClock:
|
|
34
|
+
"""Wall-clock time source. Timers wait the real modeled duration."""
|
|
35
|
+
|
|
36
|
+
def now(self) -> datetime.datetime:
|
|
37
|
+
return real_now()
|
|
38
|
+
|
|
39
|
+
def arm(self, wake: datetime.datetime) -> float:
|
|
40
|
+
return max((wake - self.now()).total_seconds(), 0.0)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SkipClock:
|
|
44
|
+
"""Virtual clock that skips over armed timer horizons.
|
|
45
|
+
|
|
46
|
+
Tracks the total duration skipped so far: ``now`` returns real time
|
|
47
|
+
plus the accumulated skip, so the clock advances with real time
|
|
48
|
+
between jumps and every read is distinct and monotonic. Arming a
|
|
49
|
+
wake beyond virtual now adds the remaining gap to the accumulated
|
|
50
|
+
skip and returns a zero delay, so the wake fires on the next
|
|
51
|
+
event-loop turn rather than after the modeled duration. Guards its
|
|
52
|
+
state with a lock because ``arm`` and ``now`` run on different
|
|
53
|
+
threads.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
self._skipped: datetime.timedelta = datetime.timedelta(0)
|
|
58
|
+
self._lock: threading.Lock = threading.Lock()
|
|
59
|
+
|
|
60
|
+
def now(self) -> datetime.datetime:
|
|
61
|
+
with self._lock:
|
|
62
|
+
return real_now() + self._skipped
|
|
63
|
+
|
|
64
|
+
def arm(self, wake: datetime.datetime) -> float:
|
|
65
|
+
with self._lock:
|
|
66
|
+
gap: datetime.timedelta = wake - (real_now() + self._skipped)
|
|
67
|
+
if gap > datetime.timedelta(0):
|
|
68
|
+
self._skipped += gap
|
|
69
|
+
return 0.0
|
|
@@ -109,6 +109,20 @@ class DurableFunctionsTestError(Exception):
|
|
|
109
109
|
"""Base class for testing errors."""
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
class InvalidInvocationOutputError(DurableFunctionsTestError):
|
|
113
|
+
"""The handler's output cannot be parsed as an invocation output."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, message: str, request_id: str | None = None) -> None:
|
|
116
|
+
"""Initialize with the message and the invocation's request ID.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
message: Why the output cannot be parsed
|
|
120
|
+
request_id: Request ID of the invocation that returned the output
|
|
121
|
+
"""
|
|
122
|
+
self.request_id = request_id
|
|
123
|
+
super().__init__(message)
|
|
124
|
+
|
|
125
|
+
|
|
112
126
|
# endregion Testing
|
|
113
127
|
|
|
114
128
|
|