Skip to content

Add start_or_cancel() to trionics._taskc#464

Merged
goodboy merged 3 commits into
mainfrom
trionics_start_or_cancel
Jun 24, 2026
Merged

Add start_or_cancel() to trionics._taskc#464
goodboy merged 3 commits into
mainfrom
trionics_start_or_cancel

Conversation

@goodboy

@goodboy goodboy commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Add start_or_cancel() to trionics._taskc

Motivation

trio.Nursery.start() collapses an out-of-band cancellation into a
lossy startup error. When the started task exits before calling
task_status.started() — including the very common case where it was
cancelled out-of-band by an ancestor cancel-scope erroring or
cancelling — trio raises a generic RuntimeError("child exited without calling task_status.started()") and the original
trio.Cancelled is swallowed. The caller is left holding an opaque,
root-cause-detached RuntimeError instead of the real cancellation
(whose trio-generated reason points at the true root exc).

Summary of changes

  • Add start_or_cancel() — a trio.Nursery.start() wrapper that, on
    the "didn't call .started()" RuntimeError, re-surfaces any
    ambient (effective, hence ancestor-inclusive) cancellation via
    trio.lowlevel.checkpoint_if_cancelled() so the real
    trio.Cancelled propagates. Only when not under cancellation is
    that RuntimeError a genuine startup-protocol bug, so it's
    re-raised as-is. Exported from tractor.trionics.
  • Fix a dropped for/else re-raise in maybe_raise_from_masking_exc
    (the masking CM) — the trailing else: raise belongs to the loop's
    else clause and had been mis-attached.

TODOs before landing

  • Stacked PR (base main) — this is the bottom of the remaining
    stack and lands first; trio_033_upgradecustom_log_levels_api
    mtf_backend stack above it.

Links

  • #457 — per-actor setproctitle (closed; rode along the
    stack, drop before landing)

(this pr content was generated in some part by claude-code)

goodboy added 2 commits June 22, 2026 16:28
Wrapper around `trio.Nursery.start()` that DOESN'T mask
out-of-band cancellation as a lossy startup failure.
Picks the right re-raise: ambient `Cancelled` when
present, the genuine startup-protocol `RuntimeError`
otherwise.

The problem,
- `trio.Nursery.start()` raises a generic
  `RuntimeError("child exited without calling
  task_status.started()")` whenever the started task
  exits BEFORE calling `task_status.started()` —
  INCLUDING the common case where the child was
  cancelled out-of-band by an *ancestor* cancel-scope
  erroring/cancelling.
- In that case the original `trio.Cancelled` is
  swallowed and the caller is left w/ an opaque,
  root-cause-detached `RuntimeError`.

The fix,
- Catch the "...started" RTE.
- `await trio.lowlevel.checkpoint_if_cancelled()` —
  re-raises the in-flight `Cancelled` IFF we're under
  effective cancellation (ancestor-inclusive), carrying
  trio's auto-generated reason which points at the true
  root exc.
- If we're NOT cancelled the `checkpoint_if_cancelled()`
  is a cheap no-op and we fall through to re-raise the
  genuine startup-protocol RTE.

Re-export from `tractor.trionics` so callers don't have
to reach into `_taskc`.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code

(cherry picked from commit 30e1592)
(cherry picked from commit 2b4589b)
`30e15925` ("Add `start_or_cancel()` to `trionics._taskc`")
inserted `async def start_or_cancel()` — whose body opens its
own col-4 `try:` — immediately before the trailing `else:
raise`. Because the edit was a pure insertion (0 deletions),
the *same* `else: raise` lines were silently REPARENTED: they
used to be the `for exc_match in matching: ... else: raise`
of `maybe_raise_from_masking_exc`, but now bind to
`start_or_cancel`'s `try/except` where they're unreachable
dead code.

Net effect: `maybe_raise_from_masking_exc` lost the `for/else`
re-raise of the un-masked exception, so a masked child
cancellation gets swallowed instead of surfaced.

- restore the `for/else: raise` to `maybe_raise_from_masking_exc`
- drop the now-dead `else: raise` from `start_or_cancel`

Surfaced as 2 deterministic failures in
`test_sigint_closes_lifetime_stack[wait_for_ctx-bg_aio_task-
send_SIGINT_to=child-*]` (the SIGINT-to-child "silent-abandon"
regime). Bisected with `trio` held at `0.29.0`: clean at
`9c36363b` (0/8), broken at `30e15925` (8/8), fixed (0/8).
NOT a `trio` (0.29↔0.33 identical) nor logging-plugin
regression.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code

(cherry picked from commit 325574c)
(cherry picked from commit 0b8033f)
Copilot AI review requested due to automatic review settings June 22, 2026 21:19

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR extends tractor.trionics with a start_or_cancel() helper to preserve out-of-band trio.Cancelled when a started task exits before calling task_status.started(), and it also adjusts maybe_raise_from_masking_exc’s loop/else re-raise behavior.

Changes:

  • Add start_or_cancel() wrapper around trio.Nursery.start() that re-surfaces effective cancellation via trio.lowlevel.checkpoint_if_cancelled().
  • Export start_or_cancel() from tractor.trionics.
  • Adjust maybe_raise_from_masking_exc control-flow around the post-loop re-raise.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
tractor/trionics/_taskc.py Adds start_or_cancel() and modifies maybe_raise_from_masking_exc’s loop/else re-raise behavior.
tractor/trionics/__init__.py Re-exports start_or_cancel from tractor.trionics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 299 to 300
else:
raise

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Verified empirically — this is a false positive. In a generator / @acm context manager the exception thrown in at yield stays the active exception for the frame, and there is no await between the except-block exit and this raise to clear exc_info, so the bare raise correctly re-raises the original group. Tested both sync and async @acm: each re-raises the thrown-in exception (no RuntimeError: No active exception to reraise). Keeping as-is.

Comment on lines +340 to +344
if (
rte.args
and
'started' in rte.args[0]
):

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch — accepting. Guarding with isinstance(rte.args[0], str) and also tightening the match from the bare 'started' substring to trio's exact 'child exited without calling' wording (so a child task's own RuntimeError that happens to contain "started" can't get demoted to Cancelled under cancellation). Fix incoming on this branch.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

📎 Fixed in 3fbc77c8 (Tighten start_or_cancel's startup-RTE match) — once trionics_start_or_cancel is re-pushed: 3fbc77c8

The `'started' in rte.args[0]` check was too loose: it
matched a child task's OWN `RuntimeError(...started...)`
(not just trio's "child exited without calling
task_status.started()"), and would `TypeError` on a
non-`str` `args[0]`. Guard with `isinstance(..., str)` and
match trio's exact wording so a real child error can't be
demoted to `Cancelled` under cancellation.

Review: PR #464 (copilot-pull-request-reviewer)
#464 (review)

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
@goodboy goodboy added enhancement New feature or request api supervision cancellation SC teardown semantics and anti-zombie semantics trionics trio_updates keeping up to date with our parent labels Jun 24, 2026
@goodboy goodboy merged commit fd41f88 into main Jun 24, 2026
4 checks passed
@goodboy goodboy deleted the trionics_start_or_cancel branch June 24, 2026 19:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api cancellation SC teardown semantics and anti-zombie semantics enhancement New feature or request supervision trio_updates keeping up to date with our parent trionics

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants