Compare commits
No commits in common. "584ea4e9add1f084a4fca03bf43b3251dfb3f925" and "0b63af020e82569054ab5a421f5d84e047c662f3" have entirely different histories.
584ea4e9ad
...
0b63af020e
|
|
@ -130,10 +130,9 @@ UDS: same-host, creds included
|
||||||
|
|
||||||
Pass ``enable_transports=['uds']`` and actors instead talk over
|
Pass ``enable_transports=['uds']`` and actors instead talk over
|
||||||
unix-domain sockets, with socket files placed in the per-user
|
unix-domain sockets, with socket files placed in the per-user
|
||||||
runtime dir: ``$XDG_RUNTIME_DIR/tractor/`` on linux, a short
|
runtime dir (``$XDG_RUNTIME_DIR/tractor/`` on linux, the
|
||||||
owner-only ``/tmp/tractor-<uid>`` dir on Darwin, and the
|
``platformdirs`` equivalent elsewhere). Two perks over tcp on a
|
||||||
``platformdirs`` equivalent elsewhere. Two perks over tcp on a single
|
single host:
|
||||||
host:
|
|
||||||
|
|
||||||
- no ports to fight over; addrs are just file paths,
|
- no ports to fight over; addrs are just file paths,
|
||||||
- the kernel snitches on your peer for free: the listening side
|
- the kernel snitches on your peer for free: the listening side
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,8 @@ clan shares one registry with zero config on your part.
|
||||||
The bootstrap rule inside ``open_root_actor()`` is delightfully
|
The bootstrap rule inside ``open_root_actor()`` is delightfully
|
||||||
simple:
|
simple:
|
||||||
|
|
||||||
- on boot, probe every addr in ``registry_addrs`` with a bounded
|
- on boot, ping every socket addr in ``registry_addrs``; when none
|
||||||
Tractor ``Aid`` handshake; when none are passed the per-transport
|
are passed the per-transport defaults are used: for TCP the
|
||||||
defaults are used: for TCP the
|
|
||||||
loopback ``('127.0.0.1', 1616)``, for UDS a
|
loopback ``('127.0.0.1', 1616)``, for UDS a
|
||||||
``registry@1616.sock`` file,
|
``registry@1616.sock`` file,
|
||||||
|
|
||||||
|
|
@ -54,11 +53,9 @@ simple:
|
||||||
actor and register with the *existing* registry; your own IPC
|
actor and register with the *existing* registry; your own IPC
|
||||||
server binds random same-transport addrs instead,
|
server binds random same-transport addrs instead,
|
||||||
|
|
||||||
- if every address is absent, congratulations: you just became the
|
- if **nothing answers, congratulations: you just became the
|
||||||
registrar. Your transport server binds the registry addrs
|
registrar**. Your transport server binds the registry addrs
|
||||||
themselves and you start serving lookups for everyone else,
|
themselves and you start serving lookups for everyone else.
|
||||||
- if no registrar answers but an address is occupied by a foreign or
|
|
||||||
non-responsive endpoint, startup fails instead of binding over it.
|
|
||||||
|
|
||||||
Pass ``ensure_registry=True`` when your program *requires* being
|
Pass ``ensure_registry=True`` when your program *requires* being
|
||||||
the one-and-only registrar; boot then fails loudly with a
|
the one-and-only registrar; boot then fails loudly with a
|
||||||
|
|
@ -199,10 +196,9 @@ the existing registrar:
|
||||||
|
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
||||||
Per the bootstrap rules above, if those addrs are absent this process
|
Per the bootstrap rules above, if the registrar at those addrs is
|
||||||
becomes its own registrar root, so the same code works standalone and
|
*not* reachable this process simply becomes its own (registrar)
|
||||||
as a tree-joiner. An occupied address that does not complete a Tractor
|
root — so the same code works standalone and as a tree-joiner.
|
||||||
registrar handshake fails startup instead of being rebound.
|
|
||||||
|
|
||||||
"Arbiter"? A legacy naming note
|
"Arbiter"? A legacy naming note
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
|
||||||
|
|
@ -185,10 +185,8 @@ first with a bounded grace window — so actor runtimes can run
|
||||||
their ``trio`` teardown paths — escalating to ``SIGKILL`` only as
|
their ``trio`` teardown paths — escalating to ``SIGKILL`` only as
|
||||||
a last resort. The ``--shm`` sweep unlinks ``/dev/shm/`` segments
|
a last resort. The ``--shm`` sweep unlinks ``/dev/shm/`` segments
|
||||||
that no live process has open (it leans on psutil_, already in
|
that no live process has open (it leans on psutil_, already in
|
||||||
your dev venv, to check live mappings and fds) and ``--uds`` clears
|
your dev venv, to check live mappings and fds) and ``--uds``
|
||||||
dead-binder sockets from Tractor's platform-specific runtime dir. It
|
clears socket files whose binder pid is dead.
|
||||||
also unconditionally removes ``registry@1616.sock``; do not run the UDS
|
|
||||||
sweep while a live registrar is serving from that default address.
|
|
||||||
|
|
||||||
Testing your own ``tractor`` app
|
Testing your own ``tractor`` app
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
Fix Unix-domain-socket actor trees and registrar discovery on macOS.
|
|
||||||
Runtime sockets now use a short, owner-only runtime directory,
|
|
||||||
generated socket names remain within platform limits, and transient
|
|
||||||
or reset pre-handshake connections no longer destabilize discovery.
|
|
||||||
|
|
@ -23,10 +23,10 @@ Two cleanup phases (run in order when both are enabled):
|
||||||
hard-crashing actor leaves leaked segments that
|
hard-crashing actor leaves leaked segments that
|
||||||
nothing else GCs.
|
nothing else GCs.
|
||||||
|
|
||||||
3. **UDS sweep** (`--uds` / `--uds-only`) — unlinks socket
|
3. **UDS sweep** (`--uds` / `--uds-only`) — unlinks
|
||||||
files from Tractor's platform-specific default bindspace whose
|
`${XDG_RUNTIME_DIR}/tractor/<name>@<pid>.sock` files
|
||||||
binder pid is dead (or the `1616` registry sentinel). Needed
|
whose binder pid is dead (or the `1616` registry
|
||||||
because the IPC server's
|
sentinel). Needed because the IPC server's
|
||||||
`os.unlink()` cleanup lives in a `finally:` block
|
`os.unlink()` cleanup lives in a `finally:` block
|
||||||
that doesn't always run on hard exits (SIGKILL,
|
that doesn't always run on hard exits (SIGKILL,
|
||||||
escaped `KeyboardInterrupt`, etc.) — see issue #452.
|
escaped `KeyboardInterrupt`, etc.) — see issue #452.
|
||||||
|
|
@ -137,8 +137,8 @@ def main() -> int:
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help=(
|
help=(
|
||||||
'after process reap, also unlink orphaned '
|
'after process reap, also unlink orphaned '
|
||||||
'sockets from Tractor\'s platform default '
|
'${XDG_RUNTIME_DIR}/tractor/*.sock files '
|
||||||
'bindspace whose binder pid is dead (or the 1616 '
|
'whose binder pid is dead (or the 1616 '
|
||||||
'registry sentinel). See issue #452.'
|
'registry sentinel). See issue #452.'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -212,9 +212,7 @@ def main() -> int:
|
||||||
|
|
||||||
# --- phase 3: UDS sweep (opt-in) ---
|
# --- phase 3: UDS sweep (opt-in) ---
|
||||||
if args.uds or args.uds_only:
|
if args.uds or args.uds_only:
|
||||||
leaked_uds: list[str] = find_orphaned_uds(
|
leaked_uds: list[str] = find_orphaned_uds()
|
||||||
include_registry_sentinel=True,
|
|
||||||
)
|
|
||||||
if not leaked_uds:
|
if not leaked_uds:
|
||||||
print(
|
print(
|
||||||
'[tractor-reap] no orphaned UDS sock-files '
|
'[tractor-reap] no orphaned UDS sock-files '
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
'''
|
'''
|
||||||
Discovery-suite fixtures, including the `daemon` remote-registrar
|
Discovery-suite fixtures, including the `daemon`
|
||||||
subprocess used by the multi-program discovery tests.
|
remote-registrar subprocess used by the multi-program
|
||||||
|
discovery tests.
|
||||||
|
|
||||||
Lives here (vs. the parent `tests/conftest.py`)
|
Lives here (vs. the parent `tests/conftest.py`)
|
||||||
because `daemon` is a discovery-protocol primitive: it boots a child
|
because `daemon` is a discovery-protocol primitive —
|
||||||
that enters `open_root_actor()` and waits as a registrar peer for
|
boots a separate `tractor.run_daemon()` process whose
|
||||||
|
sole purpose is to serve as a registrar peer for
|
||||||
discovery-roundtrip tests. Pytest fixtures inherit
|
discovery-roundtrip tests. Pytest fixtures inherit
|
||||||
DOWNWARD through conftest hierarchy, so anything
|
DOWNWARD through conftest hierarchy, so anything
|
||||||
under `tests/discovery/` automatically picks this up.
|
under `tests/discovery/` automatically picks this up.
|
||||||
|
|
@ -47,8 +49,9 @@ def _wait_for_daemon_ready(
|
||||||
|
|
||||||
Raises `TimeoutError` on `deadline` exceeded. If
|
Raises `TimeoutError` on `deadline` exceeded. If
|
||||||
`proc` is given, ALSO raises early if the daemon
|
`proc` is given, ALSO raises early if the daemon
|
||||||
process exits before the deadline (catches a daemon startup crash
|
process exits non-zero before the deadline (catches
|
||||||
that the blind sleep used to silently mask).
|
daemon-startup-crash that the blind sleep used to
|
||||||
|
silently mask).
|
||||||
|
|
||||||
'''
|
'''
|
||||||
end: float = time.monotonic() + deadline
|
end: float = time.monotonic() + deadline
|
||||||
|
|
@ -145,9 +148,9 @@ def daemon(
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Poll the child's ready sentinel, published after actor startup,
|
# Active-poll the daemon's bind address until it's
|
||||||
# instead of connecting to its transport socket. This replaces
|
# ready to accept connections — replaces the legacy
|
||||||
# the legacy blind `time.sleep(2.2)` which was racy under load
|
# blind `time.sleep(2.2)` which was racy under load
|
||||||
# (see
|
# (see
|
||||||
# `ai/conc-anal/test_register_duplicate_name_daemon_connect_race_issue.md`).
|
# `ai/conc-anal/test_register_duplicate_name_daemon_connect_race_issue.md`).
|
||||||
#
|
#
|
||||||
|
|
@ -171,9 +174,9 @@ def daemon(
|
||||||
if proc.poll() is None:
|
if proc.poll() is None:
|
||||||
sig_prog(proc, _INT_SIGNAL)
|
sig_prog(proc, _INT_SIGNAL)
|
||||||
|
|
||||||
# NOTE: these blocking reads can hang when descendants retain
|
# XXX! yeah.. just be reaaal careful with this bc
|
||||||
# inherited pipe descriptors. Keep teardown signaling above
|
# sometimes it can lock up on the `_io.BufferedReader`
|
||||||
# them and avoid adding subprocesses outside the actor tree.
|
# and hang..
|
||||||
#
|
#
|
||||||
# NB, drain happens at TEARDOWN (post-yield), so the
|
# NB, drain happens at TEARDOWN (post-yield), so the
|
||||||
# test body has its chance to read `proc.stderr`
|
# test body has its chance to read `proc.stderr`
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ def test_daemon_ready_check_does_not_connect(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Observe completed daemon startup without a raw connection.
|
Detect a listening UDS daemon without creating a raw connection.
|
||||||
|
|
||||||
The old UDS readiness helper connected and immediately closed. That
|
The old UDS readiness helper connected and immediately closed. That
|
||||||
entered Tractor's actor-handshake handler with no `Aid` payload and
|
entered Tractor's actor-handshake handler with no `Aid` payload and
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,8 @@ def test_transport_only_listener_is_not_registrar():
|
||||||
connect alone. A non-Tractor listener, or a registrar still
|
connect alone. A non-Tractor listener, or a registrar still
|
||||||
failing its initial handshake, was therefore selected as the
|
failing its initial handshake, was therefore selected as the
|
||||||
remote registry. This test accepts the probe and closes it without
|
remote registry. This test accepts the probe and closes it without
|
||||||
replying, then proves `open_root_actor()` rejects that occupied
|
replying, then proves `open_root_actor()` ignores that endpoint and
|
||||||
endpoint instead of selecting it or binding over it.
|
elects the local actor registrar instead.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async def transport_only_handler(
|
async def transport_only_handler(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ Unit-ish tests for specific IPC transport protocol backends.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import socket
|
|
||||||
import stat
|
import stat
|
||||||
import sys
|
import sys
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
@ -100,74 +99,6 @@ def test_macos_rt_dir_rejects_symlink(
|
||||||
assert stat.S_IMODE(target_dir.stat().st_mode) == 0o755
|
assert stat.S_IMODE(target_dir.stat().st_mode) == 0o755
|
||||||
|
|
||||||
|
|
||||||
def test_reaper_uses_default_uds_bindspace(
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
tmp_path: Path,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Sweep the same platform-specific bindspace used by UDS actors.
|
|
||||||
|
|
||||||
The reaper previously consulted only `XDG_RUNTIME_DIR`, missing
|
|
||||||
Darwin sockets after the runtime moved to `/tmp/tractor-<uid>`.
|
|
||||||
This test replaces `UDSAddress.def_bindspace` and proves the test
|
|
||||||
harness resolves that shared transport default directly.
|
|
||||||
|
|
||||||
'''
|
|
||||||
from tractor._testing import _reap
|
|
||||||
from tractor.ipc._uds import UDSAddress
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
|
||||||
UDSAddress,
|
|
||||||
'def_bindspace',
|
|
||||||
tmp_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert _reap.get_uds_dir() == str(tmp_path)
|
|
||||||
|
|
||||||
|
|
||||||
def test_automatic_reaper_preserves_registry_sentinel(
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
tmp_path: Path,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Reserve unconditional registry cleanup for the explicit CLI.
|
|
||||||
|
|
||||||
The `registry@1616.sock` suffix does not encode its binder PID, so
|
|
||||||
automatic pytest cleanup cannot distinguish a leak from another
|
|
||||||
live registrar. This test creates registry and actor sockets,
|
|
||||||
proves the default sweep selects only the dead actor, then proves
|
|
||||||
explicit sentinel inclusion retains the CLI's documented behavior.
|
|
||||||
|
|
||||||
'''
|
|
||||||
from tractor._testing import _reap
|
|
||||||
|
|
||||||
registry_path: Path = tmp_path / 'registry@1616.sock'
|
|
||||||
actor_path: Path = tmp_path / 'worker@1234.sock'
|
|
||||||
socks: list[socket.socket] = []
|
|
||||||
for path in (registry_path, actor_path):
|
|
||||||
sock = socket.socket(socket.AF_UNIX)
|
|
||||||
sock.bind(str(path))
|
|
||||||
socks.append(sock)
|
|
||||||
|
|
||||||
monkeypatch.setattr(_reap, '_is_alive', lambda pid: False)
|
|
||||||
try:
|
|
||||||
assert _reap.find_orphaned_uds(
|
|
||||||
uds_dir=str(tmp_path),
|
|
||||||
) == [str(actor_path)]
|
|
||||||
assert set(
|
|
||||||
_reap.find_orphaned_uds(
|
|
||||||
uds_dir=str(tmp_path),
|
|
||||||
include_registry_sentinel=True,
|
|
||||||
)
|
|
||||||
) == {
|
|
||||||
str(registry_path),
|
|
||||||
str(actor_path),
|
|
||||||
}
|
|
||||||
finally:
|
|
||||||
for sock in socks:
|
|
||||||
sock.close()
|
|
||||||
|
|
||||||
|
|
||||||
def test_rt_dir_rejects_non_directory(
|
def test_rt_dir_rejects_non_directory(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,7 @@ High-level `.ipc._server` unit tests.
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import errno
|
import errno
|
||||||
from unittest.mock import (
|
|
||||||
AsyncMock,
|
|
||||||
Mock,
|
|
||||||
)
|
|
||||||
|
|
||||||
import msgspec
|
|
||||||
import pytest
|
import pytest
|
||||||
import trio
|
import trio
|
||||||
from tractor import (
|
from tractor import (
|
||||||
|
|
@ -21,10 +16,7 @@ from tractor._testing.addr import (
|
||||||
get_rando_addr,
|
get_rando_addr,
|
||||||
)
|
)
|
||||||
from tractor._exceptions import TransportClosed
|
from tractor._exceptions import TransportClosed
|
||||||
from tractor.ipc._chan import Channel
|
|
||||||
from tractor.ipc import _server
|
|
||||||
from tractor.ipc._transport import MsgpackTransport
|
from tractor.ipc._transport import MsgpackTransport
|
||||||
from tractor.msg.types import Aid
|
|
||||||
# TODO, use/check-roundtripping with some of these wrapper types?
|
# TODO, use/check-roundtripping with some of these wrapper types?
|
||||||
#
|
#
|
||||||
# from .._addr import Address
|
# from .._addr import Address
|
||||||
|
|
@ -38,8 +30,8 @@ def test_send_normalizes_peer_reset():
|
||||||
'''
|
'''
|
||||||
Normalize Darwin's pre-handshake peer reset as transport closure.
|
Normalize Darwin's pre-handshake peer reset as transport closure.
|
||||||
|
|
||||||
A UDS peer may disconnect before completing the actor handshake.
|
A raw UDS readiness client connects and immediately disconnects.
|
||||||
Darwin can report the server's first handshake write as
|
Darwin reports the server's first handshake write as
|
||||||
`ECONNRESET`, wrapped by `trio.BrokenResourceError`; allowing
|
`ECONNRESET`, wrapped by `trio.BrokenResourceError`; allowing
|
||||||
that raw error to escape cancels the daemon's shared IPC nursery.
|
that raw error to escape cancels the daemon's shared IPC nursery.
|
||||||
This fake stream reproduces the exact exception chain and proves
|
This fake stream reproduces the exact exception chain and proves
|
||||||
|
|
@ -77,90 +69,6 @@ def test_send_normalizes_peer_reset():
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
||||||
|
|
||||||
def test_handshake_normalizes_decode_error():
|
|
||||||
'''
|
|
||||||
Keep malformed pre-handshake frames out of the service nursery.
|
|
||||||
|
|
||||||
A non-msgpack peer can trigger `msgspec.DecodeError` before a
|
|
||||||
remote `Aid` exists. Letting that decoder error escape the inbound
|
|
||||||
handler cancels the actor's shared IPC nursery. This fake channel
|
|
||||||
proves `_do_handshake()` presents only `TransportClosed` upward.
|
|
||||||
|
|
||||||
'''
|
|
||||||
chan = object.__new__(Channel)
|
|
||||||
chan.send = AsyncMock()
|
|
||||||
chan.recv = AsyncMock(
|
|
||||||
side_effect=msgspec.DecodeError('malformed handshake'),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
with pytest.raises(TransportClosed) as exc_info:
|
|
||||||
await chan._do_handshake(
|
|
||||||
aid=Aid(
|
|
||||||
name='local',
|
|
||||||
uuid='local-uuid',
|
|
||||||
pid=1234,
|
|
||||||
),
|
|
||||||
timeout=.1,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert isinstance(
|
|
||||||
exc_info.value.src_exc,
|
|
||||||
msgspec.DecodeError,
|
|
||||||
)
|
|
||||||
|
|
||||||
trio.run(main)
|
|
||||||
|
|
||||||
|
|
||||||
def test_server_uses_independent_handshake_timeout(
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Give ordinary actor handshakes a distinct, generous deadline.
|
|
||||||
|
|
||||||
Registry probes use short retries, but ordinary portal and child
|
|
||||||
connections do not retry. Applying the probe's one-second timeout
|
|
||||||
in the server can terminate a valid delayed child and leave its
|
|
||||||
parent blocked in `IPCServer.wait_for_peer()`. This handler fake
|
|
||||||
proves the server uses its separate pre-registration budget.
|
|
||||||
|
|
||||||
'''
|
|
||||||
handshake = AsyncMock(
|
|
||||||
side_effect=TransportClosed(message='stop after assertion'),
|
|
||||||
)
|
|
||||||
chan = Mock(_do_handshake=handshake)
|
|
||||||
actor = Mock(
|
|
||||||
aid=Aid(
|
|
||||||
name='local',
|
|
||||||
uuid='local-uuid',
|
|
||||||
pid=1234,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
Channel,
|
|
||||||
'from_stream',
|
|
||||||
Mock(return_value=chan),
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
_server._state,
|
|
||||||
'current_actor',
|
|
||||||
Mock(return_value=actor),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
await _server.handle_stream_from_peer(
|
|
||||||
stream=Mock(),
|
|
||||||
server=Mock(),
|
|
||||||
)
|
|
||||||
|
|
||||||
trio.run(main)
|
|
||||||
handshake.assert_awaited_once_with(
|
|
||||||
aid=actor.aid,
|
|
||||||
timeout=_server._PRE_REG_HANDSHAKE_TIMEOUT,
|
|
||||||
)
|
|
||||||
assert _server._PRE_REG_HANDSHAKE_TIMEOUT == 10
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'_tpt_proto',
|
'_tpt_proto',
|
||||||
['uds', 'tcp']
|
['uds', 'tcp']
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,8 @@ def _wait_for_proc(
|
||||||
|
|
||||||
errmsg: str = err.decode(errors='replace')
|
errmsg: str = err.decode(errors='replace')
|
||||||
|
|
||||||
# NOTE: always include captured stdout and stderr for a non-zero
|
# XXX, ALWAYS surface the subproc's full stderr
|
||||||
# exit. Depending on the final stderr line previously hid grouped
|
# whenever it exits non-zero!
|
||||||
# exception diagnostics; see GH #473.
|
|
||||||
#
|
#
|
||||||
# The prior impl only raised when the LAST stderr
|
# The prior impl only raised when the LAST stderr
|
||||||
# line contained 'Error', swallowing any crash whose
|
# line contained 'Error', swallowing any crash whose
|
||||||
|
|
@ -247,8 +246,9 @@ def run_example_in_subproc(
|
||||||
str(script_file),
|
str(script_file),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Captured pipes are drained by `_wait_for_proc()` while the
|
# XXX: BE FOREVER WARNED: if you enable lots of tractor logging
|
||||||
# example runs.
|
# in the subprocess it may cause infinite blocking on the pipes
|
||||||
|
# due to backpressure!!!
|
||||||
proc = testdir.popen(
|
proc = testdir.popen(
|
||||||
cmdargs,
|
cmdargs,
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,8 @@ async def _probe_registry(
|
||||||
Confirm an address serves the Tractor actor handshake.
|
Confirm an address serves the Tractor actor handshake.
|
||||||
|
|
||||||
Connection and handshake work share `timeout`; each attempt gets
|
Connection and handshake work share `timeout`; each attempt gets
|
||||||
`attempt_timeout`. Shielded cleanup may add up to `close_timeout`
|
`attempt_timeout`. Shielded channel cleanup may consume at most one
|
||||||
per attempted channel.
|
additional `close_timeout` after either deadline fires.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
connected_once: bool = False
|
connected_once: bool = False
|
||||||
|
|
@ -525,10 +525,12 @@ async def open_root_actor(
|
||||||
timeout: float = 3,
|
timeout: float = 3,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Probe with a bounded Tractor actor handshake.
|
Attempt temporary connection to see if a registry is
|
||||||
|
listening at the requested address by a tranport layer
|
||||||
|
ping.
|
||||||
|
|
||||||
Classify the address as a registrar, occupied by a
|
If a connection can't be made quickly we assume none no
|
||||||
non-registrar, or absent.
|
server is listening at that addr.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
probe_status = await _probe_registry(
|
probe_status = await _probe_registry(
|
||||||
|
|
|
||||||
|
|
@ -111,13 +111,14 @@ SHM_DIR: str = '/dev/shm'
|
||||||
|
|
||||||
# UDS-socket leak sweep — see `find_orphaned_uds()` /
|
# UDS-socket leak sweep — see `find_orphaned_uds()` /
|
||||||
# `reap_uds()` below. Tractor's UDS transport
|
# `reap_uds()` below. Tractor's UDS transport
|
||||||
# (`tractor.ipc._uds`) creates sock files in its platform-specific
|
# (`tractor.ipc._uds`) creates sock files under
|
||||||
# default bindspace; a
|
# `${XDG_RUNTIME_DIR}/tractor/<name>@<pid>.sock`; a
|
||||||
# crash / SIGKILL / mid-cancel teardown can leave the
|
# crash / SIGKILL / mid-cancel teardown can leave the
|
||||||
# file behind because `os.unlink()` lives in the
|
# file behind because `os.unlink()` lives in the
|
||||||
# `_serve_ipc_eps` `finally:` block which doesn't always
|
# `_serve_ipc_eps` `finally:` block which doesn't always
|
||||||
# get to run on hard exits. The reaper here is best-effort
|
# get to run on hard exits. The reaper here is best-effort
|
||||||
# cleanup for the test harness + the `tractor-reap` CLI.
|
# cleanup for the test harness + the `tractor-reap` CLI.
|
||||||
|
_UDS_SUBDIR: str = 'tractor'
|
||||||
# `<actor-name>@<pid>.sock` — pid is the binder's pid at
|
# `<actor-name>@<pid>.sock` — pid is the binder's pid at
|
||||||
# creation time. Special sentinel: `registry@1616.sock`
|
# creation time. Special sentinel: `registry@1616.sock`
|
||||||
# uses the magic `1616` not a real pid (the root
|
# uses the magic `1616` not a real pid (the root
|
||||||
|
|
@ -737,16 +738,19 @@ def reap_shm(
|
||||||
|
|
||||||
def get_uds_dir() -> str|None:
|
def get_uds_dir() -> str|None:
|
||||||
'''
|
'''
|
||||||
Path of Tractor's platform-specific default UDS bindspace.
|
Path of tractor's per-user UDS sock-file dir
|
||||||
|
(`${XDG_RUNTIME_DIR}/tractor/`).
|
||||||
|
|
||||||
Returns `None` only when the bindspace cannot be resolved.
|
Returns `None` when `XDG_RUNTIME_DIR` is unset (e.g.
|
||||||
|
non-systemd hosts, or inside a container without the
|
||||||
|
var plumbed through). Caller should treat that as
|
||||||
|
"no UDS leaks possible to detect — skip".
|
||||||
|
|
||||||
'''
|
'''
|
||||||
try:
|
xdg: str|None = os.environ.get('XDG_RUNTIME_DIR')
|
||||||
from tractor.ipc._uds import UDSAddress
|
if not xdg:
|
||||||
return str(UDSAddress.def_bindspace)
|
|
||||||
except Exception:
|
|
||||||
return None
|
return None
|
||||||
|
return os.path.join(xdg, _UDS_SUBDIR)
|
||||||
|
|
||||||
|
|
||||||
def _parse_uds_name(filename: str) -> tuple[str, int]|None:
|
def _parse_uds_name(filename: str) -> tuple[str, int]|None:
|
||||||
|
|
@ -764,16 +768,16 @@ def _parse_uds_name(filename: str) -> tuple[str, int]|None:
|
||||||
def find_orphaned_uds(
|
def find_orphaned_uds(
|
||||||
*,
|
*,
|
||||||
uds_dir: str|None = None,
|
uds_dir: str|None = None,
|
||||||
include_registry_sentinel: bool = False,
|
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
'''
|
'''
|
||||||
`<uds_dir>/*.sock` paths whose binder pid is no
|
`<uds_dir>/*.sock` paths whose binder pid is no
|
||||||
longer alive (orphaned). Explicit callers may include the
|
longer alive (orphaned). Includes the
|
||||||
`registry@1616.sock` sentinel; automatic pytest cleanup excludes
|
`registry@1616.sock` sentinel — `1616` is a magic
|
||||||
it because binder liveness cannot be inferred from magic `1616`.
|
sentinel pid (not a real one) so the file's
|
||||||
|
presence alone signals a leak from a dead session.
|
||||||
|
|
||||||
Returns `[]` when the platform bindspace cannot be resolved or the
|
Returns `[]` on platforms without `XDG_RUNTIME_DIR`
|
||||||
dir doesn't exist. Files whose name
|
or when the dir doesn't exist. Files whose name
|
||||||
doesn't match the `<name>@<pid>.sock` pattern are
|
doesn't match the `<name>@<pid>.sock` pattern are
|
||||||
skipped (we don't unlink things we don't recognize).
|
skipped (we don't unlink things we don't recognize).
|
||||||
|
|
||||||
|
|
@ -807,8 +811,10 @@ def find_orphaned_uds(
|
||||||
continue
|
continue
|
||||||
_name, pid = parsed
|
_name, pid = parsed
|
||||||
if pid == _UDS_REGISTRY_SENTINEL_PID:
|
if pid == _UDS_REGISTRY_SENTINEL_PID:
|
||||||
if include_registry_sentinel:
|
# sentinel — never a real pid; if the file
|
||||||
leaked.append(path)
|
# exists nobody live is "owning" it via
|
||||||
|
# /proc lookup, so always orphaned
|
||||||
|
leaked.append(path)
|
||||||
continue
|
continue
|
||||||
if not _is_alive(pid):
|
if not _is_alive(pid):
|
||||||
leaked.append(path)
|
leaked.append(path)
|
||||||
|
|
@ -927,8 +933,8 @@ def track_orphaned_uds_per_test():
|
||||||
teardown that flakifies sibling tests via
|
teardown that flakifies sibling tests via
|
||||||
sock-file rebind races).
|
sock-file rebind races).
|
||||||
|
|
||||||
Snapshots Tractor's platform-specific default UDS bindspace before
|
Snapshots `${XDG_RUNTIME_DIR}/tractor/` before and
|
||||||
and after each test; any `<name>@<pid>.sock` files
|
after each test; any `<name>@<pid>.sock` files
|
||||||
created during the test that survive teardown AND
|
created during the test that survive teardown AND
|
||||||
whose creator pid is dead are surfaced as a loud
|
whose creator pid is dead are surfaced as a loud
|
||||||
warning AND reaped, so the next test starts with a
|
warning AND reaped, so the next test starts with a
|
||||||
|
|
@ -944,8 +950,8 @@ def track_orphaned_uds_per_test():
|
||||||
it (vs. blanket session-end sweep) makes blame
|
it (vs. blanket session-end sweep) makes blame
|
||||||
obvious + prevents cascade flakiness.
|
obvious + prevents cascade flakiness.
|
||||||
|
|
||||||
Cheap: 2x `os.listdir` + a few `os.stat`s per test. Skips silently
|
Cheap: 2x `os.listdir` + a few `os.stat`s per test.
|
||||||
when the platform bindspace cannot be resolved.
|
Skips silently when `XDG_RUNTIME_DIR` isn't set.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
uds_dir: str|None = get_uds_dir()
|
uds_dir: str|None = get_uds_dir()
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ from typing import (
|
||||||
)
|
)
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import msgspec
|
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from ._types import (
|
from ._types import (
|
||||||
|
|
@ -519,7 +518,6 @@ class Channel:
|
||||||
)
|
)
|
||||||
except (
|
except (
|
||||||
MsgTypeError,
|
MsgTypeError,
|
||||||
msgspec.DecodeError,
|
|
||||||
TypeError,
|
TypeError,
|
||||||
UnicodeDecodeError,
|
UnicodeDecodeError,
|
||||||
trio.TooSlowError,
|
trio.TooSlowError,
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
log = log.get_logger()
|
log = log.get_logger()
|
||||||
|
|
||||||
_PRE_REG_HANDSHAKE_TIMEOUT: float = 10
|
_PRE_REG_HANDSHAKE_TIMEOUT: float = 1
|
||||||
|
|
||||||
|
|
||||||
async def maybe_wait_on_canced_subs(
|
async def maybe_wait_on_canced_subs(
|
||||||
|
|
@ -352,9 +352,12 @@ async def handle_stream_from_peer(
|
||||||
# "kinda-error" that we expect to tolerate during
|
# "kinda-error" that we expect to tolerate during
|
||||||
# discovery-sys related pings, queires, DoS etc.
|
# discovery-sys related pings, queires, DoS etc.
|
||||||
):
|
):
|
||||||
# `TransportClosed` is expected when a peer disconnects or
|
# XXX: This may propagate up from `Channel._aiter_recv()`
|
||||||
# fails the initial typed handshake, including foreign clients
|
# and `MsgpackStream._inter_packets()` on a read from the
|
||||||
# and probes racing shutdown.
|
# stream particularly when the runtime is first starting up
|
||||||
|
# inside `open_root_actor()` where there is a check for
|
||||||
|
# a bound listener on the registrar addr. the reset will be
|
||||||
|
# because the handshake was never meant took place.
|
||||||
log.runtime(
|
log.runtime(
|
||||||
con_status
|
con_status
|
||||||
+
|
+
|
||||||
|
|
|
||||||
|
|
@ -656,7 +656,7 @@ class MsgpackUDSStream(MsgpackTransport):
|
||||||
case (bytes(), str()):
|
case (bytes(), str()):
|
||||||
sock_path: Path = Path(sockname)
|
sock_path: Path = Path(sockname)
|
||||||
|
|
||||||
# NOTE, no-autobind case (macOS): the un-bound end
|
# XXX, no-autobind case (macOS): the un-bound end
|
||||||
# is `''`, NOT a `bytes` abstract-ns addr; taking
|
# is `''`, NOT a `bytes` abstract-ns addr; taking
|
||||||
# `peername` unconditionally (as prior impl did)
|
# `peername` unconditionally (as prior impl did)
|
||||||
# delivers garbage `Path('')` addrs on the accept
|
# delivers garbage `Path('')` addrs on the accept
|
||||||
|
|
|
||||||
|
|
@ -332,9 +332,9 @@ def get_rt_dir(
|
||||||
userspace apps stick their IPC and cache related system
|
userspace apps stick their IPC and cache related system
|
||||||
util-files.
|
util-files.
|
||||||
|
|
||||||
Linux uses `${XDG_RUNTIME_DIR}/tractor/`; Darwin uses a short,
|
On linux we use a `${XDG_RUNTIME_DIR}/tractor/` subdir by
|
||||||
owner-only `/tmp/tractor-<uid>` path; other platforms use the
|
default, but equivalents are mapped for each platform using
|
||||||
lovely `platformdirs` lib.
|
the lovely `platformdirs` lib.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# lazy-imported to keep it off the eager
|
# lazy-imported to keep it off the eager
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,14 @@ Future-work TODO — authoritative UDS bind-addr tracking
|
||||||
`unlink_uds_bind_addrs()` currently has two cleanup paths:
|
`unlink_uds_bind_addrs()` currently has two cleanup paths:
|
||||||
|
|
||||||
1. Explicit `bind_addrs` (when parent set them at spawn time)
|
1. Explicit `bind_addrs` (when parent set them at spawn time)
|
||||||
2. **Convention-based reconstruction** in the platform default UDS
|
2. **Convention-based reconstruction** —
|
||||||
bindspace — for the
|
`<XDG_RUNTIME_DIR>/tractor/<name>@<pid>.sock` — for the
|
||||||
common case where the subactor self-assigned a random sock
|
common case where the subactor self-assigned a random sock
|
||||||
via `UDSAddress.get_random()`.
|
via `UDSAddress.get_random()`.
|
||||||
|
|
||||||
Path (2) delegates filename reconstruction to
|
Path (2) hardcodes the `<name>@<pid>.sock` convention from
|
||||||
`tractor.ipc._uds.UDSAddress.get_sockname()`. If the subactor binds to
|
`tractor.ipc._uds.UDSAddress`. If that convention ever
|
||||||
a non-default
|
changes — or the subactor binds to a non-default
|
||||||
`bindspace`/`filedir` — we'll silently fail to unlink.
|
`bindspace`/`filedir` — we'll silently fail to unlink.
|
||||||
|
|
||||||
A more authoritative approach would be:
|
A more authoritative approach would be:
|
||||||
|
|
@ -105,7 +105,7 @@ def unlink_uds_bind_addrs(
|
||||||
`_serve_ipc_eps` `finally:` block (which normally calls
|
`_serve_ipc_eps` `finally:` block (which normally calls
|
||||||
`os.unlink(addr.sockpath)`) never runs. Without this
|
`os.unlink(addr.sockpath)`) never runs. Without this
|
||||||
parent-side cleanup, the dead subactor's
|
parent-side cleanup, the dead subactor's
|
||||||
platform-default UDS socket file
|
`${XDG_RUNTIME_DIR}/tractor/<name>@<pid>.sock` file
|
||||||
accumulates on the filesystem (see issue #454 + the
|
accumulates on the filesystem (see issue #454 + the
|
||||||
autouse `_track_orphaned_uds_per_test` fixture).
|
autouse `_track_orphaned_uds_per_test` fixture).
|
||||||
|
|
||||||
|
|
@ -119,7 +119,7 @@ def unlink_uds_bind_addrs(
|
||||||
picked its own random sock via
|
picked its own random sock via
|
||||||
`UDSAddress.get_random()`), reconstruct the path
|
`UDSAddress.get_random()`), reconstruct the path
|
||||||
from `(subactor.aid.name, proc.pid)` using the
|
from `(subactor.aid.name, proc.pid)` using the
|
||||||
same `UDSAddress.get_sockname()` helper. We can do this
|
same `<name>@<pid>.sock` convention. We can do this
|
||||||
because the subactor uses its OWN `os.getpid()` at
|
because the subactor uses its OWN `os.getpid()` at
|
||||||
bind time, which equals `proc.pid` from the
|
bind time, which equals `proc.pid` from the
|
||||||
parent's view.
|
parent's view.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue