tractor/examples/multihost/tipc_cluster/README.md

214 lines
7.0 KiB
Markdown
Raw Normal View History

# `tractor` over `AF_TIPC`, where the address *is* the service name
TIPC is a linux-kernel cluster IPC protocol whose service names
live in a **cluster-wide name table maintained by the kernel**.
For `tractor` that means:
- an actor's IPC address is a service name `(stype, instance)`,
not a host/port,
- `.bind()`ing it **is** service registration,
- a peer's `.connect()`-by-name **is** the lookup.
So the discovery machinery `tractor.discovery` normally
implements with a registrar actor comes for free, in-kernel —
which is the ask in gh
[#378](https://github.com/goodboy/tractor/issues/378).
> **Why `examples/multihost/`?** `tests/test_docs_examples.py`
> walks `examples/` recursively and runs everything it collects
> as a subproc, asserting `rc == 0`. These need the `tipc`
> kernel module (and, for the two-host pair, a live bearer), so
> they can't satisfy that; `'multihost' not in p[0]` is already
> in the test's exclusion list, which is what keeps them out of
> CI. See "CI" below for the separate matrix-entry plan.
## the single best demo
```bash
sudo modprobe tipc
python single_host.py
```
Four actors boot, four service names appear in the kernel's
table, and all four are withdrawn on teardown — observed with
`tipc(8)`, entirely outside `tractor`:
```
--- `tipc nametable show` :: root + 3 subactors ---
Type Lower Upper Scope Port
1953628160 1616 1616 cluster 3161982128
1953628160 1219427151 1219427151 cluster 1587358717
1953628160 2641339936 2641339936 cluster 1864021571
1953628160 3344505866 3344505866 cluster 3816483388
--- `tipc nametable show` :: after teardown (all withdrawn) ---
Type Lower Upper Scope Port
```
`1953628160` is `0x74720000``tractor`'s reserved service
type, ascii `tr` in the high half. `1616` is the host-singleton
registrar, the same idiom as the TCP port and the
`registry@1616.sock` UDS filename. The other three instances are
per-actor digests (see "silent crosstalk" below).
## push-based discovery
```bash
python watch_nametable.py
```
Subscribes to the kernel's *topology service* and prints name
table transitions as they happen — no polling, no registrar
round-trip:
```
watching the TIPC name table..
[+] published instance=1616 port=0x00000000:2375440573
spawning subactors..
[+] published instance=186947472 port=0x00000000:3960753074
[+] published instance=2191362136 port=0x00000000:2263898853
[+] published instance=3484369663 port=0x00000000:2126817956
tearing down..
[-] withdrawn instance=186947472 port=0x00000000:3960753074
...
```
This is the groundwork for a push registry in
`tractor.discovery._registry` (gh
[#184](https://github.com/goodboy/tractor/issues/184),
[#216](https://github.com/goodboy/tractor/issues/216)) — a
registrar that *never polls* `find_actor()`.
## two hosts
Everything above is single-node (`modprobe` is enough). To span
hosts you need a **bearer** on both, which is the one thing that
can't be CI'd.
```bash
# on BOTH hosts
sudo modprobe tipc
# over ethernet (L2) — simplest when the hosts share a segment
sudo tipc bearer enable media eth device eth0
# ..or over UDP when L2 isn't available (pairs nicely with the
# `wg` tunnel examples in ../wg_lan/)
sudo tipc bearer enable media udp name uc localip 10.0.11.1
# verify BEFORE running anything: this must list the peer
tipc link list
tipc node list
```
Then:
```bash
# host A
python host_a_srv.py
# host B
python host_b_client.py
```
Note what's absent from both scripts: any IP, hostname or port.
Both sides name the *same service*, and the kernel routes it.
Move `host_a_srv.py` to a third node and host B's dial keeps
working, unchanged.
### scope
`TIPCAddress._scope` is the backend's `.bindspace` — literally
"the set of hosts this published name is reachable from":
| scope | meaning |
| --- | --- |
| `TIPC_NODE_SCOPE` | same host only — the UDS analogue |
| `TIPC_CLUSTER_SCOPE` | cluster-visible (the default) |
`TIPC_ZONE_SCOPE` is deprecated and aliased to cluster by modern
kernels; `tractor` accepts it on input and folds it, logging at
`transport` level.
## gotchas worth knowing before you deploy
**Silent crosstalk.** Unlike every other backend, a duplicate
bind does **not** raise `EADDRINUSE` — TIPC happily accepts
multiple publishers of one name and *round-robins* connects
between them (verified: 6 dials alternated `b,a,b,a,b,a`). So an
instance collision is silent traffic-splitting, not an error.
That's why `TIPCAddress.get_random()` derives the instance from
a `blake2b` digest of the actor identity rather than a counter.
Two `tractor` trees sharing both a cluster **and** an `_stype`
share a name space; partition them by passing a distinct
`_stype`.
**Graceful close looks like a reset.** A peer closing cleanly
surfaces as `BrokenResourceError`/`ECONNRESET` rather than the
clean 0-byte EOF you get from TCP/UDS. It's benign — the
transport layer already classifies it as a normal disconnect —
but it does look alarming in `transport`-level logs.
**Dialing an unpublished name** answers `EHOSTUNREACH`
*instantly* (no SYN-timeout wait), which is much better
discovery-ping behaviour than TCP. `tractor` normalizes it to
`ConnectionError`.
**It's opt-in, never a default.** The module isn't loaded on
most boxes and doesn't exist off-linux, so
`enable_transports=['tipc']` is always explicit. Check
`tractor.ipc._tipc.is_tipc_available()` before assuming.
## maddr form
There is no registered `/tipc` protocol in the multiaddr table
yet (upstream track: gh
[#483](https://github.com/goodboy/tractor/issues/483) +
multiformats/py-multiaddr#107), so the grammar is interim and
`str`-only:
```
/tipc/<stype>/<instance>/<scope>
```
`parse_maddr()` special-cases this prefix *before* handing
anything to `Multiaddr()`, which would otherwise reject the
unregistered name outright. Registering it upstream is what
would unblock gh
[#443](https://github.com/goodboy/tractor/issues/443)'s
"return `Multiaddr` everywhere" item.
## running the suite over TIPC
The whole test suite runs under the backend:
```bash
sudo modprobe tipc
pytest --tpt-proto tipc
```
Without the module that fails loudly and immediately with an
actionable message rather than a few hundred connect timeouts.
Backend-specific unit tests live in `tests/ipc/test_tipc.py` and
self-skip when the module is absent.
## CI
Single-host TIPC *is* CI-able — the module ships with the
standard Ubuntu kernel package, so a `sudo modprobe tipc` step
plus a `--tpt-proto tipc` matrix entry should work. That's not
wired up yet; verify in a throwaway workflow first, and fall
back to a container job with `--cap-add NET_ADMIN` if the
runners refuse. Cross-node (bearer) testing stays manual — this
README is that smoke test.
## normative refs
The tipc.io docs are stale in places (gh #378 says as much).
Treat the kernel sources as the only normative reference:
- `include/uapi/linux/tipc.h` — address flavours, sockopts, the
topology `struct`s
- `net/tipc/socket.c`, `net/tipc/topsrv.c`
- `man 8 tipc`