feat(standalone): 新增单机模式,KILOSTAR_MODE=standalone 时去掉 Ray 依赖

通过 StandaloneProxy 适配层让 .remote() 调用在单机模式下透明降级为
asyncio 协程调用,7 个 Actor 和 workflow task 均可在纯 asyncio 环境运行,
启动快、资源占用低。分布式模式行为完全不变。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 15:52:41 +00:00
parent 76a67e8237
commit 457d12834f
14 changed files with 390 additions and 108 deletions
+68 -44
View File
@@ -11,9 +11,15 @@
# 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.
import os
import time
import ray
from functools import lru_cache
from typing import Any, Dict
_STANDALONE = os.environ.get("KILOSTAR_MODE", "distributed") == "standalone"
if not _STANDALONE:
import ray
class ActorList:
@@ -37,62 +43,80 @@ class ActorList:
raise AttributeError(f"ActorList对象没有属性 '{key}'")
@lru_cache(maxsize=128)
def _get_cached_actor_handle(actor_name: str):
"""缓存接口"""
return ray.get_actor(actor_name, namespace="kilostar")
# ─── Standalone Registry ───
_standalone_registry: Dict[str, Any] = {}
def clear_actor_cache():
"""清理接口"""
_get_cached_actor_handle.cache_clear()
def register_standalone(name: str, instance: Any) -> None:
"""注册一个单机模式下的 Actor 单例(已包装为 StandaloneProxy)。"""
from kilostar.utils.standalone_proxy import StandaloneProxy
_standalone_registry[name] = StandaloneProxy(instance)
def wait_for_actor(
actor_name: str, *, timeout: float = 10.0, interval: float = 0.5
):
"""阻塞等待某个 actor 就绪,返回其句柄。
# ─── Distributed Mode Helpers ───
用于"启动期 / ray task 入口刚拉起"这类场景——被依赖的 actor 可能还没注册。
在 ``timeout`` 内按 ``interval`` 轮询 ``ray.get_actor``;拿到就立即返回,
超时则抛带清晰上下文的 ``TimeoutError``(而不是裸 ``ValueError``)。
Args:
actor_name: actor 注册名
timeout: 最长等待秒数;``<=0`` 表示只试一次(等价于直接取句柄)
interval: 轮询间隔秒数
if not _STANDALONE:
Raises:
TimeoutError: 超时仍未就绪。原始异常通过 ``raise ... from`` 链保留。
"""
deadline = time.monotonic() + max(timeout, 0.0)
last_err: Exception | None = None
while True:
try:
return _get_cached_actor_handle(actor_name)
except Exception as e: # ray.get_actor 失败一般是 ValueError
last_err = e
# 失败不能让 lru_cache 留下脏数据(异常本身不会被缓存,
# 但若底层换实现,这里清一次更稳妥)
if time.monotonic() >= deadline:
raise TimeoutError(
f"等待 actor {actor_name!r} 就绪超时({timeout}s):{last_err}"
) from last_err
time.sleep(interval)
@lru_cache(maxsize=128)
def _get_cached_actor_handle(actor_name: str):
"""缓存接口"""
return ray.get_actor(actor_name, namespace="kilostar")
def clear_actor_cache():
"""清理接口"""
_get_cached_actor_handle.cache_clear()
def wait_for_actor(
actor_name: str, *, timeout: float = 10.0, interval: float = 0.5
):
"""阻塞等待某个 actor 就绪,返回其句柄。"""
deadline = time.monotonic() + max(timeout, 0.0)
last_err: Exception | None = None
while True:
try:
return _get_cached_actor_handle(actor_name)
except Exception as e:
last_err = e
if time.monotonic() >= deadline:
raise TimeoutError(
f"等待 actor {actor_name!r} 就绪超时({timeout}s):{last_err}"
) from last_err
time.sleep(interval)
else:
def _get_cached_actor_handle(actor_name: str):
raise RuntimeError("单机模式下不应调用 _get_cached_actor_handle")
def clear_actor_cache():
pass
def wait_for_actor(actor_name: str, **kwargs):
raise RuntimeError("单机模式下不应调用 wait_for_actor")
# ─── 统一入口 ───
def ray_actor_hook(*actor_names: str, timeout: float = 0.0, interval: float = 0.5):
"""按名字批量取出 Ray Actor 句柄,组装成一个 ``ActorList`` 返回。
"""按名字批量取出 Actor 句柄,组装成一个 ActorList 返回。
例:``actors = ray_actor_hook("postgres_database", "global_state_machine")``
随后即可用 ``actors.postgres_database`` 拿到对应句柄。
Args:
timeout: ``>0`` 时对每个 actor 走 ``wait_for_actor`` 等待就绪(启动期用);
缺省 ``0`` 保持原"快速失败"语义——actor 不在立即抛异常。
interval: 等待轮询间隔,仅在 ``timeout>0`` 时生效。
单机模式从 _standalone_registry 取,分布式模式走 ray.get_actor。
"""
actor_list = ActorList()
if _STANDALONE:
for name in actor_names:
if name not in _standalone_registry:
raise ValueError(
f"Standalone registry: actor {name!r} not registered"
)
setattr(actor_list, name, _standalone_registry[name])
return actor_list
for actor_name in actor_names:
if timeout > 0:
handle = wait_for_actor(