modify: 派发逻辑统一到 dispatch,供 zenoh 回调与进程内直调共用(0.5.0)
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ requires = ["hatchling"]
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "chongming-worker"
|
name = "chongming-worker"
|
||||||
version = "0.4.5"
|
version = "0.5.0"
|
||||||
description = "chongming worker"
|
description = "chongming worker"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -0,0 +1,333 @@
|
|||||||
|
"""进程内派发器:让「zenoh 回调」与「网关直调」共用同一份业务派发逻辑。
|
||||||
|
|
||||||
|
背景:inline worker 与网关同进程、同事件循环,但业务请求仍要走
|
||||||
|
"zenoh 查询 → router → queryable 回调 → 跨线程调度回原 loop",一次 RPC 多出
|
||||||
|
线程 hop 与两次 TCP 往返。本模块把派发逻辑从 zenoh Query 上解耦出来
|
||||||
|
(信封解析 → 方法匹配 → 调用 → 回复),由两类 sink 驱动:
|
||||||
|
|
||||||
|
- ZenohQuerySink :包装 zenoh.Query,回调路径语义与改造前逐字一致
|
||||||
|
- _LocalUnarySink / _LocalStreamSink:进程内直调(网关侧快路径)
|
||||||
|
|
||||||
|
只换传输、不改语义:信封协议、错误通道(reply_err)、流式多回复与错误收尾、
|
||||||
|
in-flight 计数(优雅排空据此等待在途请求)、user_info 注入全部保持。
|
||||||
|
external(独立进程)插件不受影响:它在自己进程里注册,网关侧查不到 → 仍走 zenoh。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Protocol, Tuple
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
logger = logging.getLogger("chongming_worker.dispatch")
|
||||||
|
|
||||||
|
|
||||||
|
# ── in-flight 计数:worker_id → 在途请求数(graceful drain 等它归零)──
|
||||||
|
_inflight: Dict[str, int] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _inc_inflight(worker_id: str) -> None:
|
||||||
|
_inflight[worker_id] = _inflight.get(worker_id, 0) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def _dec_inflight(worker_id: str) -> None:
|
||||||
|
_inflight[worker_id] = _inflight.get(worker_id, 0) - 1
|
||||||
|
if _inflight[worker_id] <= 0:
|
||||||
|
_inflight.pop(worker_id, None)
|
||||||
|
|
||||||
|
|
||||||
|
# 非信封载荷只告警一次(与改造前同)
|
||||||
|
_warned_legacy_payload = False
|
||||||
|
|
||||||
|
|
||||||
|
class ReplySink(Protocol):
|
||||||
|
"""回复出口:zenoh Query 与进程内直调都归一到这两个方法"""
|
||||||
|
|
||||||
|
def reply(self, key_expr: str, payload: bytes) -> bool:
|
||||||
|
"""单块回复;实现方可用抛出异常表示"接收端已终止"(流式据此停流)"""
|
||||||
|
|
||||||
|
def reply_err(self, payload: bytes) -> None:
|
||||||
|
"""框架错误回复(网关零解析判定失败 → 503)"""
|
||||||
|
|
||||||
|
|
||||||
|
class ZenohQuerySink:
|
||||||
|
"""把 zenoh.Query 适配为 ReplySink(语义与改造前逐字一致)"""
|
||||||
|
|
||||||
|
__slots__ = ("_query",)
|
||||||
|
|
||||||
|
def __init__(self, query: Any) -> None:
|
||||||
|
self._query = query
|
||||||
|
|
||||||
|
def reply(self, key_expr: str, payload: bytes) -> bool:
|
||||||
|
self._query.reply(key_expr=key_expr, payload=payload)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def reply_err(self, payload: bytes) -> None:
|
||||||
|
self._query.reply_err(payload=payload)
|
||||||
|
|
||||||
|
|
||||||
|
async def stream_replies_to_sink(sink: ReplySink, key_expr: str, result) -> None:
|
||||||
|
"""把一个生成器结果逐块回复给 sink(zenoh 多回复 与 进程内流式 共用)。
|
||||||
|
|
||||||
|
约定(与网关 call_stream / SSE 端点配合):
|
||||||
|
- 生成器正常结束 = 流结束,无结束哨兵
|
||||||
|
- 生成器中途异常 → 发送 {"error": ...} 收尾块后停流
|
||||||
|
- sink.reply 失败(查询已终止/消费方已停)→ 立即停流,避免继续生成
|
||||||
|
"""
|
||||||
|
async def _reply(chunk) -> bool:
|
||||||
|
try:
|
||||||
|
sink.reply(key_expr, orjson.dumps(chunk))
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("[Worker] 流式回复失败(接收端可能已终止): %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if inspect.isasyncgen(result):
|
||||||
|
async for chunk in result:
|
||||||
|
if not await _reply(chunk):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
for chunk in result:
|
||||||
|
if not await _reply(chunk):
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
# 生成器中途异常:以 {"error": ...} 收尾块结束流
|
||||||
|
logger.error("[Worker] 流式生成器异常,发送错误块后停止: %s", e)
|
||||||
|
await _reply({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
class SubjectDispatch:
|
||||||
|
"""单个 subject 的派发器(一个 subject 可挂 GET/POST 等多个 RPCMethod)"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
subject: str,
|
||||||
|
methods: List[Any],
|
||||||
|
key_expr: str,
|
||||||
|
session: Any,
|
||||||
|
worker_type: str,
|
||||||
|
worker_id: str,
|
||||||
|
) -> None:
|
||||||
|
self.subject = subject
|
||||||
|
self.methods = methods
|
||||||
|
self.key_expr = key_expr
|
||||||
|
self.session = session
|
||||||
|
self.worker_type = worker_type
|
||||||
|
self.worker_id = worker_id
|
||||||
|
|
||||||
|
async def dispatch(self, payload_bytes: bytes, sink: ReplySink) -> None:
|
||||||
|
"""信封解析 → 方法匹配 → 调用 → 回复(与改造前 runtime._handle_query 等价)"""
|
||||||
|
global _warned_legacy_payload
|
||||||
|
matched = None # 方法匹配结果(异常分支判断流式错误格式用)
|
||||||
|
# in-flight 计数:供 drain 排在完成后按需退出
|
||||||
|
_inc_inflight(self.worker_id)
|
||||||
|
try:
|
||||||
|
# 解析请求负载(信封协议: {"meta": {...}, "data": {...}})
|
||||||
|
raw = orjson.loads(payload_bytes) if payload_bytes else {}
|
||||||
|
|
||||||
|
if isinstance(raw, dict) and ("meta" in raw or "data" in raw):
|
||||||
|
meta = raw.get("meta") or {}
|
||||||
|
data = raw.get("data") or {}
|
||||||
|
else:
|
||||||
|
# 非信封载荷: 整包视为业务数据,method 默认 GET(无控制字段语义)
|
||||||
|
if not _warned_legacy_payload:
|
||||||
|
logger.warning(
|
||||||
|
"收到非信封格式载荷(缺少 meta/data),按 method=GET 处理: %s",
|
||||||
|
self.subject,
|
||||||
|
)
|
||||||
|
_warned_legacy_payload = True
|
||||||
|
meta = {}
|
||||||
|
data = raw if isinstance(raw, dict) else {}
|
||||||
|
http_method = str(meta.get("method", "GET")).upper()
|
||||||
|
|
||||||
|
# 匹配方法(按 HTTP 方法)
|
||||||
|
matched = next(
|
||||||
|
(m for m in self.methods if m.method.upper() == http_method), None
|
||||||
|
)
|
||||||
|
if not matched and self.methods:
|
||||||
|
matched = self.methods[0] # 降级到第一个
|
||||||
|
if not matched:
|
||||||
|
sink.reply_err(orjson.dumps({"error": f"No method for {self.subject}"}))
|
||||||
|
return
|
||||||
|
|
||||||
|
# 调用 RPC 方法(支持同步和异步;返回生成器时进入流式模式)
|
||||||
|
# user_info 从 meta 信封注入(框架参数,不进业务 payload)
|
||||||
|
result = await matched.call(
|
||||||
|
data,
|
||||||
|
session=self.session,
|
||||||
|
worker_id=self.worker_id,
|
||||||
|
user_info=meta.get("user_info"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if inspect.isasyncgen(result) or inspect.isgenerator(result):
|
||||||
|
# 流式模式:@rpc_method 函数返回生成器,逐块多回复推送
|
||||||
|
await stream_replies_to_sink(sink, self.key_expr, result)
|
||||||
|
else:
|
||||||
|
# 一次性回复
|
||||||
|
sink.reply(self.key_expr, orjson.dumps(result))
|
||||||
|
logger.debug(
|
||||||
|
"[Worker] %s 回复 %s %s", self.worker_id, matched.method, self.subject
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
if matched is not None and matched.stream:
|
||||||
|
# 流式端点错误:以 {"error": ...} 收尾块结束流(流式语义不变)
|
||||||
|
sink.reply(self.key_expr, orjson.dumps({"error": str(e)}))
|
||||||
|
else:
|
||||||
|
error_response = {
|
||||||
|
"result": 0.0,
|
||||||
|
"operation": "error",
|
||||||
|
"error": str(e),
|
||||||
|
"worker": self.worker_id,
|
||||||
|
}
|
||||||
|
# 框架错误走 reply_err 通道:网关零解析判定失败(映射 503);
|
||||||
|
# 载荷保持原错误信封结构,旧网关仍按普通回复透传(兼容)
|
||||||
|
sink.reply_err(orjson.dumps(error_response))
|
||||||
|
logger.error("[Worker] %s 异常 %s: %s", self.worker_id, self.subject, e)
|
||||||
|
finally:
|
||||||
|
# in-flight 完成,递减计数(可能已 < 1 允许负数并 clamps)
|
||||||
|
_dec_inflight(self.worker_id)
|
||||||
|
|
||||||
|
|
||||||
|
class LocalInvokeResult:
|
||||||
|
"""进程内直调结果:ok=正常回复;ok=False 对应 zenoh 的 reply_err(网关映射 503)"""
|
||||||
|
|
||||||
|
__slots__ = ("ok", "payload")
|
||||||
|
|
||||||
|
def __init__(self, ok: bool, payload: bytes) -> None:
|
||||||
|
self.ok = ok
|
||||||
|
self.payload = payload
|
||||||
|
|
||||||
|
|
||||||
|
class _StreamClosed(Exception):
|
||||||
|
"""消费方已停止消费(等价 zenoh 路径里 query.reply 失败)"""
|
||||||
|
|
||||||
|
|
||||||
|
class _LocalUnarySink:
|
||||||
|
"""直调-单回复 sink:首个回复胜出(与网关取首个 zenoh reply 一致)"""
|
||||||
|
|
||||||
|
__slots__ = ("result",)
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.result: Optional[LocalInvokeResult] = None
|
||||||
|
|
||||||
|
def reply(self, key_expr: str, payload: bytes) -> bool:
|
||||||
|
if self.result is None:
|
||||||
|
self.result = LocalInvokeResult(True, bytes(payload))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def reply_err(self, payload: bytes) -> None:
|
||||||
|
if self.result is None:
|
||||||
|
self.result = LocalInvokeResult(False, bytes(payload))
|
||||||
|
|
||||||
|
|
||||||
|
class _LocalStreamSink:
|
||||||
|
"""直调-流式 sink:块投进队列;消费方退出后 reply 抛 _StreamClosed 让生成器尽早停流"""
|
||||||
|
|
||||||
|
__slots__ = ("_queue", "_closed")
|
||||||
|
|
||||||
|
def __init__(self, queue: "asyncio.Queue") -> None:
|
||||||
|
self._queue = queue
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self._closed = True
|
||||||
|
|
||||||
|
def reply(self, key_expr: str, payload: bytes) -> bool:
|
||||||
|
if self._closed:
|
||||||
|
raise _StreamClosed("consumer stopped")
|
||||||
|
self._queue.put_nowait(bytes(payload))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def reply_err(self, payload: bytes) -> None:
|
||||||
|
if self._closed:
|
||||||
|
raise _StreamClosed("consumer stopped")
|
||||||
|
self._queue.put_nowait(bytes(payload))
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerDispatcher:
|
||||||
|
"""一个 worker 副本的派发器集合(按 (worker_type, worker_id) 注册到本进程注册表)"""
|
||||||
|
|
||||||
|
def __init__(self, session: Any, worker_type: str, worker_id: str) -> None:
|
||||||
|
self.session = session
|
||||||
|
self.worker_type = worker_type
|
||||||
|
self.worker_id = worker_id
|
||||||
|
self._subjects: Dict[str, SubjectDispatch] = {}
|
||||||
|
|
||||||
|
def bind(self, subject: str, methods: List[Any], key_expr: str) -> SubjectDispatch:
|
||||||
|
dispatcher = SubjectDispatch(
|
||||||
|
subject, methods, key_expr, self.session, self.worker_type, self.worker_id
|
||||||
|
)
|
||||||
|
self._subjects[subject] = dispatcher
|
||||||
|
return dispatcher
|
||||||
|
|
||||||
|
def has_subject(self, subject: str) -> bool:
|
||||||
|
return subject in self._subjects
|
||||||
|
|
||||||
|
async def invoke_unary(self, subject: str, payload_bytes: bytes) -> LocalInvokeResult:
|
||||||
|
"""进程内直调(单回复):返回 ok/err 结果,不抛业务异常(框架内部已兜底)"""
|
||||||
|
dispatcher = self._subjects.get(subject)
|
||||||
|
if dispatcher is None:
|
||||||
|
return LocalInvokeResult(False, orjson.dumps({"error": f"No subject {subject}"}))
|
||||||
|
sink = _LocalUnarySink()
|
||||||
|
await dispatcher.dispatch(payload_bytes, sink)
|
||||||
|
if sink.result is None: # 理论不可达:dispatch 必走 reply 或 reply_err
|
||||||
|
return LocalInvokeResult(False, orjson.dumps({"error": "no reply"}))
|
||||||
|
return sink.result
|
||||||
|
|
||||||
|
async def invoke_stream(self, subject: str, payload_bytes: bytes) -> AsyncIterator[bytes]:
|
||||||
|
"""进程内直调(流式):逐块产出 JSON 字节;错误块沿用 {"error": ...} 约定"""
|
||||||
|
dispatcher = self._subjects.get(subject)
|
||||||
|
if dispatcher is None:
|
||||||
|
yield orjson.dumps({"error": f"No subject {subject}"})
|
||||||
|
return
|
||||||
|
|
||||||
|
queue: "asyncio.Queue" = asyncio.Queue()
|
||||||
|
sink = _LocalStreamSink(queue)
|
||||||
|
_DONE = object()
|
||||||
|
|
||||||
|
async def _run() -> None:
|
||||||
|
try:
|
||||||
|
await dispatcher.dispatch(payload_bytes, sink)
|
||||||
|
finally:
|
||||||
|
queue.put_nowait(_DONE)
|
||||||
|
|
||||||
|
task = asyncio.create_task(_run())
|
||||||
|
task.add_done_callback(lambda t: None if t.cancelled() else t.exception())
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
item = await queue.get()
|
||||||
|
if item is _DONE:
|
||||||
|
return
|
||||||
|
yield item
|
||||||
|
finally:
|
||||||
|
# 消费方提前退出(SSE 断连/网关放弃):停流并取消在途派发
|
||||||
|
sink.close()
|
||||||
|
if not task.done():
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
# ── 进程内注册表(仅本进程有效)──
|
||||||
|
# 网关进程里的 inline 副本会在此注册;external 副本注册在自己的进程里,网关查不到 → 走 zenoh
|
||||||
|
_DISPATCHERS: Dict[Tuple[str, str], WorkerDispatcher] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def register_dispatcher(dispatcher: WorkerDispatcher) -> None:
|
||||||
|
_DISPATCHERS[(dispatcher.worker_type, dispatcher.worker_id)] = dispatcher
|
||||||
|
|
||||||
|
|
||||||
|
def unregister_dispatcher(worker_type: str, worker_id: str) -> None:
|
||||||
|
_DISPATCHERS.pop((worker_type, worker_id), None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_dispatcher(worker_type: str, worker_id: str) -> Optional[WorkerDispatcher]:
|
||||||
|
return _DISPATCHERS.get((worker_type, worker_id))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_worker_key(key_expr: str) -> Optional[Tuple[str, str]]:
|
||||||
|
"""'worker/<type>/<id>' → (type, id);格式不符返回 None"""
|
||||||
|
parts = key_expr.split("/")
|
||||||
|
if len(parts) != 3 or parts[0] != "worker" or not parts[1] or not parts[2]:
|
||||||
|
return None
|
||||||
|
return parts[1], parts[2]
|
||||||
+44
-121
@@ -25,13 +25,18 @@ from chongming_worker.decorators import (
|
|||||||
RPCMethod,
|
RPCMethod,
|
||||||
get_rpc_methods,
|
get_rpc_methods,
|
||||||
)
|
)
|
||||||
|
# 派发逻辑统一在 dispatch.py:zenoh 回调与网关进程内直调共用同一份实现
|
||||||
|
from chongming_worker.dispatch import (
|
||||||
|
_inflight,
|
||||||
|
register_dispatcher,
|
||||||
|
stream_replies_to_sink,
|
||||||
|
unregister_dispatcher,
|
||||||
|
WorkerDispatcher,
|
||||||
|
ZenohQuerySink,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger("chongming_worker.runtime")
|
logger = logging.getLogger("chongming_worker.runtime")
|
||||||
|
|
||||||
# 是否已提示过"非信封载荷"警告(每个进程仅提示一次,避免刷屏)
|
|
||||||
_warned_legacy_payload = False
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 单副本管理协议(admin Queryable)
|
# 单副本管理协议(admin Queryable)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -53,9 +58,8 @@ ADMIN_CMD_RESCALE = "rescale"
|
|||||||
# 优雅排空时,等待在途请求完成的最大时长(秒),超时则强制关闭该副本
|
# 优雅排空时,等待在途请求完成的最大时长(秒),超时则强制关闭该副本
|
||||||
DRAIN_INFLIGHT_TIMEOUT = 30.0
|
DRAIN_INFLIGHT_TIMEOUT = 30.0
|
||||||
|
|
||||||
# 进程级 in-flight 请求计数(key=worker_id)。单线程事件循环内读写,无需加锁。
|
# 进程级 in-flight 请求计数(key=worker_id)定义在 dispatch.py(与派发逻辑同处一处,
|
||||||
# 用于「优雅排空」:drain 摘除 liveliness/queryable 后,等待正在处理的请求完成再退出。
|
# 直调与 zenoh 两条路径共用同一份计数);此处从那里导入,供 _wait_inflight_zero 读取。
|
||||||
_inflight: Dict[str, int] = {}
|
|
||||||
|
|
||||||
# 进程级 Worker 池注册表(key=worker_type)。由 create_workers/WorkerPool 在同一进程内
|
# 进程级 Worker 池注册表(key=worker_type)。由 create_workers/WorkerPool 在同一进程内
|
||||||
# 建立,供 rescale 管理指令按目标副本数扩缩当前进程内槽位(inline 与 external 共用)。
|
# 建立,供 rescale 管理指令按目标副本数扩缩当前进程内槽位(inline 与 external 共用)。
|
||||||
@@ -171,18 +175,24 @@ def set_current_instance_id(iid: Optional[str]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def current_instance_id() -> str:
|
def current_instance_id() -> str:
|
||||||
"""外部 worker 进程的稳定全局实例标识。
|
"""worker 进程的稳定全局实例标识。
|
||||||
|
|
||||||
优先取进程内显式设定的身份(process.py --instance);其次 env
|
优先取进程内显式设定的身份(process.py --instance);其次 env
|
||||||
CHONGMING_WORKER_INSTANCE(编排时显式给定、跨重启稳定);缺省回退
|
CHONGMING_WORKER_INSTANCE(编排时显式给定、跨重启稳定);再次
|
||||||
"{hostname}:{pid}"——同一台机器上并行多个未指定身份的实例时,凭 pid 区分,
|
CHONGMING_GATEWAY_ID——inline 副本就加载在网关进程内,容器环境(如 K8s)通常
|
||||||
避免它们被误并为一个参与方、导致副本翻倍或 UI 只显示一个实例。
|
给网关进程注入了这个 id,此时网关侧的实例定向用同一 id 即可命中这些副本,
|
||||||
注意:缺省 id 随进程重启变化;若需跨重启稳定且被逐实例精细扩缩持续命中,
|
无需再为 worker 侧单独配置;缺省回退 "{hostname}:{pid}"——同一台机器上并行
|
||||||
请显式设 --instance / CHONGMING_WORKER_INSTANCE。
|
多个未指定身份的实例时,凭 pid 区分,避免它们被误并为一个参与方、导致副本
|
||||||
|
翻倍或 UI 只显示一个实例。
|
||||||
|
注意:缺省 id 随进程重启变化;若需跨重启稳定且被逐实例精细扩缩/实例定向
|
||||||
|
持续命中,请显式设 --instance / CHONGMING_WORKER_INSTANCE。
|
||||||
"""
|
"""
|
||||||
if _current_instance_id:
|
if _current_instance_id:
|
||||||
return _current_instance_id
|
return _current_instance_id
|
||||||
iid = os.environ.get("CHONGMING_WORKER_INSTANCE")
|
iid = (
|
||||||
|
os.environ.get("CHONGMING_WORKER_INSTANCE")
|
||||||
|
or os.environ.get("CHONGMING_GATEWAY_ID")
|
||||||
|
)
|
||||||
if not iid:
|
if not iid:
|
||||||
iid = f"{socket.gethostname()}:{os.getpid()}"
|
iid = f"{socket.gethostname()}:{os.getpid()}"
|
||||||
return iid
|
return iid
|
||||||
@@ -790,35 +800,14 @@ async def stream_replies_to_query(query: zenoh.Query, result) -> None:
|
|||||||
- query.reply 失败(查询已终止,如客户端断开/网关放弃)-> 立即停流,
|
- query.reply 失败(查询已终止,如客户端断开/网关放弃)-> 立即停流,
|
||||||
避免 Worker 对已无接收方的查询继续生成
|
避免 Worker 对已无接收方的查询继续生成
|
||||||
|
|
||||||
|
实现已统一到 dispatch.stream_replies_to_sink(与进程内直调共用同一份逻辑),
|
||||||
|
本函数保留为兼容入口。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: Zenoh Query 对象(仅使用 key_expr 与 reply)
|
query: Zenoh Query 对象(仅使用 key_expr 与 reply)
|
||||||
result: 异步或同步生成器,逐块 yield 可 JSON 序列化的对象
|
result: 异步或同步生成器,逐块 yield 可 JSON 序列化的对象
|
||||||
"""
|
"""
|
||||||
async def _reply(chunk) -> bool:
|
await stream_replies_to_sink(ZenohQuerySink(query), query.key_expr, result)
|
||||||
"""单块回复;返回 False 表示查询已终止,应停流"""
|
|
||||||
try:
|
|
||||||
query.reply(
|
|
||||||
key_expr=query.key_expr,
|
|
||||||
payload=orjson.dumps(chunk),
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug("[Worker] 流式回复失败(查询可能已终止): %s", e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
if inspect.isasyncgen(result):
|
|
||||||
async for chunk in result:
|
|
||||||
if not await _reply(chunk):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
for chunk in result:
|
|
||||||
if not await _reply(chunk):
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
# 生成器中途异常:以 {"error": ...} 收尾块结束流
|
|
||||||
logger.error("[Worker] 流式生成器异常,发送错误块后停止: %s", e)
|
|
||||||
await _reply({"error": str(e)})
|
|
||||||
|
|
||||||
async def create_worker(
|
async def create_worker(
|
||||||
session: zenoh.Session,
|
session: zenoh.Session,
|
||||||
@@ -875,88 +864,18 @@ async def create_worker(
|
|||||||
exit_event = controller.control_event if controller is not None else asyncio.Event()
|
exit_event = controller.control_event if controller is not None else asyncio.Event()
|
||||||
|
|
||||||
# 5. 为每个 subject 创建回调函数并声明 Queryable
|
# 5. 为每个 subject 创建回调函数并声明 Queryable
|
||||||
|
#
|
||||||
|
# 派发逻辑集中在 WorkerDispatcher(dispatch.py):zenoh 回调只做
|
||||||
|
# "取载荷 → 交给 dispatcher",网关侧的进程内直调走同一个 dispatcher,
|
||||||
|
# 两条路径共用信封解析/方法匹配/错误通道/流式语义与 in-flight 计数。
|
||||||
|
dispatcher = WorkerDispatcher(session, worker_type, worker_id)
|
||||||
|
|
||||||
def make_callback(subject: str, methods: List[RPCMethod]):
|
def make_callback(subject: str, bound) -> Callable[[zenoh.Query], None]:
|
||||||
"""创建处理特定 subject 的回调函数"""
|
"""创建处理特定 subject 的回调函数(bound = 已绑定的 SubjectDispatch)"""
|
||||||
async def _handle_query(query: zenoh.Query):
|
|
||||||
matched = None # 方法匹配结果(异常分支判断流式错误格式用)
|
|
||||||
# in-flight 计数:供 drain 排在完成后按需退出
|
|
||||||
_inflight[worker_id] = _inflight.get(worker_id, 0) + 1
|
|
||||||
try:
|
|
||||||
# 解析请求负载(信封协议: {"meta": {...}, "data": {...}})
|
|
||||||
payload_bytes = bytes(query.payload) if query.payload else b""
|
|
||||||
raw = orjson.loads(payload_bytes) if payload_bytes else {}
|
|
||||||
|
|
||||||
if isinstance(raw, dict) and ("meta" in raw or "data" in raw):
|
async def _handle_query(query: zenoh.Query) -> None:
|
||||||
meta = raw.get("meta") or {}
|
payload_bytes = bytes(query.payload) if query.payload else b""
|
||||||
data = raw.get("data") or {}
|
await bound.dispatch(payload_bytes, ZenohQuerySink(query))
|
||||||
else:
|
|
||||||
# 非信封载荷: 整包视为业务数据,method 默认 GET(无控制字段语义)
|
|
||||||
global _warned_legacy_payload
|
|
||||||
if not _warned_legacy_payload:
|
|
||||||
logger.warning(
|
|
||||||
"收到非信封格式载荷(缺少 meta/data),按 method=GET 处理: %s",
|
|
||||||
subject,
|
|
||||||
)
|
|
||||||
_warned_legacy_payload = True
|
|
||||||
meta = {}
|
|
||||||
data = raw if isinstance(raw, dict) else {}
|
|
||||||
http_method = str(meta.get("method", "GET")).upper()
|
|
||||||
|
|
||||||
# 匹配方法(按 HTTP 方法)
|
|
||||||
matched = next((m for m in methods if m.method.upper() == http_method), None)
|
|
||||||
if not matched and methods:
|
|
||||||
matched = methods[0] # 降级到第一个
|
|
||||||
if not matched:
|
|
||||||
query.reply_err(
|
|
||||||
payload=orjson.dumps({"error": f"No method for {subject}"})
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# 调用 RPC 方法(支持同步和异步;返回生成器时进入流式模式)
|
|
||||||
# user_info 从 meta 信封注入(框架参数,不进业务 payload)
|
|
||||||
result = await matched.call(
|
|
||||||
data,
|
|
||||||
session=session,
|
|
||||||
worker_id=worker_id,
|
|
||||||
user_info=meta.get("user_info"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if inspect.isasyncgen(result) or inspect.isgenerator(result):
|
|
||||||
# 流式模式:@rpc_method 函数返回生成器,逐块多回复推送
|
|
||||||
await stream_replies_to_query(query, result)
|
|
||||||
else:
|
|
||||||
# 一次性回复(原有逻辑)
|
|
||||||
query.reply(
|
|
||||||
key_expr=query.key_expr,
|
|
||||||
payload=orjson.dumps(result)
|
|
||||||
)
|
|
||||||
logger.debug("[Worker] %s 回复 %s %s", worker_id, matched.method, subject)
|
|
||||||
except Exception as e:
|
|
||||||
if matched is not None and matched.stream:
|
|
||||||
# 流式端点错误:以 {"error": ...} 收尾块结束流(流式语义不变)
|
|
||||||
query.reply(
|
|
||||||
key_expr=query.key_expr,
|
|
||||||
payload=orjson.dumps({"error": str(e)})
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
error_response = {
|
|
||||||
"result": 0.0,
|
|
||||||
"operation": "error",
|
|
||||||
"error": str(e),
|
|
||||||
"worker": worker_id
|
|
||||||
}
|
|
||||||
# 框架错误走 reply_err 通道:网关零解析判定失败(映射 503);
|
|
||||||
# 载荷保持原错误信封结构,旧网关仍按普通回复透传(兼容)
|
|
||||||
query.reply_err(
|
|
||||||
payload=orjson.dumps(error_response)
|
|
||||||
)
|
|
||||||
logger.error("[Worker] %s 异常 %s: %s", worker_id, subject, e)
|
|
||||||
finally:
|
|
||||||
# in-flight 完成,递减计数(可能已 < 1 允许负数并 clamps)
|
|
||||||
_inflight[worker_id] = _inflight.get(worker_id, 0) - 1
|
|
||||||
if _inflight[worker_id] <= 0:
|
|
||||||
_inflight.pop(worker_id, None)
|
|
||||||
|
|
||||||
def callback(query: zenoh.Query):
|
def callback(query: zenoh.Query):
|
||||||
# 当前 create_worker 运行在事件循环中,可直接 create_task
|
# 当前 create_worker 运行在事件循环中,可直接 create_task
|
||||||
@@ -968,10 +887,13 @@ async def create_worker(
|
|||||||
queryables: List[zenoh.Queryable] = []
|
queryables: List[zenoh.Queryable] = []
|
||||||
for subject, methods in subject_methods.items():
|
for subject, methods in subject_methods.items():
|
||||||
key = f"worker/{worker_type}/{worker_id}/{subject}"
|
key = f"worker/{worker_type}/{worker_id}/{subject}"
|
||||||
|
bound = dispatcher.bind(subject, methods, key)
|
||||||
# 使用回调方式声明
|
# 使用回调方式声明
|
||||||
q = session.declare_queryable(key, make_callback(subject, methods))
|
q = session.declare_queryable(key, make_callback(subject, bound))
|
||||||
queryables.append(q)
|
queryables.append(q)
|
||||||
logger.info("[Worker] %s Queryable: '%s' [%s]", worker_id, key, ", ".join(m.method for m in methods))
|
logger.info("[Worker] %s Queryable: '%s' [%s]", worker_id, key, ", ".join(m.method for m in methods))
|
||||||
|
# 业务 Queryable 就绪后登记直调入口(同进程调用方经此免 zenoh 往返)
|
||||||
|
register_dispatcher(dispatcher)
|
||||||
|
|
||||||
# 声明 admin Queryable(管理指令 > restart / stop / 状态查询)。仅在有 controller 时注册,
|
# 声明 admin Queryable(管理指令 > restart / stop / 状态查询)。仅在有 controller 时注册,
|
||||||
# 直接独立调用 create_worker(controller=None)保持原有行为、不暴露管理通道。
|
# 直接独立调用 create_worker(controller=None)保持原有行为、不暴露管理通道。
|
||||||
@@ -1004,9 +926,10 @@ async def create_worker(
|
|||||||
# 退出非由管理指令触发(正常返回 / 取消 / 异常)时,标记 stop —— 管理循环据此不自动重启
|
# 退出非由管理指令触发(正常返回 / 取消 / 异常)时,标记 stop —— 管理循环据此不自动重启
|
||||||
if controller is not None and not controller.exit_request:
|
if controller is not None and not controller.exit_request:
|
||||||
controller.exit_request = ADMIN_CMD_STOP
|
controller.exit_request = ADMIN_CMD_STOP
|
||||||
# 清理:先摘除 liveliness + 业务 Queryable,隔绝新请求
|
# 清理:先摘除 liveliness + 业务 Queryable + 直调入口,隔绝新请求
|
||||||
if worker_manager is not None:
|
if worker_manager is not None:
|
||||||
worker_manager.unregister_worker(worker_type, worker_id)
|
worker_manager.unregister_worker(worker_type, worker_id)
|
||||||
|
unregister_dispatcher(worker_type, worker_id)
|
||||||
token.undeclare()
|
token.undeclare()
|
||||||
for q in queryables:
|
for q in queryables:
|
||||||
q.undeclare()
|
q.undeclare()
|
||||||
|
|||||||
Reference in New Issue
Block a user