Compare commits
3 Commits
08f7a3a9b8
...
f22bfdd282
| Author | SHA1 | Date |
|---|---|---|
|
|
f22bfdd282 | |
|
|
d9532d1270 | |
|
|
5bf6417cc0 |
|
|
@ -91,6 +91,11 @@ jobs:
|
||||||
name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}'
|
name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}'
|
||||||
timeout-minutes: 16
|
timeout-minutes: 16
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
# Windows support is nascent: keep its legs informational so a
|
||||||
|
# known-incomplete area doesn't block merges. The `import
|
||||||
|
# tractor` smoke step below is the hard signal for this job;
|
||||||
|
# promote the whole leg to required once the suite is green.
|
||||||
|
continue-on-error: ${{ matrix.os == 'windows-latest' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
@ -98,6 +103,7 @@ jobs:
|
||||||
os: [
|
os: [
|
||||||
ubuntu-latest,
|
ubuntu-latest,
|
||||||
macos-latest,
|
macos-latest,
|
||||||
|
windows-latest,
|
||||||
]
|
]
|
||||||
python-version: [
|
python-version: [
|
||||||
'3.13',
|
'3.13',
|
||||||
|
|
@ -123,6 +129,10 @@ jobs:
|
||||||
# don't do UDS run on macOS (for now)
|
# don't do UDS run on macOS (for now)
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
tpt_proto: 'uds'
|
tpt_proto: 'uds'
|
||||||
|
# UDS is POSIX-only; Windows has no `AF_UNIX` so the
|
||||||
|
# backend is intentionally unavailable there.
|
||||||
|
- os: windows-latest
|
||||||
|
tpt_proto: 'uds'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -150,6 +160,12 @@ jobs:
|
||||||
- name: List deps tree
|
- name: List deps tree
|
||||||
run: uv tree
|
run: uv tree
|
||||||
|
|
||||||
|
# hard signal for the Windows import-safety fix: `import
|
||||||
|
# tractor` must succeed everywhere, and `HAS_UDS` reflects
|
||||||
|
# platform capability (False on Windows, True on POSIX).
|
||||||
|
- name: 'Smoke: import tractor'
|
||||||
|
run: uv run python -c "import tractor; from tractor.ipc._uds import HAS_UDS; print('import tractor OK | HAS_UDS=', HAS_UDS)"
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: >
|
run: >
|
||||||
uv run
|
uv run
|
||||||
|
|
|
||||||
|
|
@ -189,11 +189,16 @@ def test_dynamic_pub_sub(
|
||||||
# sits forever until external SIGINT. The `afk_alarm_w_trace`
|
# sits forever until external SIGINT. The `afk_alarm_w_trace`
|
||||||
# outer guard below is the AFK-safety counterpart (SIGALRM
|
# outer guard below is the AFK-safety counterpart (SIGALRM
|
||||||
# raises in the main thread regardless of trio scope state).
|
# raises in the main thread regardless of trio scope state).
|
||||||
fail_after_s: int = (
|
# scale the budget for slow/noisy CI runners (esp. macOS, where
|
||||||
|
# `cpu_scaling_factor()` applies a 3x CI bump) so a sluggish
|
||||||
|
# runner doesn't trip the deadline and inject a `TooSlowError`
|
||||||
|
# that collides with the `expect_cancel_exc=TooSlowError` param.
|
||||||
|
from .conftest import cpu_scaling_factor
|
||||||
|
fail_after_s: float = (
|
||||||
8
|
8
|
||||||
if is_forking_spawner
|
if is_forking_spawner
|
||||||
else 20
|
else 20
|
||||||
)
|
) * cpu_scaling_factor()
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
# bug-class-3 breadcrumb: tag each level of the cancel path
|
# bug-class-3 breadcrumb: tag each level of the cancel path
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,22 @@
|
||||||
import time
|
import time
|
||||||
|
import platform
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
|
# `tractor.ipc._ringbuf` is built on linux `eventfd(2)`; importing
|
||||||
|
# it pulls in `tractor.ipc._linux` whose module-level
|
||||||
|
# `ffi.dlopen(None)` raises on non-linux. Skip the whole module at
|
||||||
|
# COLLECTION before that crashing import runs (a `pytestmark` skip
|
||||||
|
# is too late — markers apply only after the import succeeds).
|
||||||
|
if platform.system() != 'Linux':
|
||||||
|
pytest.skip(
|
||||||
|
'ringbuf (eventfd) IPC is linux-only',
|
||||||
|
allow_module_level=True,
|
||||||
|
)
|
||||||
|
|
||||||
# XXX `cffi` dun build on py3.14 yet..
|
# XXX `cffi` dun build on py3.14 yet..
|
||||||
pytest.importorskip("cffi")
|
pytest.importorskip("cffi")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,21 @@ from threading import (
|
||||||
RLock,
|
RLock,
|
||||||
)
|
)
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
|
|
||||||
|
import platform
|
||||||
|
|
||||||
from signal import (
|
from signal import (
|
||||||
signal,
|
signal,
|
||||||
getsignal,
|
getsignal,
|
||||||
SIGUSR1,
|
|
||||||
SIGINT,
|
SIGINT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if platform.system() != "Windows":
|
||||||
|
from signal import SIGUSR1
|
||||||
|
else:
|
||||||
|
SIGUSR1 = None
|
||||||
|
|
||||||
# import traceback
|
# import traceback
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import (
|
from typing import (
|
||||||
|
|
@ -347,8 +356,8 @@ def dump_tree_on_sig(
|
||||||
|
|
||||||
|
|
||||||
def enable_stack_on_sig(
|
def enable_stack_on_sig(
|
||||||
sig: int = SIGUSR1,
|
sig: int|None = SIGUSR1,
|
||||||
) -> ModuleType:
|
) -> ModuleType|None:
|
||||||
'''
|
'''
|
||||||
Enable `stackscope` tracing on reception of a signal; by
|
Enable `stackscope` tracing on reception of a signal; by
|
||||||
default this is SIGUSR1.
|
default this is SIGUSR1.
|
||||||
|
|
@ -367,6 +376,16 @@ def enable_stack_on_sig(
|
||||||
>> pkill --signal SIGUSR1 -f <part-of-cmd: str>
|
>> pkill --signal SIGUSR1 -f <part-of-cmd: str>
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
# no `SIGUSR1` on this platform (e.g. Windows) -> nothing to
|
||||||
|
# wire up; degrade gracefully instead of crashing callers that
|
||||||
|
# only guard against a missing `stackscope` (`ImportError`).
|
||||||
|
if sig is None:
|
||||||
|
log.warning(
|
||||||
|
'No `SIGUSR1` on this platform;\n'
|
||||||
|
'skipping `stackscope` trace-on-signal setup!\n'
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# NOTE, `stackscope._glue` does intentional async-gen type
|
# NOTE, `stackscope._glue` does intentional async-gen type
|
||||||
# introspection at import-time which trips
|
# introspection at import-time which trips
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,16 @@ from ..runtime._state import (
|
||||||
_def_tpt_proto,
|
_def_tpt_proto,
|
||||||
)
|
)
|
||||||
from ..ipc._tcp import TCPAddress
|
from ..ipc._tcp import TCPAddress
|
||||||
from ..ipc._uds import UDSAddress
|
from ..ipc._uds import (
|
||||||
|
UDSAddress,
|
||||||
|
HAS_UDS,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..runtime._runtime import Actor
|
from ..runtime._runtime import Actor
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger()
|
||||||
|
|
||||||
|
|
||||||
# TODO, maybe breakout the netns key to a struct?
|
# TODO, maybe breakout the netns key to a struct?
|
||||||
# class NetNs(Struct)[str, int]:
|
# class NetNs(Struct)[str, int]:
|
||||||
# ...
|
# ...
|
||||||
|
|
@ -172,19 +174,18 @@ class Address(Protocol):
|
||||||
|
|
||||||
_address_types: bidict[str, Type[Address]] = {
|
_address_types: bidict[str, Type[Address]] = {
|
||||||
'tcp': TCPAddress,
|
'tcp': TCPAddress,
|
||||||
'uds': UDSAddress
|
|
||||||
}
|
}
|
||||||
|
if HAS_UDS:
|
||||||
|
_address_types['uds'] = UDSAddress
|
||||||
|
|
||||||
|
|
||||||
# TODO! really these are discovery sys default addrs ONLY useful for
|
# TODO! really these are discovery sys default addrs ONLY useful for
|
||||||
# when none is provided to a root actor on first boot.
|
# when none is provided to a root actor on first boot.
|
||||||
_default_lo_addrs: dict[
|
_default_lo_addrs: dict[str, UnwrappedAddress] = {
|
||||||
str,
|
|
||||||
UnwrappedAddress
|
|
||||||
] = {
|
|
||||||
'tcp': TCPAddress.get_root().unwrap(),
|
'tcp': TCPAddress.get_root().unwrap(),
|
||||||
'uds': UDSAddress.get_root().unwrap(),
|
|
||||||
}
|
}
|
||||||
|
if HAS_UDS:
|
||||||
|
_default_lo_addrs['uds'] = UDSAddress.get_root().unwrap()
|
||||||
|
|
||||||
|
|
||||||
def get_address_cls(name: str) -> Type[Address]:
|
def get_address_cls(name: str) -> Type[Address]:
|
||||||
|
|
|
||||||
|
|
@ -62,16 +62,17 @@ from .. import log
|
||||||
from ..discovery._addr import Address
|
from ..discovery._addr import Address
|
||||||
from ._chan import Channel
|
from ._chan import Channel
|
||||||
from ._transport import MsgTransport
|
from ._transport import MsgTransport
|
||||||
from ._uds import UDSAddress
|
|
||||||
from ._tcp import TCPAddress
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..runtime._runtime import Actor
|
from ..runtime._runtime import Actor
|
||||||
from ..runtime._supervise import ActorNursery
|
from ..runtime._supervise import ActorNursery
|
||||||
|
|
||||||
|
|
||||||
log = log.get_logger()
|
from ._tcp import TCPAddress
|
||||||
|
from ._uds import UDSAddress
|
||||||
|
|
||||||
|
log = log.get_logger()
|
||||||
|
|
||||||
async def maybe_wait_on_canced_subs(
|
async def maybe_wait_on_canced_subs(
|
||||||
uid: tuple[str, str],
|
uid: tuple[str, str],
|
||||||
|
|
|
||||||
|
|
@ -18,17 +18,13 @@
|
||||||
IPC subsys type-lookup helpers?
|
IPC subsys type-lookup helpers?
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from typing import (
|
from typing import Type
|
||||||
Type,
|
|
||||||
# TYPE_CHECKING,
|
|
||||||
)
|
|
||||||
|
|
||||||
import trio
|
|
||||||
import socket
|
import socket
|
||||||
|
import trio
|
||||||
|
|
||||||
from tractor.ipc._transport import (
|
from tractor.ipc._transport import (
|
||||||
MsgTransportKey,
|
MsgTransportKey,
|
||||||
MsgTransport
|
MsgTransport,
|
||||||
)
|
)
|
||||||
from tractor.ipc._tcp import (
|
from tractor.ipc._tcp import (
|
||||||
TCPAddress,
|
TCPAddress,
|
||||||
|
|
@ -37,87 +33,86 @@ from tractor.ipc._tcp import (
|
||||||
from tractor.ipc._uds import (
|
from tractor.ipc._uds import (
|
||||||
UDSAddress,
|
UDSAddress,
|
||||||
MsgpackUDSStream,
|
MsgpackUDSStream,
|
||||||
|
HAS_UDS,
|
||||||
)
|
)
|
||||||
|
|
||||||
# if TYPE_CHECKING:
|
# the UDS backend is importable everywhere but only *usable* where
|
||||||
# from tractor._addr import Address
|
# `trio` reports `has_unix` (i.e. POSIX). On Windows / no-`AF_UNIX`
|
||||||
|
# hosts `HAS_UDS` is `False` and the runtime registers TCP only.
|
||||||
|
|
||||||
Address = TCPAddress|UDSAddress
|
Address = TCPAddress|UDSAddress
|
||||||
|
|
||||||
# manually updated list of all supported msg transport types
|
# manually updated list of all supported msg transport types
|
||||||
_msg_transports = [
|
_msg_transports: list[Type[MsgTransport]] = [
|
||||||
MsgpackTCPStream,
|
MsgpackTCPStream,
|
||||||
MsgpackUDSStream
|
|
||||||
]
|
]
|
||||||
|
if HAS_UDS:
|
||||||
|
_msg_transports.append(MsgpackUDSStream)
|
||||||
|
|
||||||
|
# map a `MsgTransportKey` to its `MsgTransport` type
|
||||||
# convert a MsgTransportKey to the corresponding transport type
|
_key_to_transport: dict[MsgTransportKey, Type[MsgTransport]] = {
|
||||||
_key_to_transport: dict[
|
|
||||||
MsgTransportKey,
|
|
||||||
Type[MsgTransport],
|
|
||||||
] = {
|
|
||||||
('msgpack', 'tcp'): MsgpackTCPStream,
|
('msgpack', 'tcp'): MsgpackTCPStream,
|
||||||
('msgpack', 'uds'): MsgpackUDSStream,
|
|
||||||
}
|
}
|
||||||
|
if HAS_UDS:
|
||||||
|
_key_to_transport[('msgpack', 'uds')] = MsgpackUDSStream
|
||||||
|
|
||||||
# convert an Address wrapper to its corresponding transport type
|
# map an `Address`-wrapper to its `MsgTransport` type
|
||||||
_addr_to_transport: dict[
|
_addr_to_transport: dict[Type[Address], Type[MsgTransport]] = {
|
||||||
Type[TCPAddress|UDSAddress],
|
|
||||||
Type[MsgTransport]
|
|
||||||
] = {
|
|
||||||
TCPAddress: MsgpackTCPStream,
|
TCPAddress: MsgpackTCPStream,
|
||||||
UDSAddress: MsgpackUDSStream,
|
|
||||||
}
|
}
|
||||||
|
if HAS_UDS:
|
||||||
|
_addr_to_transport[UDSAddress] = MsgpackUDSStream
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ------------------------------------------------------------
|
||||||
def transport_from_addr(
|
def transport_from_addr(
|
||||||
addr: Address,
|
addr: Address,
|
||||||
codec_key: str = 'msgpack',
|
codec_key: str = "msgpack",
|
||||||
) -> Type[MsgTransport]:
|
) -> Type[MsgTransport]:
|
||||||
'''
|
"""
|
||||||
Given a destination address and a desired codec, find the
|
Given a destination address and a desired codec, find the
|
||||||
corresponding `MsgTransport` type.
|
corresponding `MsgTransport` type.
|
||||||
|
"""
|
||||||
'''
|
|
||||||
try:
|
try:
|
||||||
return _addr_to_transport[type(addr)]
|
return _addr_to_transport[type(addr)] # type: ignore[call-arg]
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f'No known transport for address {repr(addr)}'
|
f"No known transport for address {repr(addr)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def transport_from_stream(
|
def transport_from_stream(
|
||||||
stream: trio.abc.Stream,
|
stream: trio.abc.Stream,
|
||||||
codec_key: str = 'msgpack'
|
codec_key: str = "msgpack",
|
||||||
) -> Type[MsgTransport]:
|
) -> Type[MsgTransport]:
|
||||||
'''
|
"""
|
||||||
Given an arbitrary `trio.abc.Stream` and a desired codec,
|
Given an arbitrary `trio.abc.Stream` and a desired codec,
|
||||||
find the corresponding `MsgTransport` type.
|
find the corresponding `MsgTransport` type.
|
||||||
|
"""
|
||||||
'''
|
|
||||||
transport = None
|
transport = None
|
||||||
|
|
||||||
if isinstance(stream, trio.SocketStream):
|
if isinstance(stream, trio.SocketStream):
|
||||||
sock: socket.socket = stream.socket
|
sock: socket.socket = stream.socket
|
||||||
match sock.family:
|
fam = sock.family
|
||||||
case socket.AF_INET | socket.AF_INET6:
|
|
||||||
transport = 'tcp'
|
|
||||||
|
|
||||||
case socket.AF_UNIX:
|
if fam in (socket.AF_INET, getattr(socket, "AF_INET6", None)):
|
||||||
transport = 'uds'
|
transport = "tcp"
|
||||||
|
|
||||||
case _:
|
# only consider `AF_UNIX` when the UDS backend is active;
|
||||||
raise NotImplementedError(
|
# `HAS_UDS` short-circuits before `socket.AF_UNIX` so this
|
||||||
f'Unsupported socket family: {sock.family}'
|
# stays safe on hosts where that constant is absent.
|
||||||
)
|
if transport is None and HAS_UDS and fam == socket.AF_UNIX: # type: ignore[attr-defined]
|
||||||
|
transport = "uds"
|
||||||
|
|
||||||
|
if transport is None:
|
||||||
|
raise NotImplementedError(f"Unsupported socket family: {fam}")
|
||||||
|
|
||||||
if not transport:
|
if not transport:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f'Could not figure out transport type for stream type {type(stream)}'
|
f"Could not figure out transport type for stream type {type(stream)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
key = (codec_key, transport)
|
key = (codec_key, transport)
|
||||||
|
|
||||||
return _key_to_transport[key]
|
return _key_to_transport[key]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,21 @@ from pathlib import Path
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from socket import (
|
from socket import (
|
||||||
AF_UNIX,
|
|
||||||
SOCK_STREAM,
|
SOCK_STREAM,
|
||||||
SOL_SOCKET,
|
SOL_SOCKET,
|
||||||
error as socket_error,
|
error as socket_error,
|
||||||
)
|
)
|
||||||
|
# NOTE, `AF_UNIX` is absent on Windows / any CPython built without
|
||||||
|
# unix-domain-socket support. Keep this module importable
|
||||||
|
# everywhere (so `UDSAddress` stays referenceable for type and
|
||||||
|
# `isinstance()` checks plus registry lookups); the `AF_UNIX`-using
|
||||||
|
# code paths below are runtime-only and are never reached when the
|
||||||
|
# UDS backend is unusable (gated on `trio`'s `has_unix`, see
|
||||||
|
# `HAS_UDS`).
|
||||||
|
try:
|
||||||
|
from socket import AF_UNIX
|
||||||
|
except ImportError:
|
||||||
|
AF_UNIX = None
|
||||||
import struct
|
import struct
|
||||||
from typing import (
|
from typing import (
|
||||||
Type,
|
Type,
|
||||||
|
|
@ -90,6 +100,13 @@ else:
|
||||||
log = get_logger()
|
log = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
# single source of truth for "is the UDS backend usable on this
|
||||||
|
# host?" — reuse `trio`'s `has_unix` (the same predicate that gates
|
||||||
|
# `trio.open_unix_socket()`) rather than re-deriving an `AF_UNIX`
|
||||||
|
# probe in every consumer module.
|
||||||
|
HAS_UDS: bool = has_unix
|
||||||
|
|
||||||
|
|
||||||
def unwrap_sockpath(
|
def unwrap_sockpath(
|
||||||
sockpath: Path,
|
sockpath: Path,
|
||||||
) -> tuple[Path, Path]:
|
) -> tuple[Path, Path]:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue