123 lines
4.0 KiB
Python
123 lines
4.0 KiB
Python
|
|
'''
|
||
|
|
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()
|