214 lines
7.0 KiB
Markdown
214 lines
7.0 KiB
Markdown
|
|
# `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`
|