Repository navigation
fix: checkpoint the wrapped error for failed contexts #807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
69af454
9322e90
90b12ea
528f021
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -305,25 +305,45 @@ def result(self) -> T | None: | |
| def _raise_terminal_failure( | ||
| self, checkpointed_result: CheckpointedResult | ||
| ) -> NoReturn: | ||
| """Raise the graded error for a terminal callback failure. | ||
| _raise_callback_terminal_failure(checkpointed_result) | ||
|
|
||
| TIMED_OUT -> CallbackTimeoutError; any other non-succeeded terminal | ||
| (external FAILED, cancelled, stopped) -> CallbackExternalError. | ||
| Carries the message/data/stack_trace; the external error's own type | ||
| stays on the callback operation's details. | ||
| """ | ||
| error: ErrorObject | None = checkpointed_result.error | ||
| is_timeout: bool = checkpointed_result.is_timed_out() | ||
| error_cls: type[CallbackError] = ( | ||
| CallbackTimeoutError if is_timeout else CallbackExternalError | ||
| ) | ||
| default_message: str = "Callback timed out" if is_timeout else "Callback failed" | ||
| message: str = error.message if error and error.message else default_message | ||
| raise error_cls( | ||
| message=message, | ||
| data=error.data if error else None, | ||
| stack_trace=error.stack_trace if error else None, | ||
| ) | ||
|
|
||
| def _is_blank_type(error_type: str | None) -> bool: | ||
| """A missing type, or one made only of ASCII whitespace.""" | ||
| return error_type is None or all(ch in " \t\n\r\f\v" for ch in error_type) | ||
|
|
||
|
|
||
| def _raise_callback_terminal_failure( | ||
| checkpointed_result: CheckpointedResult, | ||
| ) -> NoReturn: | ||
| """Raise the graded error for a terminal callback failure. | ||
|
|
||
| TIMED_OUT -> CallbackTimeoutError; any other non-succeeded terminal | ||
| (external FAILED, cancelled, stopped) -> CallbackExternalError. | ||
| Carries the message/data/stack_trace. An external failure also carries | ||
| the sender's ErrorType as ``error_type``. | ||
| """ | ||
| error: ErrorObject | None = checkpointed_result.error | ||
| is_timeout: bool = checkpointed_result.is_timed_out() | ||
| error_cls: type[CallbackError] = ( | ||
| CallbackTimeoutError if is_timeout else CallbackExternalError | ||
| ) | ||
| default_message: str = "Callback timed out" if is_timeout else "Callback failed" | ||
| message: str = error.message if error and error.message else default_message | ||
| # An external failure keeps the sender's ErrorType, the explicit type | ||
| # "Error" included, so it is recorded and surfaced as that type. Only a | ||
| # missing or blank type falls back to the SDK class. | ||
| error_type: str | None = ( | ||
| error.type | ||
| if not is_timeout and error is not None and not _is_blank_type(error.type) | ||
| else None | ||
| ) | ||
| raise error_cls( | ||
| message=message, | ||
| error_type=error_type, | ||
| data=error.data if error else None, | ||
| stack_trace=error.stack_trace if error else None, | ||
| ) | ||
|
|
||
|
|
||
| class DurableContext(DurableContextProtocol): | ||
|
|
@@ -908,29 +928,65 @@ def wait_for_callback( | |
| def wait_in_child_context(context: DurableContext): | ||
| return wait_for_callback_handler(context, submitter, step_name, config) | ||
|
|
||
| # run_in_child_context is generic and raises ChildContextError with the | ||
| # inner failure on __cause__; translate to callback-typed errors here, at | ||
| # the call site, so it re-fires identically on replay. | ||
| # The context records the wrapped error's type, so the callback-typed | ||
| # 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() | ||
| try: | ||
| return self.run_in_child_context( | ||
| wait_in_child_context, | ||
| step_name, | ||
| ChildConfig(sub_type=OperationSubType.WAIT_FOR_CALLBACK), | ||
| ) | ||
| except ChildContextError as e: | ||
| inner: BaseException | None = e.__cause__ | ||
| # Callback failures (external/timeout/base) pass through. | ||
| if isinstance(inner, CallbackError): | ||
| raise inner | ||
| # A failed submitter step surfaces as CallbackSubmitterError, keeping | ||
| # the step's data/stack_trace (the StepError stays as its cause). | ||
| if isinstance(inner, StepError): | ||
| raise CallbackSubmitterError( | ||
| inner.message or "Callback submitter failed", | ||
| data=inner.data, | ||
| stack_trace=inner.stack_trace, | ||
| ) from inner | ||
| raise | ||
| self._raise_wait_for_callback_error(e, context_operation_id) | ||
|
|
||
| def _raise_wait_for_callback_error( | ||
| self, error: ChildContextError, context_operation_id: str | ||
| ) -> NoReturn: | ||
| """Translate a failed wait_for_callback context to a callback error. | ||
|
|
||
| A failed submitter step surfaces as CallbackSubmitterError and a | ||
| callback failure as CallbackExternalError or CallbackTimeoutError. | ||
| Checkpoints written before the context recorded the wrapped error hold | ||
| the callback error or StepError itself; those surface as before. | ||
| """ | ||
| inner: BaseException | None = error.__cause__ | ||
| # The context holds a callback error type: a timeout, a callback | ||
| # failure without a sender type, or the base CallbackError. | ||
| if isinstance(inner, CallbackError): | ||
| raise inner | ||
| if isinstance(inner, StepError): | ||
|
Comment on lines
+958
to
+960
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Codex AI review · Finding [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 To suppress repeats, reply |
||
| # The context holds StepError itself. | ||
| raise CallbackSubmitterError( | ||
| inner.message or "Callback submitter failed", | ||
| data=inner.data, | ||
| stack_trace=inner.stack_trace, | ||
| ) from inner | ||
| namespace = OperationIdNamespace(prefix=context_operation_id) | ||
| # wait_for_callback_handler creates the callback first, then the step. | ||
| callback_result: CheckpointedResult = self.state.get_checkpoint_result( | ||
| namespace.create_id_for_step(1) | ||
| ) | ||
| submitter_result: CheckpointedResult = self.state.get_checkpoint_result( | ||
| namespace.create_id_for_step(2) | ||
| ) | ||
| if submitter_result.is_failed(): | ||
| raise CallbackSubmitterError( | ||
| error.message or "Callback submitter failed", | ||
| error_type=error.error_type, | ||
| data=error.data, | ||
| stack_trace=error.stack_trace, | ||
| ) from inner | ||
| if ( | ||
| callback_result.is_failed() | ||
| or callback_result.is_cancelled() | ||
| or callback_result.is_timed_out() | ||
| or callback_result.is_stopped() | ||
| ): | ||
| _raise_callback_terminal_failure(callback_result) | ||
| raise error | ||
|
|
||
| def wait_for_condition( | ||
| self, | ||
|
|
||
There was a problem hiding this comment.
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_contextallocates distinct IDs. The second caller then classifies its failure using the first operation's children, potentially raising the wrong callback error and message or leakingChildContextError. 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-scopeon the first nonblank line. Add rationale on later lines. A new/ai verdict acceptedallows future repeats again.