Compare commits
No commits in common. "98bc642e72f72eb55fc4cd9e0bc9d2acda454dab" and "bf46f5cc5e2fc2463c1cd61ba12c9dadf6371b66" have entirely different histories.
98bc642e72
...
bf46f5cc5e
|
|
@ -6,12 +6,11 @@ from pathlib import Path
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
|
|
||||||
import tractor
|
import tractor
|
||||||
import trio
|
|
||||||
|
|
||||||
|
|
||||||
class CancellationMarkers:
|
class CancellationMarkers:
|
||||||
'''
|
'''
|
||||||
Mark a test endpoint and require cancellation-driven teardown.
|
Mark a test endpoint's lifetime without cleanup checkpoints.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -31,8 +30,6 @@ class CancellationMarkers:
|
||||||
exc_value: BaseException|None,
|
exc_value: BaseException|None,
|
||||||
traceback: TracebackType|None,
|
traceback: TracebackType|None,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert exc_type is trio.Cancelled
|
|
||||||
assert isinstance(exc_value, trio.Cancelled)
|
|
||||||
Path(self.cancelled_path).touch()
|
Path(self.cancelled_path).touch()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1359,8 +1359,6 @@ def test_ctxep_pauses_n_maybe_ipc_breaks(
|
||||||
expect_prompt=False,
|
expect_prompt=False,
|
||||||
)
|
)
|
||||||
child.expect(EOF)
|
child.expect(EOF)
|
||||||
before += ansi_strip(child.before.decode())
|
|
||||||
assert 'KeyboardInterrupt' in before
|
|
||||||
assert child.flag_eof
|
assert child.flag_eof
|
||||||
assert not child.isalive()
|
assert not child.isalive()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,19 +33,18 @@ def test_cancelled_transport_send_completes_frame():
|
||||||
|
|
||||||
A cancelled `send_all()` may leave an arbitrary frame prefix on the
|
A cancelled `send_all()` may leave an arbitrary frame prefix on the
|
||||||
wire. Closing the actor-wide stream avoids decoder corruption but
|
wire. Closing the actor-wide stream avoids decoder corruption but
|
||||||
also destroys unrelated contexts using that channel. On its first
|
also destroys unrelated contexts using that channel. The fake
|
||||||
call, the fake stream publishes two header bytes and blocks until
|
stream publishes two header bytes and blocks, letting this test
|
||||||
the parent test releases it. This lets the parent cancel the sender
|
cancel the sender inside frame publication. The sender must remain
|
||||||
while frame publication is suspended. The sender must remain inside
|
blocked until the complete frame is written, then observe pending
|
||||||
`send_first()` until the complete frame is written, then observe
|
cancellation; a second complete frame proves channel reuse remains
|
||||||
pending cancellation; a second sender proves sibling contexts can
|
safe.
|
||||||
safely reuse the still frame-aligned stream.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
class PartialSendStream:
|
class PartialSendStream:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.send_all_entered = trio.Event()
|
self.send_entered = trio.Event()
|
||||||
self.send_all_release = trio.Event()
|
self.release = trio.Event()
|
||||||
self.closed = False
|
self.closed = False
|
||||||
self.wire = bytearray()
|
self.wire = bytearray()
|
||||||
|
|
||||||
|
|
@ -56,8 +55,8 @@ def test_cancelled_transport_send_completes_frame():
|
||||||
assert data
|
assert data
|
||||||
if not self.wire:
|
if not self.wire:
|
||||||
self.wire.extend(data[:2])
|
self.wire.extend(data[:2])
|
||||||
self.send_all_entered.set()
|
self.send_entered.set()
|
||||||
await self.send_all_release.wait()
|
await self.release.wait()
|
||||||
self.wire.extend(data[2:])
|
self.wire.extend(data[2:])
|
||||||
else:
|
else:
|
||||||
self.wire.extend(data)
|
self.wire.extend(data)
|
||||||
|
|
@ -116,29 +115,21 @@ def test_cancelled_transport_send_completes_frame():
|
||||||
tn.start_soon(
|
tn.start_soon(
|
||||||
send_first,
|
send_first,
|
||||||
)
|
)
|
||||||
await stream.send_all_entered.wait()
|
await stream.send_entered.wait()
|
||||||
sender_scopes[0].cancel()
|
sender_scopes[0].cancel()
|
||||||
await wait_all_tasks_blocked()
|
await wait_all_tasks_blocked()
|
||||||
|
|
||||||
assert not stream.closed
|
assert not stream.closed
|
||||||
# Cancellation is pending, but complete-frame shielding
|
|
||||||
# keeps `send_first()` suspended in `.send_all()`.
|
|
||||||
assert not sender_done.is_set()
|
assert not sender_done.is_set()
|
||||||
|
|
||||||
# Let the underlying frame write finish after the parent
|
stream.release.set()
|
||||||
# has requested sender cancellation.
|
|
||||||
stream.send_all_release.set()
|
|
||||||
await sender_done.wait()
|
await sender_done.wait()
|
||||||
|
|
||||||
assert cancelled_caught
|
assert cancelled_caught
|
||||||
assert not stream.closed
|
assert not stream.closed
|
||||||
# The initial two-byte prefix was completed into one valid
|
|
||||||
# frame before cancellation reached `send_first()`.
|
|
||||||
assert count_frames(stream.wire) == 1
|
assert count_frames(stream.wire) == 1
|
||||||
|
|
||||||
await transport.send(second_msg)
|
await transport.send(second_msg)
|
||||||
# A sibling sender can append and decode another frame only
|
|
||||||
# because the first cancellation preserved stream alignment.
|
|
||||||
assert count_frames(stream.wire) == 2
|
assert count_frames(stream.wire) == 2
|
||||||
|
|
||||||
tn.cancel_scope.cancel()
|
tn.cancel_scope.cancel()
|
||||||
|
|
@ -148,15 +139,14 @@ def test_cancelled_transport_send_completes_frame():
|
||||||
|
|
||||||
def test_transport_send_deadline_closes_partial_frame():
|
def test_transport_send_deadline_closes_partial_frame():
|
||||||
'''
|
'''
|
||||||
Destroy a stalled partial frame before another sender can append.
|
Bound one shielded frame without exposing a corrupt stream.
|
||||||
|
|
||||||
Ordinary cancellation cannot interrupt complete-frame publication.
|
Ordinary cancellation cannot interrupt complete-frame publication.
|
||||||
Bounded actor/context cancellation instead passes its absolute
|
Actor-wide cancellation instead passes its absolute deadline into
|
||||||
deadline into this operation. The fake stream writes a partial
|
this operation. The fake stream writes a partial header and stalls;
|
||||||
header and stalls; when the send's own deadline fires, the transport
|
when the send's own deadline fires, the transport must close the
|
||||||
must close the stream before releasing its shared send lock. This
|
stream before releasing its shared send lock and report the channel
|
||||||
prevents the next sender from appending bytes which a decoder would
|
unusable.
|
||||||
treat as the remainder of the corrupt first frame.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
class StalledStream:
|
class StalledStream:
|
||||||
|
|
@ -196,9 +186,9 @@ def test_transport_send_deadline_closes_partial_frame():
|
||||||
send_deadline=1,
|
send_deadline=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert stream.closed # partial-frame timeout destroys stream
|
assert stream.closed
|
||||||
assert len(stream.wire) == 2 # only a header fragment was sent
|
assert len(stream.wire) == 2
|
||||||
assert not transport._send_lock.locked() # cleanup released lock
|
assert not transport._send_lock.locked()
|
||||||
|
|
||||||
trio.run(
|
trio.run(
|
||||||
main,
|
main,
|
||||||
|
|
@ -210,35 +200,30 @@ def test_cancelled_transport_send_preserves_cancellation():
|
||||||
'''
|
'''
|
||||||
Prefer sender cancellation when teardown closes the stream.
|
Prefer sender cancellation when teardown closes the stream.
|
||||||
|
|
||||||
`MsgpackTransport.send()` shields frame publication at
|
`MsgpackTransport.send()` shields frame publication. Before this
|
||||||
`tractor.ipc._transport:MsgpackTransport.send`. Before this fix, an
|
regression fix, if an outer scope cancelled the sender and actor
|
||||||
outer `move_on_after()`/cancel scope could cancel `Channel.send()`
|
teardown then made `send_all()` raise `ClosedResourceError`, the
|
||||||
while actor teardown closed the shared stream. The resulting
|
transport error escaped instead of the pending cancellation. That
|
||||||
`ClosedResourceError` escaped from the transport handler instead of
|
defeated `move_on_after()` and failed otherwise orderly teardown.
|
||||||
its `checkpoint_if_cancelled()` redelivering pending cancellation.
|
|
||||||
|
|
||||||
The fake stream blocks inside the shield until the test cancels the
|
The fake stream blocks inside the shield until the test cancels the
|
||||||
sender. Parent-controlled release then simulates actor teardown
|
sender, then raises the same close error seen on macOS UDS. Observing
|
||||||
closing the socket and raises the `ClosedResourceError` observed on
|
`CancelScope.cancelled_caught` proves cancellation wins once the
|
||||||
macOS UDS. `CancelScope.cancelled_caught` proves the handler's
|
shield unwinds.
|
||||||
checkpoint preserved cancellation as the primary outcome instead of
|
|
||||||
leaking that secondary close error.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
class ClosingStream:
|
class ClosingStream:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.send_all_entered = trio.Event()
|
self.send_entered = trio.Event()
|
||||||
self.send_all_release = trio.Event()
|
self.release = trio.Event()
|
||||||
|
|
||||||
async def send_all(
|
async def send_all(
|
||||||
self,
|
self,
|
||||||
data: bytes,
|
data: bytes,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert data
|
assert data
|
||||||
self.send_all_entered.set()
|
self.send_entered.set()
|
||||||
await self.send_all_release.wait()
|
await self.release.wait()
|
||||||
# Model actor teardown closing the shared transport while
|
|
||||||
# this sender is still inside the complete-frame shield.
|
|
||||||
raise trio.ClosedResourceError(
|
raise trio.ClosedResourceError(
|
||||||
'this socket was already closed'
|
'this socket was already closed'
|
||||||
)
|
)
|
||||||
|
|
@ -271,12 +256,12 @@ def test_cancelled_transport_send_preserves_cancellation():
|
||||||
|
|
||||||
async with trio.open_nursery() as tn:
|
async with trio.open_nursery() as tn:
|
||||||
tn.start_soon(send)
|
tn.start_soon(send)
|
||||||
await stream.send_all_entered.wait()
|
await stream.send_entered.wait()
|
||||||
sender_scopes[0].cancel()
|
sender_scopes[0].cancel()
|
||||||
await wait_all_tasks_blocked()
|
await wait_all_tasks_blocked()
|
||||||
|
|
||||||
assert not sender_done.is_set()
|
assert not sender_done.is_set()
|
||||||
stream.send_all_release.set()
|
stream.release.set()
|
||||||
await sender_done.wait()
|
await sender_done.wait()
|
||||||
|
|
||||||
assert cancelled_caught
|
assert cancelled_caught
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ from ._helpers import (
|
||||||
|
|
||||||
def test_overrun_error_send_tolerates_transport_close(
|
def test_overrun_error_send_tolerates_transport_close(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
):
|
||||||
'''
|
'''
|
||||||
Preserve a stream overrun when its error can not be shipped.
|
Preserve a stream overrun when its error can not be shipped.
|
||||||
|
|
||||||
|
|
@ -108,13 +108,10 @@ def test_overrun_error_send_tolerates_transport_close(
|
||||||
)
|
)
|
||||||
packed: dict[str, object] = {}
|
packed: dict[str, object] = {}
|
||||||
|
|
||||||
# Spy on the generated `StreamOverrun` and return a stable wire
|
|
||||||
# `Error`; the real packer adds traceback/relay details unrelated to
|
|
||||||
# this test's secondary transport-close contract.
|
|
||||||
def pack_overrun(
|
def pack_overrun(
|
||||||
local_err: BaseException,
|
local_err: BaseException,
|
||||||
cid: str,
|
cid: str,
|
||||||
**kwargs: object,
|
**kwargs,
|
||||||
) -> tractor.msg.Error:
|
) -> tractor.msg.Error:
|
||||||
packed['local_err'] = local_err
|
packed['local_err'] = local_err
|
||||||
packed['cid'] = cid
|
packed['cid'] = cid
|
||||||
|
|
@ -278,7 +275,7 @@ async def simple_setup_teardown(
|
||||||
_state = False
|
_state = False
|
||||||
|
|
||||||
|
|
||||||
async def assert_state(value: bool) -> None:
|
async def assert_state(value: bool):
|
||||||
global _state
|
global _state
|
||||||
assert _state == value
|
assert _state == value
|
||||||
|
|
||||||
|
|
@ -289,7 +286,7 @@ async def test_cancel_during_context_startup(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
start_method: str,
|
start_method: str,
|
||||||
debug_mode: bool,
|
debug_mode: bool,
|
||||||
) -> None:
|
):
|
||||||
'''
|
'''
|
||||||
Cancel a context after sending `Start` but before its ack.
|
Cancel a context after sending `Start` but before its ack.
|
||||||
|
|
||||||
|
|
@ -306,7 +303,6 @@ async def test_cancel_during_context_startup(
|
||||||
started_path = tmp_path / 'startup_started'
|
started_path = tmp_path / 'startup_started'
|
||||||
cancelled_path = tmp_path / 'startup_cancelled'
|
cancelled_path = tmp_path / 'startup_cancelled'
|
||||||
start_sent = trio.Event()
|
start_sent = trio.Event()
|
||||||
start_funcs: list[str] = []
|
|
||||||
original_send = tractor.Channel.send
|
original_send = tractor.Channel.send
|
||||||
|
|
||||||
async def delay_after_start(
|
async def delay_after_start(
|
||||||
|
|
@ -321,11 +317,7 @@ async def test_cancel_during_context_startup(
|
||||||
hide_tb=hide_tb,
|
hide_tb=hide_tb,
|
||||||
send_deadline=send_deadline,
|
send_deadline=send_deadline,
|
||||||
)
|
)
|
||||||
# The patched method keeps `Channel.send()`'s broad message
|
|
||||||
# contract. It sees both the requested endpoint `Start` and the
|
|
||||||
# internal `self._cancel_task` startup RPC used for cleanup.
|
|
||||||
if isinstance(payload, tractor.msg.Start):
|
if isinstance(payload, tractor.msg.Start):
|
||||||
start_funcs.append(payload.func)
|
|
||||||
if payload.func == 'startup_cancel_target':
|
if payload.func == 'startup_cancel_target':
|
||||||
start_sent.set()
|
start_sent.set()
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
@ -341,7 +333,7 @@ async def test_cancel_during_context_startup(
|
||||||
raise AssertionError('context startup should be cancelled')
|
raise AssertionError('context startup should be cancelled')
|
||||||
|
|
||||||
async with tractor.open_nursery() as an:
|
async with tractor.open_nursery() as an:
|
||||||
actor: Actor = tractor.current_actor()
|
actor = tractor.current_actor()
|
||||||
portal: tractor.Portal = await an.start_actor(
|
portal: tractor.Portal = await an.start_actor(
|
||||||
'startup_cancel_worker',
|
'startup_cancel_worker',
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
|
|
@ -373,10 +365,6 @@ async def test_cancel_during_context_startup(
|
||||||
'return_one',
|
'return_one',
|
||||||
) == 1
|
) == 1
|
||||||
assert non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
assert start_funcs == [
|
|
||||||
'startup_cancel_target',
|
|
||||||
'_cancel_task',
|
|
||||||
]
|
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -384,7 +372,7 @@ async def test_cancel_during_context_startup(
|
||||||
async def test_start_serialization_error_cleans_context(
|
async def test_start_serialization_error_cleans_context(
|
||||||
start_method: str,
|
start_method: str,
|
||||||
debug_mode: bool,
|
debug_mode: bool,
|
||||||
) -> None:
|
):
|
||||||
'''
|
'''
|
||||||
Deallocate caller state when `Start` can not be serialized.
|
Deallocate caller state when `Start` can not be serialized.
|
||||||
|
|
||||||
|
|
@ -397,7 +385,7 @@ async def test_start_serialization_error_cleans_context(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async with tractor.open_nursery() as an:
|
async with tractor.open_nursery() as an:
|
||||||
actor: Actor = tractor.current_actor()
|
actor = tractor.current_actor()
|
||||||
portal: tractor.Portal = await an.start_actor(
|
portal: tractor.Portal = await an.start_actor(
|
||||||
'serialization_error_worker',
|
'serialization_error_worker',
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
|
|
@ -426,7 +414,7 @@ async def test_start_serialization_error_cleans_context(
|
||||||
async def test_start_module_error_cleans_context(
|
async def test_start_module_error_cleans_context(
|
||||||
start_method: str,
|
start_method: str,
|
||||||
debug_mode: bool,
|
debug_mode: bool,
|
||||||
) -> None:
|
):
|
||||||
'''
|
'''
|
||||||
Deallocate caller state after a remote startup rejection.
|
Deallocate caller state after a remote startup rejection.
|
||||||
|
|
||||||
|
|
@ -439,7 +427,7 @@ async def test_start_module_error_cleans_context(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async with tractor.open_nursery() as an:
|
async with tractor.open_nursery() as an:
|
||||||
actor: Actor = tractor.current_actor()
|
actor = tractor.current_actor()
|
||||||
portal: tractor.Portal = await an.start_actor(
|
portal: tractor.Portal = await an.start_actor(
|
||||||
'module_error_worker',
|
'module_error_worker',
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,9 @@ async def mark_task_cancellation(
|
||||||
|
|
||||||
|
|
||||||
async def echo_startup_control(
|
async def echo_startup_control(
|
||||||
cancel_on_startup: str,
|
_cancel_on_startup: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
return cancel_on_startup
|
return _cancel_on_startup
|
||||||
|
|
||||||
|
|
||||||
async def collect_args(
|
async def collect_args(
|
||||||
|
|
@ -88,6 +88,13 @@ async def collect_args(
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
async def collect_call(
|
||||||
|
*args: object,
|
||||||
|
**kwargs: object,
|
||||||
|
) -> tuple[tuple[object, ...], dict[str, object]]:
|
||||||
|
return args, kwargs
|
||||||
|
|
||||||
|
|
||||||
def test_public_module_alias() -> None:
|
def test_public_module_alias() -> None:
|
||||||
'''
|
'''
|
||||||
Keep the public trampoline alias separate from its private module.
|
Keep the public trampoline alias separate from its private module.
|
||||||
|
|
@ -580,12 +587,9 @@ async def test_reuse_existing_actor_via_portal(
|
||||||
Pass `portal=` to schedule the one-shot task in an
|
Pass `portal=` to schedule the one-shot task in an
|
||||||
already-running actor; no spawn, no implicit reap.
|
already-running actor; no spawn, no implicit reap.
|
||||||
|
|
||||||
The low-level call uses `__name__` to select the remote module and
|
The low-level `Portal.run_from_ns()` assertion also proves its
|
||||||
`'echo_startup_control'` to select its function. Public
|
target kwargs remain separate from the private startup-cancel
|
||||||
`Portal.run_from_ns()` packages `cancel_on_startup` inside the
|
policy used by context cleanup.
|
||||||
target `kwargs` passed to private `Portal._run_from_ns()`. Receiving
|
|
||||||
`'target_value'` back proves the value reached the target instead of
|
|
||||||
binding the private Boolean startup-cancellation policy parameter.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async with tractor.open_nursery() as an:
|
async with tractor.open_nursery() as an:
|
||||||
|
|
@ -608,7 +612,7 @@ async def test_reuse_existing_actor_via_portal(
|
||||||
assert await portal.run_from_ns(
|
assert await portal.run_from_ns(
|
||||||
__name__,
|
__name__,
|
||||||
'echo_startup_control',
|
'echo_startup_control',
|
||||||
cancel_on_startup='target_value',
|
_cancel_on_startup='target_value',
|
||||||
) == 'target_value'
|
) == 'target_value'
|
||||||
assert non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
|
|
||||||
|
|
@ -627,8 +631,6 @@ async def test_concurrent_one_shots_from_task_nursery(
|
||||||
nursery scheduling multiple one-shot calls against
|
nursery scheduling multiple one-shot calls against
|
||||||
a shared caller-managed actor-nursery; error
|
a shared caller-managed actor-nursery; error
|
||||||
collection thus lives entirely in caller-code.
|
collection thus lives entirely in caller-code.
|
||||||
A proposed distilled task-manager API is tracked in #485:
|
|
||||||
https://github.com/goodboy/tractor/issues/485
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
results: dict[int, int] = {}
|
results: dict[int, int] = {}
|
||||||
|
|
@ -656,7 +658,7 @@ async def test_concurrent_one_shots_from_task_nursery(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_rejects_sync_fn() -> None:
|
def test_rejects_sync_fn():
|
||||||
'''
|
'''
|
||||||
Non-async callables error BEFORE any spawn (or even
|
Non-async callables error BEFORE any spawn (or even
|
||||||
runtime-boot) happens.
|
runtime-boot) happens.
|
||||||
|
|
@ -665,10 +667,7 @@ def test_rejects_sync_fn() -> None:
|
||||||
def not_async() -> None:
|
def not_async() -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(TypeError):
|
||||||
TypeError,
|
|
||||||
match='must be a non-streaming async function',
|
|
||||||
):
|
|
||||||
trio.run(
|
trio.run(
|
||||||
partial(
|
partial(
|
||||||
to_actor.run,
|
to_actor.run,
|
||||||
|
|
@ -677,7 +676,7 @@ def test_rejects_sync_fn() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_rejects_streaming_fn() -> None:
|
def test_rejects_streaming_fn():
|
||||||
'''
|
'''
|
||||||
Async-gen (streaming) fns are not one-shot-able,
|
Async-gen (streaming) fns are not one-shot-able,
|
||||||
same constraint as `Portal.run()`.
|
same constraint as `Portal.run()`.
|
||||||
|
|
@ -686,10 +685,7 @@ def test_rejects_streaming_fn() -> None:
|
||||||
async def agen():
|
async def agen():
|
||||||
yield 1
|
yield 1
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(TypeError):
|
||||||
TypeError,
|
|
||||||
match='must be a non-streaming async function',
|
|
||||||
):
|
|
||||||
trio.run(
|
trio.run(
|
||||||
partial(
|
partial(
|
||||||
to_actor.run,
|
to_actor.run,
|
||||||
|
|
@ -742,7 +738,7 @@ def test_partial_placeholder_normalization(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_nested_partial_normalization() -> None:
|
def test_nested_partial_normalization():
|
||||||
'''
|
'''
|
||||||
Flatten every retained `functools.partial` layer before RPC.
|
Flatten every retained `functools.partial` layer before RPC.
|
||||||
|
|
||||||
|
|
@ -754,12 +750,6 @@ def test_nested_partial_normalization() -> None:
|
||||||
direct nested-partial call.
|
direct nested-partial call.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async def collect_call(
|
|
||||||
*args: object,
|
|
||||||
**kwargs: object,
|
|
||||||
) -> tuple[tuple[object, ...], dict[str, object]]:
|
|
||||||
return args, kwargs
|
|
||||||
|
|
||||||
inner = partial(
|
inner = partial(
|
||||||
collect_call,
|
collect_call,
|
||||||
1,
|
1,
|
||||||
|
|
@ -781,15 +771,13 @@ def test_nested_partial_normalization() -> None:
|
||||||
assert kwargs == {'label': 'outer'}
|
assert kwargs == {'label': 'outer'}
|
||||||
|
|
||||||
|
|
||||||
def test_rejects_portal_and_an_combo() -> None:
|
def test_rejects_portal_and_an_combo():
|
||||||
'''
|
'''
|
||||||
`portal=` and `an=` are mutually exclusive actor-lifetime handles.
|
`portal=` and `an=` are mutually exclusive
|
||||||
|
placement options.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
with pytest.raises(
|
with pytest.raises(ValueError):
|
||||||
ValueError,
|
|
||||||
match='Pass at most ONE of `portal` or `an`',
|
|
||||||
):
|
|
||||||
trio.run(
|
trio.run(
|
||||||
partial(
|
partial(
|
||||||
to_actor.run,
|
to_actor.run,
|
||||||
|
|
@ -802,7 +790,7 @@ def test_rejects_portal_and_an_combo() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'lifetime_mode',
|
'placement',
|
||||||
['an', 'portal'],
|
['an', 'portal'],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -813,31 +801,28 @@ def test_rejects_portal_and_an_combo() -> None:
|
||||||
],
|
],
|
||||||
ids=['empty', 'configured'],
|
ids=['empty', 'configured'],
|
||||||
)
|
)
|
||||||
def test_rejects_runtime_kwargs_with_lifetime_mode(
|
def test_rejects_runtime_kwargs_with_placement(
|
||||||
lifetime_mode: str,
|
placement: str,
|
||||||
runtime_kwargs: dict,
|
runtime_kwargs: dict,
|
||||||
) -> None:
|
):
|
||||||
'''
|
'''
|
||||||
`runtime_kwargs` only applies when the call opens
|
`runtime_kwargs` only applies when the call opens
|
||||||
its own private actor-nursery; passing it alongside
|
its own private actor-nursery; passing it alongside
|
||||||
an actor-lifetime handle is an error, never silently
|
a placement opt is an error, never silently
|
||||||
ignored. In particular, an empty dict still means the
|
ignored. In particular, an empty dict still means the
|
||||||
caller provided this mutually exclusive option; testing
|
caller provided this mutually exclusive option; testing
|
||||||
both lifetime modes prevents truthiness checks from
|
both placement modes prevents truthiness checks from
|
||||||
accepting it before any actor runtime is started.
|
accepting it before any actor runtime is started.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
with pytest.raises(
|
with pytest.raises(ValueError):
|
||||||
ValueError,
|
|
||||||
match='`runtime_kwargs` only applies',
|
|
||||||
):
|
|
||||||
trio.run(
|
trio.run(
|
||||||
partial(
|
partial(
|
||||||
to_actor.run,
|
to_actor.run,
|
||||||
add_one,
|
add_one,
|
||||||
1,
|
1,
|
||||||
**{
|
**{
|
||||||
lifetime_mode: object(),
|
placement: object(),
|
||||||
'runtime_kwargs': runtime_kwargs,
|
'runtime_kwargs': runtime_kwargs,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -895,13 +880,12 @@ async def test_portal_task_cancelled_with_local_caller(
|
||||||
Couple a reused portal's remote task to its local caller.
|
Couple a reused portal's remote task to its local caller.
|
||||||
|
|
||||||
The former `Portal.run()` path abandoned its remote task when the
|
The former `Portal.run()` path abandoned its remote task when the
|
||||||
local `to_actor.run()` caller was cancelled. `CancellationMarkers`
|
local `to_actor.run()` caller was cancelled. The target writes
|
||||||
writes one file after entry and writes the second only after its
|
one file after starting and another from its cancellation
|
||||||
synchronous exit verifies the remote task received `trio.Cancelled`.
|
`finally`. Cancelling the local task nursery and observing the
|
||||||
Cancelling the local task nursery and observing that marker proves
|
second file proves `Portal.open_context()` propagated
|
||||||
`Portal.open_context()` propagated cancellation before the caller
|
cancellation before the caller exited. A subsequent call proves
|
||||||
exited. A subsequent call proves the caller-owned actor was not
|
the caller-owned actor was not cancelled with that task.
|
||||||
cancelled with that task.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
started_path = tmp_path / 'started'
|
started_path = tmp_path / 'started'
|
||||||
|
|
@ -934,16 +918,12 @@ async def test_portal_task_cancelled_with_local_caller(
|
||||||
tn.cancel_scope.cancel()
|
tn.cancel_scope.cancel()
|
||||||
|
|
||||||
assert cancelled_path.exists()
|
assert cancelled_path.exists()
|
||||||
# Remote-task cancellation must restore the exact application
|
|
||||||
# context snapshot captured before the one-shot call.
|
|
||||||
assert non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
assert await to_actor.run(
|
assert await to_actor.run(
|
||||||
add_one,
|
add_one,
|
||||||
1,
|
1,
|
||||||
portal=portal,
|
portal=portal,
|
||||||
) == 2
|
) == 2
|
||||||
# Reusing the actor for a later successful call must also leave
|
|
||||||
# no local or remote context registry entries behind.
|
|
||||||
assert non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
|
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
@ -978,10 +958,7 @@ async def test_context_trampoline_preserves_module_allowlist(
|
||||||
portal=portal,
|
portal=portal,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = excinfo.value
|
assert excinfo.value.boxed_type is tractor.ModuleNotExposed
|
||||||
assert err.boxed_type is tractor.ModuleNotExposed
|
|
||||||
assert add_one.__module__ in str(err)
|
|
||||||
assert 'Make sure you exposed the target module' in str(err)
|
|
||||||
assert non_registration_contexts(actor) == contexts_before
|
assert non_registration_contexts(actor) == contexts_before
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1113,10 +1113,6 @@ class Context:
|
||||||
# NOTE: we're telling the far end actor to cancel a task
|
# NOTE: we're telling the far end actor to cancel a task
|
||||||
# corresponding to *this actor*. The far end local channel
|
# corresponding to *this actor*. The far end local channel
|
||||||
# instance is passed to `Actor._cancel_task()` implicitly.
|
# instance is passed to `Actor._cancel_task()` implicitly.
|
||||||
# Use private `Portal._run_from_ns()` because cancellation
|
|
||||||
# needs its internal `cancel_on_startup=False` policy and
|
|
||||||
# the transaction's shared absolute `send_deadline`; public
|
|
||||||
# `run_from_ns()` exposes neither control.
|
|
||||||
await self._portal._run_from_ns(
|
await self._portal._run_from_ns(
|
||||||
'self',
|
'self',
|
||||||
'_cancel_task',
|
'_cancel_task',
|
||||||
|
|
@ -1959,9 +1955,6 @@ class Context:
|
||||||
# the sender; the main motivation is that using bp can block the
|
# the sender; the main motivation is that using bp can block the
|
||||||
# msg handling loop which calls into this method!
|
# msg handling loop which calls into this method!
|
||||||
except trio.WouldBlock:
|
except trio.WouldBlock:
|
||||||
# `send_chan.send_nowait(msg)` found the local receive feeder
|
|
||||||
# full. With overruns disabled below, report that primary
|
|
||||||
# local overflow to the far-end sender as `StreamOverrun`.
|
|
||||||
|
|
||||||
# XXX: always push an error even if the local receiver
|
# XXX: always push an error even if the local receiver
|
||||||
# is in overrun state - i.e. if an 'error' msg is
|
# is in overrun state - i.e. if an 'error' msg is
|
||||||
|
|
@ -2034,13 +2027,12 @@ class Context:
|
||||||
await chan.send(err_msg)
|
await chan.send(err_msg)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# The `StreamOverrun` shipment can fail secondarily when
|
# XXX: the local consumer may have closed its side of
|
||||||
# context/channel teardown has already closed shared IPC.
|
# the IPC, in which case context/channel teardown owns
|
||||||
# Local stream closure may surface as
|
# cancellation of the far-end streaming task. The same
|
||||||
# `BrokenResourceError`; either peer closing the transport
|
# shipment can raise `TransportClosed` when either peer
|
||||||
# can surface as `TransportClosed`. In both cases teardown
|
# has already closed the shared IPC channel. In both
|
||||||
# owns far-end cancellation and the primary overrun can no
|
# cases the primary overrun can no longer be reported.
|
||||||
# longer be delivered.
|
|
||||||
except (
|
except (
|
||||||
TransportClosed,
|
TransportClosed,
|
||||||
trio.BrokenResourceError,
|
trio.BrokenResourceError,
|
||||||
|
|
|
||||||
|
|
@ -448,11 +448,9 @@ class MsgpackTransport(MsgTransport):
|
||||||
If `strict_types == True` then a `MsgTypeError` will be raised on any
|
If `strict_types == True` then a `MsgTypeError` will be raised on any
|
||||||
invalid msg type
|
invalid msg type
|
||||||
|
|
||||||
`send_deadline` bounds publication of one complete
|
`send_deadline` bounds publication of this complete frame. A
|
||||||
length-prefixed frame. If it expires after a prefix or payload
|
timeout destroys the stream because a partial prefix may have
|
||||||
fragment reaches the wire, a later sender would append bytes the
|
reached the wire.
|
||||||
peer decoder treats as the remainder of that corrupt frame. The
|
|
||||||
stream is therefore destroyed before releasing `._send_lock`.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
|
|
@ -507,17 +505,18 @@ class MsgpackTransport(MsgTransport):
|
||||||
try:
|
try:
|
||||||
# Every IPC msg is length-prefixed and all contexts
|
# Every IPC msg is length-prefixed and all contexts
|
||||||
# on this actor pair share one transport stream. If
|
# on this actor pair share one transport stream. If
|
||||||
# the send deadline interrupts `send_all()`, an unknown
|
# cancellation interrupts `send_all()`, an unknown
|
||||||
# frame prefix may already be on the wire; allowing the
|
# frame prefix may already be on the wire; allowing
|
||||||
# next sender to append would corrupt framing.
|
# the next sender to append would corrupt framing.
|
||||||
|
# Closing the stream avoids that corruption but lets
|
||||||
|
# one context-local cancellation destroy every sibling
|
||||||
|
# context using the channel.
|
||||||
#
|
#
|
||||||
# The enclosing `async with self._send_lock` retains the
|
# Keep the `._send_lock` and defer cancellation only
|
||||||
# lock through shielded publication and any forced-close
|
# for complete frame publication. Broken/closed stream
|
||||||
# cleanup. Context-manager exit releases it only after a
|
# failures still escape to the handlers below. Once
|
||||||
# complete frame or destruction of the corrupt stream.
|
# the frame is complete, the explicit checkpoint
|
||||||
# Ordinary outer cancellation remains shielded until
|
# immediately delivers any pending cancellation.
|
||||||
# complete publication; only expiry of `send_deadline`
|
|
||||||
# intentionally closes a partial stream here.
|
|
||||||
#
|
#
|
||||||
# Ordinary sends may delay cancellation while the remote
|
# Ordinary sends may delay cancellation while the remote
|
||||||
# peer actor is not reading. Bounded actor/context cancel
|
# peer actor is not reading. Bounded actor/context cancel
|
||||||
|
|
@ -540,8 +539,6 @@ class MsgpackTransport(MsgTransport):
|
||||||
f'deadline of {send_deadline!r}'
|
f'deadline of {send_deadline!r}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# The frame is complete and still aligned. Redeliver any
|
|
||||||
# pending outer cancellation before normal lock release.
|
|
||||||
await trio.lowlevel.checkpoint_if_cancelled()
|
await trio.lowlevel.checkpoint_if_cancelled()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -794,9 +794,8 @@ class Actor:
|
||||||
cancel_on_startup: bool = True,
|
cancel_on_startup: bool = True,
|
||||||
|
|
||||||
# Optional absolute deadline for publishing this exact `Start`
|
# Optional absolute deadline for publishing this exact `Start`
|
||||||
# frame. Used by bounded actor/context cancel RPCs whose outer
|
# frame. Used by actor-wide cancel RPCs whose outer timeout
|
||||||
# timeout cannot penetrate `Channel.send()` forwarding into the
|
# cannot penetrate complete-frame transport shielding.
|
||||||
# shield in `MsgpackTransport.send()`.
|
|
||||||
send_deadline: float = float('inf'),
|
send_deadline: float = float('inf'),
|
||||||
|
|
||||||
) -> Context:
|
) -> Context:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue