Skip to content

fix: checkpoint the wrapped error for failed contexts - #807

Closed
ParidelPooya wants to merge 4 commits into
mainfrom
fix/checkpoint-wrapped-error
Closed

ParidelPooya wants to merge 4 commits into
mainfrom
fix/checkpoint-wrapped-error

Conversation

@ParidelPooya

@ParidelPooya ParidelPooya commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Problem

When an SDK wrapper error escaped a child context, map item, parallel branch or wait_for_callback context, the context's FAIL checkpoint recorded the wrapper's class as ErrorType. For example, a step that raised PaymentError inside run_in_child_context left aws_durable_execution_sdk_python.exceptions.StepError on the context checkpoint and on the execution result. The type of the error that caused the failure was lost one level up. On replay the context's __cause__ was rebuilt as a StepError whose error_type was StepError, so the original type was lost there too.

Change

  • ErrorObject.from_exception unwraps the SDK's own wrappers before it records an error: DurableOperationError, StepError, InvokeError, ChildContextError, WaitForConditionError, CallbackExternalError and CallbackSubmitterError. Classes are matched exactly, so a user subclass of a wrapper is never unwrapped. A wrapper already carries the wrapped error's fields, so the record uses its error_type, message, data and stack_trace. When error_type names another wrapper, the walk continues with __cause__. The walk takes at most 16 steps and stops at a cause it has already visited. A wrapper it stops at is recorded by its error_type.
  • CallbackTimeoutError, the base CallbackError, SerDesError, BatchCompletionError, ValidationError and other SDK errors are not unwrapped.
  • This covers every context FAIL checkpoint (child context, map item, parallel branch, wait_for_callback, the map/parallel operation itself), step and wait_for_condition FAIL checkpoints, and the execution result. They all build their error with from_exception.
  • A callback failed externally now puts the sender's ErrorType on CallbackExternalError.error_type. A missing or blank type (ASCII whitespace only) keeps the SDK class. The explicit type "Error" stays "Error".
  • wait_for_callback used to tell a submitter failure from a callback failure by the class rebuilt from the context checkpoint. That checkpoint now holds the user's type, so it reads the checkpoints of the context's two children instead (the callback, then the submitter step). First run and replay read the same checkpoints and raise the same error.

Live and replay still go through one path: the live failure is rebuilt from the ErrorObject it checkpointed (raise_as_operation_error), the same as replay. The new e2e test records the error shape user code sees on the first run and on replay and asserts they are equal.

User-visible changes

  1. Context checkpoint ErrorType: a failed child context, map item, parallel branch or wait_for_callback context records the wrapped error's type (for example my_module.PaymentError, or a builtin such as ValueError) instead of ...exceptions.StepError, ...ChildContextError, ...InvokeError or ...CallbackSubmitterError.
  2. Execution result Error.ErrorType: an execution that fails because a wrapper propagated now reports the wrapped type. Before: ...exceptions.StepError, ...ChildContextError, ...InvokeError or ...CallbackSubmitterError. After: the user's error type, or the invoked function's error type for an invoke. ErrorMessage is unchanged.
  3. ChildContextError.error_type (and the ChildContextError raised by BatchResult.throw_if_error) is the original type rather than ...StepError when a step failed inside the context. __cause__ is now a DurableOperationError stand-in carrying that type, instead of a StepError stand-in. Code that checked isinstance(e.__cause__, StepError) on a ChildContextError no longer matches. Use e.error_type instead.
  4. CallbackSubmitterError from wait_for_callback: error_type is the submitter's error type, and __cause__ is a DurableOperationError stand-in rather than a StepError. The class, message, data and stack trace are unchanged.
  5. CallbackExternalError.error_type carries the type sent with SendDurableExecutionCallbackFailure. It used to always be ...CallbackExternalError. A wait_for_callback context failed this way records the sender's type.
  6. Extras, microvm_session: a MicroVM error that leaves a child context the session handler created now reaches the caller with its MicroVM class (for example MicrovmJobFailedError), not as ChildContextError. The session context now records the MicroVM type instead of ChildContextError. A step failure that leaves the handler still reaches the caller as ChildContextError, and its error_type is the step's error type. The extras README and microvm_session docstring are updated.
  7. Exception classes raised to user code are unchanged, apart from item 6: StepError, ChildContextError, InvokeError, WaitForConditionError, CallbackSubmitterError, CallbackExternalError and CallbackTimeoutError are raised in the same places as before.

Compatibility with old checkpoints

Checkpoints written by an earlier SDK version still replay:

  • A context checkpoint holding ...StepError or another wrapper type is rebuilt as before. wait_for_callback still raises CallbackSubmitterError for a context holding StepError, and the callback error for a context holding a callback error type.
  • When such a checkpoint propagates to the execution result, the result records the deepest wrapper type the checkpoint holds, for example ...StepError. The earlier SDK reported ...CallbackSubmitterError there. No cause is guessed beyond what the checkpoint holds.
  • New checkpoints use the existing ErrorObject fields only. No new fields are written.

Tests

  • tests/e2e/checkpoint_wrapped_error_int_test.py: 15 scenarios through the local test runner, each run live and replayed (30 tests). The replayed run catches the error, waits so that a new invocation starts, and rethrows. Covered: child context with a failing step, nested child contexts, map item step, two map items with different types, parallel branch step, two parallel branches with different types, wait_for_condition and invoke inside a child context, wait_for_callback submitter failure, external failure with type com.acme.PaymentError, "Error", missing type and blank type, callback timeout, and a plain step. Each test asserts every failed operation's checkpointed ErrorType, the execution ErrorType and message, and that the error user code sees has the same class, error_type, message, data, stack trace and cause on the first run and on replay. Without the source change, 24 of the 30 fail.
  • tests/lambda_service_test.py: unwrap unit tests for every wrapper class, a cause walk through wrapper types, a user cause, a chain that ends at a wrapper, the 16-step bound (16 wrappers resolve, the 17th is reported), a cause cycle, and the errors that are not unwrapped (CallbackTimeoutError, CallbackError, a user subclass of StepError).
  • Tests whose expectations change on purpose: e2e/error_hierarchy_int_test.py (the execution ErrorType is ValueError, and an old StepError context checkpoint reports StepError), e2e/invoke_propagation_int_test.py (the execution ErrorType is the invoked function's type), context_test.py (CallbackExternalError.error_type, plus a mock state for the unrelated-error case), the examples test wait_for_callback/test_wait_for_callback_submitter_failure.py (the execution ErrorType is the submitter's Exception), and two extras microvm/session_test.py tests (item 6).
  • Local runs use Python 3.14 on macOS; CI covers 3.11 to 3.14. Core suite: 1965 passed, coverage 98.86% (context.py, exceptions.py and lambda_service.py at 100%). Testing, examples and conformance packages: 1801 passed, 2 skipped. Extras, otel, microvm-worker and otel conformance packages: 1409 passed. Full hatch run test:cov: see the CI run. hatch fmt --check, hatch run types:check, hatch build and lintcommit pass.

When an SDK wrapper (StepError, ChildContextError, InvokeError,
WaitForConditionError, CallbackExternalError, CallbackSubmitterError
or DurableOperationError) escaped a child context, map item, parallel
branch or wait_for_callback context, the context checkpointed the
wrapper's class as its ErrorType. The type of the error that caused
the failure was lost one level up.

ErrorObject.from_exception now unwraps the SDK's own wrappers (exact
class match, at most 16 steps, stopping at a repeated cause) and
records the wrapped error's type, message, data and stack trace. This
applies to every context checkpoint and to the execution result.
CallbackTimeoutError, CallbackError, SerDesError and other SDK errors
are not unwrapped.

A callback failed externally now carries the sender's ErrorType on
CallbackExternalError.error_type. A missing or blank type keeps the
SDK class; the explicit type "Error" is kept as "Error".

wait_for_callback tells a submitter failure from a callback failure
by reading the checkpoints of the context's two children, so first
run and replay raise the same error. Checkpoints that still hold
StepError or a callback error type surface as before.
@ParidelPooya
ParidelPooya deployed to ai-pr-review-runtime October 22, 2026 16:29 — with GitHub Actions Active
The core now records the error a ChildContextError wraps. A MicroVM
error that leaves a child context of the session handler therefore
reaches the caller with its MicroVM class, and a step failure that
leaves the handler carries the step's error type. Update the session
docs and tests to match.
# error is recovered from which child failed: the submitter step or the
# callback. Both are read from checkpoints, so first run and replay
# raise the same error.
context_operation_id: str = self._peek_next_operation_id()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review · Finding arf_v1_h4qgnm736fmhb4frpc4ll4xnns

[P2] Use the allocated context ID for failure lookup

Peeking does not reserve the operation ID. Two concurrent callers sharing a context can both capture the same ID before run_in_child_context allocates distinct IDs. The second caller then classifies its failure using the first operation's children, potentially raising the wrong callback error and message or leaking ChildContextError. Allocate the identifier once and reuse it for both child execution and failure translation. Add a shared-context concurrency test covering different callback outcomes.

To suppress repeats, reply /ai verdict deferred, /ai verdict false-positive, or /ai verdict out-of-scope on the first nonblank line. Add rationale on later lines. A new /ai verdict accepted allows future repeats again.

Comment on lines +958 to +960
if isinstance(inner, CallbackError):
raise inner
if isinstance(inner, StepError):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review · Finding arf_v1_3uosxfehowzjhkpifltveglt52

[P2] Check child outcomes before applying legacy error classification

The reconstructed cause now reflects the sender's error type, so these checks no longer establish which operation failed. An external callback failure with ErrorType="aws_durable_execution_sdk_python.exceptions.StepError" now raises CallbackSubmitterError despite a successful submitter; a sender type matching CallbackTimeoutError similarly turns an external failure into a timeout. This bypasses the correct exception handlers on both live execution and replay. Consult the child checkpoint statuses first and retain class-based classification only as a fallback when child evidence is unavailable. Add regression cases for sender types matching SDK exception names.

To suppress repeats, reply /ai verdict deferred, /ai verdict false-positive, or /ai verdict out-of-scope on the first nonblank line. Add rationale on later lines. A new /ai verdict accepted allows future repeats again.

@github-actions

Copy link
Copy Markdown
Contributor

Codex AI review

Two confirmed findings at packages/aws-durable-execution-sdk-python/src/aws_durable_execution_sdk_python/context.py:935 and packages/aws-durable-execution-sdk-python/src/aws_durable_execution_sdk_python/context.py:958. Static inspection only; tests were not executed.

Reviewed commit 528f021eae00d9b956ae326a05a16759b1613035. Workflow run

@ParidelPooya

Copy link
Copy Markdown
Contributor Author

Closing in favour of a simpler approach. We no longer try to make error.name identical across SDKs, and the core SDKs stay unchanged. Each exporter will instead write error and its cause in one shared shape, with each language keeping its own type names; the READMEs will document the differences. A new, smaller exporter-only PR replaces this one. The branch is kept, so this can be reopened if needed.

This branch was successfully deployed

1 active deployment
ai-pr-review-runtime — 528f021e Deployed Oct 22, 2026 by zhongkechen via ai-pr-review / Codex review / Generate Codex review #2048
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants