diff --git a/tractor/_testing/addr.py b/tractor/_testing/addr.py index 58acfd1a..cc583d87 100644 --- a/tractor/_testing/addr.py +++ b/tractor/_testing/addr.py @@ -99,6 +99,20 @@ def get_rando_addr( assert addr.sockpath.resolve() testrun_reg_addr = addr.unwrap() + # NOTE, `.get_random()` already derives the service + # *instance* from a `uuid4`+pid-salted seed, so both the + # within- and cross-proc isolation the other 2 protos + # hand-roll above comes for free. + # + # XXX matters MORE here than for tcp/uds: a TIPC name + # clash doesn't raise `EADDRINUSE`, it silently + # round-robins connects between both publishers. + case 'tipc': + from tractor.ipc._tipc import TIPCAddress + addr: TIPCAddress = addr_type.get_random() + assert addr.is_valid + testrun_reg_addr = addr.unwrap() + # XXX, as sanity it should never the same as the default for the # host-singleton registry actor. assert def_reg_addr != testrun_reg_addr diff --git a/tractor/_testing/pytest.py b/tractor/_testing/pytest.py index b8938fb1..88941463 100644 --- a/tractor/_testing/pytest.py +++ b/tractor/_testing/pytest.py @@ -498,14 +498,14 @@ def pytest_configure( ) config.addinivalue_line( 'markers', - 'tipc: test targets the `AF_TIPC` tpt backend; the kernel- ' - 'touching cases self-skip unless the `tipc` module is loaded ' - '(`sudo modprobe tipc`).' + 'trio: legacy mark for tests meant to run under the `trio` ' + 'spawn backend (e.g. `test_local.py`).' ) config.addinivalue_line( 'markers', - 'trio: legacy mark for tests meant to run under the `trio` ' - 'spawn backend (e.g. `test_local.py`).' + 'tipc: test targets the `AF_TIPC` tpt backend; the kernel- ' + 'touching cases self-skip unless the `tipc` module is loaded ' + '(`sudo modprobe tipc`).' ) # `--enable-stackscope`: install SIGUSR1 → trio task-tree @@ -803,6 +803,27 @@ def tpt_protos( addr_type = _addr._address_types[proto_key] assert addr_type.proto_key == proto_key + # XXX, generic capability gate: an env-dependent tpt + # whose backing kernel-mod/lib/netns isn't present here + # must fail LOUDLY and EARLY rather than as a few hundred + # confusing connect-timeouts downstream. + # + # Any `Address` type MAY expose `.is_available()` + # returning `(ok, why_not)`; absence means "always + # available" (i.e. tcp/uds). + is_avail = getattr( + addr_type, + 'is_available', + None, + ) + if is_avail: + avail, why_not = is_avail() + if not avail: + pytest.fail( + f'--tpt-proto={proto_key!r} is NOT usable here!\n' + f'{why_not}\n' + ) + yield proto_keys diff --git a/tractor/discovery/_addr.py b/tractor/discovery/_addr.py index d0ca1861..29258b92 100644 --- a/tractor/discovery/_addr.py +++ b/tractor/discovery/_addr.py @@ -38,6 +38,7 @@ from ..ipc._uds import ( UDSAddress, HAS_UDS, ) +from ..ipc._tipc import TIPCAddress if TYPE_CHECKING: # ONLY type-annots, the eager import costs ~4.5ms @@ -88,9 +89,16 @@ TaggedUDSAlias: TypeAlias = tuple[ Literal['uds'], str, ] +TaggedTIPCAddress: TypeAlias = tuple[ + Literal['tipc'], + int, + int, + int, +] TaggedAddress: TypeAlias = ( TaggedTCPAddress |TaggedUnixAddress + |TaggedTIPCAddress ) # Input-only compatibility forms retained for older callers and @@ -211,12 +219,14 @@ class Address(Protocol): ... -# the address types available on this host: TCP always, UDS only -# where usable (`HAS_UDS`). Both registries derive from this single -# list via each type's `proto_key`. +# the address types supported by this build: TCP and TIPC are +# importable everywhere, while UDS is registered only where usable +# (`HAS_UDS`). Runtime TIPC availability is checked separately. +# Both registries derive from this list via each type's `proto_key`. _address_protos: list[Type[Address]] = [TCPAddress] if HAS_UDS: _address_protos.append(UDSAddress) +_address_protos.append(TIPCAddress) _address_types: dict[str, Type[Address]] = { cls.proto_key: cls @@ -313,6 +323,16 @@ def wrap_address( ): return UDSAddress.from_addr(addr) + # XXX, the explicitly proto-keyed form (spelled with the + # `multiaddr` proto name) which is where ALL backends + # should eventually land per the `UnwrappedAddress` + # migration note above. + # + # NOTE, a bare seq-pattern matches `list` too, which is + # what `msgpack` decodes our tuples back to. + case ('tipc', *_): + cls = TIPCAddress + # classic network socket-address as tuple/list case ( (str(), int()) diff --git a/tractor/ipc/_tipc.py b/tractor/ipc/_tipc.py index a3c8617a..1a038231 100644 --- a/tractor/ipc/_tipc.py +++ b/tractor/ipc/_tipc.py @@ -80,6 +80,7 @@ from tractor.runtime._state import ( ) if TYPE_CHECKING: + from tractor.discovery._addr import TaggedTIPCAddress from tractor.runtime._runtime import Actor @@ -209,7 +210,7 @@ class TIPCAddress( maybe_ref: int|None = None proto_key: ClassVar[str] = 'tipc' - unwrapped_type: ClassVar[type] = tuple[str, int, int, int] + unwrapped_type: ClassVar[type] = tuple def_bindspace: ClassVar[int] = TIPC_CLUSTER_SCOPE # XXX, TIPC's `getsockname()` answers a `TIPC_ADDR_ID` port-id @@ -256,7 +257,7 @@ class TIPCAddress( @classmethod def from_addr( cls, - addr: tuple[str, int, int, int], + addr: tuple|list, ) -> TIPCAddress: match addr: # our proto-keyed unwrapped form, w/ scope optional @@ -304,7 +305,7 @@ class TIPCAddress( f'{addr!r}\n' ) - def unwrap(self) -> tuple[str, int, int, int]: + def unwrap(self) -> TaggedTIPCAddress: # NOTE, proto-keyed (w/ the `multiaddr` proto spelling) so # `wrap_address()` can dispatch unambiguously against the # other backends' 2-tuple forms; see contract §1.1. diff --git a/tractor/ipc/_types.py b/tractor/ipc/_types.py index 244da48b..5ff2088f 100644 --- a/tractor/ipc/_types.py +++ b/tractor/ipc/_types.py @@ -35,20 +35,25 @@ from tractor.ipc._uds import ( MsgpackUDSStream, HAS_UDS, ) +from tractor.ipc._tipc import ( + AF_TIPC, + TIPCAddress, + MsgpackTIPCStream, +) -# the UDS backend is importable everywhere but only *usable* when -# `HAS_UDS` is `True`; otherwise the runtime registers TCP only. -Address = TCPAddress|UDSAddress +Address = TCPAddress|UDSAddress|TIPCAddress -# the available msg-transport backends on this host: TCP always, -# UDS only where usable (`HAS_UDS`). The lookup maps below derive -# from this single list via each backend's `codec_key` and -# `address_type`: register a backend here and every map picks it up. +# the msg-transport backends supported by this build: TCP and TIPC +# are importable everywhere, while UDS is registered only where +# usable (`HAS_UDS`). Runtime TIPC availability is checked separately. +# The lookup maps derive from this list via each backend's `codec_key` +# and `address_type`. _msg_transports: list[Type[MsgTransport]] = [ MsgpackTCPStream, ] if HAS_UDS: _msg_transports.append(MsgpackUDSStream) +_msg_transports.append(MsgpackTIPCStream) # map a `MsgTransportKey` -> `MsgTransport` type _key_to_transport: dict[MsgTransportKey, Type[MsgTransport]] = { @@ -109,6 +114,12 @@ def transport_from_stream( ): transport = 'uds' + # NOTE, `AF_TIPC` is linux-only in CPython so we + # match the `._tipc` constant (which carries a uapi + # fallback) rather than `socket.AF_TIPC`. + case fam if fam == AF_TIPC: + transport = 'tipc' + case fam: raise NotImplementedError( f'Unsupported socket family: {fam}' diff --git a/tractor/runtime/_state.py b/tractor/runtime/_state.py index 5caa92e3..8c8357b0 100644 --- a/tractor/runtime/_state.py +++ b/tractor/runtime/_state.py @@ -52,6 +52,7 @@ _DARWIN_TMPDIR: Path = Path('/tmp') TransportProtocolKey = Literal[ 'tcp', 'uds', + 'tipc', ] _def_tpt_proto: TransportProtocolKey = 'tcp'