27 KiB
Plan 01 — TIPC transport backend (tractor/ipc/_tipc.py)
Tracks gh #378. Prereq reading: 00_shared_backend_contract.md.
Thesis: TIPC is the cheapest new backend we can add and simultaneously the only one that gives us cluster-wide service discovery for free, in the kernel, replacing (for TIPC-capable deployments) the whole tractor.discovery registrar round-trip with a bind()/connect() on a service name. It is stdlib-only: zero new dependencies.
1. Why this is small: three verified facts
- CPython already speaks TIPC.
socket.AF_TIPCplus 23TIPC_*constants are present in the stdlib on Linux (verified on the dev box, py3.13):AF_TIPC, SOL_TIPC, TIPC_ADDR_ID, TIPC_ADDR_NAME, TIPC_ADDR_NAMESEQ, TIPC_CFG_SRV, TIPC_CLUSTER_SCOPE, TIPC_CONN_TIMEOUT, TIPC_{CRITICAL,HIGH,MEDIUM,LOW}_IMPORTANCE, TIPC_DEST_DROPPABLE, TIPC_IMPORTANCE, TIPC_NODE_SCOPE, TIPC_PUBLISHED, TIPC_SRC_DROPPABLE, TIPC_SUBSCR_TIMEOUT, TIPC_SUB_CANCEL, TIPC_SUB_PORTS, TIPC_SUB_SERVICE, TIPC_TOP_SRV, TIPC_WAIT_FOREVER, TIPC_WITHDRAWN, TIPC_ZONE_SCOPE.sock.bind()/connect()/getsockname()take/return the 5-tuple(addr_type, v1, v2, v3, scope)— the last element is optional on input and defaults to0. triodoesn’t care about the address family. Per contract §1.5,trio.SocketStreamandtrio.SocketListeneronly require a trio socket object of typeSOCK_STREAM. TIPC’sSOCK_STREAMis a real connection-oriented reliable byte stream. So we reusetrio.SocketStream,trio.SocketListener,trio.serve_listeners(),MsgpackTransport’s framing — all of it.- It is not available by default. On this box
socket.socket(AF_TIPC, SOCK_STREAM)→OSError(97, 'Address family not supported by protocol')with notipcin/proc/modules.modprobe tipcis required; cross-node needs a bearer (tipc bearer enable media eth device <if>ormedia udp name <n> localip <ip>). Everything about this plan’s testability hinges on gating (§7).
Non-goals: SOCK_RDM/SOCK_DGRAM/SOCK_SEQPACKET message modes, multicast fan-out, and TIPC group messaging. They are genuinely interesting for a future tractor broadcast/pubsub transport but they do not fit MsgTransport’s stream-of-length-prefixed-msgs shape. Note them in the follow-up issue, do not build them here.
2. TIPCAddress
2.1 the three TIPC address flavours, and which we use
| flavour | tuple | meaning |
|---|---|---|
TIPC_ADDR_NAMESEQ |
(type, lower, upper, scope) |
a published range — what a server bind()s |
TIPC_ADDR_NAME |
(type, instance, domain, scope) |
a lookup — what a client connect()s |
TIPC_ADDR_ID |
(node, ref, 0, scope) |
a concrete port id — the “physical” address |
The design decision that makes this backend coherent:
A
tractoractor’s TIPC address is a service name(type, instance);bind()publishes the singleton range(type, instance, instance); peersconnect()by name and the kernel resolves + load-balances.TIPC_ADDR_IDis only ever an observed address (getpeername()), never a user-facing one.
This is exactly the “leverage the built-in discovery machinery” ask in #378: publishing a bind is registration, and connect() on a name is a lookup, with no registrar actor in the loop.
2.2 the struct
class TIPCAddress(
msgspec.Struct,
frozen=True,
):
_stype: int # TIPC "type" == service class
_instance: int # service instance within the type
_scope: int = TIPC_CLUSTER_SCOPE
# observed-only, never part of identity/equality-by-intent
maybe_node: int|None = None # from TIPC_ADDR_ID getpeername()
maybe_ref: int|None = None
proto_key: ClassVar[str] = 'tipc'
unwrapped_type: ClassVar[type] = tuple[str, int]
def_bindspace: ClassVar[int] = TIPC_CLUSTER_SCOPEUnwrapped form (the wire/SpawnSpec shape).
TIPC’s natural form is (stype, instance, scope) — but a 2-tuple squeeze of it is a (str, int), i.e. the same coarse shape as TCPAddress, so wrap_address()’s case (str(), int()) steals it. This backend is therefore the forcing function for the contract-doc’s conclusion (§1.1):
make the unwrapped form carry an explicit proto-key, spelled with the
multiaddrprotocol name.
def unwrap(self) -> tuple[str, int, int, int]:
return ('tipc', self._stype, self._instance, self._scope)wrap_address() then dispatches _address_types[addr[0]] and the collision class disappears. This is a prerequisite migration commit, not part of this backend — see contract §1.1 for its blast radius (wire format + every fixture + piker config) and for the follow-on “stop handing raw tuples to users at all, à la ipaddress” direction.
⚠️ an earlier revision of this plan proposed a self-tagging ('tipc:<stype>:<scope>', instance) string-prefix hack with an ordered case guard. Dropped — it papers over the problem, keeps wrap_address() order-sensitive, and doesn’t help iroh’s (str, str)-vs-UDS collision at all. Do not resurrect it.
Note TIPCAddress is the first backend where .unwrap() is not a lossless view of the live socket — maybe_node/ maybe_ref are observed metadata, exactly like UDSAddress.maybe_pid (which is likewise excluded from .unwrap()). Follow that precedent, including its __repr__ treatment (_uds.py:242).
2.3 how to pick _stype and _instance
_stype= atractor-reserved service class. TIPC reserves 0..63 for internal use (TIPC_TOP_SRV == 1,TIPC_CFG_SRV == 0). Use a module constantTRACTOR_STYPE: int = 0x74_72_00_00(“tr\0\0”) as the default and make it overridable viaTIPCAddress._stypeso an app can partition service classes. Document that twotractortrees sharing a cluster and a_stypeshare a namespace._instanceforget_root():1616— mirrors theTCPAddress.get_root()port and theregistry@1616.sockUDS filename, so the “1616 is tractor’s registrar” idiom holds across all backends._instanceforget_random(): TIPC gives us no kernel-assigned-instance analogue ofport=0, so we must choose. Use a pure fn of the actor identity so it is reproducible and collision-free:# 32-bit instance derived from the actor's uuid4 (+ pid when # there's no live runtime, per the UDS precedent). inst: int = int.from_bytes( blake2b(seed.encode(), digest_size=4).digest(), 'big', )where
seed = f'{actor.aid.name}@{pid}'ifcurrent_actor(err_on_no_runtime=False)elsef'{prefix}.{uuid4().hex[:8]}@{pid}'. Must avoid the reserved low range:inst = 64 + (inst % (2**32 - 64)). ⚠️ unlikeport=0, a collision here surfaces as a successful-but-shared publication (TIPC allows multiple binders on the same name and round-robins!) rather thanEADDRINUSE. That is a silent-crosstalk failure mode; §7 has the test that proves the 4-byte digest is enough and §9 has the mitigation if it isn’t._scope:TIPC_NODE_SCOPEfor a same-host-only actor (the UDS-equivalent),TIPC_CLUSTER_SCOPE(default) for cluster-visible. This is.bindspace:@property def bindspace(self) -> int: return self._scopeIt is the honest analogue of “the set of hosts this bind is reachable from”, which is precisely the docstring in
Address.bindspace. (TIPC_ZONE_SCOPEis deprecated/aliased to cluster in modern kernels — accept it on input, normalize to cluster, log attransportlevel.)
2.4 is_valid
@property
def is_valid(self) -> bool:
return (
self._instance != 0
and
self._stype not in _tipc_reserved_stypes # {0, 1, ...}
and
self._scope in (TIPC_NODE_SCOPE, TIPC_CLUSTER_SCOPE)
)3. Listener + stream
3.1 start_listener()
async def start_listener(
addr: TIPCAddress,
backlog: int = 128,
**kwargs,
) -> SocketListener:
sock = trio.socket.socket(
socket.AF_TIPC,
socket.SOCK_STREAM,
)
# publish the singleton name-range == "register the service"
await sock.bind((
socket.TIPC_ADDR_NAMESEQ,
addr._stype,
addr._instance,
addr._instance,
addr._scope,
))
sock.listen(backlog)
return SocketListener(sock)Notes / hazards:
bind()onAF_TIPCis not a filesystem or port-table operation and can’t block on DNS, but keep itawaited throughtrio.socketanyway for uniformity.backlog=128matching_uds.start_listener()’s hard-won value (see its comment at_uds.py:317-331re: concurrent deregistration storms). Do not use1.- no
close_listener()needed — nothing to unlink. Omit the function entirely (contract §1.2: absence means implicit). Withdrawal of the published name happens on socket close. - ⚠️
SocketListener.__init__will trygetsockopt(SOL_SOCKET, SO_ACCEPTCONN). If TIPC rejects it, trio’sexcept OSError: passcovers us. Assert this in a unit test rather than assuming. - Wrap the bind in a
_reraise_as_connerr()-style@cm(copy the_uds.py:256pattern) soEADDRINUSE-ish andEAFNOSUPPORTbecomeConnectionErrorwith the addr in the message.EAFNOSUPPORThere means “kernel module not loaded” and deserves a specifically actionable message:'TIPC unavailable — trysudo modprobe tipc\n'.
3.2 the getsockname() reconciliation
Endpoint.start_listener() does if lstnr.socket.getsockname() != self.addr.unwrap(): self.addr = self.addr.from_addr(unwrapped).
For TIPC, getsockname() on a bound-but-listening socket returns a TIPC_ADDR_ID-flavoured 5-tuple (the port id), not the name-seq we bound. So the != is always true and from_addr() will be handed a 5-tuple.
Handle it inside TIPCAddress.from_addr() — do not patch _server.py:
@classmethod
def from_addr(cls, addr) -> TIPCAddress:
match addr:
# our own unwrapped form
case (str() as tag, int() as inst) if tag.startswith('tipc:'):
_, stype, scope = tag.split(':')
return TIPCAddress(int(stype), inst, int(scope))
# a kernel-observed TIPC_ADDR_ID 5-tuple: keep the
# *service* identity we already know and only annotate
# the observed port-id.
case (int() as atype, *rest) if atype == socket.TIPC_ADDR_ID:
...The TIPC_ADDR_ID case cannot reconstruct (stype, instance) — that info isn’t in a port id. So from_addr() alone is insufficient for the reconciliation path. Resolution: make from_addr() raise a clear ValueError for the bare TIPC_ADDR_ID case, and instead prevent the reconciliation from firing by having start_listener() return a listener whose getsockname() we never need — i.e. land this two-line upstream fix in _server.py:664:
if (
(unwrapped := lstnr.socket.getsockname()) != self.addr.unwrap()
and
self.addr.rebind_from_sockname # ClassVar[bool] = True on tcp/uds
):with TIPCAddress.rebind_from_sockname: ClassVar[bool] = False (and True on TCPAddress/UDSAddress, preserving today’s behaviour exactly). Rationale: the reconciliation exists only to learn the kernel-assigned port for port=0 TCP binds (its own comment says so, _server.py:662); TIPC has no such late-binding, so opting out is semantically right rather than a hack. Land this as its own commit, ahead of the backend, with a test that tcp’s port=0 behaviour is unchanged.
Keep the observed port-id available anyway: annotate ep.addr = ep.addr.with_port_id(*getsockname()[1:3]) (a pure msgspec.structs.replace() helper) purely for logging/repr.
3.3 MsgpackTIPCStream
class MsgpackTIPCStream(MsgpackTransport):
address_type = TIPCAddress
layer_key: int = 4
@property
def maddr(self) -> Multiaddr|str:
return mk_maddr(self.raddr)
def connected(self) -> bool:
return self.stream.socket.fileno() != -1
@classmethod
async def connect_to(
cls,
destaddr: TIPCAddress,
prefix_size: int = 4,
codec: MsgCodec|None = None,
**kwargs,
) -> MsgpackTIPCStream:
sock = trio.socket.socket(AF_TIPC, SOCK_STREAM)
with close_on_error(sock):
# NOTE: connect by *name* -> kernel does the lookup,
# so this is our "discovery" call.
await sock.connect((
socket.TIPC_ADDR_NAME,
destaddr._stype,
destaddr._instance,
0, # domain: 0 == "anywhere in scope"
destaddr._scope,
))
return cls(
trio.SocketStream(sock),
prefix_size=prefix_size,
codec=codec,
)- reuse
trio._highlevel_open_unix_stream.close_on_error(the UDS backend already imports it) or inline the equivalenttry/except: sock.close(); raise. SO_/TIPC_opts worth setting and documenting:setsockopt(SOL_TIPC, TIPC_IMPORTANCE, TIPC_HIGH_IMPORTANCE)for the parent<->child lifetime channel — this is a real win TIPC gives us that TCP can’t: the runtime’s supervision channel can outrank bulk app traffic under congestion. Wire it as aconnect_to(..., importance=...)kwarg defaulted from a module constant, and have_runtime.py’s parent-chan path pass the high value in a follow-up (don’t couple it to this PR).TIPC_CONN_TIMEOUT— the kernel-side connect timeout; leave at default, we havetriocancel scopes.TIPC_DEST_DROPPABLE = 0on the connection so undeliverable msgs come back as errors rather than being silently dropped.
connect_to()on a name with no publisher: TIPC returnsECONNREFUSED/EHOSTUNREACHpromptly (no SYN-timeout wait), which is better discovery-ping behaviour than TCP. Confirm the errno and make sure it surfaces asConnectionError(contract §4 — the registrar ping path depends on it).
3.4 get_stream_addrs()
@classmethod
def get_stream_addrs(cls, stream) -> tuple[TIPCAddress, TIPCAddress]:
sock = stream.socket
# both return TIPC_ADDR_ID 5-tuples for a connected sock
l_id = sock.getsockname()
r_id = sock.getpeername()
...Problem: neither end’s port-id tells us the service name. The laddr/raddr are used for logging, Channel.raddr, Server._peers keying-adjacent repr, and maddr. Design:
- the connecting side knows the destaddr it dialled →
connect_to()overrides_raddrafter construction with the known-goodTIPCAddress, exactly asMsgpackUDSStream.connect_to()does for the peer-pid case (_uds.py:539-543). - the accepting side does not know the peer’s service name from the socket. Two honest options:
- (a) accept it:
raddrcarries only(node, ref)viamaybe_node/maybe_ref,_stype/_instanceset to a sentinel-1, and__repr__rendersTIPCAddress[<peer-node:0x...>:<ref>]. TheAidfrom the handshake already gives us the peer’s logical identity, so nothing in the runtime actually needs the peer’s service name. Recommended. - piggyback the peer’s own bound name in the handshake. Rejected for this PR: touches
Aid/msg-spec.
- piggyback the peer’s own bound name in the handshake. Rejected for this PR: touches
- (a) accept it:
laddron the accepting side: theEndpointknows its ownaddr; butget_stream_addrs()is a@classmethodwith only the stream. UseTIPC_ADDR_IDforladdrtoo and letEndpoint.peer_tptskeying (which is by peer addr) still work. Verify nothing assertsladdr == ep.addr— grep for.laddruses before committing (_server.py’scon_statuslogging,Channel.pformat()).
4. Multiaddr representation
There is no /tipc in the multiaddr protocol table. Interim grammar, mirroring how uds maps to the spec-legal /unix:
/tipc/<stype>/<instance> # scope implied = cluster
/tipc/<stype>/<instance>/<scope> # explicit
_tpt_proto_to_maddr['tipc'] = 'tipc'and amk_maddr()case 'tipc':building the above.parse_maddr()getscase ['tipc']:— but notepy-multiaddrwill reject an unregistered protocol name outright, so this requires an upstream registration (same track as thewgwork, gh #483 / multiformats/py-multiaddr#107). Until that lands:MsgpackTIPCStream.maddrreturns thestrform (theMsgTransport.maddrreturn type is alreadyMultiaddr|str, andMsgpackUDSStream.maddralready exercises thestrbranch), andparse_maddr()special-cases the/tipc/prefix before handing the string toMultiaddr(). Document this as the reason gh #443’s “standardize on returningMultiaddreverywhere” item stays blocked.
Propose /tipc/ upstream as: name tipc, code TBD, size variable, value <stype>:<instance>:<scope> — or as three composed protos. Prefer one proto with a structured value so the maddr stays 2-segment like /unix/....
5. Discovery: the actually-interesting part
Two independently-shippable layers. Layer A is in scope for the first PR; layer B is a fast-follow.
5.1 Layer A — “discovery by bind” (free)
Because bind(TIPC_ADDR_NAMESEQ) publishes and connect(TIPC_ADDR_NAME) resolves, a tractor tree whose registry_addrs are TIPC service names needs no registrar liveness at all for the connect path: find_actor()’s “connect to the registrar and ask” becomes “connect to the service name directly”. Concretely:
tractor.discovery._api.find_actor()etc. keep working unchanged (they go through the registrar), and- a new, TIPC-only fast path becomes possible: derive an actor’s service name from its
(name, uuid)and dial it without any registrar hop.
Do not build the fast path in PR 1. Instead, prove the property with a test (§7.4) and file the follow-up: it changes discovery semantics (name→instance derivation must be a documented, stable, cross-language-able hash) and deserves its own design.
5.2 Layer B — the topology service (TIPC_TOP_SRV)
This is what makes #378’s “end game cluster proto” claim real: a subscription to name-table events, i.e. push-based register/deregister for free, replacing the registrar’s polled find_actor().
Mechanics (verify each field against linux/include/uapi/linux/tipc.h + net/tipc/topsrv.c at implementation time — the struct layout below is from the uapi header and the byte-order caveat is real):
# SOCK_SEQPACKET connected to the topology server
sock = trio.socket.socket(AF_TIPC, SOCK_SEQPACKET)
await sock.connect((
socket.TIPC_ADDR_NAME,
socket.TIPC_TOP_SRV, # == 1
socket.TIPC_TOP_SRV,
0,
))
# struct tipc_subscr {
# struct tipc_name_seq seq; /* 3 * __u32: type, lower, upper */
# __u32 timeout; /* TIPC_WAIT_FOREVER == ~0 */
# __u32 filter; /* TIPC_SUB_{PORTS,SERVICE,CANCEL} */
# char usr_handle[8];
# } /* == 28 bytes */
_SUBSCR_FMT: str = '=IIIII8s' # ⚠ 5*I is 20 -> use '=5I8s'- byte order: the topology server historically accepts both host and swapped order and auto-detects; modern kernels are strict-ish. Pack native (
'=') first, and if the server closes the connection immediately, retry with'>'. Encode that as a one-time probe helper_detect_topsrv_endianness()cached at module level — and put a# ?TODOpointing atnet/tipc/topsrv.cfor someone to make it deterministic. - events:
struct tipc_eventisevent: u32,found_lower: u32,found_upper: u32,port: {ref: u32, node: u32}, then the 28-byte subscription echo → 40 bytes.event ∈ {TIPC_PUBLISHED, TIPC_WITHDRAWN, TIPC_SUBSCR_TIMEOUT}. - trio shape — this is where the “nearly-functional, modern-async” style pays off; expose it as an
@acmyielding atrioreceive-channel of typed events, not a class:
@acm
async def open_topology_events(
stype: int = TRACTOR_STYPE,
lower: int = 0,
upper: int = 0xFFFFFFFF,
filter: int = TIPC_SUB_SERVICE,
timeout: int = TIPC_WAIT_FOREVER,
buf_size: int = 64,
) -> AsyncGenerator[
trio.MemoryReceiveChannel[TIPCNameEvent],
None,
]:
...with TIPCNameEvent(msgspec.Struct, frozen=True) fields kind: Literal['published','withdrawn','timeout'], addr: TIPCAddress, node: int, ref: int. One trio.lowlevel-free implementation: a nursery-spawned reader task doing await sock.recv(40) in a loop and send_nowait()ing decoded events, with the @acm closing the socket on exit → reader gets ClosedResourceError → cancel scope collapses. Standard tractor @acm discipline. - consumer: tractor/discovery/_registry.py gains an optional “watch” mode so a registrar (or any actor) can keep a live view of the actor set without polling. Sketch the integration in the follow-up issue; do not wire it in PR 1. - SOCK_SEQPACKET is fine here because this socket never goes through MsgpackTransport — it’s a plain trio socket used with recv(). The contract’s “SOCK_STREAM only” constraint applies to MsgTransport streams, not to this.
6. Commit sequencing (each independently reviewable + green)
_server.py: addAddress.rebind_from_sockname: ClassVar[bool], gate thegetsockname()reconciliation on it,Truefor tcp/uds. Test: tcpport=0unchanged.tractor/ipc/_tipc.py:TIPCAddress+is_tipc_available()predicate +start_listener(). No transport yet. Tests: address round-trip (unwrap/from_addr/wrap_address),get_random()uniqueness, bind/listen +SO_ACCEPTCONNtolerance,EAFNOSUPPORT→ actionableConnectionError.MsgpackTIPCStream+connect_to()+get_stream_addrs(). Test: twotriotasks in one proc exchange a msg overMsgpackframing (notractorruntime).- registration tables (contract §2 items 1-6, 9) +
pyproject.tomlmark/extra. Test: full suite under--tpt-proto tipc(§7.3). - maddr support (
strform + prefix special-case) + docs. open_topology_events()@acm + its tests (layer B).- docs page +
docs/example.
Per project convention, a reproducing/guard test lands in its own commit before the fix it guards.
7. Testing
7.1 the capability predicate (in _tipc.py, public)
def is_tipc_available() -> bool:
'''
True iff this kernel can create an `AF_TIPC` socket, i.e.
the `tipc` module is loaded.
'''
try:
socket.socket(socket.AF_TIPC, socket.SOCK_STREAM).close()
return True
except OSError:
return FalseCache it in a module global (it can’t change without a modprobe, and a cold call costs a syscall). Pure predicate, no side effects, no logging.
7.2 gating
pytest.mark.tipcregistered inpyproject.toml.- module-level
pytestmark = pytest.mark.skipif(not is_tipc_available(), reason='tipckernel module not loaded (modprobe tipc)')intests/ipc/test_tipc.py. --tpt-proto tipcwith no module must fail loudly and early with the actionable message, not with 400 confusing timeouts. Add the check to thetpt_protosfixture’s existing per-proto validation loop (_testing/pytest.py:795): if the chosenAddresstype exposes anis_available()-style classmethod, call it andpytest.fail()with its reason. Generalize (don’t special-case tipc) — plans 02/03 need the same hook.
7.3 CI
- add a job matrix entry
--tpt-proto tipcthat runssudo modprobe tipcin abeforestep. GH’subuntu-latestrunners do allowmodprobe tipc(the module ships with the standard Ubuntu kernel package); verify in a throwaway workflow before wiring the matrix. If it turns out to be unavailable, fall back to a container job with--privileged/--cap-add NET_ADMIN, and mark the jobcontinue-on-erroruntil it’s proven stable. - cross-node TIPC (bearer) cannot be CI’d; cover it with a documented manual smoke test in the docs page, in the style of gh #482’s LAN examples.
7.4 backend-specific tests worth writing
- name-publication is discovery: bind a listener on
(stype, inst), then from a second taskconnect()by name and assert it lands — without anytractorregistrar. get_random()collision resistance: 10kget_random()calls with no live runtime → 10k distinct_instances. (This is the silent-crosstalk risk from §2.3; if the 4-byte digest ever collides in this test, escalate to §9.)- round-robin surprise: two listeners bound to the same
(stype, inst)both succeed (TIPC allows it) and connects distribute. Assert the observed behaviour and reference it from theget_random()docstring so the next reader knows why the hash matters. - scope isolation: a
TIPC_NODE_SCOPEbind is not visible to a cluster-scope lookup from another node (manual/marked). - importance opt round-trips via
getsockopt. - graceful + abrupt close produce
TransportClosedwith the sameloglevelclassification as tcp/uds — i.e. re-run the relevanttests/ipc/test_each_tpt.pycases parametrized over the new proto rather than writing new ones.
8. Deployment / docs deliverable
A docs/ page (and/or an examples/ script) covering:
# single host, node-scope only
sudo modprobe tipc
tipc node get addr
# multi-host over ethernet (pairs beautifully with plan 03's wg)
sudo tipc bearer enable media eth device eth0
# ...or over UDP when L2 isn't available:
sudo tipc bearer enable media udp name uc localip 10.0.11.1
tipc link list
tipc nametable show # <- see tractor's published services!tipc nametable show displaying live tractor actors is the single best demo this backend has; lead with it.
9. Known risks + escalations
| risk | mitigation |
|---|---|
_instance hash collision → silent crosstalk (two actors share a service name, TIPC round-robins connects between them) |
§7.4 test; if it bites, add a post-bind verification handshake, or bump to a 6-byte digest folded into (stype_low, instance) |
| kernel/module unavailability everywhere (dev boxes, macOS, CI) | hard gating (§7.2); TIPC is explicitly an opt-in cluster transport, never a default |
getsockname() returns port-id not name |
the rebind_from_sockname opt-out (§3.2), landed first |
unregistered /tipc multiaddr proto |
str maddr fallback (§4) + upstream track gh #483 |
| stale docs (#378 notes tipc.io docs may be out of date) | treat include/uapi/linux/tipc.h + net/tipc/ as the only normative source; cite file+symbol in code comments |
SOCK_SEQPACKET topology framing byte-order |
probe helper + ?TODO (§5.2) |
10. Follow-up issue seeds
- register
/tipcin the multiaddr spec, mirroring thewgtrack (multiformats/py-multiaddr#107/#108 + gh #483). Same shape of work: propose the proto + code, land a codec inpy-multiaddr, then drop ourstr-maddr fallback (§4). Worth filing alongside thewgspec-submission issue so both proposals go up together rather than as one-offs. - registrar-less discovery fast path via name derivation (§5.1)
TIPC_TOP_SRV-driven push registry indiscovery/_registry.py(§5.2)TIPC_IMPORTANCEfor the parent<->child lifetime channel (§3.3) — genuinely novel supervision QoS, no other backend can do it- TIPC multicast / group messaging as a broadcast transport for
tractor.trionicsfan-out (explicitly notMsgTransport) - dual-link resiliency / multi-homing (#378’s “hybrid dual link”) once bearers are scripted in the docs