diff --git a/ai/prompt-io/opencode/20260824T222033Z_ce38cb6f_prompt_io.md b/ai/prompt-io/opencode/20260824T222033Z_ce38cb6f_prompt_io.md new file mode 100644 index 00000000..a16fcf9b --- /dev/null +++ b/ai/prompt-io/opencode/20260824T222033Z_ce38cb6f_prompt_io.md @@ -0,0 +1,37 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +session: d9d7df2c-7044-463f-8768-ec024718eac9 +timestamp: 2026-08-24T22:20:33Z +git_ref: ce38cb6f +scope: code +substantive: true +raw_file: 20260824T222033Z_ce38cb6f_prompt_io.raw.md +--- + +## Prompt + +Continue the PR #481 review after triage. The human explicitly accepted +the proposed merge-blocking `Context.cancel()` deadline update with +"keep" and required the work to remain limited to that review item. + +## Response summary + +Update `Context.cancel()` so one absolute deadline bounds both shielded +cancel-request publication and acknowledgement waiting. Add a focused +mocked-clock regression for the blocked-publication failure mode and run +the narrow cancellation tests. + +## Files changed + +- `tractor/_context.py` - forward the cancel transaction's absolute + deadline to frame publication. +- `tests/test_to_actor.py` - prove blocked context-cancel publication is + bounded by the shared deadline. + +## Human edits + +The human retained ownership of review scope and explicitly selected +"keep" for this item after receiving keep/defer/drop options. The human +required no unrelated cancellation changes and did not directly edit +source lines. diff --git a/ai/prompt-io/opencode/20260824T222033Z_ce38cb6f_prompt_io.raw.md b/ai/prompt-io/opencode/20260824T222033Z_ce38cb6f_prompt_io.raw.md new file mode 100644 index 00000000..6ce1be50 --- /dev/null +++ b/ai/prompt-io/opencode/20260824T222033Z_ce38cb6f_prompt_io.raw.md @@ -0,0 +1,26 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +timestamp: 2026-08-24T22:20:33Z +git_ref: ce38cb6f +diff_cmd: git diff HEAD~1..HEAD +--- + +Implement the approved PR #481 review update for `Context.cancel()`. +Use one absolute deadline for both cancellation-request frame +publication and acknowledgement waiting, without broadening the change +to unrelated cancellation behavior. + +> `git diff HEAD~1..HEAD -- tractor/_context.py` + +`Context.cancel()` computes one absolute cancellation deadline, uses it +for the outer bounded wait, and forwards it through +`Portal._run_from_ns()` to shielded frame publication. + +> `git diff HEAD~1..HEAD -- tests/test_to_actor.py` + +A deterministic mocked-clock regression arranges a shielded blocked +publication and proves that `Context.cancel()` forwards the same deadline +which bounds the complete cancel transaction. + +Run the focused cancellation deadline regressions after the edit. diff --git a/tests/test_to_actor.py b/tests/test_to_actor.py index a4b18f9c..1b98819a 100644 --- a/tests/test_to_actor.py +++ b/tests/test_to_actor.py @@ -319,6 +319,67 @@ def test_cancel_actor_timeout_closes_blocked_send(): ) +def test_context_cancel_timeout_closes_blocked_send(): + ''' + Bound context-cancel publication and acknowledgement together. + + `Context.cancel()` shields its transaction from outer cancellation, + while `MsgpackTransport.send()` separately shields complete frame + publication. Previously the context's timeout was not forwarded to + that inner shield, so a peer which stopped reading could leave the + cancel task blocked forever instead of respecting `timeout`. + + The fake private RPC records the forwarded absolute deadline and + blocks under a send-like shield until that deadline. The mock clock + advances directly to it; completion and the exact recorded value + prove that publication shares the context's one-second budget. + + ''' + async def main() -> None: + deadlines: list[float] = [] + + async def blocked_cancel( + namespace: str, + function: str, + kwargs: dict[str, object], + cancel_on_startup: bool, + send_deadline: float, + ) -> None: + assert (namespace, function) == ('self', '_cancel_task') + assert kwargs == {'cid': 'blocked-context'} + assert not cancel_on_startup + deadlines.append(send_deadline) + with trio.CancelScope( + deadline=send_deadline, + shield=True, + ): + await trio.sleep_forever() + await trio.lowlevel.checkpoint_if_cancelled() + + peer_aid = tractor.msg.Aid( + name='blocked_peer', + uuid='test', + ) + ctx = object.__new__(tractor.Context) + ctx.chan = SimpleNamespace( + aid=peer_aid, + connected=lambda: True, + transport=SimpleNamespace(maddr='test://blocked'), + ) + ctx.cid = 'blocked-context' + ctx._portal = SimpleNamespace(_run_from_ns=blocked_cancel) + ctx._nsf = NamespacePath.from_ref(add_one) + + await ctx.cancel(timeout=1) + + assert deadlines == [1.] + + trio.run( + main, + clock=MockClock(autojump_threshold=0), + ) + + def _mock_actor_nursery() -> tractor.ActorNursery: an = object.__new__(tractor.ActorNursery) an._children = {} diff --git a/tractor/_context.py b/tractor/_context.py index 214eea12..0b383d92 100644 --- a/tractor/_context.py +++ b/tractor/_context.py @@ -1097,7 +1097,12 @@ class Context: ) cid: str = self.cid - with trio.move_on_after(timeout) as cs: + cancel_deadline: float = ( + trio.current_time() + + + timeout + ) + with trio.move_on_at(cancel_deadline) as cs: cs.shield = True log.cancel( header @@ -1113,6 +1118,7 @@ class Context: '_cancel_task', kwargs={'cid': cid}, cancel_on_startup=False, + send_deadline=cancel_deadline, ) if cs.cancelled_caught: