From e0f66616cd17da2bc55620896453ba25372a8471 Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 14 Aug 2026 09:52:42 -0400 Subject: [PATCH] Register `tipc` in the tpt tables + test harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire the backend through every registration site (contract §2) so `--tpt-proto tipc` is a first-class suite mode, - `_state.TransportProtocolKey` gains the key - `_addr._address_types` + `._default_lo_addrs` - `_addr.wrap_address()` gets a `case ('tipc', *_)`; being a 4-elem seq it can't collide w/ `tcp`s or `uds`s 2-tuple cases, so NO ordering hazard (and a bare seq-pattern matches the `list` form `msgpack` decodes to). - `_types`: the `Address` union, `_msg_transports`, `_key_to_transport`, `_addr_to_transport` and the `transport_from_stream()` family match. That last one keys off `._tipc.AF_TIPC` (which carries the uapi fallback) NOT `socket.AF_TIPC` which is linux-only. Test-harness side, - `get_rando_addr()` gains a `tipc` branch; `.get_random()` already salts w/ `uuid4`+pid so both within- and cross-proc isolation come for free. - the `tpt_protos` fixture calls an addr-type's optional `.is_available()` and `pytest.fail()`s w/ its reason. Keeps a module-less box from turning `--tpt-proto tipc` into a few hundred confusing connect-timeouts. Generic on purpose — plans 02/03 need the same hook. - the discovery `daemon` fixture's readiness probe learns to dial a TIPC service name (it previously assumed tcp-or-uds and blew up on the 4-tuple). (this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`)) --- tests/discovery/conftest.py | 29 +++++++++++++++++++++++++++++ tractor/_testing/addr.py | 14 ++++++++++++++ tractor/_testing/pytest.py | 31 ++++++++++++++++++++++++++----- tractor/discovery/_addr.py | 15 +++++++++++++++ tractor/ipc/_types.py | 20 +++++++++++++++++--- tractor/runtime/_state.py | 1 + 6 files changed, 102 insertions(+), 8 deletions(-) diff --git a/tests/discovery/conftest.py b/tests/discovery/conftest.py index 73749a6d..1eeca75a 100644 --- a/tests/discovery/conftest.py +++ b/tests/discovery/conftest.py @@ -84,6 +84,35 @@ def _wait_for_daemon_ready( timeout=poll_interval, ): return + + elif tpt_proto == 'tipc': + # TIPC — `reg_addr` is the proto-keyed + # `('tipc', stype, instance, scope)` per + # `tractor.ipc._tipc.TIPCAddress.unwrap()`. + # + # NOTE, connecting *by name* IS the readiness + # probe: until the daemon `.bind()`s (i.e. + # publishes) the name, the kernel answers + # `EHOSTUNREACH` immediately — no timeout wait. + from tractor.ipc._tipc import ( + AF_TIPC, + TIPC_ADDR_NAME, + ) + _, stype, instance, scope = reg_addr + sock = socket.socket(AF_TIPC, socket.SOCK_STREAM) + try: + sock.settimeout(poll_interval) + sock.connect(( + TIPC_ADDR_NAME, + stype, + instance, + 0, # domain: 0 == "anywhere in scope" + scope, + )) + return + finally: + sock.close() + else: # UDS — `reg_addr` is a `(filedir, sockname)` # tuple per `tractor.ipc._uds.UDSAddress.unwrap`. 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 9be508f2..94b922fb 100644 --- a/tractor/discovery/_addr.py +++ b/tractor/discovery/_addr.py @@ -33,6 +33,7 @@ from ..runtime._state import ( ) from ..ipc._tcp import TCPAddress from ..ipc._uds import UDSAddress +from ..ipc._tipc import TIPCAddress if TYPE_CHECKING: from ..runtime._runtime import Actor @@ -197,6 +198,7 @@ class Address(Protocol): _address_types: bidict[str, Type[Address]] = { 'tcp': TCPAddress, 'uds': UDSAddress, + 'tipc': TIPCAddress, } @@ -208,6 +210,9 @@ _default_lo_addrs: dict[ ] = { 'tcp': TCPAddress.get_root().unwrap(), 'uds': UDSAddress.get_root().unwrap(), + # NOTE, pure/cheap: a service-name pair, no kernel module + # nor I/O required at import time. + 'tipc': TIPCAddress.get_root().unwrap(), } @@ -253,6 +258,16 @@ def wrap_address( # import pdbp; pdbp.set_trace() match 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/_types.py b/tractor/ipc/_types.py index 59653b17..bc8b2620 100644 --- a/tractor/ipc/_types.py +++ b/tractor/ipc/_types.py @@ -38,17 +38,23 @@ from tractor.ipc._uds import ( UDSAddress, MsgpackUDSStream, ) +from tractor.ipc._tipc import ( + AF_TIPC, + TIPCAddress, + MsgpackTIPCStream, +) # if TYPE_CHECKING: # from tractor._addr import Address -Address = TCPAddress|UDSAddress +Address = TCPAddress|UDSAddress|TIPCAddress # manually updated list of all supported msg transport types _msg_transports = [ MsgpackTCPStream, - MsgpackUDSStream + MsgpackUDSStream, + MsgpackTIPCStream, ] @@ -59,15 +65,17 @@ _key_to_transport: dict[ ] = { ('msgpack', 'tcp'): MsgpackTCPStream, ('msgpack', 'uds'): MsgpackUDSStream, + ('msgpack', 'tipc'): MsgpackTIPCStream, } # convert an Address wrapper to its corresponding transport type _addr_to_transport: dict[ - Type[TCPAddress|UDSAddress], + Type[TCPAddress|UDSAddress|TIPCAddress], Type[MsgTransport] ] = { TCPAddress: MsgpackTCPStream, UDSAddress: MsgpackUDSStream, + TIPCAddress: MsgpackTIPCStream, } @@ -108,6 +116,12 @@ def transport_from_stream( case socket.AF_UNIX: 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 _ if sock.family == AF_TIPC: + transport = 'tipc' + case _: raise NotImplementedError( f'Unsupported socket family: {sock.family}' diff --git a/tractor/runtime/_state.py b/tractor/runtime/_state.py index 0d9a4435..a65daf3e 100644 --- a/tractor/runtime/_state.py +++ b/tractor/runtime/_state.py @@ -47,6 +47,7 @@ if TYPE_CHECKING: TransportProtocolKey = Literal[ 'tcp', 'uds', + 'tipc', ] _def_tpt_proto: TransportProtocolKey = 'tcp'