11 KiB
TIPC backend — handoff
Status: PR #493 is feature-complete and green; what remains is landing logistics plus a named follow-up track.
Audience: any agent or human picking this up cold, from any provider. Nothing here assumes a particular harness or tooling.
Read in this order,
- this file (orientation + what’s already settled)
00_shared_backend_contract.md— normative description of what atractortransport backend is01_tipc_backend.md— the plan, already reconciled against the live-kernel findings
Do not re-derive the design or re-select libraries. Where this doc and the code disagree, the code wins — fix the doc in the same change (contract §0).
1. Environment
The backend needs a linux kernel module that is not loaded by default:
sudo modprobe tipc
tipc node get address # confirms the module is liveEverything else is stdlib — TIPC adds zero dependencies.
This repo is a git worktree with a uv-managed venv at ./py313. Run things through it:
./py313/bin/python -m pytest tests/ipc -q
./py313/bin/python -m pytest tests/ -q --tpt-proto tipcTo rebuild docs you need the docs dep-group, which mutates that venv — uv sync afterwards to restore it:
UV_PROJECT_ENVIRONMENT=py313 uv run --group docs \
python -m sphinx -b html docs /tmp/docbuild
UV_PROJECT_ENVIRONMENT=py313 uv syncWithout the module, --tpt-proto tipc fails loudly and immediately (by design); tests/ipc/test_tipc.py’s kernel-touching cases self-skip.
2. What the backend is, in three sentences
An actor’s TIPC address is a service name (stype, instance) — no host, no port. Binding the singleton TIPC_ADDR_NAMESEQ range publishes it into a kernel-maintained cluster-wide name table (visible via tipc nametable show), and a peer’s .connect()-by-name is the discovery lookup, resolved in-kernel. TIPC_ADDR_ID port-ids are only ever observed, never user-facing.
Everything lives in tractor/ipc/_tipc.py.
3. Hard-won facts — do not re-litigate these
All verified against a live kernel. Several contradict what the plan originally assumed.
| fact | why it matters |
|---|---|
| A duplicate name bind succeeds, and connects round-robin between publishers | An instance collision is silent crosstalk, never EADDRINUSE. Hence the blake2b instance digest. |
Dialing an unpublished name gives EHOSTUNREACH instantly, as a bare OSError — not a ConnectionError subtype |
_reraise_as_connerr() is REQUIRED by contract §4, not polish |
getsockname() always answers a TIPC_ADDR_ID port-id, even pre-bind |
why TIPCAddress.rebind_from_sockname = False |
A connect-then-drop peer makes getpeername() raise ENOTCONN |
Unguarded, this kills the whole actor — .get_stream_addrs() runs before the handshake, so it escapes handshake tolerance. See _maybe_sockaddr(). |
SO_ACCEPTCONN works fine (answers 1) |
trio’s except OSError carve-out is not load-bearing here |
Graceful peer close arrives as BrokenResourceError/ECONNRESET, not a clean 0-byte EOF |
benign; _iter_packets() already classifies it as a normal disconnect |
Topology struct tipc_event is 48 bytes (4+4+4+8+28) |
the plan said 40 |
The topology server accepts native '=' byte-order |
the plan’s proposed '>'-retry endianness probe was deleted as unnecessary |
TIPC_WAIT_FOREVER is -1 in python |
must be masked (& 0xFFFFFFFF) before packing as unsigned |
TIPC has AES-GCM encryption (tipc node set key, linux 5.9+) |
cluster/master/per-node keys + rekeying. Symmetric pre-shared, which is why a wg mesh is still preferred — see §6. |
A wg interface is L3/tun (POINTOPOINT,NOARP, link/none) |
TIPC’s eth media cannot bind it; the udp bearer is mandatory over wg |
Two design decisions that are closed, with reasons:
- The accepting side does not learn the peer’s service name. A port-id can’t be reversed into one. It gets a
TIPC_NAME_UNKNOWNsentinel plus the observed(node, ref), and that’s fine — theAidfrom the handshake already carries the peer’s logical identity. (udshas the same wart.) - Do not fold digest bits into the service
_stypeto widen the collision space. A topology subscription can only watch onestype, so varying it per-actor would need 65536 subscriptions and kills the push-registry work outright. If crosstalk ever bites, the escalation is a post-bind verification handshake (#501).
4. What landed
15 commits, 22ef362d..2d082373, on wkt/tipc_backend_378, based on ng_tpts_planning (PR #492, docs-only).
TIPCAddress+is_tipc_available()+start_listener()MsgpackTIPCStream(connect_to(),get_stream_addrs())open_topology_events()— theTIPC_TOP_SRVpush feed- registration across every table in contract §2
- interim
str-only/tipc/…maddr grammar - a
--tpt-proto=tipcCI leg, non-blocking for now docs/guide/tipc.rst+examples/multihost/tipc_cluster/
Two fixes fell out that are not TIPC-specific:
devx/pformat.py—pformat_caller_frame()passed anindent=''kwargpformat_boxed_tb()never accepted, so every send-sideMsgTypeErrordied with aTypeErrorwhile formatting itself. Onmainand every branch since888af602. Wants cherry-picking out of this stack.SpawnSpec.reg_addrs/.bind_addrspinned the wire shape to a 2-tuple. Widened toUnwrappedAddress, which had to become variadic (tuple[str|int, ...]) becausemsgspecrefuses a union holding more than one array-like type.
Acceptance bar met: 122 passed / 1 xfailed / 2 xpassed under --tpt-proto tipc across ipc, discovery, runtime, spawning, local, rpc, cancellation. tcp/uds unchanged.
5. Immediate next steps (pre-land)
These live on #493’s body as ### TODOs before landing. They are not mirrored into an issue — if the PR is ever superseded they need re-homing.
- Cherry-pick the
pformatfix ontomainas its own PR —22ef362d(red guard test) thenf9f98eeb(the 1-line fix). Unrelated to TIPC; every branch has the bug. - Watch the
tipcCI leg. It’s gatedcontinue-on-error: ${{ matrix.tpt_proto == 'tipc' }}because GH’s runners have never been asked tomodprobefor us. Once it has a few green runs, drop the gate. If the runners refuse themodprobe, fall back to a container job with--cap-add NET_ADMIN. - Rebase onto
mainonce #492 merges.
6. The follow-up track
All filed with the follow-up label.
| issue | what |
|---|---|
| #495 | TIPC_IMPORTANCE supervision QoS on the parent↔︎child chan |
| #496 | TIPC_TOP_SRV push registry in discovery._registry |
| #497 | dual-link resiliency / multi-homing |
| #498 | /tipc multiaddr spec submission |
| #499 | registrar-less discovery via name derivation |
| #500 | multicast/group msging as a broadcast transport |
| #501 | post-bind verification for instance collisions |
| #502 | TIPC over a wg mesh — the reference multihost deployment |
the wg direction
#502 is the strategic one. The intent is that TIPC-over-wg becomes our go-to multihost transport deployment, with composed addresses of the form:
/ip4/<pub>/udp/51820/wg/u<key>/tipc/<stype>/<inst>/<scope>
\____ wg bearer ________/\_key_/\______ tractor ep ________/
Note the structural point, which matters for the spec proposal in #498: the tcp equivalent repeats an /ip4/…/tcp/… inner segment because a tcp endpoint is located. The tipc inner segment has no locative component at all — a service name is location-independent by design. So in the composed form the wg segments carry all the routing and the tipc segment carries pure identity.
Prerequisites already established:
- py-multiaddr #108 (merged) proved the composed
wg+ tcp form parses and round-trips (['ip4','udp','wg','ip4','tcp']) examples/multihost/wg_lan/is the existing wg example set to generalize from rather than duplicate- the udp-bearer-only constraint and MTU caveat are documented in both
docs/guide/tipc.rstand thetipc_clusterREADME
7. Working conventions in this repo
Provider-neutral, but they are enforced by review:
- Never commit, push, rebase or amend on your own. Prepare changes, report them, and let the maintainer stage. Asking “should we commit?” is a question for you to answer, not permission to act.
- A failing/guard test lands in its own commit before the fix it guards. Red first, then green.
- One commit per logical step, so history shows why. Never squash unrelated changes.
- Commit subjects: present-tense verb, ~50 chars (hard max 67), backticks around every code element. Bodies wrap at 67 cols.
- Never write a line containing only whitespace.
- Annotate everything including locals; prefer
match/caseoverisinstancechains; multi-line calls with trailing commas;@acmover classes with.start()/.stop(). - New modules get the
# tractor: distributed structured concurrency.header tagline plus the AGPL block. - Do not change task/checkbox state in issues, plans or trackers unless explicitly asked for that exact transition.
- Fix warnings at source; only genuinely-unfixable ones get filtered, with a documented reason.
8. Where things are
tractor/ipc/_tipc.py the whole backend
tests/ipc/test_tipc.py 28 backend unit tests
tests/ipc/test_server.py the reconciliation guard
tests/devx/test_pformat.py the cherry-pick candidate
docs/guide/tipc.rst the docs page
examples/multihost/tipc_cluster/ runnable demos + manual
smoke test
ai/tpt-backends/00_shared_backend_contract.md
ai/tpt-backends/01_tipc_backend.md the (reconciled) plan
.github/workflows/ci.yml the gated tipc leg
Both single-host examples have been run against a live kernel — the output pasted in their README is real, not illustrative.