Files
chongming-core/test/test_inline_config_align.py
T
chakcy b4a5985fe6 modify: inline 插件配置按模式对齐(网关侧兜底,兼容未升级的 worker 包)
现象:cache 模式下应用重启后插件仍按 toml 跑,而前端(读缓存)显示的是缓存值。
根因:跑起来的产物里 chongming-worker 是索引里的发布版(uv run build / uv sync 会把
uv pip install -e utils/worker 的可编辑安装替换回锁文件版本),旧版 Plugin.create_workers
在 on_init 时只拿到 config/workers/<type>/config.toml 展平出来的配置。

改动(两层对齐,规则与 SDK 一致且幂等):
- WorkerManager.align_inline_config:创建 inline 副本之前按 [cache].launch_worker_count
  对齐 [workers.config]——cache 模式取缓存值(缺失才用 toml 引导并写回),toml 模式以
  文件为准并把该段覆盖写入缓存;未配置缓存后端时原样返回。网关侧这层让网关与 worker
  包版本解耦:worker 包仍是旧版时 cache 模式同样生效。
- _discover_plugins 透出 [workers.config] 展平前的原始段;load_workers 在注册 starter/
  创建副本之前完成对齐(插件实例 on_init 用的就是这份配置)。
- test/test_inline_config_align.py:两模式 + 无缓存/无段的边界用例,并校验缓存键与 SDK 一致。
- README:补打包陷阱(可编辑安装会被 uv sync 替换回发布版)与对齐时机说明。
2026-09-18 15:23:13 +08:00

164 lines
6.9 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.
"""
inline 插件 [workers.config] 对齐(网关侧兜底)单测 —— WorkerManager.align_inline_config。
网关侧在创建 inline 副本之前,按「配置优先来源模式」(根 config.toml 的
[cache].launch_worker_count)对齐插件配置:cache 模式取共享缓存里的段,
toml 模式以 config/workers/<type>/config.toml 为准并覆盖缓存。目的是让网关与
worker 包版本解耦——worker 包仍是旧版时,旧版 on_init 只拿到 toml 展平配置,
会出现“前端显示缓存值、实际跑 toml”。
用 dict 作缓存桩,验证两种模式下插件拿到的配置与缓存/文件两侧的取值一致。
"""
import os
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(REPO, "src"))
os.chdir(REPO) # 网关模块导入期读根 config.toml / 日志配置
from chongming.app.core.worker_manager import _CONFIGS_KEY, WorkerManager # noqa: E402
PASS = 0
FAIL = 0
WT = "aibox_sophon_adapter"
# _discover_plugins 展平出来的形态(toml 值)
FLAT = {"type": WT, "count": 1, "data_dir": "./data", "mediamtx_addr": "rtsp://toml:8554"}
# 该插件 config.toml 的 [workers.config] 原始段
SECTION = {"data_dir": "./data", "mediamtx_addr": "rtsp://toml:8554", "heartbeat": {"timeout": 15}}
# 前端下发后缓存里的段(改过 mediamtx_addr 与 heartbeat)
CACHED = {"data_dir": "./data", "mediamtx_addr": "rtsp://cache:9999", "heartbeat": {"timeout": 30}}
def report(name, cond, detail=""):
global PASS, FAIL
if cond:
PASS += 1
print(f"✅ {name}" + (f" — {detail}" if detail else ""))
else:
FAIL += 1
print(f"❌ {name}" + (f" — {detail}" if detail else ""))
def manager(cache, mode):
m = WorkerManager.__new__(WorkerManager) # 免会话构造:仅用到 cache + config_mode
m.cache = cache
m._launch_worker_count = mode
return m
def run() -> int:
key = _CONFIGS_KEY.format(type=WT)
# ── 1. cache 模式:缓存有段 → 插件拿到缓存值(本次修复目标)──────────
cache = {key: dict(CACHED)}
got = manager(cache, "cache").align_inline_config(WT, dict(FLAT), dict(SECTION))
report(
"cache 模式:插件配置取缓存值(覆盖 toml 同名键)",
got.get("mediamtx_addr") == "rtsp://cache:9999"
and got.get("heartbeat") == {"timeout": 30},
f"mediamtx_addr={got.get('mediamtx_addr')}",
)
report("cache 模式:不覆盖已存在的缓存段", cache[key] == CACHED, f"cache={cache[key]}")
# ── 2. cache 模式:缓存缺失 → 用 toml 段引导并写回 ──────────────────
cache2 = {}
got2 = manager(cache2, "cache").align_inline_config(WT, dict(FLAT), dict(SECTION))
report(
"cache 模式:缓存缺失用 config.toml 引导并写回",
cache2.get(key) == SECTION and got2.get("mediamtx_addr") == "rtsp://toml:8554",
f"cache={cache2.get(key)}",
)
# ── 3. toml 模式:以文件为准并覆盖缓存(前端随之显示文件值)─────────
cache3 = {key: dict(CACHED)}
got3 = manager(cache3, "toml").align_inline_config(WT, dict(FLAT), dict(SECTION))
report(
"toml 模式:插件配置为 toml 值",
got3.get("mediamtx_addr") == "rtsp://toml:8554",
f"mediamtx_addr={got3.get('mediamtx_addr')}",
)
report(
"toml 模式:启动时用 toml 覆盖缓存(前端与运行值一致)",
cache3.get(key) == SECTION,
f"cache={cache3.get(key)}",
)
# ── 4. 边界:无缓存后端 / 无段(旧 config/workers/config.toml 兼容路径)──
cache4 = {key: dict(CACHED)}
m4 = manager(cache4, "cache")
m4.cache = None
report(
"无缓存后端:原样返回(经典 toml 行为)",
m4.align_inline_config(WT, dict(FLAT), dict(SECTION)) == FLAT
and cache4[key] == CACHED,
)
cache5 = {}
got5 = manager(cache5, "toml").align_inline_config(WT, dict(FLAT), None)
report("无 [workers.config] 段(旧写法):不写缓存、原样返回", got5 == FLAT and not cache5)
# ── 5. 与 SDK 侧一致:缓存键必须相同;SDK 为本地版本时才比对解析结果 ──
# 注意:venv 里的 chongming-worker 可能是索引里的发布版(uv sync 会把可编辑安装
# 换回锁文件版本),新版才有 resolve_startup_config / 正确的 toml 分支语义。
sys.path.insert(0, os.path.join(REPO, "utils", "worker", "src"))
import chongming_worker.runtime as r # noqa: E402
report(
"缓存键与 SDK 一致(两端必须读写同一键)",
r._configs_cache_key(WT) == _CONFIGS_KEY.format(type=WT),
f"sdk={r._configs_cache_key(WT)}",
)
local_src = os.path.join(REPO, "utils", "worker", "src") in os.path.abspath(r.__file__)
if not local_src:
print(f"⚠️ 跳过解析结果比对:当前 SDK 为 {r.__file__}(非本地源码,缺本次改动)")
else:
c = {r._configs_cache_key(WT): dict(CACHED)}
sdk = r.resolve_runtime_config(WT, dict(FLAT), c, "cache")
report(
"cache 模式:网关侧兜底与 SDK 解析结果一致",
sdk.get("mediamtx_addr") == CACHED["mediamtx_addr"],
f"sdk={sdk.get('mediamtx_addr')}",
)
# ── 6. 端到端:网关对齐 + 插件初始化(部署组合:网关新、worker 包为发布版)──
# 该断言即线上现象的复现与验证:插件 on_init 拿到的必须是网关对齐后的缓存值,
# 与 venv 里 chongming-worker 是否为含本次改动的版本无关。
import asyncio
from chongming_worker.plugin import Plugin # noqa: E402
seen = {}
class DemoPlugin(Plugin):
type = WT
auto_bind = False
async def _bootstrap(self): # 隔离数据库/对象存储引导,只看配置来源
return None
async def on_init(self):
seen["on_init"] = self.config.get("mediamtx_addr")
def _create_workers_impl(self, session, config, count=1, worker_manager=None):
seen["impl"] = (config or {}).get("mediamtx_addr")
return []
aligned = manager({key: dict(CACHED)}, "cache").align_inline_config(
WT, dict(FLAT), dict(SECTION)
)
asyncio.run(DemoPlugin().create_workers(None, aligned, 1))
report(
"端到端:网关对齐后插件 on_init 读到缓存值(不依赖 worker 包版本)",
seen.get("on_init") == CACHED["mediamtx_addr"] == seen.get("impl"),
f"on_init={seen.get('on_init')}, impl={seen.get('impl')}, "
f"sdk={os.path.abspath(__import__('chongming_worker').__file__)}",
)
print("\n" + "=" * 56)
print(f"测试结果: ✅ {PASS} 通过 ❌ {FAIL} 失败")
return 0 if FAIL == 0 else 1
if __name__ == "__main__":
sys.exit(run())