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
|
||||
import ipaddress
|
||||
import socket
|
||||
from typing import (
|
||||
AsyncGenerator,
|
||||
AsyncContextManager,
|
||||
|
|
@ -33,10 +35,12 @@ from ..trionics import (
|
|||
collapse_eg,
|
||||
)
|
||||
from ..ipc import _connect_chan, Channel
|
||||
from ..ipc._tcp import TCPAddress
|
||||
from ..ipc._uds import UDSAddress
|
||||
from ._addr import (
|
||||
UnwrappedAddress,
|
||||
Address,
|
||||
wrap_address
|
||||
wrap_address,
|
||||
)
|
||||
from ..runtime._portal import (
|
||||
Portal,
|
||||
|
|
@ -56,6 +60,94 @@ if TYPE_CHECKING:
|
|||
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
|
||||
async def get_registry(
|
||||
addr: UnwrappedAddress|None = None,
|
||||
|
|
@ -187,13 +279,17 @@ async def query_actor(
|
|||
reg_portal: Portal|LocalPortal
|
||||
regaddr: Address = wrap_address(regaddr) or actor.reg_addrs[0]
|
||||
async with get_registry(regaddr) as reg_portal:
|
||||
# TODO: return portals to all available actors - for now
|
||||
# just the last one that registered
|
||||
addr: UnwrappedAddress = await reg_portal.run_from_ns(
|
||||
addrs: list[UnwrappedAddress]|None = (
|
||||
await reg_portal.run_from_ns(
|
||||
'self',
|
||||
'find_actor',
|
||||
name=name,
|
||||
)
|
||||
)
|
||||
if addrs:
|
||||
addr: UnwrappedAddress = prefer_addr(addrs)
|
||||
else:
|
||||
addr = None
|
||||
yield addr, reg_portal
|
||||
|
||||
@acm
|
||||
|
|
@ -370,9 +466,9 @@ async def wait_for_actor(
|
|||
name=name,
|
||||
)
|
||||
|
||||
# get latest registered addr by default?
|
||||
# TODO: offer multi-portal yields in multi-homed case?
|
||||
addr: UnwrappedAddress = addrs[-1]
|
||||
# select the best transport addr from
|
||||
# the (possibly multihomed) addr list.
|
||||
addr: UnwrappedAddress = prefer_addr(addrs)
|
||||
|
||||
async with _connect_chan(addr) as chan:
|
||||
async with open_portal(chan) as portal:
|
||||
|
|
|
|||
|
|
@ -101,11 +101,11 @@ class Registrar(Actor):
|
|||
self,
|
||||
name: str,
|
||||
|
||||
) -> UnwrappedAddress|None:
|
||||
) -> list[UnwrappedAddress]|None:
|
||||
|
||||
for uid, addrs in self._registry.items():
|
||||
if name in uid:
|
||||
return addrs[0] if addrs else None
|
||||
return addrs if addrs else None
|
||||
|
||||
return None
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue