modify: 微服务演进
This commit is contained in:
@@ -16,12 +16,13 @@
|
||||
D. WorkerManager 单测(FakeSession):
|
||||
9. call_raw 零解析返回原始字节;call() 仍解析为 dict(MQTT 语义保留)
|
||||
10. cacheable: 缓存原始 bytes、二次命中不再发查询、业务错误不缓存
|
||||
11. reply.err -> 重试 -> RuntimeError;空回复兜底 b"{}"
|
||||
11. reply.err -> 换副本重试/无副本可换时立即 RuntimeError;空回复兜底 b"{}"
|
||||
|
||||
运行: uv run python test_passthrough_optimization.py
|
||||
"""
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import random
|
||||
import logging
|
||||
import sys
|
||||
import types
|
||||
@@ -32,7 +33,7 @@ import orjson
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT / "src"))
|
||||
sys.path.insert(0, str(REPO_ROOT / "utils" / "worker" / "src"))
|
||||
|
||||
@@ -49,6 +50,7 @@ _auth_stub = types.ModuleType("chongming.app.core.auth")
|
||||
_auth_stub.store_user_to_request = lambda x: x # 仅用于 Depends,桩掉鉴权
|
||||
_cache_stub = types.ModuleType("chongming_cache")
|
||||
_cache_stub.Cache = object
|
||||
_cache_stub.DistributedLock = object # worker_manager 顶层导入(本测试不触达)
|
||||
for mod in (_pkg, _app, _core, _logger_stub, _auth_stub, _cache_stub):
|
||||
sys.modules[mod.__name__] = mod
|
||||
|
||||
@@ -101,7 +103,8 @@ class FakeWorkerManager:
|
||||
self.error = error
|
||||
self.call_raw_payloads = [] # 捕获的预序列化请求字节
|
||||
|
||||
async def call_raw(self, worker_type, subject, payload, timeout=3.0, retries=1, cacheable=False):
|
||||
async def call_raw(self, worker_type, subject, payload, timeout=3.0, retries=1, cacheable=False,
|
||||
instance_id=None):
|
||||
self.call_raw_payloads.append(payload)
|
||||
if self.error == "no_worker":
|
||||
raise RuntimeError(f"类型 '{worker_type}' 没有可用 Worker。")
|
||||
@@ -282,16 +285,27 @@ def test_openapi_request_body_injected():
|
||||
# ---- D. WorkerManager 单测(FakeSession)----
|
||||
|
||||
class FakeSession:
|
||||
"""模拟 zenoh Session:get() 返回单回复迭代器"""
|
||||
"""模拟 zenoh Session:get() 返回单回复迭代器
|
||||
|
||||
def __init__(self, payload=b'{"result": 5}', err_payload=None):
|
||||
err_payload 非空时整体报错;err_target 给定时只对该 key 前缀报错(其余正常回复),
|
||||
用于验证「失败换副本重试」。
|
||||
"""
|
||||
|
||||
def __init__(self, payload=b'{"result": 5}', err_payload=None, err_target=None):
|
||||
self.payload = payload
|
||||
self.err_payload = err_payload
|
||||
self.err_target = err_target
|
||||
self.get_calls = 0
|
||||
self.call_keys = []
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
self.get_calls += 1
|
||||
if self.err_payload is not None:
|
||||
key = args[0] if args else kwargs.get("key_expr", "")
|
||||
self.call_keys.append(str(key))
|
||||
failing = self.err_payload is not None and (
|
||||
self.err_target is None or str(key).startswith(self.err_target)
|
||||
)
|
||||
if failing:
|
||||
reply = SimpleNamespace(ok=None, err=SimpleNamespace(payload=self.err_payload))
|
||||
else:
|
||||
reply = SimpleNamespace(ok=SimpleNamespace(payload=self.payload), err=None)
|
||||
@@ -341,14 +355,49 @@ async def test_wm_cache_bytes():
|
||||
|
||||
|
||||
async def test_wm_reply_err_retry():
|
||||
# 单副本:无其它副本可换 —— 立即失败(不空转重试同一个坏副本),保留具体错误
|
||||
session = FakeSession(err_payload=b'{"error": "boom"}')
|
||||
wm = make_wm(session)
|
||||
try:
|
||||
await wm.call_raw("demo", "calc.add", b'{}')
|
||||
check("reply.err 应抛 RuntimeError", False)
|
||||
except RuntimeError as e:
|
||||
check("reply.err 重试后抛 RuntimeError", "boom" in str(e), str(e))
|
||||
check("reply.err 触发重试(2 次查询)", session.get_calls == 2, str(session.get_calls))
|
||||
check("reply.err 抛 RuntimeError 且保留具体错误", "boom" in str(e), str(e))
|
||||
check("单副本无其它可换时不空转重试(1 次查询)",
|
||||
session.get_calls == 1, str(session.get_calls))
|
||||
|
||||
|
||||
async def test_wm_reply_err_switches_worker():
|
||||
"""多副本:先打的副本报错则换另一个副本重试,且不重复打已失败的副本。
|
||||
|
||||
首个副本由 random.choice 决定,故对多个 seed 试跑:必有一个覆盖「先失败后成功」
|
||||
路径(另一个覆盖「首个即成功」),两条路径都不允许重复打同一个失败的副本。
|
||||
"""
|
||||
ok, detail = False, "无 seed 覆盖到失败换副本路径"
|
||||
for seed in (0, 1):
|
||||
random.seed(seed)
|
||||
session = FakeSession(
|
||||
payload=b'{"result": 5}', err_payload=b'{"error": "boom"}',
|
||||
err_target="worker/demo/w-dead",
|
||||
)
|
||||
wm = make_wm(session)
|
||||
wm._workers_cache = {"demo": ["worker/demo/w-dead", "worker/demo/w-alive"]}
|
||||
try:
|
||||
result = await wm.call_raw("demo", "calc.add", b'{}', retries=2)
|
||||
except RuntimeError as e: # 未换到正常副本(不该发生)
|
||||
detail = f"seed={seed} 意外失败: {e}"
|
||||
continue
|
||||
assert result == b'{"result": 5}', result
|
||||
dead_calls = session.call_keys.count("worker/demo/w-dead/calc.add")
|
||||
if dead_calls > 1:
|
||||
detail = f"seed={seed} 重复打失败副本: {session.call_keys}"
|
||||
continue
|
||||
if session.call_keys == [
|
||||
"worker/demo/w-dead/calc.add",
|
||||
"worker/demo/w-alive/calc.add",
|
||||
]:
|
||||
ok, detail = True, f"seed={seed} keys={session.call_keys}"
|
||||
check("失败副本 -> 换副本重试成功(且不重复打失败副本)", ok, detail)
|
||||
|
||||
|
||||
async def test_wm_empty_reply_fallback():
|
||||
@@ -375,6 +424,7 @@ def run():
|
||||
asyncio.run(test_wm_call_dict_semantics())
|
||||
asyncio.run(test_wm_cache_bytes())
|
||||
asyncio.run(test_wm_reply_err_retry())
|
||||
asyncio.run(test_wm_reply_err_switches_worker())
|
||||
asyncio.run(test_wm_empty_reply_fallback())
|
||||
|
||||
print(f"\n结果: {len(failures)} 个失败")
|
||||
|
||||
Reference in New Issue
Block a user