Assert paired `ActorNursery` reap state

`._mark_child_reaped()` previously discarded the reap-request event
without checking that its completion-event peer existed. A one-sided
entry would silently lose process-reap synchronization.

Capture both pops and assert paired presence while allowing the valid
both-absent startup-failure path. Keep an unset request valid because
backend cancellation can reap immediately after registration.

Extend graceful and failed-cancel-ack runtime tests to require all
child and reap mappings empty before `to_actor.run()` returns.

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

Prompt-IO: ai/prompt-io/opencode/20260824T223614Z_88d538e3_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
wkt/to_actor_subpkg
Gud Boi 2026-08-24 18:50:59 -04:00
parent 88d538e3a6
commit 2f86dd1a33
4 changed files with 78 additions and 6 deletions

View File

@ -0,0 +1,35 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: d9d7df2c-7044-463f-8768-ec024718eac9
timestamp: 2026-08-24T22:36:14Z
git_ref: 88d538e3
scope: code
substantive: true
raw_file: 20260824T223614Z_88d538e3_prompt_io.raw.md
---
## Prompt
Continue PR #481 review remediation after committing the shared
`Context.cancel()` deadline fix. The human accepted the proposed
child-reap bookkeeping invariant, asking only that the first fix receive
its own commit plan and commit before this update began.
## Response summary
Check that `ActorNursery` removes its paired reap-coordination entries
together while preserving valid pre-registration and immediate-cancel
paths. Extend the existing real-runtime reap tests to prove all three
child bookkeeping mappings are empty before `to_actor.run()` returns.
## Files changed
- `tractor/runtime/_supervise.py` - assert paired reap-map cleanup.
- `tests/test_to_actor.py` - verify graceful and hard-reap bookkeeping.
## Human edits
The human explicitly accepted this invariant update but directed the
preceding cancellation fix to be planned and committed as a separate
boundary first. No direct source-line edits were made by the human.

View File

@ -0,0 +1,26 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-24T22:36:14Z
git_ref: 88d538e3
diff_cmd: git diff HEAD~1..HEAD
---
Implement the approved PR #481 child-reap bookkeeping update after the
preceding `Context.cancel()` fix was committed separately.
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
`ActorNursery._mark_child_reaped()` captures both reap-coordination
entries and asserts that they are either both present or both absent.
It intentionally does not require the reap-request event to be set,
because backend cancellation can reap immediately after registration.
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
Existing real-runtime graceful and hard-reap tests verify that
`ActorNursery._children`, `ActorNursery._child_reap_requests`, and
`ActorNursery._child_reaped` are all empty before the one-shot call
returns.
Run focused bookkeeping and real-runtime reap tests after the edit.

View File

@ -201,10 +201,10 @@ async def test_spawn_from_caller_nursery(
Previously `to_actor.run()` treated an actor-runtime cancel ack
as process reaping, so the call returned while the child monitor
and its `ActorNursery._children` record remained alive until the
entire nursery exited. The assertion inside the still-open
nursery proves child-process joining and record removal now
complete before the one-shot call returns.
and its `ActorNursery` child/reap bookkeeping remained alive until
the entire nursery exited. The assertions inside the still-open
nursery prove child-process joining and removal from all three
mappings complete before the one-shot call returns.
'''
async with tractor.open_nursery() as an:
@ -214,6 +214,8 @@ async def test_spawn_from_caller_nursery(
an=an,
) == 11
assert not an._children
assert not an._child_reap_requests
assert not an._child_reaped
@tractor_test
@ -231,7 +233,7 @@ async def test_cancel_ack_failure_hard_reaps_child(
gate and then waited forever for a still-running process. This
test forces that exact result without cancelling the actor, caps
the call to detect the former hang and verifies the child monitor
removes its `ActorNursery._children` record before returning.
removes every `ActorNursery` child/reap entry before returning.
'''
async def cancel_without_ack(
@ -256,6 +258,8 @@ async def test_cancel_ack_failure_hard_reaps_child(
an=an,
) == 21
assert not an._children
assert not an._child_reap_requests
assert not an._child_reaped
def test_cancel_actor_timeout_closes_blocked_send():

View File

@ -370,11 +370,18 @@ class ActorNursery:
'''
self._children.pop(uid, None)
self._child_reap_requests.pop(uid, None)
reap_request: trio.Event|None = (
self._child_reap_requests.pop(uid, None)
)
reaped: trio.Event|None = self._child_reaped.pop(
uid,
None,
)
assert (
(reap_request is None)
==
(reaped is None)
)
if reaped is not None:
reaped.set()