Normalize registrar addrs with `wrap_address()`

Canonicalize legacy socket pairs and tagged tuples before storing
or comparing `Registry.addrs`, keeping existing config readable
when tractor emits protocol-tagged forms.

Also,
- broaden registry/runtime addr annotations for both wire shapes
- compare service fixture addrs through tractor's address codec
- use tractor's shared `--ll` pytest option instead of
  registering it twice
- cover mixed legacy/tagged registry state and `pikerd` startup

Prompt-IO: ai/prompt-io/opencode/20260820T163519Z_5f631e19_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
wkt/tractor_tagged_addrs_compat
Gud Boi 2026-08-20 22:44:52 -04:00
parent 5f631e1996
commit b7aca4c0dc
6 changed files with 185 additions and 30 deletions

View File

@ -0,0 +1,43 @@
---
model: gpt-5.6-sol
service: opencode
session: tractor-addr-unpacking
timestamp: 2026-08-20T16:35:19Z
git_ref: 5f631e19
scope: code
substantive: true
raw_file: 20260820T163519Z_5f631e19_prompt_io.raw.md
---
## Prompt
The human asked to implement the Piker compatibility work caused by
tractor's canonical tagged-address migration, run the previously
deferred tractor verification, and keep runtime-managed WireGuard
bindspaces in a later separate PR.
## Response summary
Made Piker normalize legacy and tagged registry inputs before semantic
comparison and process-global storage. Updated runtime address typing and
test assertions for canonical output, retained legacy caller input, and
removed a duplicate pytest log-level option that blocked downstream test
collection with current tractor.
## Files changed
- `piker/service/_registry.py` - canonical registry normalization and
shared address typing.
- `piker/service/_actor_runtime.py` - tagged-compatible runtime API
annotations.
- `tests/conftest.py` - canonical fixture comparison and shared pytest
option ownership.
- `tests/test_services.py` - registry regression and canonical runtime
assertions.
## Human edits
The human explicitly requested the downstream implementation and chose
to keep WireGuard bindspace lifecycle work in a separate future tractor
PR. The agent implemented and tested those directions; no direct manual
source edits were observed.

View File

@ -0,0 +1,25 @@
---
model: gpt-5.6-sol
service: opencode
timestamp: 2026-08-20T16:35:19Z
git_ref: 5f631e19
diff_cmd: git diff HEAD~1..HEAD
---
# Raw output - tractor tagged-address compatibility
The human requested downstream Piker qualification after tractor PR
#505 switched transport address emission to canonical tagged tuples.
> `git diff HEAD~1..HEAD -- piker/service/_registry.py piker/service/_actor_runtime.py tests/conftest.py tests/test_services.py`
Normalized legacy and tagged registry addresses through tractor's
address codec before Piker stores or compares them. Extended service
runtime annotations to cover both tuple shapes, updated runtime tests to
compare canonical serialized values, and removed Piker's duplicate
registration of tractor's shared `--ll` pytest option.
Ruff passed. The focused normalization regression and Piker runtime boot
passed against the local tractor checkout at `5d92595f`. The wider
service module reached an unrelated pre-existing logger-name assertion
in `maybe_spawn_daemon()` after both address-focused tests passed.

View File

@ -44,6 +44,7 @@ from ._registry import ( # noqa
_tractor_kwargs,
_default_reg_addr,
open_registry,
RegistryAddress,
)
@ -58,7 +59,7 @@ def get_runtime_vars() -> dict[str, Any]:
@acm
async def open_piker_runtime(
name: str,
registry_addrs: list[tuple[str, int]] = [],
registry_addrs: list[RegistryAddress] = [],
tpt_bind_addrs: list|None = None,
enable_modules: list[str] = [],
@ -77,7 +78,7 @@ async def open_piker_runtime(
) -> tuple[
tractor.Actor,
list[tuple[str, int]],
list[RegistryAddress],
]:
'''
Start a piker actor who's runtime will automatically sync with
@ -168,7 +169,7 @@ _root_modules: list[str] = [
@acm
async def open_pikerd(
registry_addrs: list[tuple[str, int]],
registry_addrs: list[RegistryAddress],
tpt_bind_addrs: list|None = None,
loglevel: str|None = None,
@ -272,7 +273,7 @@ async def open_pikerd(
@acm
async def maybe_open_pikerd(
registry_addrs: list[tuple[str, int]] | None = None,
registry_addrs: list[RegistryAddress] | None = None,
loglevel: str | None = None,
**kwargs,
@ -306,7 +307,7 @@ async def maybe_open_pikerd(
# async with open_portal(chan) as arb_portal:
# yield arb_portal
registry_addrs: list[tuple[str, int]] = (
registry_addrs: list[RegistryAddress] = (
registry_addrs
or
[_default_reg_addr]

View File

@ -24,6 +24,7 @@ from contextlib import (
)
from typing import (
Any,
TypeAlias,
)
import tractor
@ -32,11 +33,23 @@ from tractor import (
Actor,
Portal,
)
from tractor.discovery._addr import (
LegacyUnwrappedAddress,
UnwrappedAddress,
wrap_address,
)
from piker.log import get_logger
log = get_logger(name=__name__)
# Piker config still accepts legacy socket pairs while tractor now
# emits tagged `UnwrappedAddress` values.
RegistryAddress: TypeAlias = (
LegacyUnwrappedAddress
|UnwrappedAddress
)
# TODO? default path-space for UDS registry?
# [ ] needs to be Xplatform tho!
# _default_registry_path: Path = (
@ -46,10 +59,7 @@ log = get_logger(name=__name__)
_default_registry_host: str = '127.0.0.1'
_default_registry_port: int = 6116
_default_reg_addr: tuple[
str,
int, # |str TODO, once we support UDS, see above.
] = (
_default_reg_addr: RegistryAddress = (
_default_registry_host,
_default_registry_port,
)
@ -63,24 +73,37 @@ _registry: Registry | None = None
class Registry:
# TODO: should this be a set or should we complain
# on duplicates?
addrs: list[tuple[str, int]] = []
addrs: list[RegistryAddress] = []
# TODO: table of uids to sockaddrs
peers: dict[
tuple[str, str],
tuple[str, int],
RegistryAddress,
] = {}
_tractor_kwargs: dict[str, Any] = {}
def _normalize_addresses(
addrs: list[RegistryAddress],
) -> list[RegistryAddress]:
'''
Normalize legacy and tagged addresses through tractor's codec.
'''
return [
wrap_address(addr).unwrap()
for addr in addrs
]
@acm
async def open_registry(
addrs: list[tuple[str, int]],
addrs: list[RegistryAddress],
ensure_exists: bool = True,
) -> list[tuple[str, int]]:
) -> list[RegistryAddress]:
'''
Open the service-actor-discovery registry by returning a set of
tranport socket-addrs to registrar actors which may be
@ -92,9 +115,12 @@ async def open_registry(
actor: Actor = tractor.current_actor()
aid: msg.Aid = actor.aid
uid: tuple[str, str] = aid.uid
preset_reg_addrs: list[
tuple[str, int]
] = Registry.addrs
addrs = _normalize_addresses(addrs)
preset_reg_addrs: list[RegistryAddress] = (
_normalize_addresses(Registry.addrs)
)
if preset_reg_addrs:
Registry.addrs = preset_reg_addrs
if (
preset_reg_addrs
and
@ -102,9 +128,10 @@ async def open_registry(
):
if preset_reg_addrs != addrs:
# if any(addr in preset_reg_addrs for addr in addrs):
diff: set[
tuple[str, int]
] = set(preset_reg_addrs) - set(addrs)
diff: set[RegistryAddress] = (
set(preset_reg_addrs)
-set(addrs)
)
if diff:
log.warning(
f'`{uid}` requested only subset of registrars: {addrs}\n'
@ -123,7 +150,9 @@ async def open_registry(
and
not Registry.addrs
):
Registry.addrs.extend(actor.reg_addrs)
Registry.addrs.extend(
_normalize_addresses(actor.reg_addrs)
)
if (
ensure_exists
@ -139,13 +168,13 @@ async def open_registry(
Registry.addrs = (
addrs
or
[_default_reg_addr]
_normalize_addresses([_default_reg_addr])
)
# NOTE: only spot this seems currently used is inside
# `.ui._exec` which is the (eventual qtloops) bootstrapping
# with guest mode.
reg_addrs: list[tuple[str, str|int]] = Registry.addrs
reg_addrs: list[RegistryAddress] = Registry.addrs
# !TODO, a struct-API to stringently allow this only in special
# cases?
# -> better would be to have some way to (atomically) rewrite
@ -159,13 +188,13 @@ async def open_registry(
# next (set of) calls will apply whatever new one is passed
# in.
if was_set:
Registry.addrs = None
Registry.addrs = []
@acm
async def find_service(
service_name: str,
registry_addrs: list[tuple[str, int]] | None = None,
registry_addrs: list[RegistryAddress] | None = None,
first_only: bool = True,
@ -175,7 +204,7 @@ async def find_service(
| None
):
# try:
reg_addrs: list[tuple[str, int|str]]
reg_addrs: list[RegistryAddress]
async with open_registry(
addrs=(
registry_addrs
@ -219,7 +248,7 @@ async def find_service(
async def check_for_service(
service_name: str,
) -> None|tuple[str, int]:
) -> None|RegistryAddress:
'''
Service daemon "liveness" predicate.

View File

@ -27,8 +27,6 @@ pytest_plugins: tuple[str] = (
def pytest_addoption(parser):
parser.addoption("--ll", action="store", dest='loglevel',
default=None, help="logging level to set when testing")
parser.addoption("--confdir", default=None,
help="Use a practice API account")
@ -286,7 +284,12 @@ async def _open_test_pikerd(
) as portal:
raddr = portal.chan.raddr
uw_raddr: tuple = raddr.unwrap()
assert uw_raddr == reg_addr
canonical_addr: tuple = (
tractor.discovery._addr.wrap_address(
reg_addr,
).unwrap()
)
assert uw_raddr == canonical_addr
yield (
raddr._host,
raddr._port,

View File

@ -7,6 +7,7 @@ from typing import (
Callable,
)
from contextlib import asynccontextmanager as acm
from types import SimpleNamespace
from exceptiongroup import BaseExceptionGroup
import pytest
@ -17,6 +18,7 @@ from piker.service import (
find_service,
Services,
)
from piker.service import _registry
from piker.data import (
open_feed,
)
@ -57,7 +59,12 @@ def test_runtime_boot(
) as portal,
):
uw_raddr: tuple = pikerd_portal.chan.raddr.unwrap()
assert uw_raddr == daemon_addr
canonical_addr: tuple = (
_registry._normalize_addresses(
[daemon_addr],
)[0]
)
assert uw_raddr == canonical_addr
assert uw_raddr == portal.chan.raddr.unwrap()
# no service tasks should be started
@ -66,6 +73,53 @@ def test_runtime_boot(
trio.run(main)
def test_registry_normalizes_tagged_addresses(
monkeypatch: pytest.MonkeyPatch,
):
'''
Tractor now emits tagged registry addresses while existing Piker
callers still pass legacy socket pairs. Direct list and set
comparisons treated the two forms as different and retained the
legacy value in `Registry.addrs`.
Seed `Registry.addrs` with a legacy pair, request the equivalent
tagged address, and enter `open_registry()` as a root actor. The
yielded and stored values must both use tractor's canonical form.
'''
legacy = ('127.0.0.1', 6116)
tagged = ('tcp', '127.0.0.1', 6116)
actor = SimpleNamespace(
aid=SimpleNamespace(uid=('test', 'uid')),
)
monkeypatch.setattr(
tractor,
'current_actor',
lambda: actor,
)
monkeypatch.setattr(
tractor,
'is_root_process',
lambda: True,
)
_registry.Registry.addrs = [legacy]
async def main():
async with _registry.open_registry(
[tagged],
) as addrs:
assert addrs == [tagged]
assert _registry.Registry.addrs == [tagged]
assert _registry._tractor_kwargs[
'registry_addrs'
] == [tagged]
try:
trio.run(main)
finally:
_registry.Registry.addrs = []
def test_datad_spawn(
open_test_pikerd: AsyncContextManager,
loglevel: str,