Compare commits

...

3 Commits

Author SHA1 Message Date
Gud Boi bc132f0b4c Document canonical tagged `Address` forms
Separate legacy declaration inputs from canonical serialized outputs
in IPC and architecture docs, and state the same-version registrar
tree contract while no wire-format negotiation exists.

Add PR #505's feature news fragment for `TunnelledAddress` and tagged
TCP/Unix output.

Review: PR #505 (goodboy)
https://github.com/goodboy/tractor/pull/505#pullrequestreview-5094473850

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-09-02 17:07:38 -04:00
Gud Boi 8e428527ff Fix `wg_lan` server's tagged addr unpack
Unpack TCP's canonical proto tag before selecting the overlay host,
then pass a tagged bind declaration to the child actor. The example
otherwise raises before `echo_srv` starts.

Review: PR #505 (goodboy)
https://github.com/goodboy/tractor/pull/505#pullrequestreview-5094473850

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-09-02 16:57:52 -04:00
Gud Boi 9d1e476d04 Canonicalize addrs in `Registrar.register_actor()`
Store the validated `waddr.unwrap()` result so legacy and tagged
declarations share one registry identity. This lets stale-entry
eviction replace an older actor which used the compatibility form.

Cover TCP and UDS registrations crossing from legacy to tagged addrs.

Review: PR #505 (goodboy)
https://github.com/goodboy/tractor/pull/505#pullrequestreview-5094473850

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-09-02 16:57:43 -04:00
6 changed files with 82 additions and 12 deletions

View File

@ -14,10 +14,11 @@ transport may currently be enabled per actor.
:margin: :margin:
:alt: layered runtime stack from app code down to transports :alt: layered runtime stack from app code down to transports
Addresses are "unwrapped" tuples at the API edges: Address declarations still accept the legacy ``(host, port)``
``('host', port)`` for TCP, filesystem-path pairs for UDS. For TCP pair and ``(directory, filename)`` UDS pair. Wrapped addresses
the full layering story — transport protocols, the IPC server, emit canonical, protocol-tagged tuples: ``('tcp', host, port)``
address types and the msg loop — see and ``('unix', path)``. For the full layering story — transport
protocols, the IPC server, address types and the msg loop — see
:doc:`/explain/architecture`. :doc:`/explain/architecture`.
.. currentmodule:: tractor .. currentmodule:: tractor

View File

@ -104,17 +104,24 @@ msg-spec *is* the protocol, which is exactly what lets payloads
be type-limited per-context (see ``pld_spec`` in be type-limited per-context (see ``pld_spec`` in
:doc:`/guide/context`). :doc:`/guide/context`).
Addresses come in two spellings: Address declarations and serialized values have distinct spellings:
- *unwrapped*: the plain-tuple form you pass to user APIs — - *legacy declarations*: the plain tuples accepted from existing
``('127.0.0.1', 1616)`` for tcp, or a callers — ``('127.0.0.1', 1616)`` for tcp, or a
``(<filedir>, <filename>)`` path-pair for uds; ``(<filedir>, <filename>)`` path-pair for uds;
- *canonical serialized values*: protocol-tagged tuples emitted by
address objects — ``('tcp', '127.0.0.1', 1616)`` and
``('unix', <path>)``;
- *wrapped*: the internal ``TCPAddress``/``UDSAddress`` struct - *wrapped*: the internal ``TCPAddress``/``UDSAddress`` struct
types (plus libp2p-style multiaddr helpers over in types (plus libp2p-style multiaddr helpers over in
``tractor.discovery``). ``tractor.discovery``).
You only ever need the tuple form; the runtime wraps and The runtime accepts either declaration spelling, wraps it at the
unwraps at the boundaries. boundary and emits the canonical tagged form.
Actors sharing a registrar are expected to run the same Tractor
version; the runtime does not negotiate address formats between
versions.
TCP: the boring default TCP: the boring default
*********************** ***********************

View File

@ -58,10 +58,10 @@ async def main():
registry_addrs=[addr.overlay], registry_addrs=[addr.overlay],
enable_transports=[addr.overlay.proto_key], enable_transports=[addr.overlay.proto_key],
) as an: ) as an:
overlay_host, _ = addr.unwrap() _, overlay_host, _ = addr.unwrap()
await an.start_actor( await an.start_actor(
'echo_srv', 'echo_srv',
bind_addrs=[(overlay_host, 0)], bind_addrs=[('tcp', overlay_host, 0)],
enable_transports=[addr.overlay.proto_key], enable_transports=[addr.overlay.proto_key],
enable_modules=['host_a_srv'], enable_modules=['host_a_srv'],
) )

View File

@ -0,0 +1,3 @@
Add transparent ``TunnelledAddress`` declarations for WireGuard
multiaddrs and emit canonical protocol-tagged TCP and Unix transport
addresses. Legacy untagged address pairs remain accepted as inputs.

View File

@ -3,10 +3,13 @@ Canonical tagged-address decoding and legacy input compatibility.
''' '''
from pathlib import Path from pathlib import Path
from types import SimpleNamespace
import pytest import pytest
import trio
from tractor.discovery._addr import wrap_address from tractor.discovery._addr import wrap_address
from tractor.discovery._registry import Registrar
from tractor.ipc._tcp import TCPAddress from tractor.ipc._tcp import TCPAddress
from tractor.ipc._uds import UDSAddress from tractor.ipc._uds import UDSAddress
@ -98,3 +101,59 @@ def test_tcp_from_native_ipv6_sockname():
) )
assert addr.unwrap() == ('tcp', '::1', 1616) assert addr.unwrap() == ('tcp', '::1', 1616)
@pytest.mark.parametrize(
'legacy, canonical',
[
(
('127.0.0.1', 1616),
('tcp', '127.0.0.1', 1616),
),
(
('/tmp/tractor', 'registry.sock'),
('unix', '/tmp/tractor/registry.sock'),
),
],
)
def test_registrar_stores_canonical_addresses(
legacy: tuple,
canonical: tuple,
):
'''
Normalize registrar entries before stale-address eviction.
During the tagged-address migration an older actor can register
an untagged address before a newer actor reuses that endpoint with
its canonical tag. Store the first declaration canonically, then
register the tagged spelling under another uid. The old uid must
be evicted and the registry must retain exactly one canonical
address for the replacement actor.
'''
registrar = SimpleNamespace(
_registry={},
_waiters={},
)
old_uid = ('old', 'old-uid')
new_uid = ('new', 'new-uid')
async def register_both():
await Registrar.register_actor(
registrar,
old_uid,
legacy,
)
assert registrar._registry[old_uid] == [canonical]
await Registrar.register_actor(
registrar,
new_uid,
canonical,
)
trio.run(register_both)
assert registrar._registry == {
new_uid: [canonical],
}

View File

@ -184,7 +184,7 @@ class Registrar(Actor):
# should never be 0-dynamic-os-alloc # should never be 0-dynamic-os-alloc
await debug.pause() await debug.pause()
addr_tup: tuple = tuple(addr) addr_tup: tuple = waddr.unwrap()
# Evict stale entries: if a *different* uid claims # Evict stale entries: if a *different* uid claims
# this addr (e.g. after unclean shutdown or # this addr (e.g. after unclean shutdown or