aws-durable-execution-sdk-python 1.4.0__py3-none-any.whl → 1.5.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/__about__.py +1 -1
- aws_durable_execution_sdk_python/__init__.py +5 -0
- aws_durable_execution_sdk_python/concurrency/executor.py +34 -13
- aws_durable_execution_sdk_python/config.py +108 -14
- aws_durable_execution_sdk_python/context.py +120 -13
- aws_durable_execution_sdk_python/exceptions.py +100 -42
- aws_durable_execution_sdk_python/execution.py +57 -51
- aws_durable_execution_sdk_python/lambda_service.py +12 -0
- aws_durable_execution_sdk_python/operation/child.py +43 -29
- aws_durable_execution_sdk_python/operation/map.py +15 -2
- aws_durable_execution_sdk_python/operation/parallel.py +25 -4
- aws_durable_execution_sdk_python/operation/step.py +2 -2
- aws_durable_execution_sdk_python/state.py +74 -9
- aws_durable_execution_sdk_python/types.py +3 -1
- {aws_durable_execution_sdk_python-1.4.0.dist-info → aws_durable_execution_sdk_python-1.5.0.dist-info}/METADATA +6 -29
- {aws_durable_execution_sdk_python-1.4.0.dist-info → aws_durable_execution_sdk_python-1.5.0.dist-info}/RECORD +19 -19
- {aws_durable_execution_sdk_python-1.4.0.dist-info → aws_durable_execution_sdk_python-1.5.0.dist-info}/WHEEL +0 -0
- {aws_durable_execution_sdk_python-1.4.0.dist-info → aws_durable_execution_sdk_python-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {aws_durable_execution_sdk_python-1.4.0.dist-info → aws_durable_execution_sdk_python-1.5.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -7,8 +7,10 @@ from aws_durable_execution_sdk_python.__about__ import __version__
|
|
|
7
7
|
# Helper decorators - commonly used for step functions
|
|
8
8
|
# Concurrency
|
|
9
9
|
from aws_durable_execution_sdk_python.concurrency.models import BatchResult
|
|
10
|
+
from aws_durable_execution_sdk_python.config import ParallelBranch
|
|
10
11
|
from aws_durable_execution_sdk_python.context import (
|
|
11
12
|
DurableContext,
|
|
13
|
+
durable_parallel_branch,
|
|
12
14
|
durable_step,
|
|
13
15
|
durable_wait_for_callback,
|
|
14
16
|
durable_with_child_context,
|
|
@@ -27,15 +29,18 @@ from aws_durable_execution_sdk_python.execution import durable_execution
|
|
|
27
29
|
# Essential context types - passed to user functions
|
|
28
30
|
from aws_durable_execution_sdk_python.types import StepContext
|
|
29
31
|
|
|
32
|
+
|
|
30
33
|
__all__ = [
|
|
31
34
|
"BatchResult",
|
|
32
35
|
"DurableContext",
|
|
33
36
|
"DurableExecutionsError",
|
|
34
37
|
"InvocationError",
|
|
38
|
+
"ParallelBranch",
|
|
35
39
|
"StepContext",
|
|
36
40
|
"ValidationError",
|
|
37
41
|
"__version__",
|
|
38
42
|
"durable_execution",
|
|
43
|
+
"durable_parallel_branch",
|
|
39
44
|
"durable_step",
|
|
40
45
|
"durable_wait_for_callback",
|
|
41
46
|
"durable_with_child_context",
|
|
@@ -20,7 +20,10 @@ from aws_durable_execution_sdk_python.concurrency.models import (
|
|
|
20
20
|
ExecutionCounters,
|
|
21
21
|
SuspendResult,
|
|
22
22
|
)
|
|
23
|
-
from aws_durable_execution_sdk_python.config import
|
|
23
|
+
from aws_durable_execution_sdk_python.config import (
|
|
24
|
+
ChildConfig,
|
|
25
|
+
NestingType,
|
|
26
|
+
)
|
|
24
27
|
from aws_durable_execution_sdk_python.exceptions import (
|
|
25
28
|
OrphanedChildException,
|
|
26
29
|
SuspendExecution,
|
|
@@ -143,6 +146,7 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
|
|
|
143
146
|
serdes: SerDes | None,
|
|
144
147
|
item_serdes: SerDes | None = None,
|
|
145
148
|
summary_generator: SummaryGenerator | None = None,
|
|
149
|
+
nesting_type: NestingType = NestingType.NESTED,
|
|
146
150
|
):
|
|
147
151
|
"""Initialize ConcurrentExecutor.
|
|
148
152
|
|
|
@@ -160,6 +164,7 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
|
|
|
160
164
|
self.sub_type_iteration = sub_type_iteration
|
|
161
165
|
self.name_prefix = name_prefix
|
|
162
166
|
self.summary_generator = summary_generator
|
|
167
|
+
self.nesting_type = nesting_type
|
|
163
168
|
|
|
164
169
|
# Event-driven state tracking for when the executor is done
|
|
165
170
|
self._completion_event = threading.Event()
|
|
@@ -189,6 +194,14 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
|
|
|
189
194
|
"""Execute a single executable in a child context and return the result."""
|
|
190
195
|
raise NotImplementedError
|
|
191
196
|
|
|
197
|
+
def get_iteration_name(self, index: int) -> str:
|
|
198
|
+
"""Get the display name for an iteration/branch at the given index.
|
|
199
|
+
|
|
200
|
+
Subclasses can override this to provide custom naming (e.g., from item_namer
|
|
201
|
+
or branch names). The default returns "{name_prefix}{index}".
|
|
202
|
+
"""
|
|
203
|
+
return f"{self.name_prefix}{index}"
|
|
204
|
+
|
|
192
205
|
def execute(
|
|
193
206
|
self, execution_state: ExecutionState, executor_context: DurableContext
|
|
194
207
|
) -> BatchResult[ResultType]:
|
|
@@ -391,29 +404,36 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
|
|
|
391
404
|
"""
|
|
392
405
|
Execute a single item in a derived child context.
|
|
393
406
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
directly. This avoids the hidden mutation of the context's
|
|
397
|
-
|
|
398
|
-
|
|
407
|
+
Instead of relying on `executor_context.run_in_child_context` we
|
|
408
|
+
generate an operation_id for the child, then call `child_handler`
|
|
409
|
+
directly. This avoids the hidden mutation of the context's
|
|
410
|
+
internal counter. We explicitly derive the child's operation_id
|
|
411
|
+
from `executable.index` so that the same input always produces
|
|
412
|
+
the same id regardless of the order branches actually run in.
|
|
399
413
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
and execution order invariant.
|
|
414
|
+
Invariant: `operation_id` for a given executable is deterministic
|
|
415
|
+
and execution-order invariant.
|
|
403
416
|
"""
|
|
404
417
|
|
|
405
|
-
operation_id = executor_context._create_step_id_for_logical_step( # noqa: SLF001
|
|
418
|
+
operation_id: str = executor_context._create_step_id_for_logical_step( # noqa: SLF001
|
|
406
419
|
executable.index
|
|
407
420
|
)
|
|
408
|
-
name =
|
|
409
|
-
|
|
421
|
+
name: str = self.get_iteration_name(executable.index)
|
|
422
|
+
is_virtual: bool = self.nesting_type is NestingType.FLAT
|
|
423
|
+
|
|
424
|
+
child_context: DurableContext = executor_context.create_child_context(
|
|
425
|
+
operation_id, is_virtual=is_virtual
|
|
426
|
+
)
|
|
427
|
+
# For NESTED this is for branch's START/SUCCEED/FAIL checkpoints (not the children of the branch).
|
|
428
|
+
# For FLAT `child_handler` skips checkpoints, so not used.
|
|
429
|
+
# Construct it unconditionally to keep the call simple.
|
|
410
430
|
operation_identifier = OperationIdentifier(
|
|
411
431
|
operation_id,
|
|
412
432
|
executor_context._parent_id, # noqa: SLF001
|
|
413
433
|
name,
|
|
414
434
|
)
|
|
415
435
|
|
|
416
|
-
def run_in_child_handler():
|
|
436
|
+
def run_in_child_handler() -> ResultType:
|
|
417
437
|
return self.execute_item(child_context, executable)
|
|
418
438
|
|
|
419
439
|
result: ResultType = child_handler(
|
|
@@ -424,6 +444,7 @@ class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
|
|
|
424
444
|
serdes=self.item_serdes or self.serdes,
|
|
425
445
|
sub_type=self.sub_type_iteration,
|
|
426
446
|
summary_generator=self.summary_generator,
|
|
447
|
+
is_virtual=is_virtual,
|
|
427
448
|
),
|
|
428
449
|
)
|
|
429
450
|
child_context.state.track_replay(operation_id=operation_id)
|
|
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Generic, TypeVar
|
|
|
9
9
|
|
|
10
10
|
from aws_durable_execution_sdk_python.exceptions import ValidationError
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
P = TypeVar("P") # Payload type
|
|
13
14
|
R = TypeVar("R") # Result type
|
|
14
15
|
T = TypeVar("T")
|
|
@@ -76,6 +77,42 @@ class TerminationMode(Enum):
|
|
|
76
77
|
ABANDON = "ABANDON"
|
|
77
78
|
|
|
78
79
|
|
|
80
|
+
class NestingType(Enum):
|
|
81
|
+
"""Control how child contexts are created for batch operations.
|
|
82
|
+
|
|
83
|
+
Applies to `map` and `parallel`. Each branch or iteration runs inside a
|
|
84
|
+
child context.
|
|
85
|
+
|
|
86
|
+
- NESTED: full checkpointed context
|
|
87
|
+
- FLAT: a virtual context that skips checkpoints for the branch/iteration.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
NESTED = "NESTED"
|
|
92
|
+
"""Create CONTEXT operations for each branch/iteration with full checkpointing.
|
|
93
|
+
|
|
94
|
+
Operations within each branch/iteration are wrapped in their own context.
|
|
95
|
+
|
|
96
|
+
- Observability: high — each branch/iteration appears as a separate
|
|
97
|
+
operation in execution history.
|
|
98
|
+
- Cost: higher — consumes more operations due to CONTEXT creation
|
|
99
|
+
overhead.
|
|
100
|
+
- Scale: lower maximum iterations due to operation limits.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
FLAT = "FLAT"
|
|
104
|
+
"""Skip CONTEXT operations for branches/iterations using virtual contexts.
|
|
105
|
+
|
|
106
|
+
Operations execute directly without individual context wrapping.
|
|
107
|
+
|
|
108
|
+
- Observability: lower — branches/iterations don't appear as separate
|
|
109
|
+
operations in execution history.
|
|
110
|
+
- Cost: ~30% lower — reduces operation consumption by skipping CONTEXT
|
|
111
|
+
overhead.
|
|
112
|
+
- Scale: higher maximum iterations possible within operation limits.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
79
116
|
@dataclass(frozen=True)
|
|
80
117
|
class CompletionConfig:
|
|
81
118
|
"""Configuration for determining when parallel/map operations complete.
|
|
@@ -187,6 +224,10 @@ class ParallelConfig:
|
|
|
187
224
|
Used internally by map/parallel operations to handle large BatchResult payloads.
|
|
188
225
|
Signature: (result: T) -> str
|
|
189
226
|
|
|
227
|
+
nesting_type: How child operations should inherit context from their parent.
|
|
228
|
+
- NESTED: Each branch runs in its own isolated context (default)
|
|
229
|
+
- FLAT: All branches share the same parent context
|
|
230
|
+
|
|
190
231
|
Example:
|
|
191
232
|
# Run at most 3 branches concurrently, succeed if any one succeeds
|
|
192
233
|
config = ParallelConfig(
|
|
@@ -202,6 +243,42 @@ class ParallelConfig:
|
|
|
202
243
|
serdes: SerDes | None = None
|
|
203
244
|
item_serdes: SerDes | None = None
|
|
204
245
|
summary_generator: SummaryGenerator | None = None
|
|
246
|
+
nesting_type: NestingType = NestingType.NESTED
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass(frozen=True)
|
|
250
|
+
class ParallelBranch(Generic[T]):
|
|
251
|
+
"""A named branch for parallel execution.
|
|
252
|
+
|
|
253
|
+
Use this to provide custom names for parallel branches, improving
|
|
254
|
+
observability in execution history.
|
|
255
|
+
|
|
256
|
+
Type Parameters:
|
|
257
|
+
T: The return type of the branch function.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
func: The callable to execute in this branch. Receives a DurableContext.
|
|
261
|
+
name: Optional custom name for this branch. When provided, replaces
|
|
262
|
+
the default "parallel-branch-{index}" naming in execution history.
|
|
263
|
+
This affects observability but not replay determinism.
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
context.parallel(
|
|
267
|
+
functions=[
|
|
268
|
+
ParallelBranch(func=lambda ctx: fetch_user(ctx), name="fetch-user-data"),
|
|
269
|
+
ParallelBranch(func=lambda ctx: fetch_orders(ctx), name="fetch-order-history"),
|
|
270
|
+
],
|
|
271
|
+
name="load-data",
|
|
272
|
+
config=ParallelConfig(max_concurrency=2),
|
|
273
|
+
)
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
func: Callable
|
|
277
|
+
name: str | None = None
|
|
278
|
+
|
|
279
|
+
def __call__(self, *args, **kwargs):
|
|
280
|
+
"""Delegate to the wrapped function, making ParallelBranch itself callable."""
|
|
281
|
+
return self.func(*args, **kwargs)
|
|
205
282
|
|
|
206
283
|
|
|
207
284
|
class StepSemantics(Enum):
|
|
@@ -218,12 +295,6 @@ class StepConfig:
|
|
|
218
295
|
serdes: SerDes | None = None
|
|
219
296
|
|
|
220
297
|
|
|
221
|
-
class CheckpointMode(Enum):
|
|
222
|
-
NO_CHECKPOINT = ("NO_CHECKPOINT",)
|
|
223
|
-
CHECKPOINT_AT_FINISH = ("CHECKPOINT_AT_FINISH",)
|
|
224
|
-
CHECKPOINT_AT_START_AND_FINISH = "CHECKPOINT_AT_START_AND_FINISH"
|
|
225
|
-
|
|
226
|
-
|
|
227
298
|
@dataclass(frozen=True)
|
|
228
299
|
class ChildConfig(Generic[T]):
|
|
229
300
|
"""Configuration options for child context operations.
|
|
@@ -259,21 +330,23 @@ class ChildConfig(Generic[T]):
|
|
|
259
330
|
|
|
260
331
|
Used internally by map/parallel operations to handle large BatchResult payloads.
|
|
261
332
|
Signature: (result: T) -> str
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
333
|
+
|
|
334
|
+
is_virtual: When True, skip all checkpoints (START, SUCCEED,
|
|
335
|
+
FAIL) for this child context and propagate the caller's reporting
|
|
336
|
+
parent id through to operations created inside the child. The
|
|
337
|
+
branch is a logical scope for step-id prefixing but does not
|
|
338
|
+
appear in the execution history. Used internally by
|
|
339
|
+
NestingType.FLAT branches. Use this to group operations without
|
|
340
|
+
adding a CONTEXT entry to the execution history.
|
|
268
341
|
|
|
269
342
|
See TypeScript reference: aws-durable-execution-sdk-js/src/types/index.ts
|
|
270
343
|
"""
|
|
271
344
|
|
|
272
|
-
# checkpoint_mode: CheckpointMode = CheckpointMode.CHECKPOINT_AT_START_AND_FINISH
|
|
273
345
|
serdes: SerDes | None = None
|
|
274
346
|
item_serdes: SerDes | None = None
|
|
275
347
|
sub_type: OperationSubType | None = None
|
|
276
348
|
summary_generator: SummaryGenerator | None = None
|
|
349
|
+
is_virtual: bool = False
|
|
277
350
|
|
|
278
351
|
|
|
279
352
|
class ItemsPerBatchUnit(Enum):
|
|
@@ -317,12 +390,15 @@ class ItemBatcher(Generic[T]):
|
|
|
317
390
|
|
|
318
391
|
|
|
319
392
|
@dataclass(frozen=True)
|
|
320
|
-
class MapConfig:
|
|
393
|
+
class MapConfig(Generic[T]):
|
|
321
394
|
"""Configuration options for map operations over collections.
|
|
322
395
|
|
|
323
396
|
This class configures how map operations process collections of items,
|
|
324
397
|
including concurrency, batching, completion criteria, and serialization.
|
|
325
398
|
|
|
399
|
+
Type Parameters:
|
|
400
|
+
T: The type of items being processed in the map operation.
|
|
401
|
+
|
|
326
402
|
Args:
|
|
327
403
|
max_concurrency: Maximum number of items to process concurrently.
|
|
328
404
|
If None, no limit is imposed and all items are processed concurrently.
|
|
@@ -361,6 +437,16 @@ class MapConfig:
|
|
|
361
437
|
Used internally by map/parallel operations to handle large BatchResult payloads.
|
|
362
438
|
Signature: (result: T) -> str
|
|
363
439
|
|
|
440
|
+
nesting_type: How child operations should inherit context from their parent.
|
|
441
|
+
- NESTED: Each item runs in its own isolated context (default)
|
|
442
|
+
- FLAT: All items share the same parent context
|
|
443
|
+
|
|
444
|
+
item_namer: Optional callable to generate custom names for each map iteration.
|
|
445
|
+
When provided, replaces the default "map-item-{index}" naming scheme.
|
|
446
|
+
Receives the item and its index, and returns a string name for that iteration.
|
|
447
|
+
This affects observability (execution history names) but not replay determinism.
|
|
448
|
+
If None, uses the default naming: "map-item-{index}".
|
|
449
|
+
|
|
364
450
|
Example:
|
|
365
451
|
# Process 5 items at a time, batch by count, require all to succeed
|
|
366
452
|
config = MapConfig(
|
|
@@ -368,6 +454,12 @@ class MapConfig:
|
|
|
368
454
|
item_batcher=ItemBatcher(max_items_per_batch=10),
|
|
369
455
|
completion_config=CompletionConfig.all_successful()
|
|
370
456
|
)
|
|
457
|
+
|
|
458
|
+
# With custom iteration names
|
|
459
|
+
config = MapConfig(
|
|
460
|
+
max_concurrency=5,
|
|
461
|
+
item_namer=lambda item, index: f"process-order-{item.id}"
|
|
462
|
+
)
|
|
371
463
|
"""
|
|
372
464
|
|
|
373
465
|
max_concurrency: int | None = None
|
|
@@ -376,6 +468,8 @@ class MapConfig:
|
|
|
376
468
|
serdes: SerDes | None = None
|
|
377
469
|
item_serdes: SerDes | None = None
|
|
378
470
|
summary_generator: SummaryGenerator | None = None
|
|
471
|
+
nesting_type: NestingType = NestingType.NESTED
|
|
472
|
+
item_namer: Callable[[T, int], str] | None = None
|
|
379
473
|
|
|
380
474
|
|
|
381
475
|
@dataclass(frozen=True)
|
|
@@ -12,6 +12,7 @@ from aws_durable_execution_sdk_python.config import (
|
|
|
12
12
|
Duration,
|
|
13
13
|
InvokeConfig,
|
|
14
14
|
MapConfig,
|
|
15
|
+
ParallelBranch,
|
|
15
16
|
ParallelConfig,
|
|
16
17
|
StepConfig,
|
|
17
18
|
WaitForCallbackConfig,
|
|
@@ -55,6 +56,7 @@ from aws_durable_execution_sdk_python.types import (
|
|
|
55
56
|
WaitForConditionCheckContext,
|
|
56
57
|
)
|
|
57
58
|
|
|
59
|
+
|
|
58
60
|
if TYPE_CHECKING:
|
|
59
61
|
from collections.abc import Callable, Sequence
|
|
60
62
|
|
|
@@ -119,6 +121,52 @@ def durable_with_child_context(
|
|
|
119
121
|
return wrapper
|
|
120
122
|
|
|
121
123
|
|
|
124
|
+
def durable_parallel_branch(
|
|
125
|
+
name: str | None = None,
|
|
126
|
+
) -> Callable[
|
|
127
|
+
[Callable[Concatenate[DurableContext, Params], T]],
|
|
128
|
+
Callable[Params, ParallelBranch[T]],
|
|
129
|
+
]:
|
|
130
|
+
"""Wrap your callable into a named ParallelBranch for use with context.parallel().
|
|
131
|
+
|
|
132
|
+
This is a decorator factory — call it with an optional name to produce
|
|
133
|
+
the actual decorator.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
name: Optional custom name for this branch. When provided, replaces
|
|
137
|
+
the default "parallel-branch-{index}" naming in execution history.
|
|
138
|
+
If None, the function's __name__ is used.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
@durable_parallel_branch(name="fetch-user-data")
|
|
142
|
+
def fetch_user(ctx: DurableContext, user_id: str) -> dict:
|
|
143
|
+
return ctx.step(lambda _: {"id": user_id, "name": "Jane"}, name="load_user")
|
|
144
|
+
|
|
145
|
+
@durable_parallel_branch(name="fetch-orders")
|
|
146
|
+
def fetch_orders(ctx: DurableContext, user_id: str) -> list:
|
|
147
|
+
return ctx.step(lambda _: ["order1", "order2"], name="load_orders")
|
|
148
|
+
|
|
149
|
+
# Usage in a durable handler:
|
|
150
|
+
results = context.parallel(
|
|
151
|
+
functions=[fetch_user(user_id), fetch_orders(user_id)],
|
|
152
|
+
name="load-data",
|
|
153
|
+
)
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def decorator(
|
|
157
|
+
func: Callable[Concatenate[DurableContext, Params], T],
|
|
158
|
+
) -> Callable[Params, ParallelBranch[T]]:
|
|
159
|
+
def wrapper(*args, **kwargs) -> ParallelBranch[T]:
|
|
160
|
+
def function_with_arguments(ctx: DurableContext) -> T:
|
|
161
|
+
return func(ctx, *args, **kwargs)
|
|
162
|
+
|
|
163
|
+
return ParallelBranch(func=function_with_arguments, name=name)
|
|
164
|
+
|
|
165
|
+
return wrapper
|
|
166
|
+
|
|
167
|
+
return decorator
|
|
168
|
+
|
|
169
|
+
|
|
122
170
|
def durable_wait_for_callback(
|
|
123
171
|
func: Callable[Concatenate[str, WaitForCallbackContext, Params], T],
|
|
124
172
|
) -> Callable[Params, Callable[[str, WaitForCallbackContext], T]]:
|
|
@@ -237,11 +285,20 @@ class DurableContext(DurableContextProtocol):
|
|
|
237
285
|
lambda_context: LambdaContext | None = None,
|
|
238
286
|
parent_id: str | None = None,
|
|
239
287
|
logger: Logger | None = None,
|
|
288
|
+
step_id_prefix: str | None = None,
|
|
240
289
|
) -> None:
|
|
241
290
|
self.state: ExecutionState = state
|
|
242
291
|
self.execution_context: ExecutionContext = execution_context
|
|
243
292
|
self.lambda_context = lambda_context
|
|
293
|
+
# operations inside this context use this id as their parent
|
|
244
294
|
self._parent_id: str | None = parent_id
|
|
295
|
+
# child operations use this to generate deterministic step ids.
|
|
296
|
+
# differs from `parent_id` only for virtual contexts.
|
|
297
|
+
self._step_id_prefix: str | None = (
|
|
298
|
+
step_id_prefix if step_id_prefix is not None else parent_id
|
|
299
|
+
)
|
|
300
|
+
# cached at construction to make invariant even if parent/prefix mutates.
|
|
301
|
+
self._is_virtual: bool = self._parent_id != self._step_id_prefix
|
|
245
302
|
self._step_counter: OrderedCounter = OrderedCounter()
|
|
246
303
|
|
|
247
304
|
log_info = LogInfo(
|
|
@@ -254,6 +311,18 @@ class DurableContext(DurableContextProtocol):
|
|
|
254
311
|
info=log_info,
|
|
255
312
|
)
|
|
256
313
|
|
|
314
|
+
@property
|
|
315
|
+
def is_virtual(self) -> bool:
|
|
316
|
+
"""True if this context does not checkpoint its own start and completion.
|
|
317
|
+
|
|
318
|
+
You create a virtual context by `create_child_context(..., is_virtual=True)`.
|
|
319
|
+
FLAT-mode `map`/`parallel` branches uses virtual contexts. Inner operations
|
|
320
|
+
use the grandfather as parent (enclosing non-virtual ancestor, skipping the branch level
|
|
321
|
+
in the hierarchy), while step ids are still prefixed with the branch's own
|
|
322
|
+
operation id so replay stays deterministic.
|
|
323
|
+
"""
|
|
324
|
+
return self._is_virtual
|
|
325
|
+
|
|
257
326
|
# region factories
|
|
258
327
|
@staticmethod
|
|
259
328
|
def from_lambda_context(
|
|
@@ -269,18 +338,43 @@ class DurableContext(DurableContextProtocol):
|
|
|
269
338
|
parent_id=None,
|
|
270
339
|
)
|
|
271
340
|
|
|
272
|
-
def create_child_context(
|
|
273
|
-
|
|
274
|
-
|
|
341
|
+
def create_child_context(
|
|
342
|
+
self, operation_id: str, *, is_virtual: bool = False
|
|
343
|
+
) -> DurableContext:
|
|
344
|
+
"""Create a child context for the given operation.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
operation_id: The operation id that owns the child context. Used as
|
|
348
|
+
the child's step-id prefix in all cases.
|
|
349
|
+
is_virtual: When `True`, create a virtual child whose inner
|
|
350
|
+
operations report to this context's own `_parent_id` (one
|
|
351
|
+
level up the hierarchy). When `False` (default), produce a
|
|
352
|
+
regular child whose inner operations report to
|
|
353
|
+
`operation_id`.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
A new `DurableContext` child for the current context.
|
|
357
|
+
"""
|
|
358
|
+
# For a virtual child, propagate the current `_parent_id` so its
|
|
359
|
+
# inner operations refer to the grandparent rather than the parent.
|
|
360
|
+
# For a regular non-virtual child, the child's own `operation_id` is
|
|
361
|
+
# the parent id for its inner operations (standard nesting).
|
|
362
|
+
child_parent_id: str | None = self._parent_id if is_virtual else operation_id
|
|
363
|
+
logger.debug(
|
|
364
|
+
"Creating child context for operation %s (is_virtual=%s)",
|
|
365
|
+
operation_id,
|
|
366
|
+
is_virtual,
|
|
367
|
+
)
|
|
275
368
|
return DurableContext(
|
|
276
369
|
state=self.state,
|
|
277
370
|
execution_context=self.execution_context,
|
|
278
371
|
lambda_context=self.lambda_context,
|
|
279
|
-
parent_id=
|
|
372
|
+
parent_id=child_parent_id,
|
|
373
|
+
step_id_prefix=operation_id,
|
|
280
374
|
logger=self.logger.with_log_info(
|
|
281
375
|
LogInfo(
|
|
282
376
|
execution_state=self.state,
|
|
283
|
-
parent_id=
|
|
377
|
+
parent_id=child_parent_id,
|
|
284
378
|
)
|
|
285
379
|
),
|
|
286
380
|
)
|
|
@@ -310,7 +404,8 @@ class DurableContext(DurableContextProtocol):
|
|
|
310
404
|
This allows us to recover operation ids or even look
|
|
311
405
|
forward without changing the internal state of this context.
|
|
312
406
|
"""
|
|
313
|
-
|
|
407
|
+
prefix: str | None = self._step_id_prefix
|
|
408
|
+
step_id: str = f"{prefix}-{step}" if prefix else str(step)
|
|
314
409
|
return hashlib.blake2b(step_id.encode()).hexdigest()[:64]
|
|
315
410
|
|
|
316
411
|
def _create_step_id(self) -> str:
|
|
@@ -347,7 +442,9 @@ class DurableContext(DurableContextProtocol):
|
|
|
347
442
|
executor: CallbackOperationExecutor = CallbackOperationExecutor(
|
|
348
443
|
state=self.state,
|
|
349
444
|
operation_identifier=OperationIdentifier(
|
|
350
|
-
operation_id=operation_id,
|
|
445
|
+
operation_id=operation_id,
|
|
446
|
+
parent_id=self._parent_id,
|
|
447
|
+
name=name,
|
|
351
448
|
),
|
|
352
449
|
config=config,
|
|
353
450
|
)
|
|
@@ -409,9 +506,11 @@ class DurableContext(DurableContextProtocol):
|
|
|
409
506
|
|
|
410
507
|
operation_id = self._create_step_id()
|
|
411
508
|
operation_identifier = OperationIdentifier(
|
|
412
|
-
operation_id=operation_id,
|
|
509
|
+
operation_id=operation_id,
|
|
510
|
+
parent_id=self._parent_id,
|
|
511
|
+
name=map_name,
|
|
413
512
|
)
|
|
414
|
-
map_context = self.create_child_context(
|
|
513
|
+
map_context = self.create_child_context(operation_id=operation_id)
|
|
415
514
|
|
|
416
515
|
def map_in_child_context() -> BatchResult[R]:
|
|
417
516
|
# map_context is a child_context of the context upon which `.map`
|
|
@@ -445,14 +544,14 @@ class DurableContext(DurableContextProtocol):
|
|
|
445
544
|
|
|
446
545
|
def parallel(
|
|
447
546
|
self,
|
|
448
|
-
functions: Sequence[Callable[[DurableContext], T]],
|
|
547
|
+
functions: Sequence[Callable[[DurableContext], T] | ParallelBranch[T]],
|
|
449
548
|
name: str | None = None,
|
|
450
549
|
config: ParallelConfig | None = None,
|
|
451
550
|
) -> BatchResult[T]:
|
|
452
551
|
"""Execute multiple callables in parallel."""
|
|
453
552
|
# _create_step_id() is thread-safe. rest of method is safe, since using local copy of parent id
|
|
454
553
|
operation_id = self._create_step_id()
|
|
455
|
-
parallel_context = self.create_child_context(
|
|
554
|
+
parallel_context = self.create_child_context(operation_id=operation_id)
|
|
456
555
|
operation_identifier = OperationIdentifier(
|
|
457
556
|
operation_id=operation_id, parent_id=self._parent_id, name=name
|
|
458
557
|
)
|
|
@@ -508,14 +607,22 @@ class DurableContext(DurableContextProtocol):
|
|
|
508
607
|
# _create_step_id() is thread-safe. rest of method is safe, since using local copy of parent id
|
|
509
608
|
operation_id = self._create_step_id()
|
|
510
609
|
|
|
610
|
+
is_virtual: bool = config.is_virtual if config else False
|
|
611
|
+
|
|
511
612
|
def callable_with_child_context():
|
|
512
|
-
return func(
|
|
613
|
+
return func(
|
|
614
|
+
self.create_child_context(
|
|
615
|
+
operation_id=operation_id, is_virtual=is_virtual
|
|
616
|
+
)
|
|
617
|
+
)
|
|
513
618
|
|
|
514
619
|
result: T = child_handler(
|
|
515
620
|
func=callable_with_child_context,
|
|
516
621
|
state=self.state,
|
|
517
622
|
operation_identifier=OperationIdentifier(
|
|
518
|
-
operation_id=operation_id,
|
|
623
|
+
operation_id=operation_id,
|
|
624
|
+
parent_id=self._parent_id,
|
|
625
|
+
name=step_name,
|
|
519
626
|
),
|
|
520
627
|
config=config,
|
|
521
628
|
)
|