Compare commits
12 Commits
ce38cb6f0e
...
bf46f5cc5e
| Author | SHA1 | Date |
|---|---|---|
|
|
bf46f5cc5e | |
|
|
773370a423 | |
|
|
a99a4c9353 | |
|
|
b58889f625 | |
|
|
0a580df63d | |
|
|
617ca1de43 | |
|
|
9373e9434d | |
|
|
ce430fca64 | |
|
|
e42ecb559d | |
|
|
5327b25e1b | |
|
|
2f86dd1a33 | |
|
|
88d538e3a6 |
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
session: d9d7df2c-7044-463f-8768-ec024718eac9
|
||||
timestamp: 2026-08-24T22:53:56Z
|
||||
git_ref: 2f86dd1a
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260824T225356Z_2f86dd1a_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Continue PR #481 review remediation after committing the paired
|
||||
`ActorNursery` reap-state invariant. The human selected "keep" for the
|
||||
reviewer's request to factor a duplicated debugger predicate in
|
||||
`_try_cancel_then_kill()`.
|
||||
|
||||
## Response summary
|
||||
|
||||
Factor the child/tree debugger predicate into a local sampler used both
|
||||
before and after the cancel-RPC checkpoint. Preserve dynamic debugger
|
||||
lock re-evaluation and its distinction from root-wide debug mode.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/runtime/_supervise.py` - factor the duplicated debugger
|
||||
predicate without changing cancellation behavior.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human explicitly selected "keep" after receiving keep/defer/drop
|
||||
options for this isolated review item. During commit-plan review, the
|
||||
agent found that a single pre-checkpoint snapshot could become stale;
|
||||
the human selected a local helper which re-evaluates the lock after the
|
||||
cancel RPC. The human then considered moving the predicate into
|
||||
`.devx.debug` and accepted keeping it local after confirming that no
|
||||
existing helper shares its supervisor-owned semantics. No direct
|
||||
source-line edits were made by the human.
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-24T22:53:56Z
|
||||
git_ref: 2f86dd1a
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
Implement the approved PR #481 review refactor in
|
||||
`_try_cancel_then_kill()` without changing debugger behavior.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
|
||||
|
||||
Compute the child/tree debugger predicate once, reuse it in the broader
|
||||
hard-kill protection predicate, and pass it directly to
|
||||
`debug.maybe_wait_for_debugger()`.
|
||||
|
||||
Run focused debugger/cancellation coverage and lint after the edit.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
session: d9d7df2c-7044-463f-8768-ec024718eac9
|
||||
timestamp: 2026-08-24T23:39:57Z
|
||||
git_ref: 5327b25e
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260824T233957Z_5327b25e_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Continue PR #481 review remediation after committing the debugger-state
|
||||
sampler. The human selected "keep" for the paired review request to use
|
||||
`Aid` objects as keys in the newly added reap-coordination maps.
|
||||
|
||||
## Response summary
|
||||
|
||||
Migrate only `ActorNursery._child_reap_requests` and
|
||||
`ActorNursery._child_reaped` to `Aid` keys. Preserve the legacy
|
||||
`ActorNursery._children` `.uid` key and pass full actor identities
|
||||
through the narrow process-monitor bookkeeping path.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/runtime/_supervise.py` - key fresh reap maps by `Aid`.
|
||||
- `tractor/spawn/_spawn.py` - pass `Aid` into completed-reap cleanup.
|
||||
- `tests/test_to_actor.py` - exercise `Aid` registration keys.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human explicitly selected "keep" after reviewing the scope,
|
||||
performance, and mutability tradeoffs. The human retained the legacy
|
||||
tuple key for `_children` and accepted `Aid` for only the two fresh
|
||||
private mappings. No direct source-line edits were made by the human.
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-24T23:39:57Z
|
||||
git_ref: 5327b25e
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
Implement the approved PR #481 review update which uses `Aid` keys for
|
||||
the two fresh `ActorNursery` reap-coordination maps while preserving the
|
||||
legacy `.uid` key for `ActorNursery._children`.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
|
||||
|
||||
Type and access `_child_reap_requests` and `_child_reaped` by `Aid`.
|
||||
Pass full actor identities through registration, cancellation, and
|
||||
completed-reap bookkeeping, deriving `.uid` only for `_children`.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_spawn.py`
|
||||
|
||||
Forward `subactor.aid` when publishing completed process teardown.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
|
||||
|
||||
Update deterministic registration tests to exercise `Aid` map keys.
|
||||
|
||||
Run focused registration/reaping tests and the full `to_actor` suite.
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
session: d9d7df2c-7044-463f-8768-ec024718eac9
|
||||
timestamp: 2026-08-25T01:57:42Z
|
||||
git_ref: e42ecb55
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260825T015742Z_e42ecb55_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Continue PR #481 review remediation after committing the `Aid` reap-map
|
||||
migration. The human selected "keep" for comments explaining why both
|
||||
spawn backends provisionally register children with `portal=None`.
|
||||
|
||||
## Response summary
|
||||
|
||||
Document that a child has no `Portal` until its IPC handshake yields a
|
||||
`Channel`, and make `portal=None` explicit at both registration calls.
|
||||
Identify the later replacement of each provisional entry with
|
||||
`Portal(chan)`. Update the MP registration test double to accept and
|
||||
assert the explicit provisional portal state. Name every registration
|
||||
argument consistently in both backends.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/spawn/_mp.py` - clarify provisional MP registration.
|
||||
- `tractor/spawn/_trio.py` - clarify provisional Trio registration.
|
||||
- `tests/test_to_actor.py` - model explicit provisional registration.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human explicitly selected "keep" after receiving keep/defer/drop
|
||||
options for this paired clarification. During local review, the human
|
||||
then requested that `subactor` and `proc` also be passed by name in both
|
||||
backend calls. No direct source-line edits were made by the human.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-25T01:57:42Z
|
||||
git_ref: e42ecb55
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
Implement the approved PR #481 clarification for provisional child
|
||||
registration in both process-spawn backends.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_mp.py`
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_trio.py`
|
||||
|
||||
Explain that `portal=None` is provisional because no `Portal` can exist
|
||||
until the child completes its IPC handshake and returns a `Channel`.
|
||||
Use an explicit keyword argument and identify the later replacement with
|
||||
`Portal(chan)`.
|
||||
|
||||
Run lint and the full `to_actor` runtime suite.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
session: d9d7df2c-7044-463f-8768-ec024718eac9
|
||||
timestamp: 2026-08-25T02:13:19Z
|
||||
git_ref: ce430fca
|
||||
scope: code
|
||||
substantive: true
|
||||
raw_file: 20260825T021319Z_ce430fca_prompt_io.raw.md
|
||||
---
|
||||
|
||||
## Prompt
|
||||
|
||||
Continue PR #481 review remediation after committing provisional child
|
||||
registration clarifications. The human selected "keep" for inlining the
|
||||
guarded `functools.Placeholder` lookup with a walrus assignment.
|
||||
|
||||
## Response summary
|
||||
|
||||
Remove the standalone placeholder assignment and bind the optional
|
||||
Python 3.14 sentinel directly in the existing conditional while
|
||||
preserving compatibility behavior.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `tractor/to_actor/_api.py` - inline placeholder feature detection.
|
||||
|
||||
## Human edits
|
||||
|
||||
The human explicitly selected "keep" after receiving keep/defer/drop
|
||||
options for this isolated cleanup. No direct source-line edits were made
|
||||
by the human.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
model: openai/gpt-5.6-sol
|
||||
service: opencode
|
||||
timestamp: 2026-08-25T02:13:19Z
|
||||
git_ref: ce430fca
|
||||
diff_cmd: git diff HEAD~1..HEAD
|
||||
---
|
||||
|
||||
Implement the approved PR #481 review cleanup for Python 3.14 partial
|
||||
placeholder detection.
|
||||
|
||||
> `git diff HEAD~1..HEAD -- tractor/to_actor/_api.py`
|
||||
|
||||
Inline the guarded `functools.Placeholder` lookup into the existing
|
||||
condition with a walrus assignment, preserving fallback behavior when
|
||||
the attribute is unavailable.
|
||||
|
||||
Run partial/placeholder normalization tests and the full `to_actor`
|
||||
suite.
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
'''
|
||||
Shared helpers for actor-runtime test suites.
|
||||
|
||||
'''
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
|
||||
import tractor
|
||||
|
||||
|
||||
class CancellationMarkers:
|
||||
'''
|
||||
Mark a test endpoint's lifetime without cleanup checkpoints.
|
||||
|
||||
'''
|
||||
def __init__(
|
||||
self,
|
||||
started_path: str,
|
||||
cancelled_path: str,
|
||||
) -> None:
|
||||
self.started_path = started_path
|
||||
self.cancelled_path = cancelled_path
|
||||
|
||||
def __enter__(self) -> None:
|
||||
Path(self.started_path).touch()
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException]|None,
|
||||
exc_value: BaseException|None,
|
||||
traceback: TracebackType|None,
|
||||
) -> None:
|
||||
Path(self.cancelled_path).touch()
|
||||
|
||||
|
||||
def non_registration_contexts(
|
||||
actor: tractor.Actor,
|
||||
) -> dict[tuple, str]:
|
||||
'''
|
||||
Snapshot application contexts without registrar-service traffic.
|
||||
|
||||
'''
|
||||
return {
|
||||
key: str(ctx._nsf)
|
||||
for key, ctx in actor._contexts.items()
|
||||
if str(ctx._nsf) != (
|
||||
'tractor.discovery._registry:'
|
||||
'Registrar.register_actor'
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
'''
|
||||
`NamespacePath` Python-object reference tests.
|
||||
|
||||
'''
|
||||
import pytest
|
||||
|
||||
from tractor.msg import ptr as msgptr
|
||||
from tractor.msg.ptr import NamespacePath
|
||||
|
||||
|
||||
def example_target() -> None:
|
||||
'''
|
||||
Provide a module-addressable reference for pointer tests.
|
||||
|
||||
'''
|
||||
|
||||
|
||||
def test_retains_target_ref(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
'''
|
||||
Reuse a retained target ref when splitting its namespace path.
|
||||
|
||||
`NamespacePath.from_ref()` previously discarded `example_target`,
|
||||
so `to_tuple()` imported and resolved the just-created string again.
|
||||
Replacing `resolve_name()` with a failure proves the retained ref
|
||||
supplies the tuple without a redundant lookup.
|
||||
|
||||
'''
|
||||
target = NamespacePath.from_ref(example_target)
|
||||
|
||||
def fail_resolve(name: str) -> object:
|
||||
raise AssertionError(f'unexpected lookup for {name!r}')
|
||||
|
||||
monkeypatch.setattr(
|
||||
msgptr,
|
||||
'resolve_name',
|
||||
fail_resolve,
|
||||
)
|
||||
assert target.to_tuple() == (
|
||||
example_target.__module__,
|
||||
example_target.__name__,
|
||||
)
|
||||
|
|
@ -40,6 +40,11 @@ from tractor._testing import (
|
|||
expect_ctxc,
|
||||
)
|
||||
|
||||
from ._helpers import (
|
||||
CancellationMarkers,
|
||||
non_registration_contexts,
|
||||
)
|
||||
|
||||
# ``Context`` semantics are as follows,
|
||||
# ------------------------------------
|
||||
|
||||
|
|
@ -164,31 +169,18 @@ def test_overrun_error_send_tolerates_transport_close(
|
|||
_state: bool = False
|
||||
|
||||
|
||||
def _non_registration_contexts(
|
||||
actor: Actor,
|
||||
) -> dict[tuple, str]:
|
||||
return {
|
||||
key: str(ctx._nsf)
|
||||
for key, ctx in actor._contexts.items()
|
||||
if str(ctx._nsf) != (
|
||||
'tractor.discovery._registry:'
|
||||
'Registrar.register_actor'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@tractor.context
|
||||
async def startup_cancel_target(
|
||||
ctx: Context,
|
||||
started_path: str,
|
||||
cancelled_path: str,
|
||||
) -> None:
|
||||
Path(started_path).touch()
|
||||
try:
|
||||
with CancellationMarkers(
|
||||
started_path,
|
||||
cancelled_path,
|
||||
):
|
||||
await ctx.started()
|
||||
await trio.sleep_forever()
|
||||
finally:
|
||||
Path(cancelled_path).touch()
|
||||
|
||||
|
||||
async def return_one() -> int:
|
||||
|
|
@ -317,11 +309,13 @@ async def test_cancel_during_context_startup(
|
|||
chan: tractor.Channel,
|
||||
payload: object,
|
||||
hide_tb: bool = False,
|
||||
send_deadline: float = float('inf'),
|
||||
) -> None:
|
||||
await original_send(
|
||||
chan,
|
||||
payload,
|
||||
hide_tb=hide_tb,
|
||||
send_deadline=send_deadline,
|
||||
)
|
||||
if isinstance(payload, tractor.msg.Start):
|
||||
if payload.func == 'startup_cancel_target':
|
||||
|
|
@ -344,7 +338,7 @@ async def test_cancel_during_context_startup(
|
|||
'startup_cancel_worker',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
contexts_before = _non_registration_contexts(actor)
|
||||
contexts_before = non_registration_contexts(actor)
|
||||
monkeypatch.setattr(
|
||||
tractor.Channel,
|
||||
'send',
|
||||
|
|
@ -365,12 +359,12 @@ async def test_cancel_during_context_startup(
|
|||
original_send,
|
||||
)
|
||||
assert cancelled_path.exists()
|
||||
assert _non_registration_contexts(actor) == contexts_before
|
||||
assert non_registration_contexts(actor) == contexts_before
|
||||
assert await portal.run_from_ns(
|
||||
__name__,
|
||||
'return_one',
|
||||
) == 1
|
||||
assert _non_registration_contexts(actor) == contexts_before
|
||||
assert non_registration_contexts(actor) == contexts_before
|
||||
await portal.cancel_actor()
|
||||
|
||||
|
||||
|
|
@ -396,7 +390,7 @@ async def test_start_serialization_error_cleans_context(
|
|||
'serialization_error_worker',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
contexts_before = _non_registration_contexts(actor)
|
||||
contexts_before = non_registration_contexts(actor)
|
||||
with pytest.raises(tractor.MsgTypeError):
|
||||
async with portal.open_context(
|
||||
simple_setup_teardown,
|
||||
|
|
@ -404,7 +398,7 @@ async def test_start_serialization_error_cleans_context(
|
|||
):
|
||||
raise AssertionError('invalid `Start` was accepted')
|
||||
|
||||
assert _non_registration_contexts(actor) == contexts_before
|
||||
assert non_registration_contexts(actor) == contexts_before
|
||||
async with portal.open_context(
|
||||
simple_setup_teardown,
|
||||
data=1,
|
||||
|
|
@ -412,7 +406,7 @@ async def test_start_serialization_error_cleans_context(
|
|||
assert started == 2
|
||||
assert await ctx.wait_for_result() == 'yo'
|
||||
|
||||
assert _non_registration_contexts(actor) == contexts_before
|
||||
assert non_registration_contexts(actor) == contexts_before
|
||||
await portal.cancel_actor()
|
||||
|
||||
|
||||
|
|
@ -437,7 +431,7 @@ async def test_start_module_error_cleans_context(
|
|||
portal: tractor.Portal = await an.start_actor(
|
||||
'module_error_worker',
|
||||
)
|
||||
contexts_before = _non_registration_contexts(actor)
|
||||
contexts_before = non_registration_contexts(actor)
|
||||
with pytest.raises(tractor.RemoteActorError) as excinfo:
|
||||
async with portal.open_context(
|
||||
simple_setup_teardown,
|
||||
|
|
@ -446,7 +440,7 @@ async def test_start_module_error_cleans_context(
|
|||
raise AssertionError('unexposed context was started')
|
||||
|
||||
assert excinfo.value.boxed_type is tractor.ModuleNotExposed
|
||||
assert _non_registration_contexts(actor) == contexts_before
|
||||
assert non_registration_contexts(actor) == contexts_before
|
||||
await portal.cancel_actor()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,15 +20,26 @@ from tractor import (
|
|||
)
|
||||
from tractor._testing import tractor_test
|
||||
from tractor._exceptions import ActorTooSlowError
|
||||
from tractor.msg import ptr as msgptr
|
||||
from tractor.msg.ptr import NamespacePath
|
||||
from tractor.spawn import _mp as mp_spawn
|
||||
from tractor.to_actor import _api as to_actor_api
|
||||
|
||||
from ._helpers import (
|
||||
CancellationMarkers,
|
||||
non_registration_contexts,
|
||||
)
|
||||
|
||||
|
||||
async def add_one(
|
||||
n: int,
|
||||
) -> int:
|
||||
'''
|
||||
Increment within an active actor runtime.
|
||||
|
||||
'''
|
||||
assert tractor.current_actor(
|
||||
err_on_no_runtime=False,
|
||||
) is not None
|
||||
return n + 1
|
||||
|
||||
|
||||
|
|
@ -58,11 +69,11 @@ async def mark_task_cancellation(
|
|||
started_path: str,
|
||||
cancelled_path: str,
|
||||
) -> None:
|
||||
Path(started_path).touch()
|
||||
try:
|
||||
with CancellationMarkers(
|
||||
started_path,
|
||||
cancelled_path,
|
||||
):
|
||||
await trio.sleep_forever()
|
||||
finally:
|
||||
Path(cancelled_path).touch()
|
||||
|
||||
|
||||
async def echo_startup_control(
|
||||
|
|
@ -84,46 +95,15 @@ async def collect_call(
|
|||
return args, kwargs
|
||||
|
||||
|
||||
def _non_registration_contexts(
|
||||
actor: tractor.Actor,
|
||||
) -> dict[tuple, str]:
|
||||
return {
|
||||
key: str(ctx._nsf)
|
||||
for key, ctx in actor._contexts.items()
|
||||
if str(ctx._nsf) != (
|
||||
'tractor.discovery._registry:'
|
||||
'Registrar.register_actor'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def test_namespace_path_retains_target_ref(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
def test_public_module_alias() -> None:
|
||||
'''
|
||||
Reuse the client-side target ref when splitting its namespace path.
|
||||
Keep the public trampoline alias separate from its private module.
|
||||
|
||||
`NamespacePath.from_ref()` previously discarded `add_one`, so
|
||||
`to_tuple()` imported and resolved the just-created string again.
|
||||
Replacing `resolve_name()` with a failure proves the retained ref
|
||||
supplies the tuple without a redundant lookup. The public module
|
||||
alias assertion also keeps internal `_api.__name__` authoritative.
|
||||
Callers use `to_actor.MODULE` to configure an existing actor's RPC
|
||||
allowlist, while `_api.__name__` remains the authoritative module
|
||||
path and does not re-export the alias internally.
|
||||
|
||||
'''
|
||||
target = NamespacePath.from_ref(add_one)
|
||||
|
||||
def fail_resolve(name: str) -> object:
|
||||
raise AssertionError(f'unexpected lookup for {name!r}')
|
||||
|
||||
monkeypatch.setattr(
|
||||
msgptr,
|
||||
'resolve_name',
|
||||
fail_resolve,
|
||||
)
|
||||
assert target.to_tuple() == (
|
||||
add_one.__module__,
|
||||
add_one.__name__,
|
||||
)
|
||||
assert to_actor.MODULE == to_actor_api.__name__
|
||||
assert not hasattr(to_actor_api, 'MODULE')
|
||||
|
||||
|
|
@ -152,7 +132,9 @@ def test_one_shot_boots_implicit_runtime(
|
|||
'''
|
||||
Outside any actor-runtime `to_actor.run()` boots one
|
||||
implicitly (just like bare `open_nursery()` usage)
|
||||
configured via pass-through `runtime_kwargs`.
|
||||
configured via pass-through `runtime_kwargs`. The remote target
|
||||
asserts its runtime exists; the caller then verifies the private
|
||||
runtime is fully torn down before `to_actor.run()` returns.
|
||||
|
||||
'''
|
||||
async def main() -> None:
|
||||
|
|
@ -169,6 +151,9 @@ def test_one_shot_boots_implicit_runtime(
|
|||
),
|
||||
)
|
||||
assert result == 42
|
||||
assert tractor.current_actor(
|
||||
err_on_no_runtime=False,
|
||||
) is None
|
||||
|
||||
trio.run(main)
|
||||
|
||||
|
|
@ -201,10 +186,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 +199,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
|
||||
|
|
@ -225,13 +212,18 @@ async def test_cancel_ack_failure_hard_reaps_child(
|
|||
'''
|
||||
Escalate a failed cancel acknowledgement and reap the child.
|
||||
|
||||
`Portal.cancel_actor()` can return `False` when its transport is
|
||||
already closed without confirming runtime cancellation. The old
|
||||
one-shot path ignored that result, released the nursery-wide join
|
||||
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.
|
||||
`Portal.cancel_actor()` catches `TransportClosed` and returns
|
||||
`False` when it can not confirm runtime cancellation. The mock
|
||||
represents that public post-transport-failure result, so no
|
||||
underlying exception remains to bubble through `to_actor.run()`.
|
||||
|
||||
The old one-shot path ignored `False`, released the nursery-wide
|
||||
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(
|
||||
|
|
@ -242,6 +234,8 @@ async def test_cancel_ack_failure_hard_reaps_child(
|
|||
assert raise_on_timeout
|
||||
return False
|
||||
|
||||
# Model `Portal.cancel_actor()` after it catches `TransportClosed`;
|
||||
# there is no transport exception left for `run()` to re-raise.
|
||||
monkeypatch.setattr(
|
||||
tractor.Portal,
|
||||
'cancel_actor',
|
||||
|
|
@ -249,6 +243,8 @@ async def test_cancel_ack_failure_hard_reaps_child(
|
|||
)
|
||||
|
||||
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):
|
||||
assert await to_actor.run(
|
||||
add_one,
|
||||
|
|
@ -256,11 +252,13 @@ 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():
|
||||
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
|
||||
shield. The fake private RPC applies the forwarded send deadline to
|
||||
|
|
@ -268,6 +266,11 @@ def test_cancel_actor_timeout_closes_blocked_send():
|
|||
bounded `ActorTooSlowError` and the recorded absolute deadline prove
|
||||
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:
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -282,6 +285,8 @@ def test_cancel_actor_timeout_closes_blocked_send():
|
|||
|
||||
async def main() -> None:
|
||||
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._chan = channel
|
||||
deadlines: list[float] = []
|
||||
|
|
@ -319,6 +324,75 @@ def test_cancel_actor_timeout_closes_blocked_send():
|
|||
)
|
||||
|
||||
|
||||
def test_context_cancel_shares_request_and_ack_deadline():
|
||||
'''
|
||||
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.
|
||||
The transport suite separately proves that deadline expiry closes
|
||||
a stream after partial frame publication.
|
||||
|
||||
'''
|
||||
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',
|
||||
)
|
||||
|
||||
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.chan = SimpleNamespace(
|
||||
aid=peer_aid,
|
||||
connected=connected,
|
||||
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 = {}
|
||||
|
|
@ -342,6 +416,8 @@ def test_late_child_registration_observes_cancel():
|
|||
|
||||
'''
|
||||
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
|
||||
aid = tractor.msg.Aid(
|
||||
name='late_child',
|
||||
|
|
@ -366,8 +442,8 @@ def test_late_child_registration_observes_cancel():
|
|||
proc,
|
||||
None,
|
||||
)
|
||||
assert an._child_reap_requests[aid.uid] is reap_request
|
||||
assert an._child_reaped[aid.uid] is reaped
|
||||
assert an._child_reap_requests[aid] is reap_request
|
||||
assert an._child_reaped[aid] is reaped
|
||||
|
||||
|
||||
def test_mp_late_registration_never_starts_process(
|
||||
|
|
@ -391,6 +467,24 @@ def test_mp_late_registration_never_starts_process(
|
|||
|
||||
process = FakeProcess()
|
||||
|
||||
def register_child(
|
||||
subactor: object,
|
||||
proc: object,
|
||||
portal: object|None,
|
||||
) -> tuple[trio.Event, trio.Event, bool]:
|
||||
'''
|
||||
Simulate provisional MP registration before child startup.
|
||||
|
||||
'''
|
||||
assert subactor
|
||||
assert proc is process
|
||||
assert portal is None
|
||||
return (
|
||||
trio.Event(),
|
||||
trio.Event(),
|
||||
True,
|
||||
)
|
||||
|
||||
class FakeContext:
|
||||
def get_start_method(self) -> str:
|
||||
return 'spawn'
|
||||
|
|
@ -400,11 +494,7 @@ def test_mp_late_registration_never_starts_process(
|
|||
return process
|
||||
|
||||
nursery = SimpleNamespace(
|
||||
_register_child=lambda *args: (
|
||||
trio.Event(),
|
||||
trio.Event(),
|
||||
True,
|
||||
),
|
||||
_register_child=register_child,
|
||||
)
|
||||
subactor = SimpleNamespace(
|
||||
aid=tractor.msg.Aid(
|
||||
|
|
@ -455,10 +545,14 @@ def test_late_child_reap_registration_is_released():
|
|||
an._child_reap_requests = {}
|
||||
an._child_reaped = {}
|
||||
|
||||
# Nursery teardown publishes its reap request while the child
|
||||
# monitor is checkpointed before per-child event registration.
|
||||
an._join_procs.set()
|
||||
reap_request, _ = an._register_child_reap(
|
||||
('late_child', 'uid'),
|
||||
aid = tractor.msg.Aid(
|
||||
name='late_child',
|
||||
uuid='uid',
|
||||
)
|
||||
reap_request, _ = an._register_child_reap(aid)
|
||||
|
||||
assert reap_request.is_set()
|
||||
|
||||
|
|
@ -507,7 +601,7 @@ async def test_reuse_existing_actor_via_portal(
|
|||
to_actor.MODULE,
|
||||
],
|
||||
)
|
||||
contexts_before = _non_registration_contexts(actor)
|
||||
contexts_before = non_registration_contexts(actor)
|
||||
for i in range(3):
|
||||
assert await to_actor.run(
|
||||
add_one,
|
||||
|
|
@ -520,7 +614,7 @@ async def test_reuse_existing_actor_via_portal(
|
|||
'echo_startup_control',
|
||||
_cancel_on_startup='target_value',
|
||||
) == 'target_value'
|
||||
assert _non_registration_contexts(actor) == contexts_before
|
||||
assert non_registration_contexts(actor) == contexts_before
|
||||
|
||||
# still alive: caller owns the actor's lifetime.
|
||||
await portal.cancel_actor()
|
||||
|
|
@ -806,7 +900,7 @@ async def test_portal_task_cancelled_with_local_caller(
|
|||
to_actor.MODULE,
|
||||
],
|
||||
)
|
||||
contexts_before = _non_registration_contexts(actor)
|
||||
contexts_before = non_registration_contexts(actor)
|
||||
|
||||
async with trio.open_nursery() as tn:
|
||||
tn.start_soon(
|
||||
|
|
@ -824,13 +918,13 @@ async def test_portal_task_cancelled_with_local_caller(
|
|||
tn.cancel_scope.cancel()
|
||||
|
||||
assert cancelled_path.exists()
|
||||
assert _non_registration_contexts(actor) == contexts_before
|
||||
assert non_registration_contexts(actor) == contexts_before
|
||||
assert await to_actor.run(
|
||||
add_one,
|
||||
1,
|
||||
portal=portal,
|
||||
) == 2
|
||||
assert _non_registration_contexts(actor) == contexts_before
|
||||
assert non_registration_contexts(actor) == contexts_before
|
||||
|
||||
await portal.cancel_actor()
|
||||
|
||||
|
|
@ -856,7 +950,7 @@ async def test_context_trampoline_preserves_module_allowlist(
|
|||
'restricted_context_worker',
|
||||
enable_modules=[to_actor.MODULE],
|
||||
)
|
||||
contexts_before = _non_registration_contexts(actor)
|
||||
contexts_before = non_registration_contexts(actor)
|
||||
with pytest.raises(RemoteActorError) as excinfo:
|
||||
await to_actor.run(
|
||||
add_one,
|
||||
|
|
@ -865,7 +959,7 @@ async def test_context_trampoline_preserves_module_allowlist(
|
|||
)
|
||||
|
||||
assert excinfo.value.boxed_type is tractor.ModuleNotExposed
|
||||
assert _non_registration_contexts(actor) == contexts_before
|
||||
assert non_registration_contexts(actor) == contexts_before
|
||||
await portal.cancel_actor()
|
||||
|
||||
|
||||
|
|
@ -889,7 +983,7 @@ async def test_portal_requires_context_trampoline(
|
|||
'no_context_trampoline_worker',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
contexts_before = _non_registration_contexts(actor)
|
||||
contexts_before = non_registration_contexts(actor)
|
||||
with pytest.raises(RemoteActorError) as excinfo:
|
||||
await to_actor.run(
|
||||
add_one,
|
||||
|
|
@ -900,5 +994,5 @@ async def test_portal_requires_context_trampoline(
|
|||
err = excinfo.value
|
||||
assert err.boxed_type is tractor.ModuleNotExposed
|
||||
assert to_actor.MODULE in str(err)
|
||||
assert _non_registration_contexts(actor) == contexts_before
|
||||
assert non_registration_contexts(actor) == contexts_before
|
||||
await portal.cancel_actor()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -518,9 +518,10 @@ class MsgpackTransport(MsgTransport):
|
|||
# the frame is complete, the explicit checkpoint
|
||||
# immediately delivers any pending cancellation.
|
||||
#
|
||||
# Ordinary sends may delay cancellation while a peer is
|
||||
# not reading. Actor-wide cancel requests pass their own
|
||||
# deadline so this operation can close a stalled stream.
|
||||
# Ordinary sends may delay cancellation while the remote
|
||||
# peer actor is not reading. Bounded actor/context cancel
|
||||
# requests pass an absolute deadline so this operation
|
||||
# can close a stalled stream.
|
||||
with trio.CancelScope(
|
||||
deadline=send_deadline,
|
||||
shield=True,
|
||||
|
|
|
|||
|
|
@ -123,6 +123,15 @@ class NamespacePath(str):
|
|||
ref: type|object,
|
||||
|
||||
) -> NamespacePath:
|
||||
'''
|
||||
Build a path while retaining its process-local object reference.
|
||||
|
||||
The originating process already holds `ref`; caching it prevents
|
||||
`to_tuple()` from immediately importing and resolving the same
|
||||
object again. Serialized paths carry only the `str` value and
|
||||
therefore resolve lazily through `load_ref()` after decoding.
|
||||
|
||||
'''
|
||||
|
||||
fqnp: tuple[str, str] = cls._mk_fqnp(ref)
|
||||
nsp = cls(':'.join(fqnp))
|
||||
|
|
|
|||
|
|
@ -882,12 +882,15 @@ class Actor:
|
|||
|
||||
except BaseException as startup_err:
|
||||
with trio.CancelScope(shield=True):
|
||||
# `MsgpackTransport.send()` closes its stream when
|
||||
# cancellation interrupts the length-prefixed write
|
||||
# because an unknown prefix may already be sent. A
|
||||
# connected channel means cancellation happened before
|
||||
# that write or after it completed, so `_cancel_task`
|
||||
# is protocol-safe (and a no-op if `Start` was unsent).
|
||||
# `MsgpackTransport.send()` shields length-prefixed frame
|
||||
# publication until complete, then checkpoints pending
|
||||
# cancellation before returning. Thus `start_published`
|
||||
# can remain false after a complete `Start` reached the
|
||||
# wire. If the send's own deadline catches a partial
|
||||
# frame, it closes the stream. A connected channel means
|
||||
# cancellation happened before the write or after frame
|
||||
# completion, so `_cancel_task` is protocol-safe (and
|
||||
# a no-op when `Start` was unsent).
|
||||
if (
|
||||
cancel_on_startup
|
||||
and
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ from ..log import (
|
|||
get_logger,
|
||||
get_loglevel,
|
||||
)
|
||||
from ..msg import Aid
|
||||
from ._runtime import Actor
|
||||
from ._portal import Portal
|
||||
from ..trionics import (
|
||||
|
|
@ -130,12 +131,21 @@ async def _try_cancel_then_kill(
|
|||
# mutated by per-child `debug_mode=True`). ORing covers
|
||||
# every flavor without false-positively skipping
|
||||
# legitimate hard-kill paths in non-debug trees.
|
||||
debug_protected: bool = (
|
||||
def child_in_debug() -> bool:
|
||||
'''
|
||||
Sample child/tree debugger protection state.
|
||||
|
||||
'''
|
||||
return (
|
||||
debug_mode_active
|
||||
or
|
||||
debug.Lock.ctx_in_debug is not None
|
||||
)
|
||||
|
||||
debug_protected: bool = (
|
||||
child_in_debug()
|
||||
or
|
||||
_state._runtime_vars.get('_debug_mode', False)
|
||||
or
|
||||
debug_mode_active
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
@ -145,11 +155,8 @@ async def _try_cancel_then_kill(
|
|||
if not cancelled:
|
||||
if debug_protected:
|
||||
await debug.maybe_wait_for_debugger(
|
||||
child_in_debug=(
|
||||
debug_mode_active
|
||||
or
|
||||
debug.Lock.ctx_in_debug is not None
|
||||
),
|
||||
# Re-sample after the cancel-RPC checkpoint.
|
||||
child_in_debug=child_in_debug(),
|
||||
header_msg=(
|
||||
'Delaying subproc hard-reap while '
|
||||
'debugger locked..\n'
|
||||
|
|
@ -240,11 +247,11 @@ class ActorNursery:
|
|||
|
||||
self._join_procs = trio.Event()
|
||||
self._child_reap_requests: dict[
|
||||
tuple[str, str],
|
||||
Aid,
|
||||
trio.Event,
|
||||
] = {}
|
||||
self._child_reaped: dict[
|
||||
tuple[str, str],
|
||||
Aid,
|
||||
trio.Event,
|
||||
] = {}
|
||||
self._at_least_one_child_in_debug: bool = False
|
||||
|
|
@ -313,7 +320,7 @@ class ActorNursery:
|
|||
|
||||
def _register_child_reap(
|
||||
self,
|
||||
uid: tuple[str, str],
|
||||
aid: Aid,
|
||||
) -> tuple[trio.Event, trio.Event]:
|
||||
'''
|
||||
Register a child monitor's process-reap events.
|
||||
|
|
@ -321,8 +328,8 @@ class ActorNursery:
|
|||
'''
|
||||
reap_request = trio.Event()
|
||||
reaped = trio.Event()
|
||||
self._child_reap_requests[uid] = reap_request
|
||||
self._child_reaped[uid] = reaped
|
||||
self._child_reap_requests[aid] = reap_request
|
||||
self._child_reaped[aid] = reaped
|
||||
if self._join_procs.is_set():
|
||||
reap_request.set()
|
||||
return reap_request, reaped
|
||||
|
|
@ -337,13 +344,14 @@ class ActorNursery:
|
|||
Atomically publish one child and its reap coordination.
|
||||
|
||||
'''
|
||||
uid: tuple[str, str] = subactor.aid.uid
|
||||
aid: Aid = subactor.aid
|
||||
uid: tuple[str, str] = aid.uid
|
||||
self._children[uid] = (
|
||||
subactor,
|
||||
proc,
|
||||
portal,
|
||||
)
|
||||
reap_request, reaped = self._register_child_reap(uid)
|
||||
reap_request, reaped = self._register_child_reap(aid)
|
||||
return (
|
||||
reap_request,
|
||||
reaped,
|
||||
|
|
@ -363,18 +371,26 @@ class ActorNursery:
|
|||
|
||||
def _mark_child_reaped(
|
||||
self,
|
||||
uid: tuple[str, str],
|
||||
aid: Aid,
|
||||
) -> None:
|
||||
'''
|
||||
Publish completed child-process teardown to its waiter.
|
||||
|
||||
'''
|
||||
uid: tuple[str, str] = aid.uid
|
||||
self._children.pop(uid, None)
|
||||
self._child_reap_requests.pop(uid, None)
|
||||
reap_request: trio.Event|None = (
|
||||
self._child_reap_requests.pop(aid, None)
|
||||
)
|
||||
reaped: trio.Event|None = self._child_reaped.pop(
|
||||
uid,
|
||||
aid,
|
||||
None,
|
||||
)
|
||||
assert (
|
||||
(reap_request is None)
|
||||
==
|
||||
(reaped is None)
|
||||
)
|
||||
if reaped is not None:
|
||||
reaped.set()
|
||||
|
||||
|
|
@ -386,14 +402,15 @@ class ActorNursery:
|
|||
Cancel, join and unregister one nursery-owned child.
|
||||
|
||||
'''
|
||||
uid: tuple[str, str] = portal.channel.aid.uid
|
||||
aid: Aid = portal.channel.aid
|
||||
uid: tuple[str, str] = aid.uid
|
||||
child_entry = self._children.get(uid)
|
||||
if child_entry is None:
|
||||
return
|
||||
|
||||
subactor, proc, _ = child_entry
|
||||
reap_request: trio.Event = self._child_reap_requests[uid]
|
||||
reaped: trio.Event = self._child_reaped[uid]
|
||||
reap_request: trio.Event = self._child_reap_requests[aid]
|
||||
reaped: trio.Event = self._child_reaped[aid]
|
||||
|
||||
with trio.CancelScope(shield=True):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -141,14 +141,16 @@ async def mp_proc(
|
|||
# `multiprocessing` only (since no async interface): publish the
|
||||
# process and its reap coordination before start so cancellation
|
||||
# can own every subsequently started child.
|
||||
# No `Portal` exists until the IPC handshake returns `chan`.
|
||||
# Replace this provisional entry with `Portal(chan)` below.
|
||||
(
|
||||
reap_request,
|
||||
_,
|
||||
cancel_during_registration,
|
||||
) = actor_nursery._register_child(
|
||||
subactor,
|
||||
proc,
|
||||
None,
|
||||
subactor=subactor,
|
||||
proc=proc,
|
||||
portal=None,
|
||||
)
|
||||
if cancel_during_registration:
|
||||
raise RuntimeError(
|
||||
|
|
|
|||
|
|
@ -460,7 +460,7 @@ async def new_proc(
|
|||
proc_kwargs=proc_kwargs
|
||||
)
|
||||
finally:
|
||||
actor_nursery._mark_child_reaped(subactor.aid.uid)
|
||||
actor_nursery._mark_child_reaped(subactor.aid)
|
||||
|
||||
|
||||
# NOTE: bottom-of-module to avoid a circular import since the
|
||||
|
|
|
|||
|
|
@ -130,14 +130,17 @@ async def trio_proc(
|
|||
f' |_{proc}\n'
|
||||
)
|
||||
|
||||
# No `Portal` exists until the IPC handshake returns
|
||||
# `chan`. Replace this provisional entry with
|
||||
# `Portal(chan)` below.
|
||||
(
|
||||
reap_request,
|
||||
_,
|
||||
cancel_during_registration,
|
||||
) = actor_nursery._register_child(
|
||||
subactor,
|
||||
proc,
|
||||
None,
|
||||
subactor=subactor,
|
||||
proc=proc,
|
||||
portal=None,
|
||||
)
|
||||
if cancel_during_registration:
|
||||
cancelled_during_spawn = True
|
||||
|
|
|
|||
|
|
@ -116,13 +116,14 @@ def _normalize_call(
|
|||
|
||||
# `functools.Placeholder` was added in Python 3.14. Drop
|
||||
# this `getattr()` guard once 3.14 is the minimum version.
|
||||
placeholder = getattr(
|
||||
if (
|
||||
(
|
||||
placeholder := getattr(
|
||||
functools,
|
||||
'Placeholder',
|
||||
None,
|
||||
)
|
||||
if (
|
||||
placeholder is not None
|
||||
) is not None
|
||||
and
|
||||
any(
|
||||
arg is placeholder
|
||||
|
|
@ -239,8 +240,8 @@ async def run(
|
|||
fn: Callable[[Unpack[ArgsT]], Awaitable[RetT]],
|
||||
*args: Unpack[ArgsT],
|
||||
|
||||
# actor "placement": reuse an already-running peer
|
||||
# via its `portal`, spawn a fresh subactor from
|
||||
# actor lifetime management: reuse an already-running peer
|
||||
# via its `portal: Portal`, spawn a fresh subactor from
|
||||
# a caller-managed `an: ActorNursery`, or, when
|
||||
# neither is provided, open a private actor-nursery
|
||||
# (implicitly booting the actor-runtime as needed)
|
||||
|
|
@ -274,9 +275,10 @@ async def run(
|
|||
|
||||
As with Trio's API, target arguments are positional. Use
|
||||
`functools.partial()` to bind target keyword arguments; all
|
||||
keyword arguments accepted here configure actor placement or
|
||||
spawning. A caller-supplied `portal` must address an actor started
|
||||
with both `tractor.to_actor.MODULE` and the target function's
|
||||
keyword arguments accepted here configure actor lifetime
|
||||
management, including actor reuse and spawning. A caller-supplied
|
||||
`portal` must address an actor started with both
|
||||
`tractor.to_actor.MODULE` and the target function's
|
||||
module in its `enable_modules` list. Calls that spawn their own
|
||||
actor add the trampoline module automatically.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue