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
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import functools
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from aws_durable_execution_sdk_python.context import DurableContext
|
|
13
|
+
from aws_durable_execution_sdk_python.exceptions import (
|
|
14
|
+
BackgroundThreadError,
|
|
15
|
+
BotoClientError,
|
|
16
|
+
CheckpointError,
|
|
17
|
+
DurableExecutionsError,
|
|
18
|
+
ExecutionError,
|
|
19
|
+
InvocationError,
|
|
20
|
+
SuspendExecution,
|
|
21
|
+
)
|
|
22
|
+
from aws_durable_execution_sdk_python.lambda_service import (
|
|
23
|
+
DurableServiceClient,
|
|
24
|
+
ErrorObject,
|
|
25
|
+
LambdaClient,
|
|
26
|
+
Operation,
|
|
27
|
+
OperationType,
|
|
28
|
+
OperationUpdate,
|
|
29
|
+
)
|
|
30
|
+
from aws_durable_execution_sdk_python.state import ExecutionState, ReplayStatus
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Callable, MutableMapping
|
|
34
|
+
|
|
35
|
+
import boto3 # type: ignore
|
|
36
|
+
|
|
37
|
+
from aws_durable_execution_sdk_python.types import LambdaContext
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
# 6MB in bytes, minus 50 bytes for envelope
|
|
43
|
+
LAMBDA_RESPONSE_SIZE_LIMIT = 6 * 1024 * 1024 - 50
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# region Invocation models
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class InitialExecutionState:
|
|
49
|
+
operations: list[Operation]
|
|
50
|
+
next_marker: str
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def from_dict(input_dict: MutableMapping[str, Any]) -> InitialExecutionState:
|
|
54
|
+
operations = []
|
|
55
|
+
if input_operations := input_dict.get("Operations"):
|
|
56
|
+
operations = [Operation.from_dict(op) for op in input_operations]
|
|
57
|
+
return InitialExecutionState(
|
|
58
|
+
operations=operations,
|
|
59
|
+
next_marker=input_dict.get("NextMarker", ""),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def get_execution_operation(self) -> Operation | None:
|
|
63
|
+
if not self.operations:
|
|
64
|
+
# Due to payload size limitations we may have an empty operations list.
|
|
65
|
+
# This will only happen when loading the initial page of results and is
|
|
66
|
+
# expected behaviour. We don't fail, but instead return None
|
|
67
|
+
# as the execution operation does not exist
|
|
68
|
+
msg: str = "No durable operations found in initial execution state."
|
|
69
|
+
logger.debug(msg)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
candidate = self.operations[0]
|
|
73
|
+
if candidate.operation_type is not OperationType.EXECUTION:
|
|
74
|
+
msg = f"First operation in initial execution state is not an execution operation: {candidate.operation_type}"
|
|
75
|
+
raise DurableExecutionsError(msg)
|
|
76
|
+
|
|
77
|
+
return candidate
|
|
78
|
+
|
|
79
|
+
def get_input_payload(self) -> str | None:
|
|
80
|
+
# It is possible that backend will not provide an execution operation
|
|
81
|
+
# for the initial page of results.
|
|
82
|
+
if not (operations := self.get_execution_operation()):
|
|
83
|
+
return None
|
|
84
|
+
if not (execution_details := operations.execution_details):
|
|
85
|
+
return None
|
|
86
|
+
return execution_details.input_payload
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> MutableMapping[str, Any]:
|
|
89
|
+
return {
|
|
90
|
+
"Operations": [op.to_dict() for op in self.operations],
|
|
91
|
+
"NextMarker": self.next_marker,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class DurableExecutionInvocationInput:
|
|
97
|
+
durable_execution_arn: str
|
|
98
|
+
checkpoint_token: str
|
|
99
|
+
initial_execution_state: InitialExecutionState
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def from_dict(
|
|
103
|
+
input_dict: MutableMapping[str, Any],
|
|
104
|
+
) -> DurableExecutionInvocationInput:
|
|
105
|
+
return DurableExecutionInvocationInput(
|
|
106
|
+
durable_execution_arn=input_dict["DurableExecutionArn"],
|
|
107
|
+
checkpoint_token=input_dict["CheckpointToken"],
|
|
108
|
+
initial_execution_state=InitialExecutionState.from_dict(
|
|
109
|
+
input_dict.get("InitialExecutionState", {})
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def to_dict(self) -> MutableMapping[str, Any]:
|
|
114
|
+
return {
|
|
115
|
+
"DurableExecutionArn": self.durable_execution_arn,
|
|
116
|
+
"CheckpointToken": self.checkpoint_token,
|
|
117
|
+
"InitialExecutionState": self.initial_execution_state.to_dict(),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class DurableExecutionInvocationInputWithClient(DurableExecutionInvocationInput):
|
|
123
|
+
"""Invocation input with Lambda boto client injected.
|
|
124
|
+
|
|
125
|
+
This is useful for testing scenarios where you want to inject a mock client.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
service_client: DurableServiceClient
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def from_durable_execution_invocation_input(
|
|
132
|
+
invocation_input: DurableExecutionInvocationInput,
|
|
133
|
+
service_client: DurableServiceClient,
|
|
134
|
+
):
|
|
135
|
+
return DurableExecutionInvocationInputWithClient(
|
|
136
|
+
durable_execution_arn=invocation_input.durable_execution_arn,
|
|
137
|
+
checkpoint_token=invocation_input.checkpoint_token,
|
|
138
|
+
initial_execution_state=invocation_input.initial_execution_state,
|
|
139
|
+
service_client=service_client,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class InvocationStatus(Enum):
|
|
144
|
+
SUCCEEDED = "SUCCEEDED"
|
|
145
|
+
FAILED = "FAILED"
|
|
146
|
+
PENDING = "PENDING"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True)
|
|
150
|
+
class DurableExecutionInvocationOutput:
|
|
151
|
+
"""Representation the DurableExecutionInvocationOutput. This is what the Durable lambda handler returns.
|
|
152
|
+
|
|
153
|
+
If the execution has been already completed via an update to the EXECUTION operation via CheckpointDurableExecution,
|
|
154
|
+
payload must be empty for SUCCEEDED/FAILED status.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
status: InvocationStatus
|
|
158
|
+
result: str | None = None
|
|
159
|
+
error: ErrorObject | None = None
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def from_dict(
|
|
163
|
+
cls, data: MutableMapping[str, Any]
|
|
164
|
+
) -> DurableExecutionInvocationOutput:
|
|
165
|
+
"""Create an instance from a dictionary.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
data: Dictionary with camelCase keys matching the original structure
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
A DurableExecutionInvocationOutput instance
|
|
172
|
+
"""
|
|
173
|
+
status = InvocationStatus(data.get("Status"))
|
|
174
|
+
error = ErrorObject.from_dict(data["Error"]) if data.get("Error") else None
|
|
175
|
+
return cls(status=status, result=data.get("Result"), error=error)
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> MutableMapping[str, Any]:
|
|
178
|
+
"""Convert to a dictionary with the original field names.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Dictionary with the original camelCase keys
|
|
182
|
+
"""
|
|
183
|
+
result: MutableMapping[str, Any] = {"Status": self.status.value}
|
|
184
|
+
|
|
185
|
+
if self.result is not None:
|
|
186
|
+
# large payloads return "", because checkpointed already
|
|
187
|
+
result["Result"] = self.result
|
|
188
|
+
if self.error:
|
|
189
|
+
result["Error"] = self.error.to_dict()
|
|
190
|
+
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def create_succeeded(cls, result: str) -> DurableExecutionInvocationOutput:
|
|
195
|
+
"""Create a succeeded invocation output."""
|
|
196
|
+
return cls(status=InvocationStatus.SUCCEEDED, result=result)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# endregion Invocation models
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def durable_execution(
|
|
203
|
+
func: Callable[[Any, DurableContext], Any] | None = None,
|
|
204
|
+
*,
|
|
205
|
+
boto3_client: boto3.client | None = None,
|
|
206
|
+
) -> Callable[[Any, LambdaContext], Any]:
|
|
207
|
+
# Decorator called with parameters
|
|
208
|
+
if func is None:
|
|
209
|
+
logger.debug("Decorator called with parameters")
|
|
210
|
+
return functools.partial(durable_execution, boto3_client=boto3_client)
|
|
211
|
+
|
|
212
|
+
logger.debug("Starting durable execution handler...")
|
|
213
|
+
|
|
214
|
+
def wrapper(event: Any, context: LambdaContext) -> MutableMapping[str, Any]:
|
|
215
|
+
invocation_input: DurableExecutionInvocationInput
|
|
216
|
+
service_client: DurableServiceClient
|
|
217
|
+
|
|
218
|
+
# event likely only to be DurableExecutionInvocationInputWithClient when directly injected by test framework
|
|
219
|
+
if isinstance(event, DurableExecutionInvocationInputWithClient):
|
|
220
|
+
logger.debug("durableExecutionArn: %s", event.durable_execution_arn)
|
|
221
|
+
invocation_input = event
|
|
222
|
+
service_client = invocation_input.service_client
|
|
223
|
+
else:
|
|
224
|
+
try:
|
|
225
|
+
logger.debug(
|
|
226
|
+
"durableExecutionArn: %s", event.get("DurableExecutionArn")
|
|
227
|
+
)
|
|
228
|
+
invocation_input = DurableExecutionInvocationInput.from_dict(event)
|
|
229
|
+
except (KeyError, TypeError, AttributeError) as e:
|
|
230
|
+
msg = (
|
|
231
|
+
"Unexpected payload provided to start the durable execution. "
|
|
232
|
+
"Check your resource configurations to confirm the durability is set."
|
|
233
|
+
)
|
|
234
|
+
raise ExecutionError(msg) from e
|
|
235
|
+
|
|
236
|
+
# Use custom client if provided, otherwise initialize from environment
|
|
237
|
+
service_client = (
|
|
238
|
+
LambdaClient(client=boto3_client)
|
|
239
|
+
if boto3_client is not None
|
|
240
|
+
else LambdaClient.initialize_from_env()
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
raw_input_payload: str | None = (
|
|
244
|
+
invocation_input.initial_execution_state.get_input_payload()
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Python RIC LambdaMarshaller just uses standard json deserialization for event
|
|
248
|
+
# https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/main/awslambdaric/lambda_runtime_marshaller.py#L46
|
|
249
|
+
input_event: MutableMapping[str, Any] = {}
|
|
250
|
+
if raw_input_payload and raw_input_payload.strip():
|
|
251
|
+
try:
|
|
252
|
+
input_event = json.loads(raw_input_payload)
|
|
253
|
+
except json.JSONDecodeError:
|
|
254
|
+
logger.exception(
|
|
255
|
+
"Failed to parse input payload as JSON: payload: %r",
|
|
256
|
+
raw_input_payload,
|
|
257
|
+
)
|
|
258
|
+
raise
|
|
259
|
+
|
|
260
|
+
execution_state: ExecutionState = ExecutionState(
|
|
261
|
+
durable_execution_arn=invocation_input.durable_execution_arn,
|
|
262
|
+
initial_checkpoint_token=invocation_input.checkpoint_token,
|
|
263
|
+
operations={},
|
|
264
|
+
service_client=service_client,
|
|
265
|
+
# If there are operations other than the initial EXECUTION one, current state is in replay mode
|
|
266
|
+
replay_status=ReplayStatus.REPLAY
|
|
267
|
+
if len(invocation_input.initial_execution_state.operations) > 1
|
|
268
|
+
else ReplayStatus.NEW,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
execution_state.fetch_paginated_operations(
|
|
272
|
+
invocation_input.initial_execution_state.operations,
|
|
273
|
+
invocation_input.checkpoint_token,
|
|
274
|
+
invocation_input.initial_execution_state.next_marker,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
durable_context: DurableContext = DurableContext.from_lambda_context(
|
|
278
|
+
state=execution_state, lambda_context=context
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Use ThreadPoolExecutor for concurrent execution of user code and background checkpoint processing
|
|
282
|
+
with (
|
|
283
|
+
ThreadPoolExecutor(
|
|
284
|
+
max_workers=2, thread_name_prefix="dex-handler"
|
|
285
|
+
) as executor,
|
|
286
|
+
contextlib.closing(execution_state) as execution_state,
|
|
287
|
+
):
|
|
288
|
+
# Thread 1: Run background checkpoint processing
|
|
289
|
+
executor.submit(execution_state.checkpoint_batches_forever)
|
|
290
|
+
|
|
291
|
+
# Thread 2: Execute user function
|
|
292
|
+
logger.debug(
|
|
293
|
+
"%s entering user-space...", invocation_input.durable_execution_arn
|
|
294
|
+
)
|
|
295
|
+
user_future = executor.submit(func, input_event, durable_context)
|
|
296
|
+
|
|
297
|
+
logger.debug(
|
|
298
|
+
"%s waiting for user code completion...",
|
|
299
|
+
invocation_input.durable_execution_arn,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
# Background checkpointing errors will propagate through CompletionEvent.wait() as BackgroundThreadError
|
|
304
|
+
result = user_future.result()
|
|
305
|
+
|
|
306
|
+
# done with userland
|
|
307
|
+
logger.debug(
|
|
308
|
+
"%s exiting user-space...",
|
|
309
|
+
invocation_input.durable_execution_arn,
|
|
310
|
+
)
|
|
311
|
+
serialized_result = json.dumps(result)
|
|
312
|
+
# large response handling here. Remember if checkpointing to complete, NOT to include
|
|
313
|
+
# payload in response
|
|
314
|
+
if (
|
|
315
|
+
serialized_result
|
|
316
|
+
and len(serialized_result) > LAMBDA_RESPONSE_SIZE_LIMIT
|
|
317
|
+
):
|
|
318
|
+
logger.debug(
|
|
319
|
+
"Response size (%s bytes) exceeds Lambda limit (%s) bytes). Checkpointing result.",
|
|
320
|
+
len(serialized_result),
|
|
321
|
+
LAMBDA_RESPONSE_SIZE_LIMIT,
|
|
322
|
+
)
|
|
323
|
+
success_operation = OperationUpdate.create_execution_succeed(
|
|
324
|
+
payload=serialized_result
|
|
325
|
+
)
|
|
326
|
+
# Checkpoint large result with blocking (is_sync=True, default).
|
|
327
|
+
# Must ensure the result is persisted before returning to Lambda.
|
|
328
|
+
# Large results exceed Lambda response limits and must be stored durably
|
|
329
|
+
# before the execution completes.
|
|
330
|
+
try:
|
|
331
|
+
execution_state.create_checkpoint(
|
|
332
|
+
success_operation, is_sync=True
|
|
333
|
+
)
|
|
334
|
+
except CheckpointError as e:
|
|
335
|
+
return handle_checkpoint_error(e).to_dict()
|
|
336
|
+
return DurableExecutionInvocationOutput.create_succeeded(
|
|
337
|
+
result=""
|
|
338
|
+
).to_dict()
|
|
339
|
+
|
|
340
|
+
return DurableExecutionInvocationOutput.create_succeeded(
|
|
341
|
+
result=serialized_result
|
|
342
|
+
).to_dict()
|
|
343
|
+
|
|
344
|
+
except BackgroundThreadError as bg_error:
|
|
345
|
+
# Background checkpoint system failed - propagated through CompletionEvent
|
|
346
|
+
# Do not attempt to checkpoint anything, just terminate immediately
|
|
347
|
+
if isinstance(bg_error.source_exception, BotoClientError):
|
|
348
|
+
logger.exception(
|
|
349
|
+
"Checkpoint processing failed",
|
|
350
|
+
extra=bg_error.source_exception.build_logger_extras(),
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
logger.exception("Checkpoint processing failed")
|
|
354
|
+
# handle the original exception
|
|
355
|
+
if isinstance(bg_error.source_exception, CheckpointError):
|
|
356
|
+
return handle_checkpoint_error(bg_error.source_exception).to_dict()
|
|
357
|
+
raise bg_error.source_exception from bg_error
|
|
358
|
+
|
|
359
|
+
except SuspendExecution:
|
|
360
|
+
# User code suspended - stop background checkpointing thread
|
|
361
|
+
logger.debug("Suspending execution...")
|
|
362
|
+
return DurableExecutionInvocationOutput(
|
|
363
|
+
status=InvocationStatus.PENDING
|
|
364
|
+
).to_dict()
|
|
365
|
+
|
|
366
|
+
except CheckpointError as e:
|
|
367
|
+
# Checkpoint system is broken - stop background thread and exit immediately
|
|
368
|
+
logger.exception(
|
|
369
|
+
"Checkpoint system failed",
|
|
370
|
+
extra=e.build_logger_extras(),
|
|
371
|
+
)
|
|
372
|
+
return handle_checkpoint_error(e).to_dict()
|
|
373
|
+
except InvocationError:
|
|
374
|
+
logger.exception("Invocation error. Must terminate.")
|
|
375
|
+
# Throw the error to trigger Lambda retry
|
|
376
|
+
raise
|
|
377
|
+
except ExecutionError as e:
|
|
378
|
+
logger.exception("Execution error. Must terminate without retry.")
|
|
379
|
+
return DurableExecutionInvocationOutput(
|
|
380
|
+
status=InvocationStatus.FAILED,
|
|
381
|
+
error=ErrorObject.from_exception(e),
|
|
382
|
+
).to_dict()
|
|
383
|
+
except Exception as e:
|
|
384
|
+
# all user-space errors go here
|
|
385
|
+
logger.exception("Execution failed")
|
|
386
|
+
|
|
387
|
+
result = DurableExecutionInvocationOutput(
|
|
388
|
+
status=InvocationStatus.FAILED, error=ErrorObject.from_exception(e)
|
|
389
|
+
).to_dict()
|
|
390
|
+
|
|
391
|
+
serialized_result = json.dumps(result)
|
|
392
|
+
|
|
393
|
+
if (
|
|
394
|
+
serialized_result
|
|
395
|
+
and len(serialized_result) > LAMBDA_RESPONSE_SIZE_LIMIT
|
|
396
|
+
):
|
|
397
|
+
logger.debug(
|
|
398
|
+
"Response size (%s bytes) exceeds Lambda limit (%s) bytes). Checkpointing result.",
|
|
399
|
+
len(serialized_result),
|
|
400
|
+
LAMBDA_RESPONSE_SIZE_LIMIT,
|
|
401
|
+
)
|
|
402
|
+
failed_operation = OperationUpdate.create_execution_fail(
|
|
403
|
+
error=ErrorObject.from_exception(e)
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Checkpoint large result with blocking (is_sync=True, default).
|
|
407
|
+
# Must ensure the result is persisted before returning to Lambda.
|
|
408
|
+
# Large results exceed Lambda response limits and must be stored durably
|
|
409
|
+
# before the execution completes.
|
|
410
|
+
try:
|
|
411
|
+
execution_state.create_checkpoint_sync(failed_operation)
|
|
412
|
+
except CheckpointError as e:
|
|
413
|
+
return handle_checkpoint_error(e).to_dict()
|
|
414
|
+
return DurableExecutionInvocationOutput(
|
|
415
|
+
status=InvocationStatus.FAILED
|
|
416
|
+
).to_dict()
|
|
417
|
+
|
|
418
|
+
return result
|
|
419
|
+
|
|
420
|
+
return wrapper
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def handle_checkpoint_error(error: CheckpointError) -> DurableExecutionInvocationOutput:
|
|
424
|
+
if error.is_retriable():
|
|
425
|
+
raise error from None # Terminate Lambda immediately and have it be retried
|
|
426
|
+
return DurableExecutionInvocationOutput(
|
|
427
|
+
status=InvocationStatus.FAILED, error=ErrorObject.from_exception(error)
|
|
428
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Operation identifier types for durable executions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class OperationIdentifier:
|
|
10
|
+
"""Container for operation id, parent id, and name."""
|
|
11
|
+
|
|
12
|
+
operation_id: str
|
|
13
|
+
parent_id: str | None = None
|
|
14
|
+
name: str | None = None
|