Make UDS reaping platform-aware
Moving Darwin sockets to `/tmp/tractor-<uid>` left pytest and the standalone reaper searching only `XDG_RUNTIME_DIR`. Enabling that shared bindspace naively would also let automatic session cleanup unlink an independent live `registry@1616.sock`. Resolve the runtime's actual default UDS bindspace on every platform. Exclude the pid-less registry sentinel from automatic cleanup while retaining explicit CLI removal, and document that destructive choice. Cover bindspace resolution and automatic-vs-explicit sentinel policy. Review: PR #480 (goodboy) https://github.com/goodboy/tractor/pull/480 (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))wkt/uds_macos_473
parent
1a9ce915f3
commit
16dd876b0c
|
|
@ -185,8 +185,10 @@ first with a bounded grace window — so actor runtimes can run
|
|||
their ``trio`` teardown paths — escalating to ``SIGKILL`` only as
|
||||
a last resort. The ``--shm`` sweep unlinks ``/dev/shm/`` segments
|
||||
that no live process has open (it leans on psutil_, already in
|
||||
your dev venv, to check live mappings and fds) and ``--uds``
|
||||
clears socket files whose binder pid is dead.
|
||||
your dev venv, to check live mappings and fds) and ``--uds`` clears
|
||||
dead-binder sockets from Tractor's platform-specific runtime dir. It
|
||||
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
|
||||
--------------------------------
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ Two cleanup phases (run in order when both are enabled):
|
|||
hard-crashing actor leaves leaked segments that
|
||||
nothing else GCs.
|
||||
|
||||
3. **UDS sweep** (`--uds` / `--uds-only`) — unlinks
|
||||
`${XDG_RUNTIME_DIR}/tractor/<name>@<pid>.sock` files
|
||||
whose binder pid is dead (or the `1616` registry
|
||||
sentinel). Needed because the IPC server's
|
||||
3. **UDS sweep** (`--uds` / `--uds-only`) — unlinks socket
|
||||
files from Tractor's platform-specific default bindspace whose
|
||||
binder pid is dead (or the `1616` registry sentinel). Needed
|
||||
because the IPC server's
|
||||
`os.unlink()` cleanup lives in a `finally:` block
|
||||
that doesn't always run on hard exits (SIGKILL,
|
||||
escaped `KeyboardInterrupt`, etc.) — see issue #452.
|
||||
|
|
@ -137,8 +137,8 @@ def main() -> int:
|
|||
action='store_true',
|
||||
help=(
|
||||
'after process reap, also unlink orphaned '
|
||||
'${XDG_RUNTIME_DIR}/tractor/*.sock files '
|
||||
'whose binder pid is dead (or the 1616 '
|
||||
'sockets from Tractor\'s platform default '
|
||||
'bindspace whose binder pid is dead (or the 1616 '
|
||||
'registry sentinel). See issue #452.'
|
||||
),
|
||||
)
|
||||
|
|
@ -212,7 +212,9 @@ def main() -> int:
|
|||
|
||||
# --- phase 3: UDS sweep (opt-in) ---
|
||||
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:
|
||||
print(
|
||||
'[tractor-reap] no orphaned UDS sock-files '
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Unit-ish tests for specific IPC transport protocol backends.
|
|||
from __future__ import annotations
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import stat
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
|
@ -99,6 +100,74 @@ def test_macos_rt_dir_rejects_symlink(
|
|||
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(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
|
|
|
|||
|
|
@ -111,14 +111,13 @@ SHM_DIR: str = '/dev/shm'
|
|||
|
||||
# UDS-socket leak sweep — see `find_orphaned_uds()` /
|
||||
# `reap_uds()` below. Tractor's UDS transport
|
||||
# (`tractor.ipc._uds`) creates sock files under
|
||||
# `${XDG_RUNTIME_DIR}/tractor/<name>@<pid>.sock`; a
|
||||
# (`tractor.ipc._uds`) creates sock files in its platform-specific
|
||||
# default bindspace; a
|
||||
# crash / SIGKILL / mid-cancel teardown can leave the
|
||||
# file behind because `os.unlink()` lives in the
|
||||
# `_serve_ipc_eps` `finally:` block which doesn't always
|
||||
# get to run on hard exits. The reaper here is best-effort
|
||||
# cleanup for the test harness + the `tractor-reap` CLI.
|
||||
_UDS_SUBDIR: str = 'tractor'
|
||||
# `<actor-name>@<pid>.sock` — pid is the binder's pid at
|
||||
# creation time. Special sentinel: `registry@1616.sock`
|
||||
# uses the magic `1616` not a real pid (the root
|
||||
|
|
@ -738,19 +737,16 @@ def reap_shm(
|
|||
|
||||
def get_uds_dir() -> str|None:
|
||||
'''
|
||||
Path of tractor's per-user UDS sock-file dir
|
||||
(`${XDG_RUNTIME_DIR}/tractor/`).
|
||||
Path of Tractor's platform-specific default UDS bindspace.
|
||||
|
||||
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".
|
||||
Returns `None` only when the bindspace cannot be resolved.
|
||||
|
||||
'''
|
||||
xdg: str|None = os.environ.get('XDG_RUNTIME_DIR')
|
||||
if not xdg:
|
||||
try:
|
||||
from tractor.ipc._uds import UDSAddress
|
||||
return str(UDSAddress.def_bindspace)
|
||||
except Exception:
|
||||
return None
|
||||
return os.path.join(xdg, _UDS_SUBDIR)
|
||||
|
||||
|
||||
def _parse_uds_name(filename: str) -> tuple[str, int]|None:
|
||||
|
|
@ -768,16 +764,16 @@ def _parse_uds_name(filename: str) -> tuple[str, int]|None:
|
|||
def find_orphaned_uds(
|
||||
*,
|
||||
uds_dir: str|None = None,
|
||||
include_registry_sentinel: bool = False,
|
||||
) -> list[str]:
|
||||
'''
|
||||
`<uds_dir>/*.sock` paths whose binder pid is no
|
||||
longer alive (orphaned). Includes the
|
||||
`registry@1616.sock` sentinel — `1616` is a magic
|
||||
sentinel pid (not a real one) so the file's
|
||||
presence alone signals a leak from a dead session.
|
||||
longer alive (orphaned). Explicit callers may include the
|
||||
`registry@1616.sock` sentinel; automatic pytest cleanup excludes
|
||||
it because binder liveness cannot be inferred from magic `1616`.
|
||||
|
||||
Returns `[]` on platforms without `XDG_RUNTIME_DIR`
|
||||
or when the dir doesn't exist. Files whose name
|
||||
Returns `[]` when the platform bindspace cannot be resolved or the
|
||||
dir doesn't exist. Files whose name
|
||||
doesn't match the `<name>@<pid>.sock` pattern are
|
||||
skipped (we don't unlink things we don't recognize).
|
||||
|
||||
|
|
@ -811,10 +807,8 @@ def find_orphaned_uds(
|
|||
continue
|
||||
_name, pid = parsed
|
||||
if pid == _UDS_REGISTRY_SENTINEL_PID:
|
||||
# sentinel — never a real pid; if the file
|
||||
# exists nobody live is "owning" it via
|
||||
# /proc lookup, so always orphaned
|
||||
leaked.append(path)
|
||||
if include_registry_sentinel:
|
||||
leaked.append(path)
|
||||
continue
|
||||
if not _is_alive(pid):
|
||||
leaked.append(path)
|
||||
|
|
@ -933,8 +927,8 @@ def track_orphaned_uds_per_test():
|
|||
teardown that flakifies sibling tests via
|
||||
sock-file rebind races).
|
||||
|
||||
Snapshots `${XDG_RUNTIME_DIR}/tractor/` before and
|
||||
after each test; any `<name>@<pid>.sock` files
|
||||
Snapshots Tractor's platform-specific default UDS bindspace before
|
||||
and after each test; any `<name>@<pid>.sock` files
|
||||
created during the test that survive teardown AND
|
||||
whose creator pid is dead are surfaced as a loud
|
||||
warning AND reaped, so the next test starts with a
|
||||
|
|
@ -950,8 +944,8 @@ def track_orphaned_uds_per_test():
|
|||
it (vs. blanket session-end sweep) makes blame
|
||||
obvious + prevents cascade flakiness.
|
||||
|
||||
Cheap: 2x `os.listdir` + a few `os.stat`s per test.
|
||||
Skips silently when `XDG_RUNTIME_DIR` isn't set.
|
||||
Cheap: 2x `os.listdir` + a few `os.stat`s per test. Skips silently
|
||||
when the platform bindspace cannot be resolved.
|
||||
|
||||
'''
|
||||
uds_dir: str|None = get_uds_dir()
|
||||
|
|
|
|||
Loading…
Reference in New Issue