From 03f458a45c81878ea33a0feafdca435ffd37e482 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 20 Aug 2023 16:22:46 -0400 Subject: [PATCH 01/11] Add `Arbiter.delete_sockaddr()` to remove addrs Since stale addrs can be leaked where the actor transport server task crashes but doesn't (successfully) unregister from the registrar, we need a remote way to remove such entries; hence this new (registrar) method. To implement this make use of the `bidict` lib for the `._registry` table thus making it super simple to do reverse uuid lookups from an input socket-address. --- tractor/_runtime.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tractor/_runtime.py b/tractor/_runtime.py index 93fb68fd..d6a5de3c 100644 --- a/tractor/_runtime.py +++ b/tractor/_runtime.py @@ -68,6 +68,7 @@ import textwrap from types import ModuleType import warnings +from bidict import bidict import trio from trio._core import _run as trio_runtime from trio import ( @@ -1920,10 +1921,10 @@ class Arbiter(Actor): **kwargs, ) -> None: - self._registry: dict[ + self._registry: bidict[ tuple[str, str], UnwrappedAddress, - ] = {} + ] = bidict({}) self._waiters: dict[ str, # either an event to sync to receiving an actor uid (which @@ -2029,4 +2030,17 @@ class Arbiter(Actor): uid = (str(uid[0]), str(uid[1])) entry: tuple = self._registry.pop(uid, None) if entry is None: - log.warning(f'Request to de-register {uid} failed?') + log.warning( + f'Request to de-register {uid!r} failed?' + ) + + async def delete_sockaddr( + self, + sockaddr: tuple[str, int|str], + ) -> tuple[str, str]: + uid: tuple = self._registry.inverse.pop(sockaddr) + log.warning( + f'Deleting registry-entry for,\n' + f'{sockaddr!r}@{uid!r}' + ) + return uid From 149b800c9faf3de785d3f53def1a163809aba9db Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 28 Aug 2023 11:26:36 -0400 Subject: [PATCH 02/11] Handle stale registrar entries; detect and delete In cases where an actor's transport server task (by default handling new TCP connections) terminates early but does not de-register from the pertaining registry (aka the registrar) actor's address table, the trying-to-connect client actor will get a connection error on that address. In the case where client handles a (local) `OSError` (meaning the target actor address is likely being contacted over `localhost`) exception, make a further call to the registrar to delete the stale entry and `yield None` gracefully indicating to calling code that no `Portal` can be delivered to the target address. This issue was originally discovered in `piker` where the `emsd` (clearing engine) actor would sometimes crash on rapid client re-connects and then leave a `pikerd` stale entry. With this fix new clients will attempt connect via an endpoint which will re-spawn the `emsd` when a `None` portal is delivered (via `maybe_spawn_em()`). --- tractor/_discovery.py | 52 ++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/tractor/_discovery.py b/tractor/_discovery.py index bf4d066a..5372df0e 100644 --- a/tractor/_discovery.py +++ b/tractor/_discovery.py @@ -60,7 +60,7 @@ log = get_logger() async def get_registry( addr: UnwrappedAddress|None = None, ) -> AsyncGenerator[ - Portal | LocalPortal | None, + Portal|LocalPortal|None, None, ]: ''' @@ -153,7 +153,7 @@ async def query_actor( regaddr: UnwrappedAddress|None = None, ) -> AsyncGenerator[ - UnwrappedAddress|None, + tuple[UnwrappedAddress|None, Portal|None], None, ]: ''' @@ -167,7 +167,8 @@ async def query_actor( actor: Actor = current_actor() if ( name == 'registrar' - and actor.is_registrar + and + actor.is_registrar ): raise RuntimeError( 'The current actor IS the registry!?' @@ -175,7 +176,7 @@ async def query_actor( maybe_peers: list[Channel]|None = get_peer_by_name(name) if maybe_peers: - yield maybe_peers[0].raddr + yield maybe_peers[0].raddr, None return reg_portal: Portal @@ -188,8 +189,7 @@ async def query_actor( 'find_actor', name=name, ) - yield addr - + yield addr, reg_portal @acm async def maybe_open_portal( @@ -204,15 +204,36 @@ async def maybe_open_portal( async with query_actor( name=name, regaddr=addr, - ) as addr: - pass + ) as (addr, reg_portal): + if not addr: + yield None + return - if addr: - async with _connect_chan(addr) as chan: - async with open_portal(chan) as portal: - yield portal - else: - yield None + try: + async with _connect_chan(addr) as chan: + async with open_portal(chan) as portal: + yield portal + + # most likely we were unable to connect the + # transport and there is likely a stale entry in + # the registry actor's table, thus we need to + # instruct it to clear that stale entry and then + # more silently (pretend there was no reason but + # to) indicate that the target actor can't be + # contacted at that addr. + except OSError: + # NOTE: ensure we delete the stale entry from the + # registar actor. + uid: tuple[str, str] = await reg_portal.run_from_ns( + 'self', + 'delete_sockaddr', + sockaddr=addr.unwrap(), + ) + log.error( + f'Deleted stale registry entry !\n' + f'addr: {addr!r}\n' + f'uid: {uid!r}\n' + ) @acm @@ -280,7 +301,7 @@ async def find_actor( if not any(portals): if raise_on_none: raise RuntimeError( - f'No actor "{name}" found registered @ {registry_addrs}' + f'No actor {name!r} found registered @ {registry_addrs!r}' ) yield None return @@ -296,6 +317,7 @@ async def find_actor( yield portals + @acm async def wait_for_actor( name: str, From a0d3741faca3a4f842e1adfd58f7ac86fbaeb3f0 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 29 Sep 2025 23:09:50 -0400 Subject: [PATCH 03/11] Ensure `._registry` values are hashable, since `bidict`! --- tractor/_runtime.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tractor/_runtime.py b/tractor/_runtime.py index d6a5de3c..f53ebd01 100644 --- a/tractor/_runtime.py +++ b/tractor/_runtime.py @@ -2013,7 +2013,8 @@ class Arbiter(Actor): # should never be 0-dynamic-os-alloc await debug.pause() - self._registry[uid] = addr + # XXX NOTE, value must also be hashable. + self._registry[uid] = tuple(addr) # pop and signal all waiter events events = self._waiters.pop(name, []) From 0dfa6f4a8adb3c403686bb448eb266c7c912ad30 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 29 Sep 2025 23:10:27 -0400 Subject: [PATCH 04/11] Don't unwrap and unwrapped addr, just warn on delete XD --- tractor/_discovery.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tractor/_discovery.py b/tractor/_discovery.py index 5372df0e..b2b19d09 100644 --- a/tractor/_discovery.py +++ b/tractor/_discovery.py @@ -227,13 +227,14 @@ async def maybe_open_portal( uid: tuple[str, str] = await reg_portal.run_from_ns( 'self', 'delete_sockaddr', - sockaddr=addr.unwrap(), + sockaddr=addr, ) - log.error( + log.warning( f'Deleted stale registry entry !\n' f'addr: {addr!r}\n' f'uid: {uid!r}\n' ) + yield None @acm @@ -317,7 +318,6 @@ async def find_actor( yield portals - @acm async def wait_for_actor( name: str, From 528012f35f2e51b180fd3286ded1f1ecdf13009c Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 28 Aug 2023 12:20:12 -0400 Subject: [PATCH 05/11] Add stale entry deleted from registrar test By spawning an actor task that immediately shuts down the transport server and then sleeps, verify that attempting to connect via the `._discovery.find_actor()` helper delivers `None` for the `Portal` value. Relates to #184 and #216 --- tests/test_discovery.py | 100 +++++++++++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 17 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index b76e3706..6050011e 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -1,7 +1,7 @@ -""" -Discovery subsys. +''' +Discovery subsystem via a "registrar" actor scenarios. -""" +''' import os import signal import platform @@ -163,7 +163,10 @@ async def unpack_reg( else: msg = await actor_or_portal.run_from_ns('self', 'get_registry') - return {tuple(key.split('.')): val for key, val in msg.items()} + return { + tuple(key.split('.')): val + for key, val in msg.items() + } async def spawn_and_check_registry( @@ -356,20 +359,24 @@ async def close_chans_before_nursery( try: get_reg = partial(unpack_reg, aportal) - async with tractor.open_nursery() as tn: - portal1 = await tn.start_actor( - name='consumer1', enable_modules=[__name__]) - portal2 = await tn.start_actor( - 'consumer2', enable_modules=[__name__]) + async with tractor.open_nursery() as an: + portal1 = await an.start_actor( + name='consumer1', + enable_modules=[__name__], + ) + portal2 = await an.start_actor( + 'consumer2', + enable_modules=[__name__], + ) - # TODO: compact this back as was in last commit once - # 3.9+, see https://github.com/goodboy/tractor/issues/207 - async with portal1.open_stream_from( - stream_forever - ) as agen1: - async with portal2.open_stream_from( + async with ( + portal1.open_stream_from( stream_forever - ) as agen2: + ) as agen1, + portal2.open_stream_from( + stream_forever + ) as agen2, + ): async with ( collapse_eg(), trio.open_nursery() as tn, @@ -392,6 +399,7 @@ async def close_chans_before_nursery( # also kill off channels cuz why not await agen1.aclose() await agen2.aclose() + finally: with trio.CancelScope(shield=True): await trio.sleep(1) @@ -427,7 +435,7 @@ def test_close_channel_explicit( @pytest.mark.parametrize('use_signal', [False, True]) -def test_close_channel_explicit_remote_arbiter( +def test_close_channel_explicit_remote_registrar( daemon: subprocess.Popen, start_method: str, use_signal: bool, @@ -448,3 +456,61 @@ def test_close_channel_explicit_remote_arbiter( remote_arbiter=True, ), ) + + +@tractor.context +async def kill_transport( + ctx: tractor.Context, +) -> None: + + await ctx.started() + actor: tractor.Actor = tractor.current_actor() + actor.ipc_server.cancel() + await trio.sleep_forever() + + + +# @pytest.mark.parametrize('use_signal', [False, True]) +def test_stale_entry_is_deleted( + debug_mode: bool, + daemon: subprocess.Popen, + start_method: str, + reg_addr: tuple, +): + ''' + Ensure that when a stale entry is detected in the registrar's + table that the `find_actor()` API takes care of deleting the + stale entry and not delivering a bad portal. + + ''' + async def main(): + + name: str = 'transport_fails_actor' + _reg_ptl: tractor.Portal + an: tractor.ActorNursery + async with ( + tractor.open_nursery( + debug_mode=debug_mode, + ) as an, + tractor.get_registry(reg_addr) as _reg_ptl, + ): + ptl: tractor.Portal = await an.start_actor( + name, + enable_modules=[__name__], + ) + async with ptl.open_context( + kill_transport, + ) as (first, ctx): + async with tractor.find_actor(name) as maybe_portal: + # because the transitive + # `._discovery.maybe_open_portal()` call should + # fail and implicitly call `.delete_sockaddr()` + assert maybe_portal is None + registry: dict = await unpack_reg(_reg_ptl) + assert ptl.chan.aid.uid not in registry + + # should fail since we knocked out the IPC tpt XD + await ptl.cancel_actor() + await an.cancel() + + trio.run(main) From 403c2174a1a7c061e9fa3a11219298cc48197406 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 15 Sep 2023 14:20:12 -0400 Subject: [PATCH 06/11] Always no-raise try-to-pop registry addrs --- tractor/_runtime.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tractor/_runtime.py b/tractor/_runtime.py index f53ebd01..5b19802e 100644 --- a/tractor/_runtime.py +++ b/tractor/_runtime.py @@ -2039,9 +2039,18 @@ class Arbiter(Actor): self, sockaddr: tuple[str, int|str], ) -> tuple[str, str]: - uid: tuple = self._registry.inverse.pop(sockaddr) + uid: tuple | None = self._registry.inverse.pop( + sockaddr, + None, + ) + if uid: + report: str = 'Deleting registry-entry for,\n' + else: + report: str = 'No registry entry for,\n' + log.warning( - f'Deleting registry-entry for,\n' + report + + f'{sockaddr!r}@{uid!r}' ) return uid From d929fb75b5d3b739c17eb6d48262ecace310e766 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 30 Sep 2025 01:09:16 -0400 Subject: [PATCH 07/11] Rename `.delete_sockaddr()` -> `.delete_addr()` --- tests/test_discovery.py | 2 +- tractor/_discovery.py | 4 ++-- tractor/_runtime.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 6050011e..c2ba479c 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -504,7 +504,7 @@ def test_stale_entry_is_deleted( async with tractor.find_actor(name) as maybe_portal: # because the transitive # `._discovery.maybe_open_portal()` call should - # fail and implicitly call `.delete_sockaddr()` + # fail and implicitly call `.delete_addr()` assert maybe_portal is None registry: dict = await unpack_reg(_reg_ptl) assert ptl.chan.aid.uid not in registry diff --git a/tractor/_discovery.py b/tractor/_discovery.py index b2b19d09..fd1d0cf8 100644 --- a/tractor/_discovery.py +++ b/tractor/_discovery.py @@ -226,8 +226,8 @@ async def maybe_open_portal( # registar actor. uid: tuple[str, str] = await reg_portal.run_from_ns( 'self', - 'delete_sockaddr', - sockaddr=addr, + 'delete_addr', + addr=addr, ) log.warning( f'Deleted stale registry entry !\n' diff --git a/tractor/_runtime.py b/tractor/_runtime.py index 5b19802e..633ec487 100644 --- a/tractor/_runtime.py +++ b/tractor/_runtime.py @@ -2035,12 +2035,12 @@ class Arbiter(Actor): f'Request to de-register {uid!r} failed?' ) - async def delete_sockaddr( + async def delete_addr( self, - sockaddr: tuple[str, int|str], + addr: tuple[str, int|str], ) -> tuple[str, str]: uid: tuple | None = self._registry.inverse.pop( - sockaddr, + addr, None, ) if uid: @@ -2051,6 +2051,6 @@ class Arbiter(Actor): log.warning( report + - f'{sockaddr!r}@{uid!r}' + f'{addr!r}@{uid!r}' ) return uid From 850219f60c040fd10ebeaa74dda8c9be9c0b41c4 Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 17 Mar 2026 17:26:57 -0400 Subject: [PATCH 08/11] Guard `reg_portal` for `None` in `maybe_open_portal()` Fix potential `AttributeError` when `query_actor()` yields a `None` portal (peer-found-locally path) and an `OSError` is raised during transport connect. Also, - fix `Arbiter.delete_addr()` return type to `tuple[str, str]|None` bc it can return `None`. - fix "registar" typo -> "registrar" in comment. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/_discovery.py | 31 +++++++++++++++++++------------ tractor/_runtime.py | 2 +- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/tractor/_discovery.py b/tractor/_discovery.py index fd1d0cf8..49829b17 100644 --- a/tractor/_discovery.py +++ b/tractor/_discovery.py @@ -222,18 +222,25 @@ async def maybe_open_portal( # to) indicate that the target actor can't be # contacted at that addr. except OSError: - # NOTE: ensure we delete the stale entry from the - # registar actor. - uid: tuple[str, str] = await reg_portal.run_from_ns( - 'self', - 'delete_addr', - addr=addr, - ) - log.warning( - f'Deleted stale registry entry !\n' - f'addr: {addr!r}\n' - f'uid: {uid!r}\n' - ) + # NOTE: ensure we delete the stale entry + # from the registrar actor when available. + if reg_portal is not None: + uid: tuple[str, str] = await reg_portal.run_from_ns( + 'self', + 'delete_addr', + addr=addr, + ) + log.warning( + f'Deleted stale registry entry !\n' + f'addr: {addr!r}\n' + f'uid: {uid!r}\n' + ) + else: + log.warning( + f'Connection to {addr!r} failed' + f' and no registry portal available' + f' to delete stale entry.' + ) yield None diff --git a/tractor/_runtime.py b/tractor/_runtime.py index 633ec487..c4c0577a 100644 --- a/tractor/_runtime.py +++ b/tractor/_runtime.py @@ -2038,7 +2038,7 @@ class Arbiter(Actor): async def delete_addr( self, addr: tuple[str, int|str], - ) -> tuple[str, str]: + ) -> tuple[str, str]|None: uid: tuple | None = self._registry.inverse.pop( addr, None, From 85457cb8394067658546f11394662a16877f183a Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 25 Mar 2026 00:21:09 -0400 Subject: [PATCH 09/11] Address Copilot review suggestions on PR #366 - Use `bidict.forceput()` in `register_actor()` to handle duplicate addr values from stale entries or actor restarts. - Fix `uid` annotation to `tuple[str, str]|None` in `maybe_open_portal()` and handle the `None` return from `delete_addr()` in log output. - Pass explicit `registry_addrs=[reg_addr]` to `open_nursery()` and `find_actor()` in `test_stale_entry_is_deleted` to ensure the test uses the remote registrar. - Update `query_actor()` docstring to document the new `(addr, reg_portal)` yield shape. Co-Authored-By: Claude Opus 4.6 --- tests/test_discovery.py | 6 +++++- tractor/_discovery.py | 25 +++++++++++++++++-------- tractor/_runtime.py | 9 +++++++-- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index c2ba479c..fcf73156 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -491,6 +491,7 @@ def test_stale_entry_is_deleted( async with ( tractor.open_nursery( debug_mode=debug_mode, + registry_addrs=[reg_addr], ) as an, tractor.get_registry(reg_addr) as _reg_ptl, ): @@ -501,7 +502,10 @@ def test_stale_entry_is_deleted( async with ptl.open_context( kill_transport, ) as (first, ctx): - async with tractor.find_actor(name) as maybe_portal: + async with tractor.find_actor( + name, + registry_addrs=[reg_addr], + ) as maybe_portal: # because the transitive # `._discovery.maybe_open_portal()` call should # fail and implicitly call `.delete_addr()` diff --git a/tractor/_discovery.py b/tractor/_discovery.py index 49829b17..244b97c2 100644 --- a/tractor/_discovery.py +++ b/tractor/_discovery.py @@ -160,8 +160,12 @@ async def query_actor( Lookup a transport address (by actor name) via querying a registrar listening @ `regaddr`. - Returns the transport protocol (socket) address or `None` if no - entry under that name exists. + Yields a `tuple` of `(addr, reg_portal)` where, + - `addr` is the transport protocol (socket) address or `None` if + no entry under that name exists, + - `reg_portal` is the `Portal` to the registrar used for the + lookup (or `None` when the peer was found locally via + `get_peer_by_name()`). ''' actor: Actor = current_actor() @@ -225,16 +229,21 @@ async def maybe_open_portal( # NOTE: ensure we delete the stale entry # from the registrar actor when available. if reg_portal is not None: - uid: tuple[str, str] = await reg_portal.run_from_ns( + uid: tuple[str, str]|None = await reg_portal.run_from_ns( 'self', 'delete_addr', addr=addr, ) - log.warning( - f'Deleted stale registry entry !\n' - f'addr: {addr!r}\n' - f'uid: {uid!r}\n' - ) + if uid: + log.warning( + f'Deleted stale registry entry !\n' + f'addr: {addr!r}\n' + f'uid: {uid!r}\n' + ) + else: + log.warning( + f'No registry entry found for addr: {addr!r}' + ) else: log.warning( f'Connection to {addr!r} failed' diff --git a/tractor/_runtime.py b/tractor/_runtime.py index c4c0577a..b67c5cda 100644 --- a/tractor/_runtime.py +++ b/tractor/_runtime.py @@ -2013,8 +2013,13 @@ class Arbiter(Actor): # should never be 0-dynamic-os-alloc await debug.pause() - # XXX NOTE, value must also be hashable. - self._registry[uid] = tuple(addr) + # XXX NOTE, value must also be hashable AND since + # `._registry` is a `bidict` values must be unique; use + # `.forceput()` to replace any prior (stale) entries + # that might map a different uid to the same addr (e.g. + # after an unclean shutdown or actor-restart reusing + # the same address). + self._registry.forceput(uid, tuple(addr)) # pop and signal all waiter events events = self._waiters.pop(name, []) From b557ec20a72f7fd75564362734fcb56a86d4e209 Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 25 Mar 2026 01:36:58 -0400 Subject: [PATCH 10/11] Coerce IPC `addr` to `tuple` in `.delete_addr()` `msgpack` deserializes tuples as lists over IPC so the `bidict.inverse.pop()` needs a `tuple`-cast to match registry keys. Regressed-by: 85457cb (`registry_addrs` change) Found-via: `/run-tests` test_stale_entry_is_deleted (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/_runtime.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tractor/_runtime.py b/tractor/_runtime.py index b67c5cda..bacb3172 100644 --- a/tractor/_runtime.py +++ b/tractor/_runtime.py @@ -2044,8 +2044,11 @@ class Arbiter(Actor): self, addr: tuple[str, int|str], ) -> tuple[str, str]|None: + # NOTE: `addr` arrives as a `list` over IPC + # (msgpack deserializes tuples -> lists) so + # coerce to `tuple` for the bidict hash lookup. uid: tuple | None = self._registry.inverse.pop( - addr, + tuple(addr), None, ) if uid: From e71eec07de4a0c1660fbf382e4ff90409497ac1e Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 25 Mar 2026 02:16:48 -0400 Subject: [PATCH 11/11] Refine type annots in `_discovery` and `_runtime` - Add `LocalPortal` union to `query_actor()` return type and `reg_portal` var annotation since the registrar yields a `LocalPortal` instance. - Update docstring to note the `LocalPortal` case. - Widen `.delete_addr()` `addr` param to accept `list[str|int]` bc msgpack deserializes tuples as lists over IPC. - Tighten `uid` annotation to `tuple[str, str]|None`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/_discovery.py | 9 +++++---- tractor/_runtime.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tractor/_discovery.py b/tractor/_discovery.py index 244b97c2..11673267 100644 --- a/tractor/_discovery.py +++ b/tractor/_discovery.py @@ -153,7 +153,7 @@ async def query_actor( regaddr: UnwrappedAddress|None = None, ) -> AsyncGenerator[ - tuple[UnwrappedAddress|None, Portal|None], + tuple[UnwrappedAddress|None, Portal|LocalPortal|None], None, ]: ''' @@ -163,8 +163,9 @@ async def query_actor( Yields a `tuple` of `(addr, reg_portal)` where, - `addr` is the transport protocol (socket) address or `None` if no entry under that name exists, - - `reg_portal` is the `Portal` to the registrar used for the - lookup (or `None` when the peer was found locally via + - `reg_portal` is the `Portal` (or `LocalPortal` when the + current actor is the registrar) used for the lookup (or + `None` when the peer was found locally via `get_peer_by_name()`). ''' @@ -183,7 +184,7 @@ async def query_actor( yield maybe_peers[0].raddr, None return - reg_portal: Portal + 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 diff --git a/tractor/_runtime.py b/tractor/_runtime.py index bacb3172..e0174f0c 100644 --- a/tractor/_runtime.py +++ b/tractor/_runtime.py @@ -2042,12 +2042,12 @@ class Arbiter(Actor): async def delete_addr( self, - addr: tuple[str, int|str], + addr: tuple[str, int|str]|list[str|int], ) -> tuple[str, str]|None: # NOTE: `addr` arrives as a `list` over IPC # (msgpack deserializes tuples -> lists) so # coerce to `tuple` for the bidict hash lookup. - uid: tuple | None = self._registry.inverse.pop( + uid: tuple[str, str]|None = self._registry.inverse.pop( tuple(addr), None, )