aws-durable-execution-sdk-python-testing 1.2.1__tar.gz → 1.3.0__tar.gz
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-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/.gitignore +9 -7
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/PKG-INFO +50 -58
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/README.md +47 -53
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/__about__.py +1 -1
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/checkpoint/core.py +171 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/checkpoint/effects.py +61 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/checkpoint/processor.py +180 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/processors/base.py +40 -14
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/processors/callback.py +5 -2
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/processors/context.py +6 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/processors/execution.py +4 -9
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/checkpoint/processors/invoke.py +139 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/processors/step.py +15 -21
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/processors/wait.py +10 -16
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/checkpoint/transformer.py +201 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/validators/checkpoint.py +65 -8
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/invoke.py +117 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/child_dispatcher.py +828 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/cli.py +48 -1
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/client.py +21 -10
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/clock.py +69 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/exceptions.py +14 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/execution.py +978 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/executor.py +2698 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/invoker.py +611 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/model.py +81 -73
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/observer.py +156 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/runner.py +407 -30
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/scheduler.py +38 -13
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/stores/filesystem.py +10 -2
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/stores/sqlite.py +18 -6
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/threads.py +157 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/token.py +11 -2
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/web/handlers.py +33 -7
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/web/serialization.py +1 -2
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/web/server.py +1 -1
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/worker/__init__.py +9 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/worker/checkpoint_tasks.py +119 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/worker/execution_worker.py +117 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/worker/lane.py +118 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/worker/registry.py +124 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/worker/status.py +27 -0
- aws_durable_execution_sdk_python_testing-1.3.0/aws_durable_execution_sdk_python_testing/worker/task.py +57 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/pyproject.toml +17 -5
- aws_durable_execution_sdk_python_testing-1.2.1/aws_durable_execution_sdk_python_testing/checkpoint/processor.py +0 -101
- aws_durable_execution_sdk_python_testing-1.2.1/aws_durable_execution_sdk_python_testing/checkpoint/transformer.py +0 -104
- aws_durable_execution_sdk_python_testing-1.2.1/aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/invoke.py +0 -56
- aws_durable_execution_sdk_python_testing-1.2.1/aws_durable_execution_sdk_python_testing/checkpoint/validators/transitions.py +0 -66
- aws_durable_execution_sdk_python_testing-1.2.1/aws_durable_execution_sdk_python_testing/execution.py +0 -444
- aws_durable_execution_sdk_python_testing-1.2.1/aws_durable_execution_sdk_python_testing/executor.py +0 -1234
- aws_durable_execution_sdk_python_testing-1.2.1/aws_durable_execution_sdk_python_testing/invoker.py +0 -340
- aws_durable_execution_sdk_python_testing-1.2.1/aws_durable_execution_sdk_python_testing/observer.py +0 -144
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/LICENSE +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/NOTICE +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/__init__.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/__init__.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/processors/__init__.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/validators/__init__.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/__init__.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/callback.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/context.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/execution.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/step.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/checkpoint/validators/operations/wait.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/py.typed +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/stores/__init__.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/stores/base.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/stores/memory.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/web/__init__.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/web/errors.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/web/models.py +0 -0
- {aws_durable_execution_sdk_python_testing-1.2.1 → aws_durable_execution_sdk_python_testing-1.3.0}/aws_durable_execution_sdk_python_testing/web/routes.py +0 -0
|
@@ -9,11 +9,13 @@ __pycache__/
|
|
|
9
9
|
*$py.class
|
|
10
10
|
*.egg-info/
|
|
11
11
|
|
|
12
|
+
*.coverage
|
|
12
13
|
/.coverage
|
|
13
14
|
/.coverage.*
|
|
14
15
|
/.cache
|
|
15
16
|
/.pytest_cache
|
|
16
17
|
/.mypy_cache
|
|
18
|
+
/.vscode
|
|
17
19
|
|
|
18
20
|
/doc/_apidoc/
|
|
19
21
|
/build
|
|
@@ -25,13 +27,13 @@ __pycache__/
|
|
|
25
27
|
|
|
26
28
|
dist/
|
|
27
29
|
|
|
28
|
-
.vscode/
|
|
29
|
-
.kiro/
|
|
30
30
|
.idea
|
|
31
|
-
.env
|
|
32
|
-
.env*
|
|
33
31
|
|
|
34
|
-
.
|
|
32
|
+
.kiro/
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
**/build/
|
|
35
|
+
**/*.zip
|
|
36
|
+
.aws-sam/
|
|
37
|
+
**/template.generated.json
|
|
38
|
+
|
|
39
|
+
.env
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: aws-durable-execution-sdk-python-testing
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: AWS Durable Execution Testing SDK for Python
|
|
5
5
|
Project-URL: Documentation, https://github.com/aws/aws-durable-execution-sdk-python-testing#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/aws/aws-durable-execution-sdk-python-testing/issues
|
|
7
7
|
Project-URL: Source, https://github.com/aws/aws-durable-execution-sdk-python-testing
|
|
8
8
|
Author-email: AWS durable-execution-dev <durable-execution-dev@amazon.com>
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
License-File: NOTICE
|
|
12
10
|
Classifier: Development Status :: 4 - Beta
|
|
13
11
|
Classifier: Programming Language :: Python
|
|
14
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -18,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
18
16
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
17
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
18
|
Requires-Python: >=3.11
|
|
21
|
-
Requires-Dist: aws-durable-execution-sdk-python>=1.0
|
|
19
|
+
Requires-Dist: aws-durable-execution-sdk-python>=2.1.0
|
|
22
20
|
Requires-Dist: boto3>=1.42.1
|
|
23
21
|
Description-Content-Type: text/markdown
|
|
24
22
|
|
|
@@ -36,6 +34,7 @@ Description-Content-Type: text/markdown
|
|
|
36
34
|
|
|
37
35
|
- [Installation](#installation)
|
|
38
36
|
- [Quick Start](#quick-start)
|
|
37
|
+
- [Testing functions that invoke other functions](#testing-functions-that-invoke-other-functions)
|
|
39
38
|
- [Architecture](#architecture)
|
|
40
39
|
- [Documentation](#documentation)
|
|
41
40
|
- [Developer Guide](#developers)
|
|
@@ -135,67 +134,60 @@ def test_my_durable_functions():
|
|
|
135
134
|
three_result: StepOperation = result.get_step("three")
|
|
136
135
|
assert three_result.result == '"5 6"'
|
|
137
136
|
```
|
|
138
|
-
##
|
|
139
|
-

|
|
140
|
-
|
|
141
|
-
## Event Flow
|
|
142
|
-

|
|
143
|
-
|
|
144
|
-
1. **DurableTestRunner** starts execution via **Executor**
|
|
145
|
-
2. **Executor** creates **Execution** and schedules initial invocation
|
|
146
|
-
3. During execution, checkpoints are processed by **CheckpointProcessor**
|
|
147
|
-
4. **Individual Processors** transform operation updates and may trigger events
|
|
148
|
-
5. **ExecutionNotifier** broadcasts events to **Executor** (observer)
|
|
149
|
-
6. **Executor** updates **Execution** state based on events
|
|
150
|
-
7. **Execution** completion triggers final event notifications
|
|
151
|
-
8. **DurableTestRunner** run() blocks until it receives completion event, and then returns `DurableFunctionTestResult`.
|
|
137
|
+
## Testing functions that invoke other functions
|
|
152
138
|
|
|
153
|
-
|
|
139
|
+
A durable function can call another function with `context.invoke`. The
|
|
140
|
+
runner needs to know each target's kind: a durable target runs as a
|
|
141
|
+
child durable execution with its own history, while a plain (non-durable)
|
|
142
|
+
target is one invocation whose return value is the result. An unknown
|
|
143
|
+
target fails the operation with `ResourceNotFoundException`.
|
|
154
144
|
|
|
155
|
-
###
|
|
156
|
-
- **DurableTestRunner** - Main entry point that orchestrates test execution
|
|
157
|
-
- **Executor** - Manages execution lifecycle. Mutates Execution.
|
|
158
|
-
- **Execution** - Represents the state and operations of a single durable execution
|
|
145
|
+
### In-process runner
|
|
159
146
|
|
|
160
|
-
|
|
161
|
-
|
|
147
|
+
Register each target with the runner. `register_durable_function` marks a
|
|
148
|
+
durable target, and takes its execution timeout and retention;
|
|
149
|
+
`register_function` marks a plain one. The runner has one handler per
|
|
150
|
+
registered name, not versions: an invoke of `child:prod` runs the handler
|
|
151
|
+
registered as `child:prod` if there is one, otherwise the one registered
|
|
152
|
+
as `child`.
|
|
162
153
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
### Execution status changes (Observer Pattern)
|
|
169
|
-
- **ExecutionNotifier** - Notifies observers of execution events
|
|
170
|
-
- **ExecutionObserver** - Interface for receiving execution lifecycle events
|
|
171
|
-
- **Executor** implements `ExecutionObserver` to handle completion events
|
|
172
|
-
|
|
173
|
-
## Component Relationships
|
|
154
|
+
```python
|
|
155
|
+
from aws_durable_execution_sdk_python.context import DurableContext
|
|
156
|
+
from aws_durable_execution_sdk_python.execution import durable_execution
|
|
157
|
+
from aws_durable_execution_sdk_python_testing.runner import DurableFunctionTestRunner
|
|
174
158
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
- **Execution** maintains the state of operations and completion status
|
|
159
|
+
@durable_execution
|
|
160
|
+
def process_payment(event: dict, context: DurableContext) -> dict:
|
|
161
|
+
return {"charged": event["amount"]}
|
|
179
162
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
- **InProcessInvoker** injects the service client into SDK via `DurableExecutionInvocationInputWithClient`
|
|
183
|
-
- When durable functions call checkpoint operations, they're intercepted by **InMemoryServiceClient**
|
|
184
|
-
- **InMemoryServiceClient** delegates to **CheckpointProcessor** for local processing
|
|
163
|
+
def lookup_price(event: dict, context) -> dict:
|
|
164
|
+
return {"price": 25}
|
|
185
165
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
166
|
+
@durable_execution
|
|
167
|
+
def place_order(event: dict, context: DurableContext) -> dict:
|
|
168
|
+
price = context.invoke("lookup-price", {"sku": event["sku"]}, name="price")
|
|
169
|
+
payment = context.invoke(
|
|
170
|
+
"process-payment", {"amount": price["price"]}, name="payment"
|
|
171
|
+
)
|
|
172
|
+
return {"sku": event["sku"], "charged": payment["charged"]}
|
|
173
|
+
|
|
174
|
+
def test_place_order_invokes_both_functions():
|
|
175
|
+
with DurableFunctionTestRunner(handler=place_order) as runner:
|
|
176
|
+
runner.register_durable_function(
|
|
177
|
+
"process-payment", process_payment, execution_timeout=60
|
|
178
|
+
)
|
|
179
|
+
runner.register_function("lookup-price", lookup_price)
|
|
180
|
+
result = runner.run(input='{"sku": "book-1"}')
|
|
181
|
+
|
|
182
|
+
assert result.result == '{"sku": "book-1", "charged": 25}'
|
|
183
|
+
assert result.get_invoke("payment").status.value == "SUCCEEDED"
|
|
184
|
+
```
|
|
190
185
|
|
|
191
|
-
|
|
192
|
-
The observer pattern enables loose coupling between checkpoint processing and execution management:
|
|
186
|
+
## Architecture
|
|
193
187
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
4. **Executor** (as ExecutionObserver) receives notifications and updates **Execution** state
|
|
198
|
-
5. **Execution** complete_* methods finalize the execution state
|
|
188
|
+
See [docs/architecture.md](docs/architecture.md) for framework
|
|
189
|
+
internals. It covers the components, the worker model, the checkpoint
|
|
190
|
+
flow, pagination, and a map of the code.
|
|
199
191
|
|
|
200
192
|
|
|
201
193
|
## Documentation
|
|
@@ -220,4 +212,4 @@ tldr; use `hatch` and it will manage virtual envs and dependencies for you, so y
|
|
|
220
212
|
|
|
221
213
|
## License
|
|
222
214
|
|
|
223
|
-
This project is licensed under the [Apache-2.0 License](LICENSE).
|
|
215
|
+
This project is licensed under the [Apache-2.0 License](https://github.com/aws/aws-durable-execution-sdk-python/blob/main/LICENSE).
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
- [Installation](#installation)
|
|
14
14
|
- [Quick Start](#quick-start)
|
|
15
|
+
- [Testing functions that invoke other functions](#testing-functions-that-invoke-other-functions)
|
|
15
16
|
- [Architecture](#architecture)
|
|
16
17
|
- [Documentation](#documentation)
|
|
17
18
|
- [Developer Guide](#developers)
|
|
@@ -111,67 +112,60 @@ def test_my_durable_functions():
|
|
|
111
112
|
three_result: StepOperation = result.get_step("three")
|
|
112
113
|
assert three_result.result == '"5 6"'
|
|
113
114
|
```
|
|
114
|
-
##
|
|
115
|
-

|
|
116
|
-
|
|
117
|
-
## Event Flow
|
|
118
|
-

|
|
119
|
-
|
|
120
|
-
1. **DurableTestRunner** starts execution via **Executor**
|
|
121
|
-
2. **Executor** creates **Execution** and schedules initial invocation
|
|
122
|
-
3. During execution, checkpoints are processed by **CheckpointProcessor**
|
|
123
|
-
4. **Individual Processors** transform operation updates and may trigger events
|
|
124
|
-
5. **ExecutionNotifier** broadcasts events to **Executor** (observer)
|
|
125
|
-
6. **Executor** updates **Execution** state based on events
|
|
126
|
-
7. **Execution** completion triggers final event notifications
|
|
127
|
-
8. **DurableTestRunner** run() blocks until it receives completion event, and then returns `DurableFunctionTestResult`.
|
|
115
|
+
## Testing functions that invoke other functions
|
|
128
116
|
|
|
129
|
-
|
|
117
|
+
A durable function can call another function with `context.invoke`. The
|
|
118
|
+
runner needs to know each target's kind: a durable target runs as a
|
|
119
|
+
child durable execution with its own history, while a plain (non-durable)
|
|
120
|
+
target is one invocation whose return value is the result. An unknown
|
|
121
|
+
target fails the operation with `ResourceNotFoundException`.
|
|
130
122
|
|
|
131
|
-
###
|
|
132
|
-
- **DurableTestRunner** - Main entry point that orchestrates test execution
|
|
133
|
-
- **Executor** - Manages execution lifecycle. Mutates Execution.
|
|
134
|
-
- **Execution** - Represents the state and operations of a single durable execution
|
|
123
|
+
### In-process runner
|
|
135
124
|
|
|
136
|
-
|
|
137
|
-
|
|
125
|
+
Register each target with the runner. `register_durable_function` marks a
|
|
126
|
+
durable target, and takes its execution timeout and retention;
|
|
127
|
+
`register_function` marks a plain one. The runner has one handler per
|
|
128
|
+
registered name, not versions: an invoke of `child:prod` runs the handler
|
|
129
|
+
registered as `child:prod` if there is one, otherwise the one registered
|
|
130
|
+
as `child`.
|
|
138
131
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
### Execution status changes (Observer Pattern)
|
|
145
|
-
- **ExecutionNotifier** - Notifies observers of execution events
|
|
146
|
-
- **ExecutionObserver** - Interface for receiving execution lifecycle events
|
|
147
|
-
- **Executor** implements `ExecutionObserver` to handle completion events
|
|
148
|
-
|
|
149
|
-
## Component Relationships
|
|
132
|
+
```python
|
|
133
|
+
from aws_durable_execution_sdk_python.context import DurableContext
|
|
134
|
+
from aws_durable_execution_sdk_python.execution import durable_execution
|
|
135
|
+
from aws_durable_execution_sdk_python_testing.runner import DurableFunctionTestRunner
|
|
150
136
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
- **Execution** maintains the state of operations and completion status
|
|
137
|
+
@durable_execution
|
|
138
|
+
def process_payment(event: dict, context: DurableContext) -> dict:
|
|
139
|
+
return {"charged": event["amount"]}
|
|
155
140
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
- **InProcessInvoker** injects the service client into SDK via `DurableExecutionInvocationInputWithClient`
|
|
159
|
-
- When durable functions call checkpoint operations, they're intercepted by **InMemoryServiceClient**
|
|
160
|
-
- **InMemoryServiceClient** delegates to **CheckpointProcessor** for local processing
|
|
141
|
+
def lookup_price(event: dict, context) -> dict:
|
|
142
|
+
return {"price": 25}
|
|
161
143
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
144
|
+
@durable_execution
|
|
145
|
+
def place_order(event: dict, context: DurableContext) -> dict:
|
|
146
|
+
price = context.invoke("lookup-price", {"sku": event["sku"]}, name="price")
|
|
147
|
+
payment = context.invoke(
|
|
148
|
+
"process-payment", {"amount": price["price"]}, name="payment"
|
|
149
|
+
)
|
|
150
|
+
return {"sku": event["sku"], "charged": payment["charged"]}
|
|
151
|
+
|
|
152
|
+
def test_place_order_invokes_both_functions():
|
|
153
|
+
with DurableFunctionTestRunner(handler=place_order) as runner:
|
|
154
|
+
runner.register_durable_function(
|
|
155
|
+
"process-payment", process_payment, execution_timeout=60
|
|
156
|
+
)
|
|
157
|
+
runner.register_function("lookup-price", lookup_price)
|
|
158
|
+
result = runner.run(input='{"sku": "book-1"}')
|
|
159
|
+
|
|
160
|
+
assert result.result == '{"sku": "book-1", "charged": 25}'
|
|
161
|
+
assert result.get_invoke("payment").status.value == "SUCCEEDED"
|
|
162
|
+
```
|
|
166
163
|
|
|
167
|
-
|
|
168
|
-
The observer pattern enables loose coupling between checkpoint processing and execution management:
|
|
164
|
+
## Architecture
|
|
169
165
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
4. **Executor** (as ExecutionObserver) receives notifications and updates **Execution** state
|
|
174
|
-
5. **Execution** complete_* methods finalize the execution state
|
|
166
|
+
See [docs/architecture.md](docs/architecture.md) for framework
|
|
167
|
+
internals. It covers the components, the worker model, the checkpoint
|
|
168
|
+
flow, pagination, and a map of the code.
|
|
175
169
|
|
|
176
170
|
|
|
177
171
|
## Documentation
|
|
@@ -196,4 +190,4 @@ tldr; use `hatch` and it will manage virtual envs and dependencies for you, so y
|
|
|
196
190
|
|
|
197
191
|
## License
|
|
198
192
|
|
|
199
|
-
This project is licensed under the [Apache-2.0 License](LICENSE).
|
|
193
|
+
This project is licensed under the [Apache-2.0 License](https://github.com/aws/aws-durable-execution-sdk-python/blob/main/LICENSE).
|
|
@@ -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
|