Files
KiloStar/kilostar/utils/actor.py
T

181 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright 2026 zhaoxi826
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""进程内 actor 运行时(位置透明的组件寻址层)。
设计目标:**一处编写,处处运行**。业务代码永远通过 ``get_actor(name)`` 拿到一个
``ActorHandle``,再 ``await handle.method(args)`` 调用,**不关心对端在哪**。
今天所有核心组件(PostgresDatabase / GlobalStateMachine / ...)都是本进程内的
普通异步对象,``get_actor`` 返回 ``LocalActorHandle``,直接转发到本地实例。
未来若要把某一层(如跑 vLLM 的重型 worker)拆到独立进程/机器上,只需新增一个
``RemoteActorHandle``gRPC / 子进程 / 消息队列后端)并在注册时选择它——业务侧的
调用代码一行都不用改。这层接缝就是当初 Ray ``.remote()`` 提供的价值,这里用零依赖
的方式把它保留下来。
"""
from __future__ import annotations
import asyncio
from typing import Any, Awaitable, Dict, Optional, Set
_registry: Dict[str, Any] = {}
_handle_cache: Dict[str, "ActorHandle"] = {}
# 已 spawn 的后台任务:持强引用,防止事件循环只持弱引用导致任务被 GC 掉
_background_tasks: Set["asyncio.Task[Any]"] = set()
class _BoundMethod:
"""包装目标对象的一个方法,使调用统一返回 awaitable。
- 目标方法是 ``async def`` → 直接返回其协程
- 目标方法是普通函数 → 把返回值包成一个已完成的 awaitable
这样调用方永远可以 ``await handle.method(...)``,无需关心同步/异步、本地/远程。
"""
__slots__ = ("_fn",)
def __init__(self, fn: Any) -> None:
self._fn = fn
def __call__(self, *args: Any, **kwargs: Any) -> Any:
result = self._fn(*args, **kwargs)
if asyncio.iscoroutine(result):
return result
async def _wrap() -> Any:
return result
return _wrap()
class ActorHandle:
"""actor 句柄基类:定义位置透明的调用接口。
未来的 ``RemoteActorHandle`` 继承本类,改写方法解析为跨进程 RPC 即可。
"""
class LocalActorHandle(ActorHandle):
"""本进程句柄:方法调用直接转发到已注册的实例。"""
__slots__ = ("_impl",)
def __init__(self, impl: Any) -> None:
object.__setattr__(self, "_impl", impl)
def __getattr__(self, name: str) -> Any:
attr = getattr(object.__getattribute__(self, "_impl"), name)
if callable(attr):
return _BoundMethod(attr)
return attr
def register_actor(name: str, instance: Any) -> None:
"""注册一个 actor 实例。重复注册会覆盖(支持插件热重载)。"""
_registry[name] = instance
_handle_cache.pop(name, None)
def unregister_actor(name: str) -> None:
"""注销一个 actor(插件卸载用);不存在时静默。"""
_registry.pop(name, None)
_handle_cache.pop(name, None)
def get_actor(name: str) -> ActorHandle:
"""按名字取 actor 句柄;未注册时抛 KeyError。
动态名(如 ``f"org_{plugin}"``)也走这个统一入口。
"""
if name not in _registry:
raise KeyError(f"actor {name!r} 未注册")
handle = _handle_cache.get(name)
if handle is None:
handle = LocalActorHandle(_registry[name])
_handle_cache[name] = handle
return handle
def try_get_actor(name: str) -> Optional[ActorHandle]:
"""按名字取 actor 句柄;未注册时返回 None(可选依赖用)。"""
try:
return get_actor(name)
except KeyError:
return None
def clear_actors() -> None:
"""清空注册表(测试用)。"""
_registry.clear()
_handle_cache.clear()
_background_tasks.clear()
def spawn_background(coro: "Awaitable[Any]") -> "asyncio.Task[Any]":
"""在当前事件循环中后台调度一个协程,并维持强引用防止被 GC。
等价于旧 Ray `.remote()` 的 fire-and-forget 语义:
- 协程被调度执行;
- 异常不会静默消失,而是记录到日志;
- 调用方无需 await,也无需保存返回值。
"""
from kilostar.utils.logger import get_logger
task: asyncio.Task[Any] = asyncio.ensure_future(coro)
_background_tasks.add(task)
def _on_done(t: "asyncio.Task[Any]") -> None:
_background_tasks.discard(t)
if not t.cancelled() and t.exception() is not None:
get_logger("actor").exception(
f"后台任务 {t.get_name()!r} 异常退出", exc_info=t.exception()
)
task.add_done_callback(_on_done)
return task
# ─── 类型化 getter(核心单例,语义糖)──────────────────────────
def get_postgres() -> ActorHandle:
return get_actor("postgres_database")
def get_gsm() -> ActorHandle:
return get_actor("global_state_machine")
def get_gwm() -> ActorHandle:
return get_actor("global_workflow_manager")
def get_regulatory() -> ActorHandle:
return get_actor("regulatory_node")
def get_consciousness() -> ActorHandle:
return get_actor("consciousness_node")
def get_worker_cluster() -> ActorHandle:
return get_actor("worker_cluster")
def get_plugin_manager() -> ActorHandle:
return get_actor("global_plugin_manager")