Add `prefer_addr()` transport selection to `_api`
New locality-aware addr preference for multihomed actors: UDS > local TCP > remote TCP. Uses `ipaddress` + `socket.getaddrinfo()` to detect whether a `TCPAddress` is on the local host. Deats, - `_is_local_addr()` checks loopback or same-host IPs via interface enumeration - `prefer_addr()` classifies an addr list into three tiers and picks the latest entry from the highest-priority non-empty tier - `query_actor()` and `wait_for_actor()` now call `prefer_addr()` instead of grabbing `addrs[-1]` or a single pre-selected addr Also, - `Registrar.find_actor()` returns full `list[UnwrappedAddress]|None` so callers can apply transport preference Prompt-IO: ai/prompt-io/claude/20260414T163300Z_befedc49_prompt_io.md (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-codesubint_spawner_backend
parent
c3d6cc9007
commit
ccb013a615
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
model: claude-opus-4-6
|
||||||
|
service: claude
|
||||||
|
session: multiaddr-support-rename-prefer
|
||||||
|
timestamp: 2026-04-14T16:33:00Z
|
||||||
|
git_ref: befedc49
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20260414T163300Z_befedc49_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
Create a helper function that determines the best transport given
|
||||||
|
actor locality (distributed vs same host). Use PID/hostname
|
||||||
|
comparison for locality detection, apply at registry addr selection
|
||||||
|
only (not spawn-time).
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
New `prefer_addr()` + `_is_local_addr()` helpers
|
||||||
|
in `_api.py` using `socket.getaddrinfo()` and
|
||||||
|
`ipaddress` for PID/hostname locality detection.
|
||||||
|
Preference: UDS > local TCP > remote TCP.
|
||||||
|
Integrated into `query_actor()` and
|
||||||
|
`wait_for_actor()`. Also changed
|
||||||
|
`Registrar.find_actor()` to return full addr list
|
||||||
|
so callers can apply preference.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tractor/discovery/_discovery.py` → `_api.py`
|
||||||
|
— renamed + added `prefer_addr()`,
|
||||||
|
`_is_local_addr()`; updated `query_actor()` and
|
||||||
|
`wait_for_actor()` call sites
|
||||||
|
- `tractor/discovery/_registry.py`
|
||||||
|
— `Registrar.find_actor()` returns
|
||||||
|
`list[UnwrappedAddress]|None`
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
---
|
||||||
|
model: claude-opus-4-6
|
||||||
|
service: claude
|
||||||
|
timestamp: 2026-04-14T16:33:00Z
|
||||||
|
git_ref: befedc49
|
||||||
|
---
|
||||||
|
|
||||||
|
### Add a `prefer_addr()` helper
|
||||||
|
|
||||||
|
Added transport preference selection to
|
||||||
|
`tractor/discovery/_api.py` with two new functions:
|
||||||
|
|
||||||
|
#### `_is_local_addr(addr: Address) -> bool`
|
||||||
|
|
||||||
|
Determines whether an `Address` is reachable on the
|
||||||
|
local host:
|
||||||
|
|
||||||
|
- `UDSAddress`: always returns `True`
|
||||||
|
(filesystem-bound, inherently local)
|
||||||
|
- `TCPAddress`: checks if `._host` is a loopback IP
|
||||||
|
via `ipaddress.ip_address().is_loopback`, then
|
||||||
|
falls back to comparing against the machine's own
|
||||||
|
interface IPs via
|
||||||
|
`socket.getaddrinfo(socket.gethostname(), None)`
|
||||||
|
|
||||||
|
#### `prefer_addr(addrs: list[UnwrappedAddress]) -> UnwrappedAddress`
|
||||||
|
|
||||||
|
Selects the "best" transport address from a
|
||||||
|
multihomed actor's address list. Wraps each
|
||||||
|
candidate via `wrap_address()` to get typed
|
||||||
|
`Address` objects, then classifies into three tiers:
|
||||||
|
|
||||||
|
1. **UDS** (same-host guaranteed, lowest overhead)
|
||||||
|
2. **TCP loopback / same-host IP** (local network)
|
||||||
|
3. **TCP remote** (only option for distributed)
|
||||||
|
|
||||||
|
Within each tier, the last-registered (latest) entry
|
||||||
|
is preferred. Falls back to `addrs[-1]` if no
|
||||||
|
heuristic matches.
|
||||||
|
|
||||||
|
#### Integration
|
||||||
|
|
||||||
|
- `Registrar.find_actor()` in `_registry.py`: changed
|
||||||
|
return type from `UnwrappedAddress|None` to
|
||||||
|
`list[UnwrappedAddress]|None` — returns the full
|
||||||
|
addr list so callers can apply transport preference.
|
||||||
|
|
||||||
|
- `query_actor()` in `_api.py`: now calls
|
||||||
|
`prefer_addr(addrs)` on the list returned by
|
||||||
|
`Registrar.find_actor()` instead of receiving a
|
||||||
|
single pre-selected addr.
|
||||||
|
|
||||||
|
- `wait_for_actor()` in `_api.py`: replaced
|
||||||
|
`addrs[-1]` with `prefer_addr(addrs)` for
|
||||||
|
consistent transport selection.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
All discovery tests pass (13/13 non-daemon).
|
||||||
|
`test_local.py` and `test_multi_program.py` also
|
||||||
|
pass (daemon fixture teardown failures are
|
||||||
|
pre-existing and unrelated).
|
||||||
|
|
@ -20,6 +20,8 @@ management of (service) actors.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import ipaddress
|
||||||
|
import socket
|
||||||
from typing import (
|
from typing import (
|
||||||
AsyncGenerator,
|
AsyncGenerator,
|
||||||
AsyncContextManager,
|
AsyncContextManager,
|
||||||
|
|
@ -33,10 +35,12 @@ from ..trionics import (
|
||||||
collapse_eg,
|
collapse_eg,
|
||||||
)
|
)
|
||||||
from ..ipc import _connect_chan, Channel
|
from ..ipc import _connect_chan, Channel
|
||||||
|
from ..ipc._tcp import TCPAddress
|
||||||
|
from ..ipc._uds import UDSAddress
|
||||||
from ._addr import (
|
from ._addr import (
|
||||||
UnwrappedAddress,
|
UnwrappedAddress,
|
||||||
Address,
|
Address,
|
||||||
wrap_address
|
wrap_address,
|
||||||
)
|
)
|
||||||
from ..runtime._portal import (
|
from ..runtime._portal import (
|
||||||
Portal,
|
Portal,
|
||||||
|
|
@ -56,6 +60,94 @@ if TYPE_CHECKING:
|
||||||
log = get_logger()
|
log = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
def _is_local_addr(addr: Address) -> bool:
|
||||||
|
'''
|
||||||
|
Determine whether `addr` is reachable on the
|
||||||
|
local host by inspecting address type and
|
||||||
|
comparing hostnames/PIDs.
|
||||||
|
|
||||||
|
- `UDSAddress` is always local (filesystem-bound)
|
||||||
|
- `TCPAddress` is local when its host is a
|
||||||
|
loopback IP or matches one of the machine's
|
||||||
|
own interface addresses.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if isinstance(addr, UDSAddress):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if isinstance(addr, TCPAddress):
|
||||||
|
try:
|
||||||
|
ip = ipaddress.ip_address(addr._host)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if ip.is_loopback:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# check if this IP belongs to any of our
|
||||||
|
# local network interfaces.
|
||||||
|
try:
|
||||||
|
local_ips: set[str] = {
|
||||||
|
info[4][0]
|
||||||
|
for info in socket.getaddrinfo(
|
||||||
|
socket.gethostname(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return addr._host in local_ips
|
||||||
|
except socket.gaierror:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def prefer_addr(
|
||||||
|
addrs: list[UnwrappedAddress],
|
||||||
|
) -> UnwrappedAddress:
|
||||||
|
'''
|
||||||
|
Select the "best" transport address from a
|
||||||
|
multihomed actor's address list based on
|
||||||
|
locality heuristics.
|
||||||
|
|
||||||
|
Preference order (highest -> lowest):
|
||||||
|
1. UDS (same-host guaranteed, lowest overhead)
|
||||||
|
2. TCP loopback / same-host IP
|
||||||
|
3. TCP remote (only option for distributed)
|
||||||
|
|
||||||
|
When multiple addrs share the same priority
|
||||||
|
tier, the last-registered (latest) entry is
|
||||||
|
preferred.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if len(addrs) == 1:
|
||||||
|
return addrs[0]
|
||||||
|
|
||||||
|
local_uds: list[UnwrappedAddress] = []
|
||||||
|
local_tcp: list[UnwrappedAddress] = []
|
||||||
|
remote: list[UnwrappedAddress] = []
|
||||||
|
|
||||||
|
for unwrapped in addrs:
|
||||||
|
wrapped: Address = wrap_address(unwrapped)
|
||||||
|
if isinstance(wrapped, UDSAddress):
|
||||||
|
local_uds.append(unwrapped)
|
||||||
|
elif _is_local_addr(wrapped):
|
||||||
|
local_tcp.append(unwrapped)
|
||||||
|
else:
|
||||||
|
remote.append(unwrapped)
|
||||||
|
|
||||||
|
# prefer UDS > local TCP > remote TCP;
|
||||||
|
# within each tier take the latest entry.
|
||||||
|
if local_uds:
|
||||||
|
return local_uds[-1]
|
||||||
|
if local_tcp:
|
||||||
|
return local_tcp[-1]
|
||||||
|
if remote:
|
||||||
|
return remote[-1]
|
||||||
|
|
||||||
|
# fallback: last registered addr
|
||||||
|
return addrs[-1]
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def get_registry(
|
async def get_registry(
|
||||||
addr: UnwrappedAddress|None = None,
|
addr: UnwrappedAddress|None = None,
|
||||||
|
|
@ -187,13 +279,17 @@ async def query_actor(
|
||||||
reg_portal: Portal|LocalPortal
|
reg_portal: Portal|LocalPortal
|
||||||
regaddr: Address = wrap_address(regaddr) or actor.reg_addrs[0]
|
regaddr: Address = wrap_address(regaddr) or actor.reg_addrs[0]
|
||||||
async with get_registry(regaddr) as reg_portal:
|
async with get_registry(regaddr) as reg_portal:
|
||||||
# TODO: return portals to all available actors - for now
|
addrs: list[UnwrappedAddress]|None = (
|
||||||
# just the last one that registered
|
await reg_portal.run_from_ns(
|
||||||
addr: UnwrappedAddress = await reg_portal.run_from_ns(
|
'self',
|
||||||
'self',
|
'find_actor',
|
||||||
'find_actor',
|
name=name,
|
||||||
name=name,
|
)
|
||||||
)
|
)
|
||||||
|
if addrs:
|
||||||
|
addr: UnwrappedAddress = prefer_addr(addrs)
|
||||||
|
else:
|
||||||
|
addr = None
|
||||||
yield addr, reg_portal
|
yield addr, reg_portal
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
|
|
@ -370,9 +466,9 @@ async def wait_for_actor(
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
# get latest registered addr by default?
|
# select the best transport addr from
|
||||||
# TODO: offer multi-portal yields in multi-homed case?
|
# the (possibly multihomed) addr list.
|
||||||
addr: UnwrappedAddress = addrs[-1]
|
addr: UnwrappedAddress = prefer_addr(addrs)
|
||||||
|
|
||||||
async with _connect_chan(addr) as chan:
|
async with _connect_chan(addr) as chan:
|
||||||
async with open_portal(chan) as portal:
|
async with open_portal(chan) as portal:
|
||||||
|
|
|
||||||
|
|
@ -101,11 +101,11 @@ class Registrar(Actor):
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
|
||||||
) -> UnwrappedAddress|None:
|
) -> list[UnwrappedAddress]|None:
|
||||||
|
|
||||||
for uid, addrs in self._registry.items():
|
for uid, addrs in self._registry.items():
|
||||||
if name in uid:
|
if name in uid:
|
||||||
return addrs[0] if addrs else None
|
return addrs if addrs else None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue