3e17417122
Lightweight actor library built on asyncio primitives (~800 lines): - Actor base class with lifecycle hooks (on_started/on_stopped/on_restart) - ActorRef with tell (fire-and-forget) and ask (request-response) - Supervision: OneForOne/AllForOne strategies with restart limits - Middleware pipeline for cross-cutting concerns - Pluggable Mailbox interface (MemoryMailbox default, RedisMailbox optional) - ReplyRegistry + ReplyChannel: ask() works across any mailbox backend - System-level thread pool for blocking I/O (run_in_executor) - Dead letter handling, poison message quarantine, parallel shutdown - 22 tests + benchmark suite
110 lines
3.2 KiB
Python
110 lines
3.2 KiB
Python
"""Actor base class and per-actor context."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
|
|
from .supervision import OneForOneStrategy, SupervisorStrategy
|
|
|
|
if TYPE_CHECKING:
|
|
from .ref import ActorRef
|
|
|
|
# Message type variable — use Actor[MyMsg] for typed actors
|
|
M = TypeVar("M")
|
|
R = TypeVar("R")
|
|
|
|
|
|
class ActorContext:
|
|
"""Per-actor runtime context, injected before ``on_started``.
|
|
|
|
Provides access to the actor's identity, parent, children,
|
|
and the ability to spawn child actors.
|
|
"""
|
|
|
|
__slots__ = ("_cell",)
|
|
|
|
def __init__(self, cell: Any) -> None:
|
|
self._cell = cell
|
|
|
|
@property
|
|
def self_ref(self) -> ActorRef:
|
|
return self._cell.ref
|
|
|
|
@property
|
|
def parent(self) -> ActorRef | None:
|
|
p = self._cell.parent
|
|
return p.ref if p is not None else None
|
|
|
|
@property
|
|
def children(self) -> dict[str, ActorRef]:
|
|
return {name: c.ref for name, c in self._cell.children.items()}
|
|
|
|
@property
|
|
def system(self) -> Any:
|
|
return self._cell.system
|
|
|
|
async def spawn(
|
|
self,
|
|
actor_cls: type[Actor],
|
|
name: str,
|
|
*,
|
|
mailbox_size: int = 256,
|
|
middlewares: list | None = None,
|
|
) -> ActorRef:
|
|
"""Spawn a child actor supervised by this actor."""
|
|
return await self._cell.spawn_child(actor_cls, name, mailbox_size=mailbox_size, middlewares=middlewares)
|
|
|
|
async def run_in_executor(self, fn: Callable[..., Any], *args: Any) -> Any:
|
|
"""Run a blocking function in the system's thread pool.
|
|
|
|
Usage::
|
|
|
|
result = await self.context.run_in_executor(requests.get, url)
|
|
"""
|
|
import asyncio
|
|
executor = self._cell.system._executor
|
|
return await asyncio.get_running_loop().run_in_executor(executor, fn, *args)
|
|
|
|
|
|
class Actor(Generic[M]):
|
|
"""Base class for all actors.
|
|
|
|
Type parameter ``M`` constrains the message type::
|
|
|
|
class Greeter(Actor[str]):
|
|
async def on_receive(self, message: str) -> str:
|
|
return f"Hello, {message}!"
|
|
|
|
class Calculator(Actor[int | tuple[str, int, int]]):
|
|
async def on_receive(self, message: int | tuple[str, int, int]) -> int:
|
|
...
|
|
|
|
Unparameterized ``Actor`` accepts ``Any`` (backward-compatible).
|
|
"""
|
|
|
|
context: ActorContext
|
|
|
|
async def on_receive(self, message: M) -> Any:
|
|
"""Handle an incoming message.
|
|
|
|
Return value is sent back as reply for ``ask`` calls.
|
|
For ``tell`` calls, the return value is discarded.
|
|
"""
|
|
|
|
async def on_started(self) -> None:
|
|
"""Called after creation, before receiving messages."""
|
|
|
|
async def on_stopped(self) -> None:
|
|
"""Called on graceful shutdown. Release resources here."""
|
|
|
|
async def on_restart(self, error: Exception) -> None:
|
|
"""Called on the *new* instance before resuming after a crash."""
|
|
|
|
def supervisor_strategy(self) -> SupervisorStrategy:
|
|
"""Override to customize how this actor supervises its children.
|
|
|
|
Default: OneForOne, up to 3 restarts per 60 seconds, always restart.
|
|
"""
|
|
return OneForOneStrategy()
|