Add `start_or_cancel()` to `trionics._taskc`
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
trionics.start_or_cancel
parent
5b3c2e3762
commit
30e15925ba
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue