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
76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
"""Supervision strategies — Erlang/Akka-inspired fault tolerance."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import enum
|
|
import time
|
|
from collections import deque
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
|
|
class Directive(enum.Enum):
|
|
"""What a supervisor should do when a child fails."""
|
|
|
|
resume = "resume" # ignore error, keep processing
|
|
restart = "restart" # discard state, create fresh instance
|
|
stop = "stop" # terminate the child permanently
|
|
escalate = "escalate" # propagate to grandparent
|
|
|
|
|
|
class SupervisorStrategy:
|
|
"""Base class for supervision strategies.
|
|
|
|
Args:
|
|
max_restarts: Maximum restarts allowed within *within_seconds*.
|
|
Exceeding this limit stops the child permanently.
|
|
within_seconds: Time window for restart counting.
|
|
decider: Maps exception → Directive. Default: always restart.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
max_restarts: int = 3,
|
|
within_seconds: float = 60.0,
|
|
decider: Callable[[Exception], Directive] | None = None,
|
|
) -> None:
|
|
self.max_restarts = max_restarts
|
|
self.within_seconds = within_seconds
|
|
self.decider = decider or (lambda _: Directive.restart)
|
|
self._restart_timestamps: dict[str, deque[float]] = {}
|
|
|
|
def decide(self, error: Exception) -> Directive:
|
|
return self.decider(error)
|
|
|
|
def record_restart(self, child_name: str) -> bool:
|
|
"""Record a restart and return True if within limits."""
|
|
now = time.monotonic()
|
|
if child_name not in self._restart_timestamps:
|
|
self._restart_timestamps[child_name] = deque()
|
|
ts = self._restart_timestamps[child_name]
|
|
# Purge old entries outside the window
|
|
cutoff = now - self.within_seconds
|
|
while ts and ts[0] < cutoff:
|
|
ts.popleft()
|
|
ts.append(now)
|
|
return len(ts) <= self.max_restarts
|
|
|
|
def apply_to_children(self, failed_child: str, all_children: list[str]) -> list[str]:
|
|
"""Return which children should be affected by the directive."""
|
|
raise NotImplementedError
|
|
|
|
|
|
class OneForOneStrategy(SupervisorStrategy):
|
|
"""Only the failed child is affected."""
|
|
|
|
def apply_to_children(self, failed_child: str, all_children: list[str]) -> list[str]:
|
|
return [failed_child]
|
|
|
|
|
|
class AllForOneStrategy(SupervisorStrategy):
|
|
"""All children are affected when any one fails."""
|
|
|
|
def apply_to_children(self, failed_child: str, all_children: list[str]) -> list[str]:
|
|
return list(all_children)
|