async-durable-execution-runner 2.0.0a1__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.
- LICENSE +175 -0
- NOTICE +8 -0
- VERSION.py +5 -0
- async_durable_execution_runner/__about__.py +33 -0
- async_durable_execution_runner/__init__.py +23 -0
- async_durable_execution_runner/checkpoint/__init__.py +1 -0
- async_durable_execution_runner/checkpoint/processor.py +101 -0
- async_durable_execution_runner/checkpoint/processors/__init__.py +1 -0
- async_durable_execution_runner/checkpoint/processors/base.py +199 -0
- async_durable_execution_runner/checkpoint/processors/callback.py +89 -0
- async_durable_execution_runner/checkpoint/processors/context.py +59 -0
- async_durable_execution_runner/checkpoint/processors/execution.py +52 -0
- async_durable_execution_runner/checkpoint/processors/step.py +124 -0
- async_durable_execution_runner/checkpoint/processors/wait.py +95 -0
- async_durable_execution_runner/checkpoint/transformer.py +104 -0
- async_durable_execution_runner/checkpoint/validators/__init__.py +1 -0
- async_durable_execution_runner/checkpoint/validators/checkpoint.py +242 -0
- async_durable_execution_runner/checkpoint/validators/operations/__init__.py +1 -0
- async_durable_execution_runner/checkpoint/validators/operations/callback.py +45 -0
- async_durable_execution_runner/checkpoint/validators/operations/context.py +73 -0
- async_durable_execution_runner/checkpoint/validators/operations/execution.py +47 -0
- async_durable_execution_runner/checkpoint/validators/operations/invoke.py +56 -0
- async_durable_execution_runner/checkpoint/validators/operations/step.py +106 -0
- async_durable_execution_runner/checkpoint/validators/operations/wait.py +54 -0
- async_durable_execution_runner/checkpoint/validators/transitions.py +66 -0
- async_durable_execution_runner/cli.py +498 -0
- async_durable_execution_runner/client.py +50 -0
- async_durable_execution_runner/exceptions.py +288 -0
- async_durable_execution_runner/execution.py +444 -0
- async_durable_execution_runner/executor.py +1234 -0
- async_durable_execution_runner/invoker.py +340 -0
- async_durable_execution_runner/model.py +3296 -0
- async_durable_execution_runner/observer.py +144 -0
- async_durable_execution_runner/py.typed +1 -0
- async_durable_execution_runner/runner.py +1167 -0
- async_durable_execution_runner/scheduler.py +246 -0
- async_durable_execution_runner/stores/__init__.py +1 -0
- async_durable_execution_runner/stores/base.py +147 -0
- async_durable_execution_runner/stores/filesystem.py +79 -0
- async_durable_execution_runner/stores/memory.py +38 -0
- async_durable_execution_runner/stores/sqlite.py +273 -0
- async_durable_execution_runner/token.py +49 -0
- async_durable_execution_runner/web/__init__.py +1 -0
- async_durable_execution_runner/web/errors.py +8 -0
- async_durable_execution_runner/web/handlers.py +813 -0
- async_durable_execution_runner/web/models.py +266 -0
- async_durable_execution_runner/web/routes.py +692 -0
- async_durable_execution_runner/web/serialization.py +235 -0
- async_durable_execution_runner/web/server.py +243 -0
- async_durable_execution_runner-2.0.0a1.dist-info/METADATA +238 -0
- async_durable_execution_runner-2.0.0a1.dist-info/RECORD +55 -0
- async_durable_execution_runner-2.0.0a1.dist-info/WHEEL +4 -0
- async_durable_execution_runner-2.0.0a1.dist-info/entry_points.txt +2 -0
- async_durable_execution_runner-2.0.0a1.dist-info/licenses/LICENSE +175 -0
- async_durable_execution_runner-2.0.0a1.dist-info/licenses/NOTICE +1 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from threading import Lock
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from async_durable_execution.execution import (
|
|
11
|
+
DurableExecutionInvocationOutput,
|
|
12
|
+
InvocationStatus,
|
|
13
|
+
)
|
|
14
|
+
from async_durable_execution.lambda_service import (
|
|
15
|
+
ErrorObject,
|
|
16
|
+
ExecutionDetails,
|
|
17
|
+
Operation,
|
|
18
|
+
OperationStatus,
|
|
19
|
+
OperationType,
|
|
20
|
+
OperationUpdate,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from async_durable_execution_runner.exceptions import (
|
|
24
|
+
IllegalStateException,
|
|
25
|
+
InvalidParameterValueException,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Import AWS exceptions
|
|
29
|
+
from async_durable_execution_runner.model import (
|
|
30
|
+
InvocationCompletedDetails,
|
|
31
|
+
StartDurableExecutionInput,
|
|
32
|
+
)
|
|
33
|
+
from async_durable_execution_runner.token import (
|
|
34
|
+
CheckpointToken,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ExecutionStatus(Enum):
|
|
39
|
+
"""Execution status for API responses."""
|
|
40
|
+
|
|
41
|
+
RUNNING = "RUNNING"
|
|
42
|
+
SUCCEEDED = "SUCCEEDED"
|
|
43
|
+
FAILED = "FAILED"
|
|
44
|
+
STOPPED = "STOPPED"
|
|
45
|
+
TIMED_OUT = "TIMED_OUT"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Execution:
|
|
49
|
+
"""Execution state."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
durable_execution_arn: str,
|
|
54
|
+
start_input: StartDurableExecutionInput,
|
|
55
|
+
operations: list[Operation],
|
|
56
|
+
):
|
|
57
|
+
self.durable_execution_arn: str = durable_execution_arn
|
|
58
|
+
# operation is frozen, it won't mutate - no need to clone/deep-copy
|
|
59
|
+
self.start_input: StartDurableExecutionInput = start_input
|
|
60
|
+
self.operations: list[Operation] = operations
|
|
61
|
+
self.updates: list[OperationUpdate] = []
|
|
62
|
+
self.invocation_completions: list[InvocationCompletedDetails] = []
|
|
63
|
+
self.used_tokens: set[str] = set()
|
|
64
|
+
# TODO: this will need to persist/rehydrate depending on inmemory vs sqllite store
|
|
65
|
+
self._token_sequence: int = 0
|
|
66
|
+
self._state_lock: Lock = Lock()
|
|
67
|
+
self.is_complete: bool = False
|
|
68
|
+
self.result: DurableExecutionInvocationOutput | None = None
|
|
69
|
+
self.consecutive_failed_invocation_attempts: int = 0
|
|
70
|
+
self.close_status: ExecutionStatus | None = None
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def token_sequence(self) -> int:
|
|
74
|
+
"""Get current token sequence value."""
|
|
75
|
+
return self._token_sequence
|
|
76
|
+
|
|
77
|
+
def current_status(self) -> ExecutionStatus:
|
|
78
|
+
"""Get execution status."""
|
|
79
|
+
if not self.is_complete:
|
|
80
|
+
return ExecutionStatus.RUNNING
|
|
81
|
+
|
|
82
|
+
if not self.close_status:
|
|
83
|
+
msg: str = "close_status must be set when execution is complete"
|
|
84
|
+
raise IllegalStateException(msg)
|
|
85
|
+
|
|
86
|
+
return self.close_status
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def new(input: StartDurableExecutionInput) -> Execution: # noqa: A002
|
|
90
|
+
# make a nicer arn
|
|
91
|
+
# Pattern: arn:(aws[a-zA-Z-]*)?:lambda:[a-z]{2}(-gov)?-[a-z]+-\d{1}:\d{12}:durable-execution:[a-zA-Z0-9-_\.]+:[a-zA-Z0-9-_\.]+:[a-zA-Z0-9-_\.]+
|
|
92
|
+
# Example: arn:aws:lambda:us-east-1:123456789012:durable-execution:myDurableFunction:myDurableExecutionName:ce67da72-3701-4f83-9174-f4189d27b0a5
|
|
93
|
+
return Execution(
|
|
94
|
+
durable_execution_arn=str(uuid4())
|
|
95
|
+
+ "/"
|
|
96
|
+
+ (input.invocation_id or str(uuid4())),
|
|
97
|
+
start_input=input,
|
|
98
|
+
operations=[],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def to_json_dict(self) -> dict[str, Any]:
|
|
102
|
+
"""Serialize execution to JSON-serializable dictionary"""
|
|
103
|
+
return {
|
|
104
|
+
"DurableExecutionArn": self.durable_execution_arn,
|
|
105
|
+
"StartInput": self.start_input.to_dict(),
|
|
106
|
+
"Operations": [op.to_json_dict() for op in self.operations],
|
|
107
|
+
"Updates": [update.to_dict() for update in self.updates],
|
|
108
|
+
"InvocationCompletions": [
|
|
109
|
+
completion.to_json_dict() for completion in self.invocation_completions
|
|
110
|
+
],
|
|
111
|
+
"UsedTokens": list(self.used_tokens),
|
|
112
|
+
"TokenSequence": self._token_sequence,
|
|
113
|
+
"IsComplete": self.is_complete,
|
|
114
|
+
"Result": self.result.to_dict() if self.result else None,
|
|
115
|
+
"ConsecutiveFailedInvocationAttempts": self.consecutive_failed_invocation_attempts,
|
|
116
|
+
"CloseStatus": self.close_status.value if self.close_status else None,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_json_dict(cls, data: dict[str, Any]) -> Execution:
|
|
121
|
+
"""Deserialize execution from dictionary."""
|
|
122
|
+
# Reconstruct start_input
|
|
123
|
+
start_input = StartDurableExecutionInput.from_dict(data["StartInput"])
|
|
124
|
+
|
|
125
|
+
# Reconstruct operations
|
|
126
|
+
operations = [
|
|
127
|
+
Operation.from_json_dict(op_data) for op_data in data["Operations"]
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
# Create execution
|
|
131
|
+
execution = cls(
|
|
132
|
+
durable_execution_arn=data["DurableExecutionArn"],
|
|
133
|
+
start_input=start_input,
|
|
134
|
+
operations=operations,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Set additional fields
|
|
138
|
+
execution.updates = [
|
|
139
|
+
OperationUpdate.from_dict(update_data) for update_data in data["Updates"]
|
|
140
|
+
]
|
|
141
|
+
execution.invocation_completions = [
|
|
142
|
+
InvocationCompletedDetails.from_json_dict(item)
|
|
143
|
+
for item in data.get("InvocationCompletions", [])
|
|
144
|
+
]
|
|
145
|
+
execution.used_tokens = set(data["UsedTokens"])
|
|
146
|
+
execution._token_sequence = data["TokenSequence"] # noqa: SLF001
|
|
147
|
+
execution.is_complete = data["IsComplete"]
|
|
148
|
+
execution.result = (
|
|
149
|
+
DurableExecutionInvocationOutput.from_dict(data["Result"])
|
|
150
|
+
if data["Result"]
|
|
151
|
+
else None
|
|
152
|
+
)
|
|
153
|
+
execution.consecutive_failed_invocation_attempts = data[
|
|
154
|
+
"ConsecutiveFailedInvocationAttempts"
|
|
155
|
+
]
|
|
156
|
+
close_status_str = data.get("CloseStatus")
|
|
157
|
+
execution.close_status = (
|
|
158
|
+
ExecutionStatus(close_status_str) if close_status_str else None
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return execution
|
|
162
|
+
|
|
163
|
+
def start(self) -> None:
|
|
164
|
+
if self.start_input.invocation_id is None:
|
|
165
|
+
msg: str = "invocation_id is required"
|
|
166
|
+
raise InvalidParameterValueException(msg)
|
|
167
|
+
with self._state_lock:
|
|
168
|
+
self.operations.append(
|
|
169
|
+
Operation(
|
|
170
|
+
operation_id=self.start_input.invocation_id,
|
|
171
|
+
parent_id=None,
|
|
172
|
+
name=self.start_input.execution_name,
|
|
173
|
+
start_timestamp=datetime.now(UTC),
|
|
174
|
+
operation_type=OperationType.EXECUTION,
|
|
175
|
+
status=OperationStatus.STARTED,
|
|
176
|
+
execution_details=ExecutionDetails(
|
|
177
|
+
input_payload=self.start_input.get_normalized_input()
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def get_operation_execution_started(self) -> Operation:
|
|
183
|
+
if not self.operations:
|
|
184
|
+
msg: str = "execution not started."
|
|
185
|
+
|
|
186
|
+
raise IllegalStateException(msg)
|
|
187
|
+
|
|
188
|
+
return self.operations[0]
|
|
189
|
+
|
|
190
|
+
def get_new_checkpoint_token(self) -> str:
|
|
191
|
+
"""Generate a new checkpoint token with incremented sequence"""
|
|
192
|
+
with self._state_lock:
|
|
193
|
+
self._token_sequence += 1
|
|
194
|
+
new_token_sequence = self._token_sequence
|
|
195
|
+
token = CheckpointToken(
|
|
196
|
+
execution_arn=self.durable_execution_arn,
|
|
197
|
+
token_sequence=new_token_sequence,
|
|
198
|
+
)
|
|
199
|
+
token_str = token.to_str()
|
|
200
|
+
self.used_tokens.add(token_str)
|
|
201
|
+
return token_str
|
|
202
|
+
|
|
203
|
+
def get_navigable_operations(self) -> list[Operation]:
|
|
204
|
+
"""Get list of operations, but exclude child operations where the parent has already completed."""
|
|
205
|
+
return self.operations
|
|
206
|
+
|
|
207
|
+
def get_assertable_operations(self) -> list[Operation]:
|
|
208
|
+
"""Get list of operations, but exclude the EXECUTION operations"""
|
|
209
|
+
# TODO: this excludes EXECUTION at start, but can there be an EXECUTION at the end if there was a checkpoint with large payload?
|
|
210
|
+
return self.operations[1:]
|
|
211
|
+
|
|
212
|
+
def has_pending_operations(self, execution: Execution) -> bool:
|
|
213
|
+
"""True if execution has pending operations."""
|
|
214
|
+
|
|
215
|
+
for operation in execution.operations:
|
|
216
|
+
if (
|
|
217
|
+
operation.operation_type == OperationType.STEP
|
|
218
|
+
and operation.status == OperationStatus.PENDING
|
|
219
|
+
) or (
|
|
220
|
+
operation.operation_type
|
|
221
|
+
in [
|
|
222
|
+
OperationType.WAIT,
|
|
223
|
+
OperationType.CALLBACK,
|
|
224
|
+
OperationType.CHAINED_INVOKE,
|
|
225
|
+
]
|
|
226
|
+
and operation.status == OperationStatus.STARTED
|
|
227
|
+
):
|
|
228
|
+
return True
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
def record_invocation_completion(
|
|
232
|
+
self, start_timestamp: datetime, end_timestamp: datetime, request_id: str
|
|
233
|
+
) -> None:
|
|
234
|
+
"""Record an invocation completion event."""
|
|
235
|
+
self.invocation_completions.append(
|
|
236
|
+
InvocationCompletedDetails(
|
|
237
|
+
start_timestamp=start_timestamp,
|
|
238
|
+
end_timestamp=end_timestamp,
|
|
239
|
+
request_id=request_id,
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def complete_success(self, result: str | None) -> None:
|
|
244
|
+
"""Complete execution successfully (DecisionType.COMPLETE_WORKFLOW_EXECUTION)."""
|
|
245
|
+
self.result = DurableExecutionInvocationOutput(
|
|
246
|
+
status=InvocationStatus.SUCCEEDED, result=result
|
|
247
|
+
)
|
|
248
|
+
self.is_complete = True
|
|
249
|
+
self.close_status = ExecutionStatus.SUCCEEDED
|
|
250
|
+
self._end_execution(OperationStatus.SUCCEEDED)
|
|
251
|
+
|
|
252
|
+
def complete_fail(self, error: ErrorObject) -> None:
|
|
253
|
+
"""Complete execution with failure (DecisionType.FAIL_WORKFLOW_EXECUTION)."""
|
|
254
|
+
self.result = DurableExecutionInvocationOutput(
|
|
255
|
+
status=InvocationStatus.FAILED, error=error
|
|
256
|
+
)
|
|
257
|
+
self.is_complete = True
|
|
258
|
+
self.close_status = ExecutionStatus.FAILED
|
|
259
|
+
self._end_execution(OperationStatus.FAILED)
|
|
260
|
+
|
|
261
|
+
def complete_timeout(self, error: ErrorObject) -> None:
|
|
262
|
+
"""Complete execution with timeout."""
|
|
263
|
+
self.result = DurableExecutionInvocationOutput(
|
|
264
|
+
status=InvocationStatus.FAILED, error=error
|
|
265
|
+
)
|
|
266
|
+
self.is_complete = True
|
|
267
|
+
self.close_status = ExecutionStatus.TIMED_OUT
|
|
268
|
+
self._end_execution(OperationStatus.TIMED_OUT)
|
|
269
|
+
|
|
270
|
+
def complete_stopped(self, error: ErrorObject) -> None:
|
|
271
|
+
"""Complete execution as terminated (TerminateWorkflowExecutionV2Request)."""
|
|
272
|
+
self.result = DurableExecutionInvocationOutput(
|
|
273
|
+
status=InvocationStatus.FAILED, error=error
|
|
274
|
+
)
|
|
275
|
+
self.is_complete = True
|
|
276
|
+
self.close_status = ExecutionStatus.STOPPED
|
|
277
|
+
self._end_execution(OperationStatus.STOPPED)
|
|
278
|
+
|
|
279
|
+
def find_operation(self, operation_id: str) -> tuple[int, Operation]:
|
|
280
|
+
"""Find operation by ID, return index and operation."""
|
|
281
|
+
for i, operation in enumerate(self.operations):
|
|
282
|
+
if operation.operation_id == operation_id:
|
|
283
|
+
return i, operation
|
|
284
|
+
msg: str = f"Attempting to update state of an Operation [{operation_id}] that doesn't exist"
|
|
285
|
+
raise IllegalStateException(msg)
|
|
286
|
+
|
|
287
|
+
def find_callback_operation(self, callback_id: str) -> tuple[int, Operation]:
|
|
288
|
+
"""Find callback operation by callback_id, return index and operation."""
|
|
289
|
+
for i, operation in enumerate(self.operations):
|
|
290
|
+
if (
|
|
291
|
+
operation.operation_type == OperationType.CALLBACK
|
|
292
|
+
and operation.callback_details
|
|
293
|
+
and operation.callback_details.callback_id == callback_id
|
|
294
|
+
):
|
|
295
|
+
return i, operation
|
|
296
|
+
msg: str = f"Callback operation with callback_id [{callback_id}] not found"
|
|
297
|
+
raise IllegalStateException(msg)
|
|
298
|
+
|
|
299
|
+
def complete_wait(self, operation_id: str) -> Operation:
|
|
300
|
+
"""Complete WAIT operation when timer fires."""
|
|
301
|
+
index, operation = self.find_operation(operation_id)
|
|
302
|
+
|
|
303
|
+
# Validate
|
|
304
|
+
if operation.status != OperationStatus.STARTED:
|
|
305
|
+
msg_wait_not_started: str = f"Attempting to transition a Wait Operation[{operation_id}] to SUCCEEDED when it's not STARTED"
|
|
306
|
+
raise IllegalStateException(msg_wait_not_started)
|
|
307
|
+
if operation.operation_type != OperationType.WAIT:
|
|
308
|
+
msg_not_wait: str = (
|
|
309
|
+
f"Expected WAIT operation, got {operation.operation_type}"
|
|
310
|
+
)
|
|
311
|
+
raise IllegalStateException(msg_not_wait)
|
|
312
|
+
|
|
313
|
+
# Thread-safe increment sequence and operation update
|
|
314
|
+
with self._state_lock:
|
|
315
|
+
self._token_sequence += 1
|
|
316
|
+
# Build and assign updated operation
|
|
317
|
+
self.operations[index] = replace(
|
|
318
|
+
operation,
|
|
319
|
+
status=OperationStatus.SUCCEEDED,
|
|
320
|
+
end_timestamp=datetime.now(UTC),
|
|
321
|
+
)
|
|
322
|
+
return self.operations[index]
|
|
323
|
+
|
|
324
|
+
def complete_retry(self, operation_id: str) -> Operation:
|
|
325
|
+
"""Complete STEP retry when timer fires."""
|
|
326
|
+
index, operation = self.find_operation(operation_id)
|
|
327
|
+
|
|
328
|
+
# Validate
|
|
329
|
+
if operation.status != OperationStatus.PENDING:
|
|
330
|
+
msg_step_not_pending: str = f"Attempting to transition a Step Operation[{operation_id}] to READY when it's not PENDING"
|
|
331
|
+
raise IllegalStateException(msg_step_not_pending)
|
|
332
|
+
if operation.operation_type != OperationType.STEP:
|
|
333
|
+
msg_not_step: str = (
|
|
334
|
+
f"Expected STEP operation, got {operation.operation_type}"
|
|
335
|
+
)
|
|
336
|
+
raise IllegalStateException(msg_not_step)
|
|
337
|
+
|
|
338
|
+
# Thread-safe increment sequence and operation update
|
|
339
|
+
with self._state_lock:
|
|
340
|
+
self._token_sequence += 1
|
|
341
|
+
# Build updated step_details with cleared next_attempt_timestamp
|
|
342
|
+
new_step_details = None
|
|
343
|
+
if operation.step_details:
|
|
344
|
+
new_step_details = replace(
|
|
345
|
+
operation.step_details, next_attempt_timestamp=None
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Build updated operation
|
|
349
|
+
updated_operation = replace(
|
|
350
|
+
operation, status=OperationStatus.READY, step_details=new_step_details
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Assign
|
|
354
|
+
self.operations[index] = updated_operation
|
|
355
|
+
return updated_operation
|
|
356
|
+
|
|
357
|
+
def complete_callback_success(
|
|
358
|
+
self, callback_id: str, result: bytes | None = None
|
|
359
|
+
) -> Operation:
|
|
360
|
+
"""Complete CALLBACK operation with success."""
|
|
361
|
+
index, operation = self.find_callback_operation(callback_id)
|
|
362
|
+
if operation.status != OperationStatus.STARTED:
|
|
363
|
+
msg: str = f"Callback operation [{callback_id}] is not in STARTED state"
|
|
364
|
+
raise IllegalStateException(msg)
|
|
365
|
+
|
|
366
|
+
with self._state_lock:
|
|
367
|
+
self._token_sequence += 1
|
|
368
|
+
updated_callback_details = None
|
|
369
|
+
if operation.callback_details:
|
|
370
|
+
updated_callback_details = replace(
|
|
371
|
+
operation.callback_details,
|
|
372
|
+
result=result.decode() if result else None,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
self.operations[index] = replace(
|
|
376
|
+
operation,
|
|
377
|
+
status=OperationStatus.SUCCEEDED,
|
|
378
|
+
end_timestamp=datetime.now(UTC),
|
|
379
|
+
callback_details=updated_callback_details,
|
|
380
|
+
)
|
|
381
|
+
return self.operations[index]
|
|
382
|
+
|
|
383
|
+
def complete_callback_failure(
|
|
384
|
+
self, callback_id: str, error: ErrorObject
|
|
385
|
+
) -> Operation:
|
|
386
|
+
"""Complete CALLBACK operation with failure."""
|
|
387
|
+
index, operation = self.find_callback_operation(callback_id)
|
|
388
|
+
|
|
389
|
+
if operation.status != OperationStatus.STARTED:
|
|
390
|
+
msg: str = f"Callback operation [{callback_id}] is not in STARTED state"
|
|
391
|
+
raise IllegalStateException(msg)
|
|
392
|
+
|
|
393
|
+
with self._state_lock:
|
|
394
|
+
self._token_sequence += 1
|
|
395
|
+
updated_callback_details = None
|
|
396
|
+
if operation.callback_details:
|
|
397
|
+
updated_callback_details = replace(
|
|
398
|
+
operation.callback_details, error=error
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
self.operations[index] = replace(
|
|
402
|
+
operation,
|
|
403
|
+
status=OperationStatus.FAILED,
|
|
404
|
+
end_timestamp=datetime.now(UTC),
|
|
405
|
+
callback_details=updated_callback_details,
|
|
406
|
+
)
|
|
407
|
+
return self.operations[index]
|
|
408
|
+
|
|
409
|
+
def complete_callback_timeout(
|
|
410
|
+
self, callback_id: str, error: ErrorObject
|
|
411
|
+
) -> Operation:
|
|
412
|
+
"""Complete CALLBACK operation with timeout."""
|
|
413
|
+
index, operation = self.find_callback_operation(callback_id)
|
|
414
|
+
|
|
415
|
+
if operation.status != OperationStatus.STARTED:
|
|
416
|
+
msg: str = f"Callback operation [{callback_id}] is not in STARTED state"
|
|
417
|
+
raise IllegalStateException(msg)
|
|
418
|
+
|
|
419
|
+
with self._state_lock:
|
|
420
|
+
self._token_sequence += 1
|
|
421
|
+
updated_callback_details = None
|
|
422
|
+
if operation.callback_details:
|
|
423
|
+
updated_callback_details = replace(
|
|
424
|
+
operation.callback_details, error=error
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
self.operations[index] = replace(
|
|
428
|
+
operation,
|
|
429
|
+
status=OperationStatus.TIMED_OUT,
|
|
430
|
+
end_timestamp=datetime.now(UTC),
|
|
431
|
+
callback_details=updated_callback_details,
|
|
432
|
+
)
|
|
433
|
+
return self.operations[index]
|
|
434
|
+
|
|
435
|
+
def _end_execution(self, status: OperationStatus) -> None:
|
|
436
|
+
"""Set the end_timestamp on the main EXECUTION operation when execution completes."""
|
|
437
|
+
execution_op: Operation = self.get_operation_execution_started()
|
|
438
|
+
if execution_op.operation_type == OperationType.EXECUTION:
|
|
439
|
+
with self._state_lock:
|
|
440
|
+
self.operations[0] = replace(
|
|
441
|
+
execution_op,
|
|
442
|
+
status=status,
|
|
443
|
+
end_timestamp=datetime.now(UTC),
|
|
444
|
+
)
|