aws-durable-execution-sdk-python 2.0.1__py3-none-any.whl → 3.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/__about__.py +1 -1
- aws_durable_execution_sdk_python/concurrency/executor.py +24 -12
- aws_durable_execution_sdk_python/concurrency/models.py +31 -21
- aws_durable_execution_sdk_python/config.py +109 -7
- aws_durable_execution_sdk_python/context.py +87 -28
- aws_durable_execution_sdk_python/execution.py +243 -128
- aws_durable_execution_sdk_python/identifier.py +12 -11
- aws_durable_execution_sdk_python/lambda_service.py +65 -63
- aws_durable_execution_sdk_python/operation/child.py +30 -5
- aws_durable_execution_sdk_python/operation/invoke.py +6 -0
- aws_durable_execution_sdk_python/operation/map.py +4 -4
- aws_durable_execution_sdk_python/operation/parallel.py +4 -4
- aws_durable_execution_sdk_python/plugin.py +1295 -123
- aws_durable_execution_sdk_python/plugin_discovery.py +393 -85
- aws_durable_execution_sdk_python/state.py +240 -63
- aws_durable_execution_sdk_python-3.0.0.dist-info/METADATA +311 -0
- {aws_durable_execution_sdk_python-2.0.1.dist-info → aws_durable_execution_sdk_python-3.0.0.dist-info}/RECORD +18 -18
- {aws_durable_execution_sdk_python-2.0.1.dist-info → aws_durable_execution_sdk_python-3.0.0.dist-info}/WHEEL +1 -1
- aws_durable_execution_sdk_python-2.0.1.dist-info/METADATA +0 -156
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
|
+
import contextvars
|
|
4
5
|
import functools
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from concurrent.futures import Future, ThreadPoolExecutor, wait
|
|
8
9
|
from dataclasses import dataclass, field
|
|
9
10
|
from typing import TYPE_CHECKING, Any
|
|
10
11
|
|
|
@@ -27,7 +28,7 @@ from aws_durable_execution_sdk_python.lambda_service import (
|
|
|
27
28
|
OperationUpdate,
|
|
28
29
|
)
|
|
29
30
|
from aws_durable_execution_sdk_python.plugin import (
|
|
30
|
-
|
|
31
|
+
DurableInstrumentationPluginFactory,
|
|
31
32
|
PluginExecutor,
|
|
32
33
|
)
|
|
33
34
|
from aws_durable_execution_sdk_python.plugin_discovery import (
|
|
@@ -169,7 +170,7 @@ def durable_execution(
|
|
|
169
170
|
func: Callable[[Any, DurableContext], Any] | None = None,
|
|
170
171
|
*,
|
|
171
172
|
boto3_client: Boto3LambdaClient | None = None,
|
|
172
|
-
plugins: list[
|
|
173
|
+
plugins: list[DurableInstrumentationPluginFactory] | None = None,
|
|
173
174
|
) -> Callable[[Any, LambdaContext], Any]:
|
|
174
175
|
"""
|
|
175
176
|
Decorator to create a durable execution handler.
|
|
@@ -177,7 +178,10 @@ def durable_execution(
|
|
|
177
178
|
Args:
|
|
178
179
|
func: The user function to decorate
|
|
179
180
|
boto3_client: Optional boto3 Lambda client to use
|
|
180
|
-
plugins: Optional list of instrumentation
|
|
181
|
+
plugins: Optional list of instrumentation plugin factories. Each
|
|
182
|
+
factory's ``create_plugin`` is called once per invocation with that
|
|
183
|
+
invocation's ``InvocationStartInfo``, and the instance it returns
|
|
184
|
+
serves only that invocation.
|
|
181
185
|
"""
|
|
182
186
|
# Decorator called with parameters
|
|
183
187
|
if func is None:
|
|
@@ -188,10 +192,13 @@ def durable_execution(
|
|
|
188
192
|
|
|
189
193
|
logger.debug("Starting durable execution handler...")
|
|
190
194
|
|
|
191
|
-
|
|
195
|
+
configured_plugins = load_configured_plugins(plugins)
|
|
192
196
|
|
|
193
|
-
@
|
|
197
|
+
@functools.wraps(func)
|
|
194
198
|
def wrapper(event: Any, context: LambdaContext) -> MutableMapping[str, Any]:
|
|
199
|
+
# Only the factory list outlives an invocation. The worker below owns
|
|
200
|
+
# this fresh executor and all plugin lifecycle hooks.
|
|
201
|
+
plugin_executor = PluginExecutor(configured_plugins)
|
|
195
202
|
invocation_input: DurableExecutionInvocationInput
|
|
196
203
|
service_client: DurableServiceClient
|
|
197
204
|
|
|
@@ -279,88 +286,75 @@ def durable_execution(
|
|
|
279
286
|
),
|
|
280
287
|
)
|
|
281
288
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
ThreadPoolExecutor(
|
|
285
|
-
max_workers=2, thread_name_prefix="dex-handler"
|
|
286
|
-
) as executor,
|
|
287
|
-
contextlib.closing(execution_state) as execution_state,
|
|
288
|
-
):
|
|
289
|
-
execution_operation = execution_state.get_execution_operation()
|
|
290
|
-
|
|
291
|
-
# execute the plugins
|
|
292
|
-
plugin_executor.on_invocation_start(
|
|
293
|
-
execution_arn=invocation_input.durable_execution_arn,
|
|
294
|
-
lambda_context=context,
|
|
295
|
-
execution_start_time=(
|
|
296
|
-
execution_operation.start_timestamp
|
|
297
|
-
if execution_operation is not None
|
|
298
|
-
else None
|
|
299
|
-
),
|
|
300
|
-
is_first_invocation=not has_prior_operations,
|
|
301
|
-
execution_input=input_event,
|
|
302
|
-
# Read the map through a callable rather than snapshotting it
|
|
303
|
-
# here: the invocation-end hook needs the state as of the end of
|
|
304
|
-
# the invocation, and neither hook pays for the conversion until
|
|
305
|
-
# a plugin actually reads it.
|
|
306
|
-
operations_provider=lambda: execution_state.operations,
|
|
307
|
-
updated_operation_ids=invocation_input.updated_operation_ids,
|
|
308
|
-
)
|
|
309
|
-
# Thread 1: Run background checkpoint processing
|
|
310
|
-
executor.submit(execution_state.checkpoint_batches_forever)
|
|
289
|
+
def invoke_handler() -> MutableMapping[str, Any]:
|
|
290
|
+
output: MutableMapping[str, Any]
|
|
311
291
|
|
|
312
|
-
# Thread 2: Execute user function
|
|
313
292
|
logger.debug(
|
|
314
293
|
"%s entering user-space...", invocation_input.durable_execution_arn
|
|
315
294
|
)
|
|
316
|
-
user_future = executor.submit(func, input_event, durable_context)
|
|
317
|
-
|
|
318
|
-
logger.debug(
|
|
319
|
-
"%s waiting for user code completion...",
|
|
320
|
-
invocation_input.durable_execution_arn,
|
|
321
|
-
)
|
|
322
|
-
|
|
323
295
|
try:
|
|
324
296
|
# Background checkpointing errors will propagate through CompletionEvent.wait() as BackgroundThreadError
|
|
325
|
-
result =
|
|
297
|
+
result = func(input_event, durable_context)
|
|
326
298
|
|
|
327
299
|
# done with userland
|
|
328
300
|
logger.debug(
|
|
329
301
|
"%s exiting user-space...",
|
|
330
302
|
invocation_input.durable_execution_arn,
|
|
331
303
|
)
|
|
332
|
-
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
304
|
+
|
|
305
|
+
# The handler may finish while background checkpoint work is
|
|
306
|
+
# still running for async operations it did not await, such as
|
|
307
|
+
# early-completing map or parallel branches. If one of those
|
|
308
|
+
# checkpoint responses omitted the next checkpoint token, this
|
|
309
|
+
# invocation can no longer record anything else with the
|
|
310
|
+
# service, including the handler's final result.
|
|
311
|
+
#
|
|
312
|
+
# In that case, return PENDING instead of SUCCEEDED. The next
|
|
313
|
+
# invocation will replay from the last durable checkpoint and
|
|
314
|
+
# produce the result again. This check catches revocation
|
|
315
|
+
# already observed before we build the terminal response; the
|
|
316
|
+
# post-with check below catches revocation discovered during
|
|
317
|
+
# cleanup of in-flight checkpoint work.
|
|
318
|
+
if execution_state.is_checkpoint_token_revoked:
|
|
339
319
|
logger.debug(
|
|
340
|
-
"
|
|
341
|
-
|
|
342
|
-
LAMBDA_RESPONSE_SIZE_LIMIT,
|
|
320
|
+
"Checkpoint token revoked; ending invocation with "
|
|
321
|
+
"PENDING instead of SUCCEEDED."
|
|
343
322
|
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
)
|
|
347
|
-
#
|
|
348
|
-
#
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
323
|
+
output = DurableExecutionInvocationOutput.create_pending().to_dict()
|
|
324
|
+
else:
|
|
325
|
+
serialized_result = json.dumps(result)
|
|
326
|
+
# large response handling here. Remember if checkpointing to complete, NOT to include
|
|
327
|
+
# payload in response
|
|
328
|
+
if (
|
|
329
|
+
serialized_result
|
|
330
|
+
and len(serialized_result) > LAMBDA_RESPONSE_SIZE_LIMIT
|
|
331
|
+
):
|
|
332
|
+
logger.debug(
|
|
333
|
+
"Response size (%s bytes) exceeds Lambda limit (%s) bytes). Checkpointing result.",
|
|
334
|
+
len(serialized_result),
|
|
335
|
+
LAMBDA_RESPONSE_SIZE_LIMIT,
|
|
354
336
|
)
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
result=
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
337
|
+
success_operation = OperationUpdate.create_execution_succeed(
|
|
338
|
+
payload=serialized_result
|
|
339
|
+
)
|
|
340
|
+
# Checkpoint large result with blocking (is_sync=True, default).
|
|
341
|
+
# Must ensure the result is persisted before returning to Lambda.
|
|
342
|
+
# Large results exceed Lambda response limits and must be stored durably
|
|
343
|
+
# before the execution completes.
|
|
344
|
+
try:
|
|
345
|
+
execution_state.create_checkpoint(
|
|
346
|
+
success_operation, is_sync=True
|
|
347
|
+
)
|
|
348
|
+
except CheckpointError as e:
|
|
349
|
+
output = handle_checkpoint_error(e).to_dict()
|
|
350
|
+
else:
|
|
351
|
+
output = DurableExecutionInvocationOutput.create_succeeded(
|
|
352
|
+
result=""
|
|
353
|
+
).to_dict()
|
|
354
|
+
else:
|
|
355
|
+
output = DurableExecutionInvocationOutput.create_succeeded(
|
|
356
|
+
result=serialized_result
|
|
357
|
+
).to_dict()
|
|
364
358
|
|
|
365
359
|
except BackgroundThreadError as bg_error:
|
|
366
360
|
# Background checkpoint system failed - propagated through CompletionEvent
|
|
@@ -378,85 +372,206 @@ def durable_execution(
|
|
|
378
372
|
"without retry.",
|
|
379
373
|
extra=bg_error.source_exception.build_logger_extras(),
|
|
380
374
|
)
|
|
381
|
-
|
|
375
|
+
output = DurableExecutionInvocationOutput(
|
|
382
376
|
status=InvocationStatus.FAILED,
|
|
383
377
|
error=ErrorObject.from_exception(bg_error.source_exception),
|
|
384
378
|
).to_dict()
|
|
379
|
+
else:
|
|
380
|
+
raise bg_error.source_exception from bg_error
|
|
385
381
|
else:
|
|
386
382
|
logger.exception("Checkpoint processing failed")
|
|
387
|
-
|
|
383
|
+
raise bg_error.source_exception from bg_error
|
|
388
384
|
|
|
389
385
|
except SuspendExecution:
|
|
390
386
|
# User code suspended - stop background checkpointing thread
|
|
391
387
|
logger.debug("Suspending execution...")
|
|
392
|
-
|
|
393
|
-
status=InvocationStatus.PENDING
|
|
394
|
-
).to_dict()
|
|
388
|
+
output = DurableExecutionInvocationOutput.create_pending().to_dict()
|
|
395
389
|
|
|
396
390
|
except CheckpointError as e:
|
|
397
|
-
# Checkpoint system is broken - stop background thread and exit immediately
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
391
|
+
# Checkpoint system is broken - stop background thread and exit immediately.
|
|
392
|
+
if execution_state.is_checkpoint_token_revoked:
|
|
393
|
+
output = DurableExecutionInvocationOutput.create_pending().to_dict()
|
|
394
|
+
else:
|
|
395
|
+
logger.exception(
|
|
396
|
+
"Checkpoint system failed",
|
|
397
|
+
extra=e.build_logger_extras(),
|
|
398
|
+
)
|
|
399
|
+
output = handle_checkpoint_error(e).to_dict()
|
|
403
400
|
except InvocationError as e:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
401
|
+
if execution_state.is_checkpoint_token_revoked:
|
|
402
|
+
output = DurableExecutionInvocationOutput.create_pending().to_dict()
|
|
403
|
+
else:
|
|
404
|
+
# Non-retryable Durable API errors (e.g., customer configuration issues,
|
|
405
|
+
# 4xx client errors) will never succeed on retry — fail the execution immediately.
|
|
406
|
+
if not e.is_retryable():
|
|
407
|
+
logger.exception(
|
|
408
|
+
"Non-retryable Durable API error. Must fail execution without retry.",
|
|
409
|
+
extra=e.build_logger_extras(), # type: ignore[attr-defined]
|
|
410
|
+
)
|
|
411
|
+
output = DurableExecutionInvocationOutput(
|
|
412
|
+
status=InvocationStatus.FAILED,
|
|
413
|
+
error=ErrorObject.from_exception(e),
|
|
414
|
+
).to_dict()
|
|
415
|
+
else:
|
|
416
|
+
logger.exception("Invocation error. Must terminate.")
|
|
417
|
+
# Throw the error to trigger Lambda retry
|
|
418
|
+
raise
|
|
419
|
+
except ExecutionError as e:
|
|
420
|
+
if execution_state.is_checkpoint_token_revoked:
|
|
421
|
+
output = DurableExecutionInvocationOutput.create_pending().to_dict()
|
|
422
|
+
else:
|
|
407
423
|
logger.exception(
|
|
408
|
-
"
|
|
409
|
-
extra=e.build_logger_extras(), # type: ignore[attr-defined]
|
|
424
|
+
"Execution error. Must fail execution without retry."
|
|
410
425
|
)
|
|
411
|
-
|
|
426
|
+
output = DurableExecutionInvocationOutput(
|
|
412
427
|
status=InvocationStatus.FAILED,
|
|
413
428
|
error=ErrorObject.from_exception(e),
|
|
414
429
|
).to_dict()
|
|
415
|
-
logger.exception("Invocation error. Must terminate.")
|
|
416
|
-
# Throw the error to trigger Lambda retry
|
|
417
|
-
raise
|
|
418
|
-
except ExecutionError as e:
|
|
419
|
-
logger.exception("Execution error. Must fail execution without retry.")
|
|
420
|
-
return DurableExecutionInvocationOutput(
|
|
421
|
-
status=InvocationStatus.FAILED,
|
|
422
|
-
error=ErrorObject.from_exception(e),
|
|
423
|
-
).to_dict()
|
|
424
430
|
except Exception as e:
|
|
425
431
|
# all user-space errors go here
|
|
426
|
-
|
|
432
|
+
if execution_state.is_checkpoint_token_revoked:
|
|
433
|
+
output = DurableExecutionInvocationOutput.create_pending().to_dict()
|
|
434
|
+
else:
|
|
435
|
+
logger.exception("Execution failed")
|
|
427
436
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
437
|
+
result = DurableExecutionInvocationOutput(
|
|
438
|
+
status=InvocationStatus.FAILED,
|
|
439
|
+
error=ErrorObject.from_exception(e),
|
|
440
|
+
).to_dict()
|
|
431
441
|
|
|
432
|
-
|
|
442
|
+
serialized_result = json.dumps(result)
|
|
433
443
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
444
|
+
if (
|
|
445
|
+
serialized_result
|
|
446
|
+
and len(serialized_result) > LAMBDA_RESPONSE_SIZE_LIMIT
|
|
447
|
+
):
|
|
448
|
+
logger.debug(
|
|
449
|
+
"Response size (%s bytes) exceeds Lambda limit (%s) bytes). Checkpointing result.",
|
|
450
|
+
len(serialized_result),
|
|
451
|
+
LAMBDA_RESPONSE_SIZE_LIMIT,
|
|
452
|
+
)
|
|
453
|
+
failed_operation = OperationUpdate.create_execution_fail(
|
|
454
|
+
error=ErrorObject.from_exception(e)
|
|
455
|
+
)
|
|
446
456
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
457
|
+
# Checkpoint large result with blocking (is_sync=True, default).
|
|
458
|
+
# Must ensure the result is persisted before returning to Lambda.
|
|
459
|
+
# Large results exceed Lambda response limits and must be stored durably
|
|
460
|
+
# before the execution completes.
|
|
461
|
+
try:
|
|
462
|
+
execution_state.create_checkpoint_sync(failed_operation)
|
|
463
|
+
except SuspendExecution:
|
|
464
|
+
# Revocation can occur after the check at the top of this handler and
|
|
465
|
+
# before this checkpoint. In that case create_checkpoint_sync raises
|
|
466
|
+
# SuspendExecution. So return PENDING instead of FAILED.
|
|
467
|
+
output = DurableExecutionInvocationOutput.create_pending().to_dict()
|
|
468
|
+
except CheckpointError as checkpoint_error:
|
|
469
|
+
output = handle_checkpoint_error(checkpoint_error).to_dict()
|
|
470
|
+
else:
|
|
471
|
+
output = DurableExecutionInvocationOutput(
|
|
472
|
+
status=InvocationStatus.FAILED
|
|
473
|
+
).to_dict()
|
|
474
|
+
else:
|
|
475
|
+
output = result
|
|
476
|
+
|
|
477
|
+
return output
|
|
478
|
+
|
|
479
|
+
def invoke_worker() -> MutableMapping[str, Any]:
|
|
480
|
+
# This task owns Start, user cleanup, output preparation, resource
|
|
481
|
+
# cleanup and End. The caller owns the executor that runs this task.
|
|
482
|
+
with plugin_executor.run():
|
|
483
|
+
checkpoint_future: Future[None] | None = None
|
|
484
|
+
|
|
485
|
+
def wait_for_checkpoint() -> None:
|
|
486
|
+
if checkpoint_future is not None:
|
|
487
|
+
# Match the former executor join: observed checkpoint
|
|
488
|
+
# errors retain their existing classification paths.
|
|
489
|
+
wait((checkpoint_future,))
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
with contextlib.ExitStack() as resources:
|
|
493
|
+
# LIFO cleanup: branches join with checkpointing alive;
|
|
494
|
+
# then close stops checkpointing and the wait completes.
|
|
495
|
+
resources.callback(wait_for_checkpoint)
|
|
496
|
+
resources.callback(execution_state.close)
|
|
497
|
+
execution_operation = execution_state.get_execution_operation()
|
|
498
|
+
|
|
499
|
+
# execute the plugins
|
|
500
|
+
plugin_executor.on_invocation_start(
|
|
501
|
+
execution_arn=invocation_input.durable_execution_arn,
|
|
502
|
+
lambda_context=context,
|
|
503
|
+
execution_start_time=(
|
|
504
|
+
execution_operation.start_timestamp
|
|
505
|
+
if execution_operation is not None
|
|
506
|
+
else None
|
|
507
|
+
),
|
|
508
|
+
is_first_invocation=not has_prior_operations,
|
|
509
|
+
execution_input=input_event,
|
|
510
|
+
# Read the map through a callable rather than snapshotting it
|
|
511
|
+
# here: the invocation-end hook needs the state as of the end of
|
|
512
|
+
# the invocation, and neither hook pays for the conversion until
|
|
513
|
+
# a plugin actually reads it.
|
|
514
|
+
operations_provider=lambda: execution_state.operations,
|
|
515
|
+
updated_operation_ids=invocation_input.updated_operation_ids,
|
|
516
|
+
)
|
|
517
|
+
# No checkpoint work starts until all Start hooks finish.
|
|
518
|
+
checkpoint_future = executor.submit(
|
|
519
|
+
execution_state.checkpoint_batches_forever
|
|
520
|
+
)
|
|
521
|
+
output = invoke_handler()
|
|
522
|
+
# The ExitStack has now closed execution_state and joined the
|
|
523
|
+
# checkpoint thread. So any revocation observed while draining
|
|
524
|
+
# in-flight checkpoint work is visible here. A terminal status
|
|
525
|
+
# built before that point can no longer be recorded with the
|
|
526
|
+
# service. So report PENDING instead; the next invocation
|
|
527
|
+
# replays from the last durable checkpoint.
|
|
528
|
+
if (
|
|
529
|
+
output.get("Status")
|
|
530
|
+
in {
|
|
531
|
+
InvocationStatus.SUCCEEDED.value,
|
|
532
|
+
InvocationStatus.FAILED.value,
|
|
533
|
+
}
|
|
534
|
+
and execution_state.is_checkpoint_token_revoked
|
|
535
|
+
):
|
|
536
|
+
logger.debug(
|
|
537
|
+
"Checkpoint token revoked during invocation cleanup; ending "
|
|
538
|
+
"invocation with PENDING instead of terminal status."
|
|
539
|
+
)
|
|
540
|
+
output = (
|
|
541
|
+
DurableExecutionInvocationOutput.create_pending().to_dict()
|
|
542
|
+
)
|
|
543
|
+
completed = DurableExecutionInvocationOutput.from_dict(output)
|
|
544
|
+
except BaseException as error:
|
|
545
|
+
# Every started plugin gets its End hook, including when
|
|
546
|
+
# cancellation or a thread-control exception escapes. The
|
|
547
|
+
# handler's original failure wins over an End-hook failure.
|
|
451
548
|
try:
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
549
|
+
plugin_executor.on_invocation_end(
|
|
550
|
+
DurableExecutionInvocationOutput.create_retry(
|
|
551
|
+
ErrorObject.from_exception(error)
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
except BaseException: # noqa: BLE001 - preserve the original failure
|
|
555
|
+
logger.exception(
|
|
556
|
+
"Plugin invocation-end hook failed while the invocation "
|
|
557
|
+
"was already failing; the original failure is raised"
|
|
558
|
+
)
|
|
559
|
+
raise
|
|
560
|
+
# End must run only once; its own failure is not a handler failure.
|
|
561
|
+
plugin_executor.on_invocation_end(completed)
|
|
562
|
+
return output
|
|
563
|
+
|
|
564
|
+
with ThreadPoolExecutor(
|
|
565
|
+
max_workers=2, thread_name_prefix="dex-handler"
|
|
566
|
+
) as executor:
|
|
567
|
+
if configured_plugins:
|
|
568
|
+
invocation_future = executor.submit(
|
|
569
|
+
contextvars.copy_context().run, invoke_worker
|
|
570
|
+
)
|
|
571
|
+
else:
|
|
572
|
+
# Preserve the original fresh-worker context without plugins.
|
|
573
|
+
invocation_future = executor.submit(invoke_worker)
|
|
574
|
+
return invocation_future.result()
|
|
460
575
|
|
|
461
576
|
return wrapper
|
|
462
577
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
|
-
from dataclasses import dataclass
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
7
|
|
|
8
8
|
from aws_durable_execution_sdk_python.exceptions import (
|
|
9
9
|
NonDeterministicExecutionError,
|
|
@@ -11,7 +11,6 @@ from aws_durable_execution_sdk_python.exceptions import (
|
|
|
11
11
|
from aws_durable_execution_sdk_python.lambda_service import (
|
|
12
12
|
Operation,
|
|
13
13
|
OperationType,
|
|
14
|
-
OperationSubType,
|
|
15
14
|
)
|
|
16
15
|
|
|
17
16
|
|
|
@@ -34,17 +33,22 @@ class OperationIdNamespace:
|
|
|
34
33
|
|
|
35
34
|
@dataclass(frozen=True)
|
|
36
35
|
class OperationIdentifier:
|
|
37
|
-
"""
|
|
36
|
+
"""Operation identity with an explicit type independent of its subtype.
|
|
37
|
+
|
|
38
|
+
``sub_type`` is the subtype the operation records: the value of an
|
|
39
|
+
``OperationSubType`` member for an SDK operation, or the caller-defined
|
|
40
|
+
string a step, a callback, or a child context was configured with.
|
|
41
|
+
"""
|
|
38
42
|
|
|
39
43
|
operation_id: str
|
|
40
|
-
sub_type:
|
|
44
|
+
sub_type: str
|
|
41
45
|
parent_id: str | None = None
|
|
42
46
|
name: str | None = None
|
|
43
|
-
operation_type: OperationType
|
|
47
|
+
operation_type: OperationType = field(kw_only=True)
|
|
44
48
|
|
|
45
49
|
@property
|
|
46
50
|
def type(self) -> OperationType:
|
|
47
|
-
return self.operation_type
|
|
51
|
+
return self.operation_type
|
|
48
52
|
|
|
49
53
|
def validate_checkpoint(self, checkpoint: Operation | None) -> None:
|
|
50
54
|
"""Ensure replay history belongs to this operation before it is consumed."""
|
|
@@ -61,12 +65,9 @@ class OperationIdentifier:
|
|
|
61
65
|
mismatches.append(
|
|
62
66
|
f"type checkpoint={checkpoint.operation_type.value!r} current={self.type.value!r}"
|
|
63
67
|
)
|
|
64
|
-
if checkpoint.sub_type
|
|
65
|
-
checkpoint_sub_type = (
|
|
66
|
-
checkpoint.sub_type.value if checkpoint.sub_type is not None else None
|
|
67
|
-
)
|
|
68
|
+
if checkpoint.sub_type != self.sub_type:
|
|
68
69
|
mismatches.append(
|
|
69
|
-
f"subtype checkpoint={
|
|
70
|
+
f"subtype checkpoint={checkpoint.sub_type!r} current={self.sub_type!r}"
|
|
70
71
|
)
|
|
71
72
|
if checkpoint_name != expected_name:
|
|
72
73
|
mismatches.append(
|