Compare commits

..

No commits in common. "584ea4e9add1f084a4fca03bf43b3251dfb3f925" and "0b63af020e82569054ab5a421f5d84e047c662f3" have entirely different histories.

18 changed files with 96 additions and 258 deletions

View File

@ -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

View File

@ -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
------------------------------- -------------------------------

View File

@ -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
-------------------------------- --------------------------------

View File

@ -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.

View File

@ -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 '

View File

@ -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`

View File

@ -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

View File

@ -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(

View File

@ -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,

View File

@ -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']

View File

@ -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,

View File

@ -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(

View File

@ -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()

View File

@ -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,

View File

@ -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
+ +

View File

@ -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

View File

@ -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

View File

@ -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.