Compare commits
15 Commits
fa537f9e73
...
cac5086153
| Author | SHA1 | Date |
|---|---|---|
|
|
cac5086153 | |
|
|
5ddb104365 | |
|
|
268f328d0a | |
|
|
2e2d988bf7 | |
|
|
bffeba952a | |
|
|
3e04968e79 | |
|
|
bc91a1bdfa | |
|
|
d55c971644 | |
|
|
dc8562cf14 | |
|
|
4607090a5d | |
|
|
727aee0f84 | |
|
|
725c4ef212 | |
|
|
c4dee6ab4b | |
|
|
e1039b4d86 | |
|
|
3335865d5d |
|
|
@ -279,4 +279,25 @@ log_cli = false
|
||||||
|
|
||||||
# https://docs.pytest.org/en/stable/reference/reference.html#confval-console_output_style
|
# https://docs.pytest.org/en/stable/reference/reference.html#confval-console_output_style
|
||||||
console_output_style = 'progress'
|
console_output_style = 'progress'
|
||||||
|
|
||||||
|
# `pexpect` (used only by the `devx` debugger REPL tests) allocates
|
||||||
|
# its child pty via stdlib `pty.py:os.forkpty()`, which CPython
|
||||||
|
# 3.12+ flags as unsafe in a multi-threaded process. This is NOT
|
||||||
|
# ours to fix at source — we never call `forkpty()`; pexpect does.
|
||||||
|
# UPSTREAM to actually resolve: pexpect would need to avoid
|
||||||
|
# `forkpty()` under multithreading (or perform the pty spawn before
|
||||||
|
# any thread starts); until then this single third-party warning is
|
||||||
|
# the one we filter rather than fix.
|
||||||
|
filterwarnings = [
|
||||||
|
'ignore:This process \(pid=\d+\) is multi-threaded, use of forkpty\(\):DeprecationWarning',
|
||||||
|
|
||||||
|
# `tests/test_legacy_one_way_streaming.py` exists *specifically*
|
||||||
|
# to exercise the DEPRECATED `@tractor.stream` async-gen API, so
|
||||||
|
# its decoration-time `DeprecationWarning` is expected — and since
|
||||||
|
# it fires at import (module-level decorators) it can't be caught
|
||||||
|
# by a per-test mark, hence this test-wide ini filter. Real
|
||||||
|
# library users still see the warning; drop this once the legacy
|
||||||
|
# `@tractor.stream` / `Portal.open_stream_from()` API is removed.
|
||||||
|
'ignore:`@tractor.stream decorated funcs:DeprecationWarning',
|
||||||
|
]
|
||||||
# ------ tool.pytest ------
|
# ------ tool.pytest ------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
"""
|
||||||
|
`hung-dump`: snapshot diagnostic state for a hung
|
||||||
|
`pytest`/`tractor` process tree.
|
||||||
|
|
||||||
|
For each pid (and all `pgrep -P` descendants), prints
|
||||||
|
- `ps` forest header,
|
||||||
|
- `/proc/<pid>/wchan` + `/proc/<pid>/stack` (kernel-side
|
||||||
|
blocked-on syscall),
|
||||||
|
- `sudo py-spy dump` (python-side stack).
|
||||||
|
|
||||||
|
Usage (xonsh):
|
||||||
|
source-foreign xonsh ./scripts/hung-dump.xsh
|
||||||
|
hung-dump <pid> [<pid> ...]
|
||||||
|
|
||||||
|
Or just:
|
||||||
|
source ./scripts/hung-dump.xsh
|
||||||
|
|
||||||
|
Tip — pipe to a paste buffer:
|
||||||
|
hung-dump 1336765 |t /tmp/hung.log
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _hung_dump(args):
|
||||||
|
import subprocess as sp
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
print('usage: hung-dump <pid> [<pid> ...]')
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def kids(pid):
|
||||||
|
try:
|
||||||
|
out = sp.check_output(
|
||||||
|
['pgrep', '-P', str(pid)],
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except sp.CalledProcessError:
|
||||||
|
return []
|
||||||
|
return [int(p) for p in out.split() if p]
|
||||||
|
|
||||||
|
def tree(pid):
|
||||||
|
out = [pid]
|
||||||
|
for k in kids(pid):
|
||||||
|
out.extend(tree(k))
|
||||||
|
return out
|
||||||
|
|
||||||
|
pids = [
|
||||||
|
p
|
||||||
|
for r in (int(a) for a in args)
|
||||||
|
for p in tree(r)
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f'# tree: {pids}')
|
||||||
|
print('\n## ps forest')
|
||||||
|
$[ps -o pid,ppid,pgid,stat,cmd -p @(','.join(map(str, pids)))]
|
||||||
|
|
||||||
|
for p in pids:
|
||||||
|
print(f'\n## pid {p}')
|
||||||
|
for f in ('wchan', 'stack'):
|
||||||
|
path = Path(f'/proc/{p}/{f}')
|
||||||
|
try:
|
||||||
|
txt = path.read_text().rstrip()
|
||||||
|
print(f'-- /proc/{p}/{f} --\n{txt}')
|
||||||
|
except PermissionError:
|
||||||
|
# `stack` requires CAP_SYS_PTRACE; retry
|
||||||
|
# via sudo -n so creds-cached users don't
|
||||||
|
# block on prompt.
|
||||||
|
try:
|
||||||
|
txt = sp.check_output(
|
||||||
|
['sudo', '-n', 'cat', str(path)],
|
||||||
|
text=True,
|
||||||
|
stderr=sp.DEVNULL,
|
||||||
|
).rstrip()
|
||||||
|
print(f'-- /proc/{p}/{f} (via sudo) --\n{txt}')
|
||||||
|
except sp.CalledProcessError:
|
||||||
|
print(
|
||||||
|
f'-- /proc/{p}/{f}: '
|
||||||
|
'PermissionError (try `sudo true` first) --'
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f'-- /proc/{p}/{f}: proc gone --')
|
||||||
|
|
||||||
|
print(f'-- py-spy {p} --')
|
||||||
|
try:
|
||||||
|
$[sudo -n py-spy dump --pid @(p)]
|
||||||
|
except Exception as e:
|
||||||
|
print(f' (py-spy failed: {e})')
|
||||||
|
|
||||||
|
|
||||||
|
aliases['hung-dump'] = _hung_dump
|
||||||
|
|
@ -92,7 +92,7 @@ def test_shield_pause(
|
||||||
expect(
|
expect(
|
||||||
child,
|
child,
|
||||||
# end-of-tree delimiter
|
# end-of-tree delimiter
|
||||||
"end-of-\('root'",
|
r"end-of-\('root'",
|
||||||
)
|
)
|
||||||
_before: str = assert_before(
|
_before: str = assert_before(
|
||||||
child,
|
child,
|
||||||
|
|
@ -157,7 +157,7 @@ def test_shield_pause(
|
||||||
expect(
|
expect(
|
||||||
child,
|
child,
|
||||||
# end-of-subactor's-tree delimiter
|
# end-of-subactor's-tree delimiter
|
||||||
"end-of-\('hanger'",
|
r"end-of-\('hanger'",
|
||||||
)
|
)
|
||||||
_before: str = assert_before(
|
_before: str = assert_before(
|
||||||
child,
|
child,
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,13 @@ def test_register_duplicate_name(
|
||||||
'test_register_duplicate_name: '
|
'test_register_duplicate_name: '
|
||||||
'`wait_for_actor` returned'
|
'`wait_for_actor` returned'
|
||||||
)
|
)
|
||||||
assert portal.channel.uid in (p2.channel.uid, p1.channel.uid)
|
assert (
|
||||||
|
portal.channel.aid.uid
|
||||||
|
in (
|
||||||
|
p2.channel.aid.uid,
|
||||||
|
p1.channel.aid.uid,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
log.cancel(
|
log.cancel(
|
||||||
'test_register_duplicate_name: '
|
'test_register_duplicate_name: '
|
||||||
|
|
@ -242,8 +248,14 @@ def test_dup_name_cancel_cascade_escalates_to_hard_kill(
|
||||||
# name; doesn't matter which one (registrar will
|
# name; doesn't matter which one (registrar will
|
||||||
# have last-wins semantics under same-name).
|
# have last-wins semantics under same-name).
|
||||||
async with tractor.wait_for_actor('doggy') as portal:
|
async with tractor.wait_for_actor('doggy') as portal:
|
||||||
expected_uids = {p.channel.uid for p in portals}
|
expected_uids = {
|
||||||
assert portal.channel.uid in expected_uids
|
p.channel.aid.uid
|
||||||
|
for p in portals
|
||||||
|
}
|
||||||
|
assert (
|
||||||
|
portal.channel.aid.uid
|
||||||
|
in expected_uids
|
||||||
|
)
|
||||||
|
|
||||||
# critical section: this MUST return within
|
# critical section: this MUST return within
|
||||||
# `fail_after_s` even when one or more cancel-RPC
|
# `fail_after_s` even when one or more cancel-RPC
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,10 @@ def enc_nsp(obj: Any) -> Any:
|
||||||
actor: Actor = tractor.current_actor(
|
actor: Actor = tractor.current_actor(
|
||||||
err_on_no_runtime=False,
|
err_on_no_runtime=False,
|
||||||
)
|
)
|
||||||
uid: tuple[str, str]|None = None if not actor else actor.uid
|
uid: tuple[str, str]|None = (
|
||||||
|
None if not actor
|
||||||
|
else actor.aid.uid
|
||||||
|
)
|
||||||
print(f'{uid} ENC HOOK')
|
print(f'{uid} ENC HOOK')
|
||||||
|
|
||||||
match obj:
|
match obj:
|
||||||
|
|
@ -95,7 +98,10 @@ def dec_nsp(
|
||||||
actor: Actor = tractor.current_actor(
|
actor: Actor = tractor.current_actor(
|
||||||
err_on_no_runtime=False,
|
err_on_no_runtime=False,
|
||||||
)
|
)
|
||||||
uid: tuple[str, str]|None = None if not actor else actor.uid
|
uid: tuple[str, str]|None = (
|
||||||
|
None if not actor
|
||||||
|
else actor.aid.uid
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
f'{uid}\n'
|
f'{uid}\n'
|
||||||
'CUSTOM DECODE\n'
|
'CUSTOM DECODE\n'
|
||||||
|
|
@ -419,7 +425,8 @@ async def send_back_values(
|
||||||
and ensure we can round trip a func ref with our parent.
|
and ensure we can round trip a func ref with our parent.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
uid: tuple = tractor.current_actor().uid
|
_aid = tractor.current_actor().aid
|
||||||
|
uid: tuple = _aid.uid
|
||||||
|
|
||||||
# init state in sub-actor should be default
|
# init state in sub-actor should be default
|
||||||
chk_codec_applied(
|
chk_codec_applied(
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ async def subscribe(
|
||||||
new_subs = set(new_subs)
|
new_subs = set(new_subs)
|
||||||
remove = new_subs - _registry.keys()
|
remove = new_subs - _registry.keys()
|
||||||
|
|
||||||
print(f'setting sub to {new_subs} for {ctx.chan.uid}')
|
print(f'setting sub to {new_subs} for {ctx.chan.aid.uid}')
|
||||||
|
|
||||||
# remove old subs
|
# remove old subs
|
||||||
for sub in remove:
|
for sub in remove:
|
||||||
|
|
@ -84,7 +84,8 @@ async def consumer(
|
||||||
subs: list[str],
|
subs: list[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
uid = tractor.current_actor().uid
|
_aid = tractor.current_actor().aid
|
||||||
|
uid = _aid.uid
|
||||||
|
|
||||||
async with tractor.wait_for_actor('publisher') as portal:
|
async with tractor.wait_for_actor('publisher') as portal:
|
||||||
async with portal.open_context(subscribe) as (ctx, first):
|
async with portal.open_context(subscribe) as (ctx, first):
|
||||||
|
|
|
||||||
|
|
@ -351,7 +351,7 @@ async def test_cancel_infinite_streamer(
|
||||||
|
|
||||||
# we support trio's cancellation system
|
# we support trio's cancellation system
|
||||||
assert cancel_scope.cancelled_caught
|
assert cancel_scope.cancelled_caught
|
||||||
assert n.cancelled
|
assert n.cancel_called
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -465,7 +465,7 @@ async def test_some_cancels_all(
|
||||||
elif isinstance(err, tractor.RemoteActorError):
|
elif isinstance(err, tractor.RemoteActorError):
|
||||||
assert err.boxed_type == err_type
|
assert err.boxed_type == err_type
|
||||||
|
|
||||||
assert an.cancelled is True
|
assert an.cancel_called is True
|
||||||
assert not an._children
|
assert not an._children
|
||||||
else:
|
else:
|
||||||
pytest.fail("Should have gotten a remote assertion error?")
|
pytest.fail("Should have gotten a remote assertion error?")
|
||||||
|
|
@ -821,21 +821,28 @@ def test_cancel_via_SIGINT_other_task(
|
||||||
async def main():
|
async def main():
|
||||||
# should never timeout since SIGINT should cancel the current program
|
# should never timeout since SIGINT should cancel the current program
|
||||||
with trio.fail_after(timeout):
|
with trio.fail_after(timeout):
|
||||||
async with (
|
async with trio.open_nursery() as tn:
|
||||||
|
|
||||||
# XXX ?TODO? why no work!?
|
|
||||||
# tractor.trionics.collapse_eg(),
|
|
||||||
trio.open_nursery(
|
|
||||||
strict_exception_groups=False,
|
|
||||||
) as tn,
|
|
||||||
):
|
|
||||||
await tn.start(spawn_and_sleep_forever)
|
await tn.start(spawn_and_sleep_forever)
|
||||||
if 'mp' in spawn_backend:
|
if 'mp' in spawn_backend:
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
os.kill(pid, signal.SIGINT)
|
os.kill(pid, signal.SIGINT)
|
||||||
|
|
||||||
with pytest.raises(KeyboardInterrupt):
|
# SIGINT -> `KeyboardInterrupt`; under `trio>=0.25`'s strict
|
||||||
|
# exception-groups the KI surfaces wrapped in a (cancel-padded)
|
||||||
|
# `BaseExceptionGroup` rather than bare — so accept either form
|
||||||
|
# (replaces the now-deprecated `strict_exception_groups=False`,
|
||||||
|
# and `collapse_eg()` can't help since the group is multi-exc:
|
||||||
|
# the KI rides alongside the child-task `Cancelled`s).
|
||||||
|
with pytest.raises(BaseException) as excinfo:
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
exc = excinfo.value
|
||||||
|
assert (
|
||||||
|
isinstance(exc, KeyboardInterrupt)
|
||||||
|
or (
|
||||||
|
isinstance(exc, BaseExceptionGroup)
|
||||||
|
and exc.subgroup(KeyboardInterrupt) is not None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def spin_for(period=3):
|
async def spin_for(period=3):
|
||||||
|
|
|
||||||
|
|
@ -311,7 +311,7 @@ def test_parent_cancels(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
) -> None:
|
) -> None:
|
||||||
actor: Actor = current_actor()
|
actor: Actor = current_actor()
|
||||||
uid: tuple = actor.uid
|
uid: tuple = actor.aid.uid
|
||||||
_ctxc: ContextCancelled|None = None
|
_ctxc: ContextCancelled|None = None
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -712,9 +712,9 @@ async def test_parent_exits_ctx_after_child_enters_stream(
|
||||||
assert (
|
assert (
|
||||||
ctxc.canceller
|
ctxc.canceller
|
||||||
==
|
==
|
||||||
current_actor().uid
|
current_actor().aid.uid
|
||||||
==
|
==
|
||||||
root.uid
|
root.aid.uid
|
||||||
)
|
)
|
||||||
|
|
||||||
# channel should still be up
|
# channel should still be up
|
||||||
|
|
@ -1219,7 +1219,7 @@ def test_maybe_allow_overruns_stream(
|
||||||
|
|
||||||
if cancel_ctx:
|
if cancel_ctx:
|
||||||
assert isinstance(res, ContextCancelled)
|
assert isinstance(res, ContextCancelled)
|
||||||
assert tuple(res.canceller) == current_actor().uid
|
assert tuple(res.canceller) == current_actor().aid.uid
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(f'RX ROOT SIDE RESULT {res}')
|
print(f'RX ROOT SIDE RESULT {res}')
|
||||||
|
|
|
||||||
|
|
@ -956,7 +956,7 @@ async def manage_file(
|
||||||
'''
|
'''
|
||||||
|
|
||||||
tmp_path: Path = Path(tmp_path_str)
|
tmp_path: Path = Path(tmp_path_str)
|
||||||
tmp_file: Path = tmp_path / f'{" ".join(ctx._actor.uid)}.file'
|
tmp_file: Path = tmp_path / f'{" ".join(ctx._actor.aid.uid)}.file'
|
||||||
|
|
||||||
# create a the tmp file and tell the parent where it's at
|
# create a the tmp file and tell the parent where it's at
|
||||||
assert not tmp_file.is_file()
|
assert not tmp_file.is_file()
|
||||||
|
|
@ -1180,7 +1180,7 @@ def test_sigint_closes_lifetime_stack(
|
||||||
):
|
):
|
||||||
await ctx.wait_for_result()
|
await ctx.wait_for_result()
|
||||||
except tractor.ContextCancelled as ctxc:
|
except tractor.ContextCancelled as ctxc:
|
||||||
assert ctxc.canceller == ctx.chan.uid
|
assert ctxc.canceller == ctx.chan.aid.uid
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except trio.TooSlowError:
|
except trio.TooSlowError:
|
||||||
|
|
@ -1300,7 +1300,7 @@ async def caching_ep(
|
||||||
},
|
},
|
||||||
|
|
||||||
# lock around current actor task access
|
# lock around current actor task access
|
||||||
key=tractor.current_actor().uid,
|
key=tractor.current_actor().aid.uid,
|
||||||
|
|
||||||
) as (cache_hit, (clients, chan)),
|
) as (cache_hit, (clients, chan)),
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ async def test_self_is_registered(reg_addr):
|
||||||
assert actor.is_registrar
|
assert actor.is_registrar
|
||||||
with trio.fail_after(0.2):
|
with trio.fail_after(0.2):
|
||||||
async with tractor.wait_for_actor('root') as portal:
|
async with tractor.wait_for_actor('root') as portal:
|
||||||
assert portal.channel.uid[0] == 'root'
|
assert portal.channel.aid.name == 'root'
|
||||||
|
|
||||||
|
|
||||||
@tractor_test
|
@tractor_test
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ async def spawn(
|
||||||
|
|
||||||
assert len(an._children) == 1
|
assert len(an._children) == 1
|
||||||
assert (
|
assert (
|
||||||
portal.channel.uid
|
portal.channel.aid.uid
|
||||||
in
|
in
|
||||||
tractor.current_actor().ipc_server._peers
|
tractor.current_actor().ipc_server._peers
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1223,7 +1223,7 @@ def pack_error(
|
||||||
str,
|
str,
|
||||||
str | tuple[str, str]
|
str | tuple[str, str]
|
||||||
] = {}
|
] = {}
|
||||||
our_uid: tuple = current_actor().uid
|
our_uid: tuple[str, str] = current_actor().aid.uid
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isinstance(exc, RemoteActorError)
|
isinstance(exc, RemoteActorError)
|
||||||
|
|
|
||||||
|
|
@ -418,7 +418,7 @@ class MsgStream(trio.abc.Channel):
|
||||||
# it can't traverse the transport.
|
# it can't traverse the transport.
|
||||||
log.warning(
|
log.warning(
|
||||||
f'Stream was already destroyed?\n'
|
f'Stream was already destroyed?\n'
|
||||||
f'actor: {ctx.chan.uid}\n'
|
f'actor: {ctx.chan.aid.uid}\n'
|
||||||
f'ctx id: {ctx.cid}'
|
f'ctx id: {ctx.cid}'
|
||||||
)
|
)
|
||||||
drained.append(re)
|
drained.append(re)
|
||||||
|
|
@ -700,7 +700,7 @@ async def open_stream_from_ctx(
|
||||||
task: str = trio.lowlevel.current_task().name
|
task: str = trio.lowlevel.current_task().name
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'Stream opened after `Context.cancel()` called..?\n'
|
'Stream opened after `Context.cancel()` called..?\n'
|
||||||
f'task: {actor.uid[0]}:{task}\n'
|
f'task: {actor.aid.name}:{task}\n'
|
||||||
f'{ctx}'
|
f'{ctx}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -823,7 +823,7 @@ async def open_stream_from_ctx(
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.warning(
|
log.warning(
|
||||||
f'Stream was already destroyed?\n'
|
f'Stream was already destroyed?\n'
|
||||||
f'actor: {ctx.chan.uid}\n'
|
f'actor: {ctx.chan.aid.uid}\n'
|
||||||
f'ctx id: {ctx.cid}'
|
f'ctx id: {ctx.cid}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,12 @@ def get_rando_addr(
|
||||||
# `test_tpt_bind_addrs::bind-subset-reg`),
|
# `test_tpt_bind_addrs::bind-subset-reg`),
|
||||||
# - across parallel pytest sessions, the pid bias
|
# - across parallel pytest sessions, the pid bias
|
||||||
# makes coincident port choices unlikely.
|
# makes coincident port choices unlikely.
|
||||||
port: int = 1000 + (
|
# NB. floor at 1024, NOT 1000: ports <1024 are PRIVILEGED
|
||||||
|
# on linux (`net.ipv4.ip_unprivileged_port_start = 1024`),
|
||||||
|
# so a non-root `.bind()` on one raises `PermissionError`.
|
||||||
|
# The old `1000 +` floor could roll 1000-1023 -> flaky
|
||||||
|
# `[Errno 13] Permission denied` registry-listener binds.
|
||||||
|
port: int = 1024 + (
|
||||||
random.randint(0, 8999) + os.getpid()
|
random.randint(0, 8999) + os.getpid()
|
||||||
) % 9000
|
) % 9000
|
||||||
testrun_reg_addr = (
|
testrun_reg_addr = (
|
||||||
|
|
|
||||||
|
|
@ -491,6 +491,16 @@ def pytest_configure(
|
||||||
'cases (e.g. the `subint` GIL-starvation class documented '
|
'cases (e.g. the `subint` GIL-starvation class documented '
|
||||||
'in `ai/conc-anal/subint_sigint_starvation_issue.md`).'
|
'in `ai/conc-anal/subint_sigint_starvation_issue.md`).'
|
||||||
)
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
'markers',
|
||||||
|
'has_nested_actors: test spawns nested (>1-level) subactor '
|
||||||
|
'trees.'
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
'markers',
|
||||||
|
'trio: legacy mark for tests meant to run under the `trio` '
|
||||||
|
'spawn backend (e.g. `test_local.py`).'
|
||||||
|
)
|
||||||
|
|
||||||
# `--enable-stackscope`: install SIGUSR1 → trio task-tree
|
# `--enable-stackscope`: install SIGUSR1 → trio task-tree
|
||||||
# dump in pytest itself + propagate to every subactor via
|
# dump in pytest itself + propagate to every subactor via
|
||||||
|
|
|
||||||
|
|
@ -92,11 +92,11 @@ async def maybe_wait_for_debugger(
|
||||||
# tearing down.
|
# tearing down.
|
||||||
ctx_in_debug: Context|None = Lock.ctx_in_debug
|
ctx_in_debug: Context|None = Lock.ctx_in_debug
|
||||||
in_debug: tuple[str, str]|None = (
|
in_debug: tuple[str, str]|None = (
|
||||||
ctx_in_debug.chan.uid
|
ctx_in_debug.chan.aid.uid
|
||||||
if ctx_in_debug
|
if ctx_in_debug
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
if in_debug == current_actor().uid:
|
if in_debug == current_actor().aid.uid:
|
||||||
log.debug(
|
log.debug(
|
||||||
msg
|
msg
|
||||||
+
|
+
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import wrapt
|
||||||
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from .._context import Context
|
from .._context import Context
|
||||||
|
from ..msg import Yield
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['pub']
|
__all__ = ['pub']
|
||||||
|
|
@ -88,7 +89,18 @@ async def fan_out_to_ctxs(
|
||||||
if ctx_payloads:
|
if ctx_payloads:
|
||||||
for ctx, payload in ctx_payloads:
|
for ctx, payload in ctx_payloads:
|
||||||
try:
|
try:
|
||||||
await ctx.send_yield(payload)
|
# NOTE: inline the (now-deprecated)
|
||||||
|
# `Context.send_yield()` body to drop its
|
||||||
|
# `DeprecationWarning` without a behaviour change.
|
||||||
|
# The "proper" fix is migrating BOTH sides to
|
||||||
|
# `open_context()`/`open_stream()`, but the
|
||||||
|
# subscriber still uses the legacy
|
||||||
|
# `Portal.open_stream_from()` (no `started()`
|
||||||
|
# handshake) so `Context.open_stream()` can't be
|
||||||
|
# used here yet — see this module's rework TODO.
|
||||||
|
await ctx.chan.send(
|
||||||
|
Yield(cid=ctx.cid, pld=payload)
|
||||||
|
)
|
||||||
except (
|
except (
|
||||||
# That's right, anything you can think of...
|
# That's right, anything you can think of...
|
||||||
trio.ClosedResourceError, ConnectionResetError,
|
trio.ClosedResourceError, ConnectionResetError,
|
||||||
|
|
@ -117,7 +129,7 @@ def modify_subs(
|
||||||
|
|
||||||
Effectively a symbol subscription api.
|
Effectively a symbol subscription api.
|
||||||
"""
|
"""
|
||||||
log.info(f"{ctx.chan.uid} changed subscription to {topics}")
|
log.info(f"{ctx.chan.aid.uid} changed subscription to {topics}")
|
||||||
|
|
||||||
# update map from each symbol to requesting client's chan
|
# update map from each symbol to requesting client's chan
|
||||||
for topic in topics:
|
for topic in topics:
|
||||||
|
|
|
||||||
|
|
@ -345,9 +345,9 @@ class PldRx(Struct):
|
||||||
exc=mte,
|
exc=mte,
|
||||||
cid=msg.cid,
|
cid=msg.cid,
|
||||||
src_uid=(
|
src_uid=(
|
||||||
ipc.chan.uid
|
ipc.chan.aid.uid
|
||||||
if not is_started_send_side
|
if not is_started_send_side
|
||||||
else ipc._actor.uid
|
else ipc._actor.aid.uid
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
mte._ipc_msg = err_msg
|
mte._ipc_msg = err_msg
|
||||||
|
|
@ -711,7 +711,7 @@ async def drain_to_final_msg(
|
||||||
'Cancelling `MsgStream` drain since '
|
'Cancelling `MsgStream` drain since '
|
||||||
f'{reason}\n'
|
f'{reason}\n'
|
||||||
f'\n'
|
f'\n'
|
||||||
f'<= {ctx.chan.uid}\n'
|
f'<= {ctx.chan.aid.uid}\n'
|
||||||
f' |_{ctx._nsf}()\n'
|
f' |_{ctx._nsf}()\n'
|
||||||
f'\n'
|
f'\n'
|
||||||
f'=> {ctx._task}\n'
|
f'=> {ctx._task}\n'
|
||||||
|
|
@ -726,7 +726,7 @@ async def drain_to_final_msg(
|
||||||
else:
|
else:
|
||||||
report: str = (
|
report: str = (
|
||||||
'Ignoring "yield" msg during `ctx.result()` drain..\n'
|
'Ignoring "yield" msg during `ctx.result()` drain..\n'
|
||||||
f'<= {ctx.chan.uid}\n'
|
f'<= {ctx.chan.aid.uid}\n'
|
||||||
f' |_{ctx._nsf}()\n\n'
|
f' |_{ctx._nsf}()\n\n'
|
||||||
f'=> {ctx._task}\n'
|
f'=> {ctx._task}\n'
|
||||||
f' |_{ctx._stream}\n\n'
|
f' |_{ctx._stream}\n\n'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue