Add a dedicated-registrar example + discovery guide
Add a runnable `examples/dedicated_registrar.py` + a "A dedicated registrar" subsection in `guide/discovery.rst` demoing the registrar decoupled from any app tree's root: boot a bare `tractor.run_daemon([], registry_addrs=[...])` as its own process (a root actor that does nothing but hold the registry), point the app tree at the same `registry_addrs`, and discover a service *through* that external registrar. This is the buildable-today form of the #472 "Registrar-as-subsystem (not the root actor)" bullet. Two constraints are called out inline as follow-ups: `enable_transports` is single-proto per runtime (no multi-backend registrar yet), and a registrar can only be a root (no `actor_cls` hook on `start_actor()` to spawn one as a subactor). (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-codewkt/big_boi_docs_472_follow_ups
parent
095c165a34
commit
adf71d74e8
|
|
@ -62,6 +62,28 @@ the one-and-only registrar; boot then fails loudly with a
|
|||
``RuntimeError`` if some other process already bound the registry
|
||||
socket(s).
|
||||
|
||||
A dedicated registrar
|
||||
---------------------
|
||||
That second rule — *"if a registrar answers, boot as a plain
|
||||
root"* — is all you need to run the registry as its own
|
||||
**standalone process**, decoupled from any app tree's root. Boot
|
||||
a bare ``tractor.run_daemon([], registry_addrs=[...])`` (a root
|
||||
actor that does nothing but hold the registry), point your app
|
||||
tree at the same ``registry_addrs``, and every actor discovers
|
||||
through that *external* registrar instead of a tree-local one:
|
||||
|
||||
.. literalinclude:: ../../examples/dedicated_registrar.py
|
||||
:caption: examples/dedicated_registrar.py
|
||||
:language: python
|
||||
|
||||
This is the "registrar as a subsystem, not the root actor" shape.
|
||||
Two caveats today (both tracked as #472 follow-ups):
|
||||
``enable_transports`` is single-proto per runtime, so a registrar
|
||||
can't yet serve multiple backends at once; and there's no way to
|
||||
spawn a registrar as a *sub*-actor of a shared tree (only as its
|
||||
own root), since ``start_actor()`` has no custom-``actor_cls``
|
||||
hook.
|
||||
|
||||
Looking up actors
|
||||
-----------------
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
'''
|
||||
Run a *dedicated* registrar as its own standalone process — decoupled
|
||||
from your app's root actor — and discover a service *through* it.
|
||||
|
||||
Normally the registrar **is** the root actor of your tree. Here we
|
||||
instead boot a separate `tractor.run_daemon([], registry_addrs=[...])`
|
||||
process whose *sole* job is to be the registry, then point our app
|
||||
tree at it via `registry_addrs`. Because a registrar is already
|
||||
reachable at that addr, our app's root actor does NOT become one — it
|
||||
registers with (and discovers through) the external daemon. That's the
|
||||
"registrar as a subsystem, not the root actor" pattern.
|
||||
|
||||
NB: `enable_transports` is single-proto per-runtime today (see
|
||||
`tractor._root`), so this demos one transport; a genuinely
|
||||
multi-backend registrar (and spawning one as a *sub*actor of a shared
|
||||
tree) are future runtime work — see the #472 follow-ups.
|
||||
|
||||
'''
|
||||
from contextlib import suppress
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import trio
|
||||
import tractor
|
||||
|
||||
|
||||
# the fixed addr the dedicated registrar binds and everyone points at.
|
||||
REG_ADDR: tuple[str, int] = ('127.0.0.1', 1717)
|
||||
|
||||
|
||||
def _wait_registrar_ready(
|
||||
addr: tuple[str, int],
|
||||
proc: subprocess.Popen,
|
||||
deadline: float = 10.0,
|
||||
) -> None:
|
||||
'''
|
||||
Active-poll the registrar's bind addr until it accepts a
|
||||
connection (proving it's booted + listening), bailing early if
|
||||
the daemon proc dies during startup.
|
||||
|
||||
'''
|
||||
end: float = time.monotonic() + deadline
|
||||
while time.monotonic() < end:
|
||||
if proc.poll() is not None:
|
||||
raise RuntimeError(
|
||||
f'registrar died on startup (rc={proc.returncode})'
|
||||
)
|
||||
with suppress(OSError):
|
||||
with socket.create_connection(addr, timeout=0.1):
|
||||
return
|
||||
time.sleep(0.05)
|
||||
raise TimeoutError(f'registrar never came up @ {addr}')
|
||||
|
||||
|
||||
async def greet() -> str:
|
||||
'''A trivial service task any peer can RPC by name.'''
|
||||
return f'hello from {tractor.current_actor().name}!'
|
||||
|
||||
|
||||
async def app() -> None:
|
||||
'''
|
||||
Point our app tree at the EXTERNAL registrar (not its own root)
|
||||
via `registry_addrs`, register a named service, then discover +
|
||||
RPC it purely by name.
|
||||
|
||||
'''
|
||||
an: tractor.ActorNursery
|
||||
async with tractor.open_nursery(
|
||||
registry_addrs=[REG_ADDR],
|
||||
enable_transports=['tcp'],
|
||||
) as an:
|
||||
# this subactor registers with the DEDICATED registrar @
|
||||
# REG_ADDR (our root is a plain peer, not the registry).
|
||||
await an.start_actor(
|
||||
'greeter',
|
||||
enable_modules=[__name__],
|
||||
)
|
||||
# discover it *through the external registrar*, by name only.
|
||||
portal: tractor.Portal
|
||||
async with tractor.wait_for_actor('greeter') as portal:
|
||||
print(f'found `greeter` via dedicated registrar @ {REG_ADDR}')
|
||||
print(await portal.run(greet))
|
||||
await an.cancel()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# boot the dedicated registrar as its own process/tree: an empty
|
||||
# `enable_modules` `run_daemon()` is just a root actor that does
|
||||
# nothing but hold + serve the registry.
|
||||
code: str = (
|
||||
'import tractor; '
|
||||
f'tractor.run_daemon([], registry_addrs={[REG_ADDR]!r}, '
|
||||
"enable_transports=['tcp'], loglevel='error')"
|
||||
)
|
||||
registrar: subprocess.Popen = subprocess.Popen(
|
||||
[sys.executable, '-c', code],
|
||||
# the registry is a quiet background service; hush its logs +
|
||||
# expected SIGINT-teardown traceback so the demo output stays
|
||||
# focused on the discovery flow.
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
try:
|
||||
_wait_registrar_ready(REG_ADDR, registrar)
|
||||
print(
|
||||
f'dedicated registrar up @ {REG_ADDR} '
|
||||
f'(pid {registrar.pid})'
|
||||
)
|
||||
trio.run(app)
|
||||
finally:
|
||||
# graceful SIGINT teardown of the standalone registrar.
|
||||
registrar.send_signal(signal.SIGINT)
|
||||
with suppress(subprocess.TimeoutExpired):
|
||||
registrar.wait(timeout=10)
|
||||
print('dedicated registrar shut down')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Reference in New Issue