Bound `Portal.cancel_actor()` frame sends
A cancel RPC could stall forever in complete-frame transport shielding before the peer received it, bypassing the outer ack timeout and blocking graceful supervision. - thread one absolute deadline from `Portal.cancel_actor()` through the private `Start` publication path - force-close a partial-frame stream before releasing its send lock - keep ordinary sends unbounded and preserve pending cancellation - document the current `Start -> StartAck -> CancelAck` exchange and link the dedicated `Cancel` msg follow-up in #506 - cover partial publication and the shared send/ack timeout budget Prompt-IO: ai/prompt-io/opencode/20260821T023537Z_ae6f2ac3_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))wkt/to_actor_subpkg
parent
a849161fa5
commit
23e26b5b32
|
|
@ -0,0 +1,65 @@
|
||||||
|
---
|
||||||
|
model: openai/gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
||||||
|
timestamp: 2026-08-21T02:35:37Z
|
||||||
|
git_ref: ae6f2ac3
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20260821T023537Z_ae6f2ac3_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
Simplify bounded actor cancellation by passing an explicit absolute
|
||||||
|
deadline from `Portal.cancel_actor()` through `_run_from_ns()`,
|
||||||
|
`Actor.start_remote_task()`, and `Channel.send()` into
|
||||||
|
`MsgpackTransport.send()`. Avoid a `ContextVar`, watcher tasks, shared
|
||||||
|
status, coalescing, and waiter state. After tracing the current
|
||||||
|
`Start -> StartAck -> CancelAck` transaction, rename the local result to
|
||||||
|
`cancel_ack_received`, document its exact semantics, and link a focused
|
||||||
|
follow-up for a dedicated `Cancel -> CancelAck` protocol.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Threaded one absolute Trio deadline through the existing private
|
||||||
|
actor-cancel RPC path. The transport retains complete-frame shielding
|
||||||
|
for ordinary sends, while a cancel-control send that overruns its
|
||||||
|
deadline force-closes the potentially corrupted stream before releasing
|
||||||
|
the send lock. The outer actor-cancel scope uses the same deadline for
|
||||||
|
ack waiting and redelivers pending caller cancellation afterward.
|
||||||
|
|
||||||
|
Renamed the completion flag to `cancel_ack_received` and documented that
|
||||||
|
the current private call consumes `StartAck`, then receives a real
|
||||||
|
`CancelAck` after `Actor.cancel()` completes; this does not establish
|
||||||
|
that the OS process exited. Added a source TODO linking issue #506 for
|
||||||
|
the future first-class `Cancel -> CancelAck` transaction.
|
||||||
|
|
||||||
|
Focused transport and actor-cancel verification passed all four tests.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tractor/runtime/_portal.py` - own the absolute deadline, accurately
|
||||||
|
record ack receipt, and link the dedicated cancellation protocol.
|
||||||
|
- `tractor/runtime/_runtime.py` - forward the optional deadline for the
|
||||||
|
exact private `Start` publication.
|
||||||
|
- `tractor/ipc/_chan.py` - pass the operation-specific deadline to the
|
||||||
|
transport without changing ordinary sends.
|
||||||
|
- `tractor/ipc/_transport.py` - bound the shielded frame publication and
|
||||||
|
close a partial-frame stream before unlocking it.
|
||||||
|
- `tests/ipc/test_each_tpt.py` - cover deadline expiry after a partial
|
||||||
|
frame prefix reaches the stream.
|
||||||
|
- `tests/test_to_actor.py` - prove actor-cancel publication and ack
|
||||||
|
waiting share one absolute timeout budget.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
The human rejected the initial watcher-task, shared `_SendStatus`, cancel
|
||||||
|
coalescing, and per-waiter design as unnecessary complexity. They also
|
||||||
|
rejected `ContextVar` propagation in favor of explicit functional
|
||||||
|
threading, selected a single absolute deadline for publication and ack
|
||||||
|
waiting, and required item 2 to remain separate from the item-3 child
|
||||||
|
reaping work. After reviewing the result, they requested the precise
|
||||||
|
`cancel_ack_received` name, a detailed protocol-trace comment, a focused
|
||||||
|
follow-up issue, and a linked source TODO. No direct source-line edits
|
||||||
|
were made by the human.
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
model: openai/gpt-5.6-sol
|
||||||
|
service: opencode
|
||||||
|
timestamp: 2026-08-21T02:35:37Z
|
||||||
|
git_ref: ae6f2ac3
|
||||||
|
diff_cmd: git diff HEAD~1..HEAD
|
||||||
|
---
|
||||||
|
|
||||||
|
Replace the actor-cancel timeout watcher/status experiment with one
|
||||||
|
explicit absolute deadline threaded through the existing private call
|
||||||
|
path. Do not use a `ContextVar`, shared result state, waiter
|
||||||
|
coalescing, or polling tasks.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tractor/runtime/_portal.py`
|
||||||
|
|
||||||
|
`Portal.cancel_actor()` computes one absolute deadline and uses it for
|
||||||
|
both `Start` frame publication and the subsequent cancel-ack wait.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tractor/runtime/_runtime.py`
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tractor/ipc/_chan.py`
|
||||||
|
|
||||||
|
The private RPC path forwards the operation-specific deadline. Lower
|
||||||
|
layers preserve the ordinary infinite-deadline call shape.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tractor/ipc/_transport.py`
|
||||||
|
|
||||||
|
`MsgpackTransport.send()` applies the deadline inside its complete-frame
|
||||||
|
shield. If the deadline expires after partial publication, it closes
|
||||||
|
the unusable stream before releasing the send lock.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tests/ipc/test_each_tpt.py`
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
|
||||||
|
|
||||||
|
Focused regressions prove a partial-frame timeout closes the stream and
|
||||||
|
that actor-cancel publication and acknowledgement share one budget.
|
||||||
|
|
||||||
|
The implementation removes the earlier `_SendStatus`, watcher task,
|
||||||
|
coalescing, shared cancel result, and per-waiter state. Four focused
|
||||||
|
transport and actor-cancel tests pass.
|
||||||
|
|
@ -15,7 +15,10 @@ from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import trio
|
import trio
|
||||||
from trio.testing import wait_all_tasks_blocked
|
from trio.testing import (
|
||||||
|
MockClock,
|
||||||
|
wait_all_tasks_blocked,
|
||||||
|
)
|
||||||
import tractor
|
import tractor
|
||||||
from tractor import Actor
|
from tractor import Actor
|
||||||
from tractor.discovery import _addr
|
from tractor.discovery import _addr
|
||||||
|
|
@ -134,6 +137,65 @@ def test_cancelled_transport_send_completes_frame():
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
||||||
|
|
||||||
|
def test_transport_send_deadline_closes_partial_frame():
|
||||||
|
'''
|
||||||
|
Bound one shielded frame without exposing a corrupt stream.
|
||||||
|
|
||||||
|
Ordinary cancellation cannot interrupt complete-frame publication.
|
||||||
|
Actor-wide cancellation instead passes its absolute deadline into
|
||||||
|
this operation. The fake stream writes a partial header and stalls;
|
||||||
|
when the send's own deadline fires, the transport must close the
|
||||||
|
stream before releasing its shared send lock and report the channel
|
||||||
|
unusable.
|
||||||
|
|
||||||
|
'''
|
||||||
|
class StalledStream:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.closed = False
|
||||||
|
self.wire = bytearray()
|
||||||
|
|
||||||
|
async def send_all(
|
||||||
|
self,
|
||||||
|
data: bytes,
|
||||||
|
) -> None:
|
||||||
|
self.wire.extend(data[:2])
|
||||||
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
async def aclose(self) -> None:
|
||||||
|
self.closed = True
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
stream = StalledStream()
|
||||||
|
transport = object.__new__(MsgpackTransport)
|
||||||
|
transport.stream = stream
|
||||||
|
transport._send_lock = trio.StrictFIFOLock()
|
||||||
|
msg = tractor.msg.Start(
|
||||||
|
ns=__name__,
|
||||||
|
func='add_one',
|
||||||
|
kwargs={'n': 1},
|
||||||
|
uid=('root', 'test'),
|
||||||
|
cid='deadline-send',
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
tractor.TransportClosed,
|
||||||
|
match='frame publication exceeded',
|
||||||
|
):
|
||||||
|
await transport.send(
|
||||||
|
msg,
|
||||||
|
send_deadline=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert stream.closed
|
||||||
|
assert len(stream.wire) == 2
|
||||||
|
assert not transport._send_lock.locked()
|
||||||
|
|
||||||
|
trio.run(
|
||||||
|
main,
|
||||||
|
clock=MockClock(autojump_threshold=0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_cancelled_transport_send_preserves_cancellation():
|
def test_cancelled_transport_send_preserves_cancellation():
|
||||||
'''
|
'''
|
||||||
Prefer sender cancellation when teardown closes the stream.
|
Prefer sender cancellation when teardown closes the stream.
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,14 @@ from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import trio
|
import trio
|
||||||
|
from trio.testing import MockClock
|
||||||
import tractor
|
import tractor
|
||||||
from tractor import (
|
from tractor import (
|
||||||
RemoteActorError,
|
RemoteActorError,
|
||||||
to_actor,
|
to_actor,
|
||||||
)
|
)
|
||||||
from tractor._testing import tractor_test
|
from tractor._testing import tractor_test
|
||||||
|
from tractor._exceptions import ActorTooSlowError
|
||||||
from tractor.msg import ptr as msgptr
|
from tractor.msg import ptr as msgptr
|
||||||
from tractor.msg.ptr import NamespacePath
|
from tractor.msg.ptr import NamespacePath
|
||||||
from tractor.to_actor import _api as to_actor_api
|
from tractor.to_actor import _api as to_actor_api
|
||||||
|
|
@ -254,6 +256,67 @@ async def test_cancel_ack_failure_hard_reaps_child(
|
||||||
assert not an._children
|
assert not an._children
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel_actor_timeout_closes_blocked_send():
|
||||||
|
'''
|
||||||
|
Thread one absolute cancel deadline into shielded frame publication.
|
||||||
|
|
||||||
|
The cancel RPC's outer timeout cannot penetrate a complete-frame
|
||||||
|
shield. The fake private RPC applies the forwarded send deadline to
|
||||||
|
its own shielded wait, then checkpoints into the outer scope. A
|
||||||
|
bounded `ActorTooSlowError` and the recorded absolute deadline prove
|
||||||
|
publication and acknowledgement share one timeout budget.
|
||||||
|
|
||||||
|
'''
|
||||||
|
class ConnectedChannel:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._cancel_called = False
|
||||||
|
self.aid = tractor.msg.Aid(
|
||||||
|
name='blocked_peer',
|
||||||
|
uuid='test',
|
||||||
|
)
|
||||||
|
|
||||||
|
def connected(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
channel = ConnectedChannel()
|
||||||
|
portal = object.__new__(tractor.Portal)
|
||||||
|
portal._chan = channel
|
||||||
|
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')
|
||||||
|
assert kwargs == {}
|
||||||
|
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()
|
||||||
|
|
||||||
|
portal._run_from_ns = blocked_cancel
|
||||||
|
with pytest.raises(ActorTooSlowError):
|
||||||
|
await portal.cancel_actor(
|
||||||
|
timeout=1,
|
||||||
|
raise_on_timeout=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert deadlines == [1.]
|
||||||
|
|
||||||
|
trio.run(
|
||||||
|
main,
|
||||||
|
clock=MockClock(autojump_threshold=0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_late_child_reap_registration_is_released():
|
def test_late_child_reap_registration_is_released():
|
||||||
'''
|
'''
|
||||||
Preserve a nursery-wide reap request across child startup.
|
Preserve a nursery-wide reap request across child startup.
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,7 @@ class Channel:
|
||||||
payload: Any,
|
payload: Any,
|
||||||
|
|
||||||
hide_tb: bool = False,
|
hide_tb: bool = False,
|
||||||
|
send_deadline: float = float('inf'),
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -320,6 +321,9 @@ class Channel:
|
||||||
expected-graceful cases, normally ephemercal
|
expected-graceful cases, normally ephemercal
|
||||||
(re/dis)connects.
|
(re/dis)connects.
|
||||||
|
|
||||||
|
`send_deadline` is an absolute Trio clock deadline forwarded
|
||||||
|
only to transports that support bounded frame publication.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
try:
|
try:
|
||||||
|
|
@ -330,10 +334,17 @@ class Channel:
|
||||||
f'{pformat(payload)}\n'
|
f'{pformat(payload)}\n'
|
||||||
)
|
)
|
||||||
# assert self._transport # but why typing?
|
# assert self._transport # but why typing?
|
||||||
await self._transport.send(
|
if send_deadline == float('inf'):
|
||||||
payload,
|
await self._transport.send(
|
||||||
hide_tb=hide_tb,
|
payload,
|
||||||
)
|
hide_tb=hide_tb,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self._transport.send(
|
||||||
|
payload,
|
||||||
|
hide_tb=hide_tb,
|
||||||
|
send_deadline=send_deadline,
|
||||||
|
)
|
||||||
except (
|
except (
|
||||||
BaseException,
|
BaseException,
|
||||||
MsgTypeError,
|
MsgTypeError,
|
||||||
|
|
|
||||||
|
|
@ -439,6 +439,7 @@ class MsgpackTransport(MsgTransport):
|
||||||
|
|
||||||
strict_types: bool = True,
|
strict_types: bool = True,
|
||||||
hide_tb: bool = True,
|
hide_tb: bool = True,
|
||||||
|
send_deadline: float = float('inf'),
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -447,6 +448,10 @@ 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 this complete frame. A
|
||||||
|
timeout destroys the stream because a partial prefix may have
|
||||||
|
reached the wire.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
|
|
||||||
|
|
@ -513,12 +518,26 @@ class MsgpackTransport(MsgTransport):
|
||||||
# the frame is complete, the explicit checkpoint
|
# the frame is complete, the explicit checkpoint
|
||||||
# immediately delivers any pending cancellation.
|
# immediately delivers any pending cancellation.
|
||||||
#
|
#
|
||||||
# This can delay cancellation while a peer is not
|
# Ordinary sends may delay cancellation while a peer is
|
||||||
# reading; peer/channel teardown must close the stream
|
# not reading. Actor-wide cancel requests pass their own
|
||||||
# to unblock a permanently stalled socket write.
|
# deadline so this operation can close a stalled stream.
|
||||||
with trio.CancelScope(shield=True):
|
with trio.CancelScope(
|
||||||
|
deadline=send_deadline,
|
||||||
|
shield=True,
|
||||||
|
) as send_cs:
|
||||||
await self.stream.send_all(size + bytes_data)
|
await self.stream.send_all(size + bytes_data)
|
||||||
|
|
||||||
|
if send_cs.cancelled_caught:
|
||||||
|
# This frame may be partial. Destroy the stream
|
||||||
|
# before releasing `_send_lock` so no later sender
|
||||||
|
# can append bytes to a corrupted frame.
|
||||||
|
await trio.aclose_forcefully(self.stream)
|
||||||
|
await trio.lowlevel.checkpoint_if_cancelled()
|
||||||
|
raise TransportClosed(
|
||||||
|
'IPC frame publication exceeded its '
|
||||||
|
f'deadline of {send_deadline!r}'
|
||||||
|
)
|
||||||
|
|
||||||
await trio.lowlevel.checkpoint_if_cancelled()
|
await trio.lowlevel.checkpoint_if_cancelled()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -319,45 +319,61 @@ class Portal:
|
||||||
or
|
or
|
||||||
self.cancel_timeout
|
self.cancel_timeout
|
||||||
)
|
)
|
||||||
|
cancel_deadline: float = (
|
||||||
|
trio.current_time()
|
||||||
|
+
|
||||||
|
cancel_timeout
|
||||||
|
)
|
||||||
|
# NOTE: Actor-runtime cancellation currently rides the normal
|
||||||
|
# RPC envelope:
|
||||||
|
#
|
||||||
|
# `Start(self.cancel)` -> `StartAck` -> `CancelAck`.
|
||||||
|
#
|
||||||
|
# `Actor.start_remote_task()` consumes the `StartAck`, then
|
||||||
|
# `._run_from_ns()` returns only after `PldRx.recv_pld()`
|
||||||
|
# decodes the final `CancelAck`. Thus this flag means that ack
|
||||||
|
# reached this portal after the peer's `Actor.cancel()` routine
|
||||||
|
# completed; it does not prove the peer OS process has exited.
|
||||||
|
# A dedicated `Cancel` request msg can eventually replace the
|
||||||
|
# internal `Start` RPC envelope and its extra `StartAck`.
|
||||||
|
cancel_ack_received: bool = False
|
||||||
try:
|
try:
|
||||||
# send cancel cmd - might not get response
|
with trio.move_on_at(cancel_deadline) as cs:
|
||||||
# XXX: sure would be nice to make this work with
|
|
||||||
# a proper shield
|
|
||||||
with trio.move_on_after(cancel_timeout) as cs:
|
|
||||||
cs.shield: bool = True
|
cs.shield: bool = True
|
||||||
await self.run_from_ns(
|
await self._run_from_ns(
|
||||||
'self',
|
'self',
|
||||||
'cancel',
|
'cancel',
|
||||||
|
kwargs={},
|
||||||
|
cancel_on_startup=False,
|
||||||
|
send_deadline=cancel_deadline,
|
||||||
)
|
)
|
||||||
return True
|
cancel_ack_received = True
|
||||||
|
|
||||||
# `move_on_after` fired — peer didn't ack within
|
# Preserve shielded actor teardown, then immediately
|
||||||
|
# redeliver any cancellation pending from an outer scope.
|
||||||
|
await trio.lowlevel.checkpoint_if_cancelled()
|
||||||
|
|
||||||
|
# `move_on_at` fired — peer didn't ack within
|
||||||
# bounded window. Behaviour depends on
|
# bounded window. Behaviour depends on
|
||||||
# `raise_on_timeout`:
|
# `raise_on_timeout`:
|
||||||
if (
|
if cs.cancelled_caught:
|
||||||
cs.cancelled_caught
|
if raise_on_timeout:
|
||||||
and
|
raise ActorTooSlowError(
|
||||||
raise_on_timeout
|
f'Peer {peer_id} did not ack its '
|
||||||
):
|
f'`Actor.cancel()` RPC within bounded wait '
|
||||||
raise ActorTooSlowError(
|
f'of {cancel_timeout!r}s'
|
||||||
f'Peer {peer_id} did not ack its '
|
)
|
||||||
f'`Actor.cancel()` RPC within bounded wait '
|
|
||||||
f'of {cancel_timeout!r}s'
|
|
||||||
)
|
|
||||||
|
|
||||||
# legacy fire-and-forget path: log + return False so
|
# Legacy fire-and-forget callers decide whether to
|
||||||
# the caller can decide whether to escalate.
|
# escalate the missed acknowledgement themselves.
|
||||||
#
|
log.debug(
|
||||||
# NOTE, we also land here in the (unexpected) case where
|
f'May have failed to cancel peer?\n'
|
||||||
# the shielded `move_on_after` block exits WITHOUT
|
f'\n'
|
||||||
# `return True` and WITHOUT the deadline firing — prefer
|
f'c)=?> {peer_id}\n'
|
||||||
# a soft `False` over an `assert`-crash mid-teardown.
|
)
|
||||||
log.debug(
|
return False
|
||||||
f'May have failed to cancel peer?\n'
|
|
||||||
f'\n'
|
return cancel_ack_received
|
||||||
f'c)=?> {peer_id}\n'
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
except TransportClosed as tpt_err:
|
except TransportClosed as tpt_err:
|
||||||
ipc_borked_report: str = (
|
ipc_borked_report: str = (
|
||||||
|
|
@ -379,16 +395,24 @@ class Portal:
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# TODO: Replace actor-runtime cancellation's internal
|
||||||
|
# `Start -> StartAck -> CancelAck` RPC with a dedicated
|
||||||
|
# `Cancel -> CancelAck` transaction:
|
||||||
|
# https://github.com/goodboy/tractor/issues/506
|
||||||
async def _run_from_ns(
|
async def _run_from_ns(
|
||||||
self,
|
self,
|
||||||
namespace_path: str,
|
namespace_path: str,
|
||||||
function_name: str,
|
function_name: str,
|
||||||
kwargs: dict[str, Any],
|
kwargs: dict[str, Any],
|
||||||
cancel_on_startup: bool = True,
|
cancel_on_startup: bool = True,
|
||||||
|
send_deadline: float = float('inf'),
|
||||||
) -> Any:
|
) -> Any:
|
||||||
'''
|
'''
|
||||||
Run a namespace target with local startup policy controls.
|
Run a namespace target with local startup policy controls.
|
||||||
|
|
||||||
|
`send_deadline` bounds only publication of the `Start` frame;
|
||||||
|
the caller owns any larger RPC/acknowledgement deadline.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
nsf = NamespacePath(
|
nsf = NamespacePath(
|
||||||
f'{namespace_path}:{function_name}'
|
f'{namespace_path}:{function_name}'
|
||||||
|
|
@ -399,6 +423,7 @@ class Portal:
|
||||||
kwargs=kwargs,
|
kwargs=kwargs,
|
||||||
portal=self,
|
portal=self,
|
||||||
cancel_on_startup=cancel_on_startup,
|
cancel_on_startup=cancel_on_startup,
|
||||||
|
send_deadline=send_deadline,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return await ctx._pld_rx.recv_pld(
|
return await ctx._pld_rx.recv_pld(
|
||||||
|
|
|
||||||
|
|
@ -793,6 +793,11 @@ class Actor:
|
||||||
ack_timeout: float = float('inf'),
|
ack_timeout: float = float('inf'),
|
||||||
cancel_on_startup: bool = True,
|
cancel_on_startup: bool = True,
|
||||||
|
|
||||||
|
# Optional absolute deadline for publishing this exact `Start`
|
||||||
|
# frame. Used by actor-wide cancel RPCs whose outer timeout
|
||||||
|
# cannot penetrate complete-frame transport shielding.
|
||||||
|
send_deadline: float = float('inf'),
|
||||||
|
|
||||||
) -> Context:
|
) -> Context:
|
||||||
'''
|
'''
|
||||||
Send a `'cmd'` msg to a remote actor, which requests the
|
Send a `'cmd'` msg to a remote actor, which requests the
|
||||||
|
|
@ -845,7 +850,13 @@ class Actor:
|
||||||
)
|
)
|
||||||
start_published: bool = False
|
start_published: bool = False
|
||||||
try:
|
try:
|
||||||
await chan.send(msg)
|
if send_deadline == float('inf'):
|
||||||
|
await chan.send(msg)
|
||||||
|
else:
|
||||||
|
await chan.send(
|
||||||
|
msg,
|
||||||
|
send_deadline=send_deadline,
|
||||||
|
)
|
||||||
start_published = True
|
start_published = True
|
||||||
|
|
||||||
# NOTE wait on first `StartAck` response msg and validate;
|
# NOTE wait on first `StartAck` response msg and validate;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue