Clarify cancellation race test contracts

Cancellation tests covered distinct hard-reap, deadline and scheduler
contracts, but their names and prose blurred public boolean outcomes,
transport closure and expected timeout behavior.

Document each deterministic unit seam and race ordering, distinguish
the five-second hang ceiling from normal teardown, and rename deadline
tests around their shared request-and-ack budget.

Review: PR #481 (goodboy)
https://github.com/goodboy/tractor/pull/481#pullrequestreview-5012942328

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
wkt/to_actor_subpkg
Gud Boi 2026-08-25 01:17:04 -04:00
parent a99a4c9353
commit 773370a423
1 changed files with 39 additions and 11 deletions

View File

@ -212,13 +212,18 @@ async def test_cancel_ack_failure_hard_reaps_child(
''' '''
Escalate a failed cancel acknowledgement and reap the child. Escalate a failed cancel acknowledgement and reap the child.
`Portal.cancel_actor()` can return `False` when its transport is `Portal.cancel_actor()` catches `TransportClosed` and returns
already closed without confirming runtime cancellation. The old `False` when it can not confirm runtime cancellation. The mock
one-shot path ignored that result, released the nursery-wide join represents that public post-transport-failure result, so no
gate and then waited forever for a still-running process. This underlying exception remains to bubble through `to_actor.run()`.
test forces that exact result without cancelling the actor, caps
the call to detect the former hang and verifies the child monitor The old one-shot path ignored `False`, released the nursery-wide
removes every `ActorNursery` child/reap entry before returning. join gate and then waited forever for a still-running process.
`_cancel_and_reap_child()` must instead hard-kill and join the child.
The five-second scope is only a generous CI hang ceiling: normal
teardown returns much sooner, while expiry fails the test. Final
assertions prove the child monitor removes every `ActorNursery`
child/reap entry before returning.
''' '''
async def cancel_without_ack( async def cancel_without_ack(
@ -229,6 +234,8 @@ async def test_cancel_ack_failure_hard_reaps_child(
assert raise_on_timeout assert raise_on_timeout
return False return False
# Model `Portal.cancel_actor()` after it catches `TransportClosed`;
# there is no transport exception left for `run()` to re-raise.
monkeypatch.setattr( monkeypatch.setattr(
tractor.Portal, tractor.Portal,
'cancel_actor', 'cancel_actor',
@ -236,6 +243,8 @@ async def test_cancel_ack_failure_hard_reaps_child(
) )
async with tractor.open_nursery() as an: async with tractor.open_nursery() as an:
# Expiry means hard reaping hung; five seconds is not the
# expected duration of the successful path.
with trio.fail_after(5): with trio.fail_after(5):
assert await to_actor.run( assert await to_actor.run(
add_one, add_one,
@ -247,9 +256,9 @@ async def test_cancel_ack_failure_hard_reaps_child(
assert not an._child_reaped assert not an._child_reaped
def test_cancel_actor_timeout_closes_blocked_send(): def test_cancel_actor_shares_request_and_ack_deadline():
''' '''
Thread one absolute cancel deadline into shielded frame publication. Share one cancel deadline across request publication and ack waiting.
The cancel RPC's outer timeout cannot penetrate a complete-frame The cancel RPC's outer timeout cannot penetrate a complete-frame
shield. The fake private RPC applies the forwarded send deadline to shield. The fake private RPC applies the forwarded send deadline to
@ -257,6 +266,11 @@ def test_cancel_actor_timeout_closes_blocked_send():
bounded `ActorTooSlowError` and the recorded absolute deadline prove bounded `ActorTooSlowError` and the recorded absolute deadline prove
publication and acknowledgement share one timeout budget. publication and acknowledgement share one timeout budget.
A real subactor can not deterministically stall the caller's
outbound frame at this exact boundary. Actual partial-frame stream
closure is covered by
`test_transport_send_deadline_closes_partial_frame()`.
''' '''
class ConnectedChannel: class ConnectedChannel:
def __init__(self) -> None: def __init__(self) -> None:
@ -271,6 +285,8 @@ def test_cancel_actor_timeout_closes_blocked_send():
async def main() -> None: async def main() -> None:
channel = ConnectedChannel() channel = ConnectedChannel()
# `Portal.__init__()` requires live actor-runtime state; this
# unit seam needs only its channel and private RPC method.
portal = object.__new__(tractor.Portal) portal = object.__new__(tractor.Portal)
portal._chan = channel portal._chan = channel
deadlines: list[float] = [] deadlines: list[float] = []
@ -308,7 +324,7 @@ def test_cancel_actor_timeout_closes_blocked_send():
) )
def test_context_cancel_timeout_closes_blocked_send(): def test_context_cancel_shares_request_and_ack_deadline():
''' '''
Bound context-cancel publication and acknowledgement together. Bound context-cancel publication and acknowledgement together.
@ -322,6 +338,8 @@ def test_context_cancel_timeout_closes_blocked_send():
blocks under a send-like shield until that deadline. The mock clock blocks under a send-like shield until that deadline. The mock clock
advances directly to it; completion and the exact recorded value advances directly to it; completion and the exact recorded value
prove that publication shares the context's one-second budget. prove that publication shares the context's one-second budget.
The transport suite separately proves that deadline expiry closes
a stream after partial frame publication.
''' '''
async def main() -> None: async def main() -> None:
@ -349,10 +367,16 @@ def test_context_cancel_timeout_closes_blocked_send():
name='blocked_peer', name='blocked_peer',
uuid='test', uuid='test',
) )
def connected() -> bool:
return True
# `Context.__init__()` requires live actor/channel registration;
# this unit seam supplies only state consumed by `.cancel()`.
ctx = object.__new__(tractor.Context) ctx = object.__new__(tractor.Context)
ctx.chan = SimpleNamespace( ctx.chan = SimpleNamespace(
aid=peer_aid, aid=peer_aid,
connected=lambda: True, connected=connected,
transport=SimpleNamespace(maddr='test://blocked'), transport=SimpleNamespace(maddr='test://blocked'),
) )
ctx.cid = 'blocked-context' ctx.cid = 'blocked-context'
@ -392,6 +416,8 @@ def test_late_child_registration_observes_cancel():
''' '''
an = _mock_actor_nursery() an = _mock_actor_nursery()
# `ActorNursery.cancel()` has set its sticky flag after taking the
# old `_children` snapshot but before backend registration resumes.
an._cancel_called = True an._cancel_called = True
aid = tractor.msg.Aid( aid = tractor.msg.Aid(
name='late_child', name='late_child',
@ -519,6 +545,8 @@ def test_late_child_reap_registration_is_released():
an._child_reap_requests = {} an._child_reap_requests = {}
an._child_reaped = {} an._child_reaped = {}
# Nursery teardown publishes its reap request while the child
# monitor is checkpointed before per-child event registration.
an._join_procs.set() an._join_procs.set()
aid = tractor.msg.Aid( aid = tractor.msg.Aid(
name='late_child', name='late_child',