aws-durable-execution-sdk-python 1.0.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/.gitignore +0 -0
- aws_durable_execution_sdk_python/__about__.py +4 -0
- aws_durable_execution_sdk_python/__init__.py +34 -0
- aws_durable_execution_sdk_python/botocore/data/lambdainternal/2015-03-31/service-2.json +7864 -0
- aws_durable_execution_sdk_python/concurrency/__init__.py +0 -0
- aws_durable_execution_sdk_python/concurrency/executor.py +436 -0
- aws_durable_execution_sdk_python/concurrency/models.py +469 -0
- aws_durable_execution_sdk_python/config.py +499 -0
- aws_durable_execution_sdk_python/context.py +551 -0
- aws_durable_execution_sdk_python/exceptions.py +374 -0
- aws_durable_execution_sdk_python/execution.py +428 -0
- aws_durable_execution_sdk_python/identifier.py +14 -0
- aws_durable_execution_sdk_python/lambda_service.py +1034 -0
- aws_durable_execution_sdk_python/logger.py +131 -0
- aws_durable_execution_sdk_python/operation/__init__.py +1 -0
- aws_durable_execution_sdk_python/operation/callback.py +123 -0
- aws_durable_execution_sdk_python/operation/child.py +162 -0
- aws_durable_execution_sdk_python/operation/invoke.py +119 -0
- aws_durable_execution_sdk_python/operation/map.py +137 -0
- aws_durable_execution_sdk_python/operation/parallel.py +122 -0
- aws_durable_execution_sdk_python/operation/step.py +269 -0
- aws_durable_execution_sdk_python/operation/wait.py +53 -0
- aws_durable_execution_sdk_python/operation/wait_for_condition.py +235 -0
- aws_durable_execution_sdk_python/py.typed +1 -0
- aws_durable_execution_sdk_python/retries.py +174 -0
- aws_durable_execution_sdk_python/serdes.py +502 -0
- aws_durable_execution_sdk_python/state.py +790 -0
- aws_durable_execution_sdk_python/suspend.py +84 -0
- aws_durable_execution_sdk_python/threading.py +222 -0
- aws_durable_execution_sdk_python/types.py +180 -0
- aws_durable_execution_sdk_python/waits.py +130 -0
- aws_durable_execution_sdk_python-1.0.0.dist-info/METADATA +679 -0
- aws_durable_execution_sdk_python-1.0.0.dist-info/RECORD +36 -0
- aws_durable_execution_sdk_python-1.0.0.dist-info/WHEEL +4 -0
- aws_durable_execution_sdk_python-1.0.0.dist-info/licenses/LICENSE +175 -0
- aws_durable_execution_sdk_python-1.0.0.dist-info/licenses/NOTICE +1 -0
|
File without changes
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""Concurrent executor for parallel and map operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import heapq
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
11
|
+
from typing import TYPE_CHECKING, Generic, Self, TypeVar
|
|
12
|
+
|
|
13
|
+
from aws_durable_execution_sdk_python.concurrency.models import (
|
|
14
|
+
BatchItem,
|
|
15
|
+
BatchItemStatus,
|
|
16
|
+
BatchResult,
|
|
17
|
+
BranchStatus,
|
|
18
|
+
Executable,
|
|
19
|
+
ExecutableWithState,
|
|
20
|
+
ExecutionCounters,
|
|
21
|
+
SuspendResult,
|
|
22
|
+
)
|
|
23
|
+
from aws_durable_execution_sdk_python.config import ChildConfig
|
|
24
|
+
from aws_durable_execution_sdk_python.exceptions import (
|
|
25
|
+
SuspendExecution,
|
|
26
|
+
TimedSuspendExecution,
|
|
27
|
+
)
|
|
28
|
+
from aws_durable_execution_sdk_python.identifier import OperationIdentifier
|
|
29
|
+
from aws_durable_execution_sdk_python.lambda_service import ErrorObject
|
|
30
|
+
from aws_durable_execution_sdk_python.operation.child import child_handler
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Callable
|
|
34
|
+
|
|
35
|
+
from aws_durable_execution_sdk_python.config import CompletionConfig
|
|
36
|
+
from aws_durable_execution_sdk_python.context import DurableContext
|
|
37
|
+
from aws_durable_execution_sdk_python.lambda_service import OperationSubType
|
|
38
|
+
from aws_durable_execution_sdk_python.serdes import SerDes
|
|
39
|
+
from aws_durable_execution_sdk_python.state import ExecutionState
|
|
40
|
+
from aws_durable_execution_sdk_python.types import SummaryGenerator
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
T = TypeVar("T")
|
|
46
|
+
R = TypeVar("R")
|
|
47
|
+
|
|
48
|
+
CallableType = TypeVar("CallableType")
|
|
49
|
+
ResultType = TypeVar("ResultType")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# region concurrency logic
|
|
53
|
+
class TimerScheduler:
|
|
54
|
+
"""Manage timed suspend tasks with a background timer thread."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self, resubmit_callback: Callable[[ExecutableWithState], None]
|
|
58
|
+
) -> None:
|
|
59
|
+
self.resubmit_callback = resubmit_callback
|
|
60
|
+
self._pending_resumes: list[tuple[float, ExecutableWithState]] = []
|
|
61
|
+
self._lock = threading.Lock()
|
|
62
|
+
self._shutdown = threading.Event()
|
|
63
|
+
self._timer_thread = threading.Thread(target=self._timer_loop, daemon=True)
|
|
64
|
+
self._timer_thread.start()
|
|
65
|
+
|
|
66
|
+
def __enter__(self) -> Self:
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
70
|
+
self.shutdown()
|
|
71
|
+
|
|
72
|
+
def schedule_resume(
|
|
73
|
+
self, exe_state: ExecutableWithState, resume_time: float
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Schedule a task to resume at the specified time."""
|
|
76
|
+
with self._lock:
|
|
77
|
+
heapq.heappush(self._pending_resumes, (resume_time, exe_state))
|
|
78
|
+
|
|
79
|
+
def shutdown(self) -> None:
|
|
80
|
+
"""Shutdown the timer thread and cancel all pending resumes."""
|
|
81
|
+
self._shutdown.set()
|
|
82
|
+
self._timer_thread.join(timeout=1.0)
|
|
83
|
+
with self._lock:
|
|
84
|
+
self._pending_resumes.clear()
|
|
85
|
+
|
|
86
|
+
def _timer_loop(self) -> None:
|
|
87
|
+
"""Background thread that processes timed resumes."""
|
|
88
|
+
while not self._shutdown.is_set():
|
|
89
|
+
next_resume_time = None
|
|
90
|
+
|
|
91
|
+
with self._lock:
|
|
92
|
+
if self._pending_resumes:
|
|
93
|
+
next_resume_time = self._pending_resumes[0][0]
|
|
94
|
+
|
|
95
|
+
if next_resume_time is None:
|
|
96
|
+
# No pending resumes, wait a bit and check again
|
|
97
|
+
self._shutdown.wait(timeout=0.1)
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
current_time = time.time()
|
|
101
|
+
if current_time >= next_resume_time:
|
|
102
|
+
# Time to resume
|
|
103
|
+
with self._lock:
|
|
104
|
+
# no branch cover because hard to test reliably - this is a double-safety check if heap mutated
|
|
105
|
+
# since the first peek on next_resume_time further up
|
|
106
|
+
if ( # pragma: no branch
|
|
107
|
+
self._pending_resumes
|
|
108
|
+
and self._pending_resumes[0][0] <= current_time
|
|
109
|
+
):
|
|
110
|
+
_, exe_state = heapq.heappop(self._pending_resumes)
|
|
111
|
+
if exe_state.can_resume:
|
|
112
|
+
exe_state.reset_to_pending()
|
|
113
|
+
self.resubmit_callback(exe_state)
|
|
114
|
+
else:
|
|
115
|
+
# Wait until next resume time
|
|
116
|
+
wait_time = min(next_resume_time - current_time, 0.1)
|
|
117
|
+
self._shutdown.wait(timeout=wait_time)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ConcurrentExecutor(ABC, Generic[CallableType, ResultType]):
|
|
121
|
+
"""Execute durable operations concurrently. This contains the execution logic for Map and Parallel."""
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
executables: list[Executable[CallableType]],
|
|
126
|
+
max_concurrency: int | None,
|
|
127
|
+
completion_config: CompletionConfig,
|
|
128
|
+
sub_type_top: OperationSubType,
|
|
129
|
+
sub_type_iteration: OperationSubType,
|
|
130
|
+
name_prefix: str,
|
|
131
|
+
serdes: SerDes | None,
|
|
132
|
+
item_serdes: SerDes | None = None,
|
|
133
|
+
summary_generator: SummaryGenerator | None = None,
|
|
134
|
+
):
|
|
135
|
+
"""Initialize ConcurrentExecutor.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
summary_generator: Optional function to generate compact summaries for large results.
|
|
139
|
+
When the serialized result exceeds 256KB, this generator creates a JSON summary
|
|
140
|
+
instead of checkpointing the full result. Used by map/parallel operations to
|
|
141
|
+
handle large BatchResult payloads efficiently. Matches TypeScript behavior in
|
|
142
|
+
run-in-child-context-handler.ts.
|
|
143
|
+
"""
|
|
144
|
+
self.executables = executables
|
|
145
|
+
self.max_concurrency = max_concurrency
|
|
146
|
+
self.completion_config = completion_config
|
|
147
|
+
self.sub_type_top = sub_type_top
|
|
148
|
+
self.sub_type_iteration = sub_type_iteration
|
|
149
|
+
self.name_prefix = name_prefix
|
|
150
|
+
self.summary_generator = summary_generator
|
|
151
|
+
|
|
152
|
+
# Event-driven state tracking for when the executor is done
|
|
153
|
+
self._completion_event = threading.Event()
|
|
154
|
+
self._suspend_exception: SuspendExecution | None = None
|
|
155
|
+
|
|
156
|
+
# ExecutionCounters will keep track of completion criteria and on-going counters
|
|
157
|
+
min_successful = self.completion_config.min_successful or len(self.executables)
|
|
158
|
+
tolerated_failure_count = self.completion_config.tolerated_failure_count
|
|
159
|
+
tolerated_failure_percentage = (
|
|
160
|
+
self.completion_config.tolerated_failure_percentage
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self.counters: ExecutionCounters = ExecutionCounters(
|
|
164
|
+
len(executables),
|
|
165
|
+
min_successful,
|
|
166
|
+
tolerated_failure_count,
|
|
167
|
+
tolerated_failure_percentage,
|
|
168
|
+
)
|
|
169
|
+
self.executables_with_state: list[ExecutableWithState] = []
|
|
170
|
+
self.serdes = serdes
|
|
171
|
+
self.item_serdes = item_serdes
|
|
172
|
+
|
|
173
|
+
@abstractmethod
|
|
174
|
+
def execute_item(
|
|
175
|
+
self, child_context: DurableContext, executable: Executable[CallableType]
|
|
176
|
+
) -> ResultType:
|
|
177
|
+
"""Execute a single executable in a child context and return the result."""
|
|
178
|
+
raise NotImplementedError
|
|
179
|
+
|
|
180
|
+
def execute(
|
|
181
|
+
self, execution_state: ExecutionState, executor_context: DurableContext
|
|
182
|
+
) -> BatchResult[ResultType]:
|
|
183
|
+
"""Execute items concurrently with event-driven state management."""
|
|
184
|
+
logger.debug(
|
|
185
|
+
"▶️ Executing concurrent operation, items: %d", len(self.executables)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
max_workers = self.max_concurrency or len(self.executables)
|
|
189
|
+
|
|
190
|
+
self.executables_with_state = [
|
|
191
|
+
ExecutableWithState(executable=exe) for exe in self.executables
|
|
192
|
+
]
|
|
193
|
+
self._completion_event.clear()
|
|
194
|
+
self._suspend_exception = None
|
|
195
|
+
|
|
196
|
+
def resubmitter(executable_with_state: ExecutableWithState) -> None:
|
|
197
|
+
"""Resubmit a timed suspended task."""
|
|
198
|
+
execution_state.create_checkpoint()
|
|
199
|
+
submit_task(executable_with_state)
|
|
200
|
+
|
|
201
|
+
with (
|
|
202
|
+
TimerScheduler(resubmitter) as scheduler,
|
|
203
|
+
ThreadPoolExecutor(max_workers=max_workers) as thread_executor,
|
|
204
|
+
):
|
|
205
|
+
|
|
206
|
+
def submit_task(executable_with_state: ExecutableWithState) -> Future:
|
|
207
|
+
"""Submit task to the thread executor and mark its state as started."""
|
|
208
|
+
future = thread_executor.submit(
|
|
209
|
+
self._execute_item_in_child_context,
|
|
210
|
+
executor_context,
|
|
211
|
+
executable_with_state.executable,
|
|
212
|
+
)
|
|
213
|
+
executable_with_state.run(future)
|
|
214
|
+
|
|
215
|
+
def on_done(future: Future) -> None:
|
|
216
|
+
self._on_task_complete(executable_with_state, future, scheduler)
|
|
217
|
+
|
|
218
|
+
future.add_done_callback(on_done)
|
|
219
|
+
return future
|
|
220
|
+
|
|
221
|
+
# Submit initial tasks
|
|
222
|
+
futures = [
|
|
223
|
+
submit_task(exe_state) for exe_state in self.executables_with_state
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
# Wait for completion
|
|
227
|
+
self._completion_event.wait()
|
|
228
|
+
|
|
229
|
+
# Cancel remaining futures so
|
|
230
|
+
# that we don't wait for them to join.
|
|
231
|
+
for future in futures:
|
|
232
|
+
future.cancel()
|
|
233
|
+
|
|
234
|
+
# Suspend execution if everything done and at least one of the tasks raised a suspend exception.
|
|
235
|
+
if self._suspend_exception:
|
|
236
|
+
raise self._suspend_exception
|
|
237
|
+
|
|
238
|
+
# Build final result
|
|
239
|
+
return self._create_result()
|
|
240
|
+
|
|
241
|
+
def should_execution_suspend(self) -> SuspendResult:
|
|
242
|
+
"""Check if execution should suspend."""
|
|
243
|
+
earliest_timestamp: float = float("inf")
|
|
244
|
+
indefinite_suspend_task: (
|
|
245
|
+
ExecutableWithState[CallableType, ResultType] | None
|
|
246
|
+
) = None
|
|
247
|
+
|
|
248
|
+
for exe_state in self.executables_with_state:
|
|
249
|
+
if exe_state.status in {BranchStatus.PENDING, BranchStatus.RUNNING}:
|
|
250
|
+
# Exit here! Still have tasks that can make progress, don't suspend.
|
|
251
|
+
return SuspendResult.do_not_suspend()
|
|
252
|
+
if exe_state.status is BranchStatus.SUSPENDED_WITH_TIMEOUT:
|
|
253
|
+
if (
|
|
254
|
+
exe_state.suspend_until
|
|
255
|
+
and exe_state.suspend_until < earliest_timestamp
|
|
256
|
+
):
|
|
257
|
+
earliest_timestamp = exe_state.suspend_until
|
|
258
|
+
elif exe_state.status is BranchStatus.SUSPENDED:
|
|
259
|
+
indefinite_suspend_task = exe_state
|
|
260
|
+
|
|
261
|
+
# All tasks are in final states and at least one of them is a suspend.
|
|
262
|
+
if earliest_timestamp != float("inf"):
|
|
263
|
+
return SuspendResult.suspend(
|
|
264
|
+
TimedSuspendExecution(
|
|
265
|
+
"All concurrent work complete or suspended pending retry.",
|
|
266
|
+
earliest_timestamp,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
if indefinite_suspend_task:
|
|
270
|
+
return SuspendResult.suspend(
|
|
271
|
+
SuspendExecution(
|
|
272
|
+
"All concurrent work complete or suspended and pending external callback."
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return SuspendResult.do_not_suspend()
|
|
277
|
+
|
|
278
|
+
def _on_task_complete(
|
|
279
|
+
self,
|
|
280
|
+
exe_state: ExecutableWithState,
|
|
281
|
+
future: Future,
|
|
282
|
+
scheduler: TimerScheduler,
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Handle task completion, suspension, or failure."""
|
|
285
|
+
|
|
286
|
+
if future.cancelled():
|
|
287
|
+
exe_state.suspend()
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
result = future.result()
|
|
292
|
+
exe_state.complete(result)
|
|
293
|
+
self.counters.complete_task()
|
|
294
|
+
except TimedSuspendExecution as tse:
|
|
295
|
+
exe_state.suspend_with_timeout(tse.scheduled_timestamp)
|
|
296
|
+
scheduler.schedule_resume(exe_state, tse.scheduled_timestamp)
|
|
297
|
+
except SuspendExecution:
|
|
298
|
+
exe_state.suspend()
|
|
299
|
+
# For indefinite suspend, don't schedule resume
|
|
300
|
+
except Exception as e: # noqa: BLE001
|
|
301
|
+
exe_state.fail(e)
|
|
302
|
+
self.counters.fail_task()
|
|
303
|
+
|
|
304
|
+
# Check if execution should complete or suspend
|
|
305
|
+
if self.counters.should_complete():
|
|
306
|
+
self._completion_event.set()
|
|
307
|
+
else:
|
|
308
|
+
suspend_result = self.should_execution_suspend()
|
|
309
|
+
if suspend_result.should_suspend:
|
|
310
|
+
self._suspend_exception = suspend_result.exception
|
|
311
|
+
self._completion_event.set()
|
|
312
|
+
|
|
313
|
+
def _create_result(self) -> BatchResult[ResultType]:
|
|
314
|
+
"""
|
|
315
|
+
Build the final BatchResult.
|
|
316
|
+
|
|
317
|
+
When this function executes, we've terminated the upper/parent context for whatever reason.
|
|
318
|
+
It follows that our items can be only in 3 states, Completed, Failed and Started (in all of the possible forms).
|
|
319
|
+
We tag each branch based on its observed value at the time of completion of the parent / upper context, and pass the
|
|
320
|
+
results to BatchResult.
|
|
321
|
+
|
|
322
|
+
Any inference wrt completion reason is left up to BatchResult, keeping the logic inference isolated.
|
|
323
|
+
"""
|
|
324
|
+
batch_items: list[BatchItem[ResultType]] = []
|
|
325
|
+
for executable in self.executables_with_state:
|
|
326
|
+
match executable.status:
|
|
327
|
+
case BranchStatus.COMPLETED:
|
|
328
|
+
batch_items.append(
|
|
329
|
+
BatchItem(
|
|
330
|
+
executable.index,
|
|
331
|
+
BatchItemStatus.SUCCEEDED,
|
|
332
|
+
executable.result,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
case BranchStatus.FAILED:
|
|
336
|
+
batch_items.append(
|
|
337
|
+
BatchItem(
|
|
338
|
+
executable.index,
|
|
339
|
+
BatchItemStatus.FAILED,
|
|
340
|
+
error=ErrorObject.from_exception(executable.error),
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
case (
|
|
344
|
+
BranchStatus.PENDING
|
|
345
|
+
| BranchStatus.RUNNING
|
|
346
|
+
| BranchStatus.SUSPENDED
|
|
347
|
+
| BranchStatus.SUSPENDED_WITH_TIMEOUT
|
|
348
|
+
):
|
|
349
|
+
batch_items.append(
|
|
350
|
+
BatchItem(executable.index, BatchItemStatus.STARTED)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
return BatchResult.from_items(batch_items, self.completion_config)
|
|
354
|
+
|
|
355
|
+
def _execute_item_in_child_context(
|
|
356
|
+
self,
|
|
357
|
+
executor_context: DurableContext,
|
|
358
|
+
executable: Executable[CallableType],
|
|
359
|
+
) -> ResultType:
|
|
360
|
+
"""
|
|
361
|
+
Execute a single item in a derived child context.
|
|
362
|
+
|
|
363
|
+
instead of relying on `executor_context.run_in_child_context`
|
|
364
|
+
we generate an operation_id for the child, and then call `child_handler`
|
|
365
|
+
directly. This avoids the hidden mutation of the context's internal counter.
|
|
366
|
+
we can do this because we explicitly control the generation of step_id and do it
|
|
367
|
+
using executable.index.
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
invariant: `operation_id` for a given executable is deterministic,
|
|
371
|
+
and execution order invariant.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
operation_id = executor_context._create_step_id_for_logical_step( # noqa: SLF001
|
|
375
|
+
executable.index
|
|
376
|
+
)
|
|
377
|
+
name = f"{self.name_prefix}{executable.index}"
|
|
378
|
+
child_context = executor_context.create_child_context(operation_id)
|
|
379
|
+
operation_identifier = OperationIdentifier(
|
|
380
|
+
operation_id,
|
|
381
|
+
executor_context._parent_id, # noqa: SLF001
|
|
382
|
+
name,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
def run_in_child_handler():
|
|
386
|
+
return self.execute_item(child_context, executable)
|
|
387
|
+
|
|
388
|
+
result: ResultType = child_handler(
|
|
389
|
+
run_in_child_handler,
|
|
390
|
+
child_context.state,
|
|
391
|
+
operation_identifier=operation_identifier,
|
|
392
|
+
config=ChildConfig(
|
|
393
|
+
serdes=self.item_serdes or self.serdes,
|
|
394
|
+
sub_type=self.sub_type_iteration,
|
|
395
|
+
summary_generator=self.summary_generator,
|
|
396
|
+
),
|
|
397
|
+
)
|
|
398
|
+
child_context.state.track_replay(operation_id=operation_id)
|
|
399
|
+
return result
|
|
400
|
+
|
|
401
|
+
def replay(self, execution_state: ExecutionState, executor_context: DurableContext):
|
|
402
|
+
"""
|
|
403
|
+
Replay rather than re-run children.
|
|
404
|
+
|
|
405
|
+
if we are here, then we are in replay_children.
|
|
406
|
+
This will pre-generate all the operation ids for the children and collect the checkpointed
|
|
407
|
+
results.
|
|
408
|
+
"""
|
|
409
|
+
items: list[BatchItem[ResultType]] = []
|
|
410
|
+
for executable in self.executables:
|
|
411
|
+
operation_id = executor_context._create_step_id_for_logical_step( # noqa: SLF001
|
|
412
|
+
executable.index
|
|
413
|
+
)
|
|
414
|
+
checkpoint = execution_state.get_checkpoint_result(operation_id)
|
|
415
|
+
|
|
416
|
+
result: ResultType | None = None
|
|
417
|
+
error = None
|
|
418
|
+
status: BatchItemStatus
|
|
419
|
+
if checkpoint.is_succeeded():
|
|
420
|
+
status = BatchItemStatus.SUCCEEDED
|
|
421
|
+
result = self._execute_item_in_child_context(
|
|
422
|
+
executor_context, executable
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
elif checkpoint.is_failed():
|
|
426
|
+
error = checkpoint.error
|
|
427
|
+
status = BatchItemStatus.FAILED
|
|
428
|
+
else:
|
|
429
|
+
status = BatchItemStatus.STARTED
|
|
430
|
+
|
|
431
|
+
batch_item = BatchItem(executable.index, status, result=result, error=error)
|
|
432
|
+
items.append(batch_item)
|
|
433
|
+
return BatchResult.from_items(items, self.completion_config)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# endregion concurrency logic
|