From 90466ec0165d1184900bea673269d57eb94ac9a0 Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 29 May 2026 19:20:29 -0400 Subject: [PATCH 1/3] Add `start_or_cancel()` to `trionics._taskc` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 30e15925bab7f047d4f77d47059c0ab5fd55353e) (cherry picked from commit 2b4589b1ee8106f0969c1c22cbb44bd09da007f9) --- tractor/trionics/__init__.py | 1 + tractor/trionics/_taskc.py | 53 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/tractor/trionics/__init__.py b/tractor/trionics/__init__.py index 2e91aa30..7271b0f3 100644 --- a/tractor/trionics/__init__.py +++ b/tractor/trionics/__init__.py @@ -36,4 +36,5 @@ from ._beg import ( ) from ._taskc import ( maybe_raise_from_masking_exc as maybe_raise_from_masking_exc, + start_or_cancel as start_or_cancel, ) diff --git a/tractor/trionics/_taskc.py b/tractor/trionics/_taskc.py index 7d00b4fd..e50b28f9 100644 --- a/tractor/trionics/_taskc.py +++ b/tractor/trionics/_taskc.py @@ -27,6 +27,9 @@ from types import ( TracebackType, ) from typing import ( + Any, + Awaitable, + Callable, Type, TYPE_CHECKING, ) @@ -293,5 +296,55 @@ async def maybe_raise_from_masking_exc( if raise_unmasked: raise exc_ctx from exc_match + +async def start_or_cancel( + nursery: trio.Nursery, + async_fn: Callable[..., Awaitable[Any]], + *args, + name: object = None, + +) -> Any: + ''' + Like `trio.Nursery.start()` but DON'T mask an out-of-band + cancellation as a (lossy) startup failure. + + `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 very + 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 + with an opaque, root-cause-detached `RuntimeError`. + + This wrapper re-surfaces any ambient (effective, hence + ancestor-inclusive) cancellation via + `trio.lowlevel.checkpoint_if_cancelled()` so the real + `trio.Cancelled` (carrying trio's auto-generated reason which + points at the true root exc) propagates instead. Only when we + are NOT under cancellation is the "didn't call `.started()`" + `RuntimeError` a genuine startup-protocol bug worth surfacing, + so it's re-raised as-is in that case. + + ''' + try: + return await nursery.start( + async_fn, + *args, + name=name, + ) + except RuntimeError as rte: + if ( + rte.args + and + 'started' in rte.args[0] + ): + # re-raises the in-flight `trio.Cancelled` IFF we're + # under effective cancellation; else a cheap no-op and + # we fall through to re-raise the genuine startup RTE. + await trio.lowlevel.checkpoint_if_cancelled() + + raise + else: raise From a722d535d97c1aa8bc4e09563ddceb7dbc628c19 Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 29 May 2026 21:10:23 -0400 Subject: [PATCH 2/3] Fix dropped `for/else` re-raise in masking CM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 325574cc0790623de48da86d229a2c60da1553fb) (cherry picked from commit 0b8033fdaaee8cd6f2439767a3c4f76daa0acebf) --- tractor/trionics/_taskc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tractor/trionics/_taskc.py b/tractor/trionics/_taskc.py index e50b28f9..13310ff1 100644 --- a/tractor/trionics/_taskc.py +++ b/tractor/trionics/_taskc.py @@ -296,6 +296,9 @@ async def maybe_raise_from_masking_exc( if raise_unmasked: raise exc_ctx from exc_match + else: + raise + async def start_or_cancel( nursery: trio.Nursery, @@ -345,6 +348,3 @@ async def start_or_cancel( await trio.lowlevel.checkpoint_if_cancelled() raise - - else: - raise From 3fbc77c812d89cf861076b9dce70270d4c9dc6fc Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 24 Jun 2026 13:34:37 -0400 Subject: [PATCH 3/3] Tighten `start_or_cancel`'s startup-RTE match 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) https://github.com/goodboy/tractor/pull/464#pullrequestreview-4548203343 (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/trionics/_taskc.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tractor/trionics/_taskc.py b/tractor/trionics/_taskc.py index 13310ff1..7cb5fff6 100644 --- a/tractor/trionics/_taskc.py +++ b/tractor/trionics/_taskc.py @@ -340,7 +340,16 @@ async def start_or_cancel( if ( rte.args and - 'started' in rte.args[0] + isinstance(rte.args[0], str) + and + # match trio's *exact* "child exited without calling + # task_status.started()" wording — a bare `'started'` + # substring would also match a child task's OWN + # `RuntimeError(...started...)` and (under cancellation) + # demote it to a `Cancelled`, losing the real error. The + # `isinstance` guard also avoids a `TypeError` when + # `rte.args[0]` isn't a `str`. + 'child exited without calling' in rte.args[0] ): # re-raises the in-flight `trio.Cancelled` IFF we're # under effective cancellation; else a cheap no-op and