# tractor: structured concurrent "actors". # Copyright 2018-eternity Tyler Goodlet. # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU Affero General Public # License as published by the Free Software Foundation, either # version 3 of the License, or (at your option) any later # version. # This program is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU Affero General Public License for more # details. # You should have received a copy of the GNU Affero General # Public License along with this program. If not, see # . ''' Actor-registry for process-tree service discovery. The `Registrar` is a special `Actor` subtype that serves as the process-tree's name-registry, tracking actor name-to-address mappings so peers can discover each other. ''' from __future__ import annotations import trio from ..runtime._runtime import Actor from ._addr import ( UnwrappedAddress, Address, wrap_address, ) from ..devx import debug from ..log import get_logger log = get_logger('tractor') class Registrar(Actor): ''' A special registrar `Actor` who can contact all other actors within its immediate process tree and keeps a registry of others meant to be discoverable in a distributed application. Normally the registrar is also the "root actor" and thus always has access to the top-most-level actor (process) nursery. By default, the registrar is always initialized when and if no other registrar socket addrs have been specified to runtime init entry-points (such as `open_root_actor()` or `open_nursery()`). Any time a new main process is launched (and thus a new root actor created) and, no existing registrar can be contacted at the provided `registry_addr`, then a new one is always created; however, if one can be reached it is used. Normally a distributed app requires at least one registrar per logical host where for that given "host space" (aka localhost IPC domain of addresses) it is responsible for making all other host (local address) bound actors *discoverable* to external actor trees running on remote hosts. ''' is_registrar = True def is_registry(self) -> bool: return self.is_registrar def __init__( self, *args, **kwargs, ) -> None: self._registry: dict[ tuple[str, str], list[UnwrappedAddress], ] = {} self._waiters: dict[ str, # either an event to sync to receiving an # actor uid (which is filled in once the actor # has sucessfully registered), or that uid # after registry is complete. list[trio.Event|tuple[str, str]] ] = {} super().__init__(*args, **kwargs) async def find_actor( self, name: str, ) -> list[UnwrappedAddress]|None: for uid, addrs in self._registry.items(): if name in uid: return addrs if addrs else None return None async def get_registry( self ) -> dict[str, list[UnwrappedAddress]]: ''' Return current name registry. This method is async to allow for cross-actor invocation. ''' # NOTE: requires ``strict_map_key=False`` to the # msgpack unpacker since we have tuples as keys # (note this makes the registrar suscetible to # hashdos): # https://github.com/msgpack/msgpack-python#major-breaking-changes-in-msgpack-10 return { '.'.join(key): val for key, val in self._registry.items() } async def wait_for_actor( self, name: str, ) -> list[UnwrappedAddress]: ''' Wait for a particular actor to register. This is a blocking call if no actor by the provided name is currently registered. ''' addrs: list[UnwrappedAddress] = [] mailbox_info: str = ( 'Actor registry contact infos:\n' ) for uid, uid_addrs in self._registry.items(): mailbox_info += ( f'|_uid: {uid}\n' f'|_addrs: {uid_addrs}\n\n' ) if name == uid[0]: addrs.extend(uid_addrs) if not addrs: waiter = trio.Event() self._waiters.setdefault( name, [] ).append(waiter) await waiter.wait() for uid in self._waiters[name]: if not isinstance(uid, trio.Event): addrs.extend( self._registry[uid] ) log.runtime(mailbox_info) return addrs async def register_actor( self, uid: tuple[str, str], addr: UnwrappedAddress ) -> None: uid = name, hash = ( str(uid[0]), str(uid[1]), ) waddr: Address = wrap_address(addr) if not waddr.is_valid: # should never be 0-dynamic-os-alloc await debug.pause() addr_tup: tuple = tuple(addr) # Evict stale entries: if a *different* uid claims # this addr (e.g. after unclean shutdown or # actor-restart reusing the same address), remove # it from the old uid's addr list. for other_uid, other_addrs in self._registry.items(): if ( other_uid != uid and addr_tup in other_addrs ): other_addrs.remove(addr_tup) if not other_addrs: del self._registry[other_uid] break # Append to this uid's addr list (avoid duplicates) entry: list = self._registry.setdefault(uid, []) if addr_tup not in entry: entry.append(addr_tup) # pop and signal all waiter events events = self._waiters.pop(name, []) self._waiters.setdefault( name, [] ).append(uid) for event in events: if isinstance(event, trio.Event): event.set() async def unregister_actor( self, uid: tuple[str, str] ) -> None: uid = (str(uid[0]), str(uid[1])) entry: list|None = self._registry.pop( uid, None ) if entry is None: log.warning( f'Request to de-register' f' {uid!r} failed?' ) async def delete_addr( self, 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 linear scan. addr = tuple(addr) uid: tuple[str, str]|None = None for _uid, addrs in self._registry.items(): if addr in addrs: addrs.remove(addr) uid = _uid # remove the uid entry entirely when it # has no remaining addrs. if not addrs: del self._registry[_uid] break if uid: report: str = ( 'Deleting registry-entry for,\n' ) else: report: str = ( 'No registry entry for,\n' ) log.warning( report + f'{addr!r}@{uid!r}' ) return uid # Backward compat alias Arbiter = Registrar