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,374 @@
|
|
|
1
|
+
"""Exceptions for the Durable Executions SDK.
|
|
2
|
+
|
|
3
|
+
Avoid any non-stdlib references in this module, it is at the bottom of the dependency chain.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import TYPE_CHECKING, Self, TypedDict
|
|
12
|
+
|
|
13
|
+
BAD_REQUEST_ERROR: int = 400
|
|
14
|
+
TOO_MANY_REQUESTS_ERROR: int = 429
|
|
15
|
+
SERVICE_ERROR: int = 500
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
import datetime
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AwsErrorObj(TypedDict):
|
|
22
|
+
Code: str | None
|
|
23
|
+
Message: str | None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AwsErrorMetadata(TypedDict):
|
|
27
|
+
RequestId: str | None
|
|
28
|
+
HostId: str | None
|
|
29
|
+
HTTPStatusCode: int | None
|
|
30
|
+
HTTPHeaders: str | None
|
|
31
|
+
RetryAttempts: str | None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TerminationReason(Enum):
|
|
35
|
+
"""Reasons why a durable execution terminated."""
|
|
36
|
+
|
|
37
|
+
UNHANDLED_ERROR = "UNHANDLED_ERROR"
|
|
38
|
+
INVOCATION_ERROR = "INVOCATION_ERROR"
|
|
39
|
+
EXECUTION_ERROR = "EXECUTION_ERROR"
|
|
40
|
+
CHECKPOINT_FAILED = "CHECKPOINT_FAILED"
|
|
41
|
+
NON_DETERMINISTIC_EXECUTION = "NON_DETERMINISTIC_EXECUTION"
|
|
42
|
+
STEP_INTERRUPTED = "STEP_INTERRUPTED"
|
|
43
|
+
CALLBACK_ERROR = "CALLBACK_ERROR"
|
|
44
|
+
SERIALIZATION_ERROR = "SERIALIZATION_ERROR"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DurableExecutionsError(Exception):
|
|
48
|
+
"""Base class for Durable Executions exceptions"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UnrecoverableError(DurableExecutionsError):
|
|
52
|
+
"""Base class for errors that terminate execution."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, message: str, termination_reason: TerminationReason):
|
|
55
|
+
super().__init__(message)
|
|
56
|
+
self.termination_reason = termination_reason
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ExecutionError(UnrecoverableError):
|
|
60
|
+
"""Error that returns FAILED status without retry."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
message: str,
|
|
65
|
+
termination_reason: TerminationReason = TerminationReason.EXECUTION_ERROR,
|
|
66
|
+
):
|
|
67
|
+
super().__init__(message, termination_reason)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class InvocationError(UnrecoverableError):
|
|
71
|
+
"""Error that should cause Lambda retry by throwing from handler."""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
message: str,
|
|
76
|
+
termination_reason: TerminationReason = TerminationReason.INVOCATION_ERROR,
|
|
77
|
+
):
|
|
78
|
+
super().__init__(message, termination_reason)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CallbackError(ExecutionError):
|
|
82
|
+
"""Error in callback handling."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, message: str, callback_id: str | None = None):
|
|
85
|
+
super().__init__(message, TerminationReason.CALLBACK_ERROR)
|
|
86
|
+
self.callback_id = callback_id
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class BotoClientError(InvocationError):
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
message: str,
|
|
93
|
+
error: AwsErrorObj | None = None,
|
|
94
|
+
response_metadata: AwsErrorMetadata | None = None,
|
|
95
|
+
termination_reason=TerminationReason.INVOCATION_ERROR,
|
|
96
|
+
):
|
|
97
|
+
super().__init__(message=message, termination_reason=termination_reason)
|
|
98
|
+
self.error: AwsErrorObj | None = error
|
|
99
|
+
self.response_metadata: AwsErrorMetadata | None = response_metadata
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_exception(cls, exception: Exception) -> Self:
|
|
103
|
+
response = getattr(exception, "response", {})
|
|
104
|
+
response_metadata = response.get("ResponseMetadata")
|
|
105
|
+
error = response.get("Error")
|
|
106
|
+
return cls(
|
|
107
|
+
message=str(exception), error=error, response_metadata=response_metadata
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def build_logger_extras(self) -> dict:
|
|
111
|
+
extras: dict = {}
|
|
112
|
+
# preserve PascalCase to be consistent with other langauges
|
|
113
|
+
if error := self.error:
|
|
114
|
+
extras["Error"] = error
|
|
115
|
+
if response_metadata := self.response_metadata:
|
|
116
|
+
extras["ResponseMetadata"] = response_metadata
|
|
117
|
+
return extras
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class NonDeterministicExecutionError(ExecutionError):
|
|
121
|
+
"""Error when execution is non-deterministic."""
|
|
122
|
+
|
|
123
|
+
def __init__(self, message: str, step_id: str | None = None):
|
|
124
|
+
super().__init__(message, TerminationReason.NON_DETERMINISTIC_EXECUTION)
|
|
125
|
+
self.step_id = step_id
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class CheckpointErrorCategory(Enum):
|
|
129
|
+
INVOCATION = "INVOCATION"
|
|
130
|
+
EXECUTION = "EXECUTION"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class CheckpointError(BotoClientError):
|
|
134
|
+
"""Failure to checkpoint. Will terminate the lambda."""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
message: str,
|
|
139
|
+
error_category: CheckpointErrorCategory,
|
|
140
|
+
error: AwsErrorObj | None = None,
|
|
141
|
+
response_metadata: AwsErrorMetadata | None = None,
|
|
142
|
+
):
|
|
143
|
+
super().__init__(
|
|
144
|
+
message,
|
|
145
|
+
error,
|
|
146
|
+
response_metadata,
|
|
147
|
+
termination_reason=TerminationReason.CHECKPOINT_FAILED,
|
|
148
|
+
)
|
|
149
|
+
self.error_category: CheckpointErrorCategory = error_category
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def from_exception(cls, exception: Exception) -> CheckpointError:
|
|
153
|
+
base = BotoClientError.from_exception(exception)
|
|
154
|
+
metadata: AwsErrorMetadata | None = base.response_metadata
|
|
155
|
+
error: AwsErrorObj | None = base.error
|
|
156
|
+
error_category: CheckpointErrorCategory = CheckpointErrorCategory.INVOCATION
|
|
157
|
+
|
|
158
|
+
# InvalidParameterValueException and error message starts with "Invalid Checkpoint Token" is an InvocationError
|
|
159
|
+
# all other 4xx errors are Execution Errors and should be retried
|
|
160
|
+
# all 5xx errors are Invocation Errors
|
|
161
|
+
status_code: int | None = (metadata and metadata.get("HTTPStatusCode")) or None
|
|
162
|
+
if (
|
|
163
|
+
status_code
|
|
164
|
+
# if we are in 4xx range (except 429) and is not an InvalidParameterValueException with Invalid Checkpoint Token
|
|
165
|
+
# then it's an execution error
|
|
166
|
+
and status_code < SERVICE_ERROR
|
|
167
|
+
and status_code >= BAD_REQUEST_ERROR
|
|
168
|
+
and status_code != TOO_MANY_REQUESTS_ERROR
|
|
169
|
+
and error
|
|
170
|
+
and (
|
|
171
|
+
# is not InvalidParam => Execution
|
|
172
|
+
(error.get("Code", "") or "") != "InvalidParameterValueException"
|
|
173
|
+
# is not Invalid Token => Execution
|
|
174
|
+
or not (error.get("Message") or "").startswith(
|
|
175
|
+
"Invalid Checkpoint Token"
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
):
|
|
179
|
+
error_category = CheckpointErrorCategory.EXECUTION
|
|
180
|
+
return CheckpointError(str(exception), error_category, error, metadata)
|
|
181
|
+
|
|
182
|
+
def is_retriable(self):
|
|
183
|
+
return self.error_category == CheckpointErrorCategory.EXECUTION
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ValidationError(DurableExecutionsError):
|
|
187
|
+
"""Incorrect arguments to a Durable Function operation."""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class GetExecutionStateError(BotoClientError):
|
|
191
|
+
"""Raised when failing to retrieve execution state"""
|
|
192
|
+
|
|
193
|
+
def __init__(
|
|
194
|
+
self,
|
|
195
|
+
message: str,
|
|
196
|
+
error: AwsErrorObj | None = None,
|
|
197
|
+
response_metadata: AwsErrorMetadata | None = None,
|
|
198
|
+
):
|
|
199
|
+
super().__init__(
|
|
200
|
+
message,
|
|
201
|
+
error,
|
|
202
|
+
response_metadata,
|
|
203
|
+
termination_reason=TerminationReason.INVOCATION_ERROR,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class InvalidStateError(DurableExecutionsError):
|
|
208
|
+
"""Raised when an operation is attempted on an object in an invalid state."""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class UserlandError(DurableExecutionsError):
|
|
212
|
+
"""Failure in user-land - i.e code passed into durable executions from the caller."""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class CallableRuntimeError(UserlandError):
|
|
216
|
+
"""This error wraps any failure from inside the callable code that you pass to a Durable Function operation."""
|
|
217
|
+
|
|
218
|
+
def __init__(
|
|
219
|
+
self,
|
|
220
|
+
message: str | None,
|
|
221
|
+
error_type: str | None,
|
|
222
|
+
data: str | None,
|
|
223
|
+
stack_trace: list[str] | None,
|
|
224
|
+
) -> None:
|
|
225
|
+
super().__init__(message)
|
|
226
|
+
self.message = message
|
|
227
|
+
self.error_type = error_type
|
|
228
|
+
self.data = data
|
|
229
|
+
self.stack_trace = stack_trace
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class StepInterruptedError(InvocationError):
|
|
233
|
+
"""Raised when a step is interrupted before it checkpointed at the end."""
|
|
234
|
+
|
|
235
|
+
def __init__(self, message: str, step_id: str | None = None):
|
|
236
|
+
super().__init__(message, TerminationReason.STEP_INTERRUPTED)
|
|
237
|
+
self.step_id = step_id
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class BackgroundThreadError(BaseException):
|
|
241
|
+
"""Critical error from background checkpoint thread.
|
|
242
|
+
|
|
243
|
+
Derives from BaseException to bypass normal exception handlers.
|
|
244
|
+
Similar to KeyboardInterrupt or SystemExit - this is a system-level
|
|
245
|
+
error that should terminate execution immediately without attempting
|
|
246
|
+
to checkpoint or process the error.
|
|
247
|
+
|
|
248
|
+
This exception is raised in the user thread when the background
|
|
249
|
+
checkpoint processing thread encounters a fatal error. It propagates
|
|
250
|
+
through CompletionEvent.wait() to interrupt blocked user code.
|
|
251
|
+
|
|
252
|
+
Attributes:
|
|
253
|
+
source_exception: The original exception from the background thread
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
def __init__(self, message: str, source_exception: Exception):
|
|
257
|
+
super().__init__(message)
|
|
258
|
+
self.source_exception = source_exception
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class SuspendExecution(BaseException):
|
|
262
|
+
"""Raise this exception to suspend the current execution by returning PENDING to DAR.
|
|
263
|
+
|
|
264
|
+
Note this derives from BaseException - in keeping with system-exiting exceptions like
|
|
265
|
+
KeyboardInterrupt or SystemExit.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def __init__(self, message: str):
|
|
269
|
+
super().__init__(message)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class TimedSuspendExecution(SuspendExecution):
|
|
273
|
+
"""Suspend execution until a specific timestamp.
|
|
274
|
+
|
|
275
|
+
This is a specialized form of SuspendExecution that includes a scheduled resume time.
|
|
276
|
+
|
|
277
|
+
Attributes:
|
|
278
|
+
scheduled_timestamp (float): Unix timestamp in seconds at which to resume.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
def __init__(self, message: str, scheduled_timestamp: float):
|
|
282
|
+
super().__init__(message)
|
|
283
|
+
self.scheduled_timestamp = scheduled_timestamp
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def from_delay(cls, message: str, delay_seconds: int) -> TimedSuspendExecution:
|
|
287
|
+
"""Create a timed suspension with the delay calculated from now.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
message: Descriptive message for the suspension
|
|
291
|
+
delay_seconds: Duration to suspend in seconds from current time
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
TimedSuspendExecution: Instance with calculated resume time
|
|
295
|
+
|
|
296
|
+
Example:
|
|
297
|
+
>>> exception = TimedSuspendExecution.from_delay("Waiting for callback", 30)
|
|
298
|
+
>>> # Will suspend for 30 seconds from now
|
|
299
|
+
"""
|
|
300
|
+
resume_time = time.time() + delay_seconds
|
|
301
|
+
return cls(message, scheduled_timestamp=resume_time)
|
|
302
|
+
|
|
303
|
+
@classmethod
|
|
304
|
+
def from_datetime(
|
|
305
|
+
cls, message: str, datetime_timestamp: datetime.datetime
|
|
306
|
+
) -> TimedSuspendExecution:
|
|
307
|
+
"""Create a timed suspension with the delay calculated from now.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
message: Descriptive message for the suspension
|
|
311
|
+
datetime_timestamp: Unix datetime timestamp in seconds at which to resume
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
TimedSuspendExecution: Instance with calculated resume time
|
|
315
|
+
"""
|
|
316
|
+
return cls(message, scheduled_timestamp=datetime_timestamp.timestamp())
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class OrderedLockError(DurableExecutionsError):
|
|
320
|
+
"""An error from OrderedLock.
|
|
321
|
+
|
|
322
|
+
Typically raised when a previous lock in the sequentially ordered chain of lock acquire requests failed.
|
|
323
|
+
|
|
324
|
+
Because of the order guarantee of OrderedLock, subsequent queued up lock acquire requests cannot proceed,
|
|
325
|
+
and will get this error instead.
|
|
326
|
+
|
|
327
|
+
Attributes:
|
|
328
|
+
source_exception (Exception): The exception that caused the lock to break.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def __init__(self, message: str, source_exception: Exception | None = None) -> None:
|
|
332
|
+
"""Initialize with the message and the exception source"""
|
|
333
|
+
msg = (
|
|
334
|
+
f"{message} {type(source_exception).__name__}: {source_exception}"
|
|
335
|
+
if source_exception
|
|
336
|
+
else message
|
|
337
|
+
)
|
|
338
|
+
super().__init__(msg)
|
|
339
|
+
self.source_exception: Exception | None = source_exception
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass(frozen=True)
|
|
343
|
+
class CallableRuntimeErrorSerializableDetails:
|
|
344
|
+
"""Serializable error details."""
|
|
345
|
+
|
|
346
|
+
type: str
|
|
347
|
+
message: str
|
|
348
|
+
|
|
349
|
+
@classmethod
|
|
350
|
+
def from_exception(
|
|
351
|
+
cls, exception: Exception
|
|
352
|
+
) -> CallableRuntimeErrorSerializableDetails:
|
|
353
|
+
"""Create an instance from an Exception, using its type and message.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
exception: An Exception instance
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
A CallableRuntimeErrorDetails instance with the exception's type name and message
|
|
360
|
+
"""
|
|
361
|
+
return cls(type=exception.__class__.__name__, message=str(exception))
|
|
362
|
+
|
|
363
|
+
def __str__(self) -> str:
|
|
364
|
+
"""
|
|
365
|
+
Return a string representation of the object.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
A string in the format "type: message"
|
|
369
|
+
"""
|
|
370
|
+
return f"{self.type}: {self.message}"
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class SerDesError(DurableExecutionsError):
|
|
374
|
+
"""Raised when serialization fails."""
|