Register `tipc` in the tpt tables + test harness

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`))
wkt/pr493_review
Gud Boi 2026-08-14 09:52:42 -04:00
parent 2204979492
commit e0f66616cd
6 changed files with 102 additions and 8 deletions

View File

@ -84,6 +84,35 @@ def _wait_for_daemon_ready(
timeout=poll_interval, timeout=poll_interval,
): ):
return 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: else:
# UDS — `reg_addr` is a `(filedir, sockname)` # UDS — `reg_addr` is a `(filedir, sockname)`
# tuple per `tractor.ipc._uds.UDSAddress.unwrap`. # tuple per `tractor.ipc._uds.UDSAddress.unwrap`.

View File

@ -99,6 +99,20 @@ def get_rando_addr(
assert addr.sockpath.resolve() assert addr.sockpath.resolve()
testrun_reg_addr = addr.unwrap() 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 # XXX, as sanity it should never the same as the default for the
# host-singleton registry actor. # host-singleton registry actor.
assert def_reg_addr != testrun_reg_addr assert def_reg_addr != testrun_reg_addr

View File

@ -498,14 +498,14 @@ def pytest_configure(
) )
config.addinivalue_line( config.addinivalue_line(
'markers', 'markers',
'tipc: test targets the `AF_TIPC` tpt backend; the kernel- ' 'trio: legacy mark for tests meant to run under the `trio` '
'touching cases self-skip unless the `tipc` module is loaded ' 'spawn backend (e.g. `test_local.py`).'
'(`sudo modprobe tipc`).'
) )
config.addinivalue_line( config.addinivalue_line(
'markers', 'markers',
'trio: legacy mark for tests meant to run under the `trio` ' 'tipc: test targets the `AF_TIPC` tpt backend; the kernel- '
'spawn backend (e.g. `test_local.py`).' 'touching cases self-skip unless the `tipc` module is loaded '
'(`sudo modprobe tipc`).'
) )
# `--enable-stackscope`: install SIGUSR1 → trio task-tree # `--enable-stackscope`: install SIGUSR1 → trio task-tree
@ -803,6 +803,27 @@ def tpt_protos(
addr_type = _addr._address_types[proto_key] addr_type = _addr._address_types[proto_key]
assert addr_type.proto_key == 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 yield proto_keys

View File

@ -33,6 +33,7 @@ from ..runtime._state import (
) )
from ..ipc._tcp import TCPAddress from ..ipc._tcp import TCPAddress
from ..ipc._uds import UDSAddress from ..ipc._uds import UDSAddress
from ..ipc._tipc import TIPCAddress
if TYPE_CHECKING: if TYPE_CHECKING:
from ..runtime._runtime import Actor from ..runtime._runtime import Actor
@ -197,6 +198,7 @@ class Address(Protocol):
_address_types: bidict[str, Type[Address]] = { _address_types: bidict[str, Type[Address]] = {
'tcp': TCPAddress, 'tcp': TCPAddress,
'uds': UDSAddress, 'uds': UDSAddress,
'tipc': TIPCAddress,
} }
@ -208,6 +210,9 @@ _default_lo_addrs: dict[
] = { ] = {
'tcp': TCPAddress.get_root().unwrap(), 'tcp': TCPAddress.get_root().unwrap(),
'uds': UDSAddress.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() # import pdbp; pdbp.set_trace()
match addr: 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 # classic network socket-address as tuple/list
case ( case (
(str(), int()) (str(), int())

View File

@ -38,17 +38,23 @@ from tractor.ipc._uds import (
UDSAddress, UDSAddress,
MsgpackUDSStream, MsgpackUDSStream,
) )
from tractor.ipc._tipc import (
AF_TIPC,
TIPCAddress,
MsgpackTIPCStream,
)
# if TYPE_CHECKING: # if TYPE_CHECKING:
# from tractor._addr import Address # from tractor._addr import Address
Address = TCPAddress|UDSAddress Address = TCPAddress|UDSAddress|TIPCAddress
# manually updated list of all supported msg transport types # manually updated list of all supported msg transport types
_msg_transports = [ _msg_transports = [
MsgpackTCPStream, MsgpackTCPStream,
MsgpackUDSStream MsgpackUDSStream,
MsgpackTIPCStream,
] ]
@ -59,15 +65,17 @@ _key_to_transport: dict[
] = { ] = {
('msgpack', 'tcp'): MsgpackTCPStream, ('msgpack', 'tcp'): MsgpackTCPStream,
('msgpack', 'uds'): MsgpackUDSStream, ('msgpack', 'uds'): MsgpackUDSStream,
('msgpack', 'tipc'): MsgpackTIPCStream,
} }
# convert an Address wrapper to its corresponding transport type # convert an Address wrapper to its corresponding transport type
_addr_to_transport: dict[ _addr_to_transport: dict[
Type[TCPAddress|UDSAddress], Type[TCPAddress|UDSAddress|TIPCAddress],
Type[MsgTransport] Type[MsgTransport]
] = { ] = {
TCPAddress: MsgpackTCPStream, TCPAddress: MsgpackTCPStream,
UDSAddress: MsgpackUDSStream, UDSAddress: MsgpackUDSStream,
TIPCAddress: MsgpackTIPCStream,
} }
@ -108,6 +116,12 @@ def transport_from_stream(
case socket.AF_UNIX: case socket.AF_UNIX:
transport = 'uds' 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 _: case _:
raise NotImplementedError( raise NotImplementedError(
f'Unsupported socket family: {sock.family}' f'Unsupported socket family: {sock.family}'

View File

@ -47,6 +47,7 @@ if TYPE_CHECKING:
TransportProtocolKey = Literal[ TransportProtocolKey = Literal[
'tcp', 'tcp',
'uds', 'uds',
'tipc',
] ]
_def_tpt_proto: TransportProtocolKey = 'tcp' _def_tpt_proto: TransportProtocolKey = 'tcp'