tractor/examples/service_daemon_discovery.py

71 lines
2.0 KiB
Python

'''
Demonstrate the "service daemon" pattern: a named,
long-lived actor spawned via `ActorNursery.start_actor()`
which any other task can locate through the registrar using
`tractor.find_actor()` / `tractor.wait_for_actor()` - no
spawn-portal required - and RPC into directly.
Teardown is explicit and graceful via `portal.cancel_actor()`
once the clients are done.
'''
import trio
import tractor
_quotes: dict[str, float] = {
'btcusdt': 66_000.5,
'ethusdt': 3_500.25,
}
async def get_quote(sym: str) -> float:
'''
Look up the "current" quote for a symbol.
'''
name: str = tractor.current_actor().name
print(f'{name}: serving quote for {sym!r}')
return _quotes[sym]
async def client_task() -> None:
'''
Locate the quote service by name and RPC it; note no
spawn-nursery/portal reference is ever passed in here!
'''
# a lookup miss yields `None` (not an error).
async with tractor.find_actor('no_such_svc') as portal:
assert portal is None
print('client: "no_such_svc" is not registered')
# block until the service shows up in the registry,
# then call into it through the delivered portal.
async with tractor.wait_for_actor('quote_svc') as portal:
quote: float = await portal.run(
get_quote,
sym='btcusdt',
)
print(f'client: got btcusdt quote {quote}')
async def main() -> None:
an: tractor.ActorNursery
async with tractor.open_nursery() as an:
portal: tractor.Portal = await an.start_actor(
'quote_svc',
enable_modules=[__name__],
)
# run the client in a separate task which discovers
# the daemon purely by its registered name.
tn: trio.Nursery
async with trio.open_nursery() as tn:
tn.start_soon(client_task)
# explicit graceful teardown of the daemon.
print('root: cancelling quote_svc')
await portal.cancel_actor()
print('root: service shut down cleanly')
if __name__ == '__main__':
trio.run(main)