From 1141d0d0f87eebacba15ca24c864323ec263eafe Mon Sep 17 00:00:00 2001 From: host Date: Sun, 1 Nov 2026 09:05:10 +0000 Subject: [PATCH] deploy: sync autoscaler unit with production host (seed-skills, imagegen-mcp, playwright-mcp) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仓库版 ExecStart 只带 --websearch-mcp,生产主机现行 unit 在此之外 还带 --seed-skills --imagegen-mcp --playwright-mcp;按仓库重装会静默 丢掉技能播种与两路 MCP。同步后仓库 ExecStart 与主机现行 ExecStart 逐 token 一致,并补 deploy/tests/test_autoscaler_unit.py(5 项)拦 后续漂移:四项齐全、无重复、均在 cloud/worker.py 有定义、不在 scaler 侧、--docker-runtime 仍为 runsc。 (cherry picked from commit bc3a23fc068cac189c0173b8e1c2befa7b832ed2) --- deploy/autoscaler/codex-autoscaler.service | 5 +- deploy/tests/test_autoscaler_unit.py | 94 ++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 deploy/tests/test_autoscaler_unit.py diff --git a/deploy/autoscaler/codex-autoscaler.service b/deploy/autoscaler/codex-autoscaler.service index 9068f9f..10403ad 100644 --- a/deploy/autoscaler/codex-autoscaler.service +++ b/deploy/autoscaler/codex-autoscaler.service @@ -14,7 +14,10 @@ EnvironmentFile=/etc/codex-app-server/env # scaler 会按实例强制追加;--db 只给 scaler,scaler 会补给 worker。 # 注意:启用 autoscaler 后必须停掉静态 codex-cloud-worker, # 否则两边同时派任务(scaler 只管理 autoscale-* 实例)。 -ExecStart=/usr/bin/python3 -m cloud.autoscaler --db /var/lib/codex-app-server/control.db --interval 30 --min-workers 1 --max-workers 2 --per-worker-capacity 2 --scale-down-after 3 --drain-timeout 60 --state-file /var/lib/codex-app-server/autoscaler-state.json --log-dir /var/lib/codex-app-server/autoscaler-logs -- --interval 10 --config /opt/codex-app-server/host-config-docker.json --executor docker --docker-image codex-app-server:latest --docker-user 1003:1004 --docker-cpus 2 --docker-memory 2g --docker-extra-mounts /opt/codex-app-server/work:/app/work,/var/lib/codex-app-server/codex-home:/var/lib/codex-app-server/codex-home,/var/lib/codex-app-server/repos:/var/lib/codex-app-server/repos,/var/lib/codex-app-server/test-repo:/var/lib/codex-app-server/test-repo --repos-dir /var/lib/codex-app-server/repos --tenant-isolation --tenants-root /var/lib/codex-app-server/tenants --tenant-template-home /var/lib/codex-app-server/codex-home --tenant-template-repo /var/lib/codex-app-server/test-repo --docker-security-opt apparmor=docker-codex --docker-pids-limit 512 --egress-proxy-port 8888 --docker-runtime runsc --websearch-mcp +# worker 侧 MCP/技能播种四项与生产主机现行 unit 一致 +# (--websearch-mcp --seed-skills --imagegen-mcp --playwright-mcp); +# 主机 unit 再改时须同步改这里,deploy/tests/test_autoscaler_unit.py 会拦漂移。 +ExecStart=/usr/bin/python3 -m cloud.autoscaler --db /var/lib/codex-app-server/control.db --interval 30 --min-workers 1 --max-workers 2 --per-worker-capacity 2 --scale-down-after 3 --drain-timeout 60 --state-file /var/lib/codex-app-server/autoscaler-state.json --log-dir /var/lib/codex-app-server/autoscaler-logs -- --interval 10 --config /opt/codex-app-server/host-config-docker.json --executor docker --docker-image codex-app-server:latest --docker-user 1003:1004 --docker-cpus 2 --docker-memory 2g --docker-extra-mounts /opt/codex-app-server/work:/app/work,/var/lib/codex-app-server/codex-home:/var/lib/codex-app-server/codex-home,/var/lib/codex-app-server/repos:/var/lib/codex-app-server/repos,/var/lib/codex-app-server/test-repo:/var/lib/codex-app-server/test-repo --repos-dir /var/lib/codex-app-server/repos --tenant-isolation --tenants-root /var/lib/codex-app-server/tenants --tenant-template-home /var/lib/codex-app-server/codex-home --tenant-template-repo /var/lib/codex-app-server/test-repo --docker-security-opt apparmor=docker-codex --docker-pids-limit 512 --egress-proxy-port 8888 --docker-runtime runsc --websearch-mcp --seed-skills --imagegen-mcp --playwright-mcp Restart=on-failure RestartSec=10 # 停 scaler 时要等它把 worker 优雅排空(drain 60 秒 + 缓冲), diff --git a/deploy/tests/test_autoscaler_unit.py b/deploy/tests/test_autoscaler_unit.py new file mode 100644 index 0000000..0346953 --- /dev/null +++ b/deploy/tests/test_autoscaler_unit.py @@ -0,0 +1,94 @@ +"""autoscaler unit 与生产主机现行 unit 的同步回归。 + +背景:仓库版 deploy/autoscaler/codex-autoscaler.service 的 ExecStart +长期只带 --websearch-mcp,而生产主机现行 unit 在此之外还带 +--seed-skills --imagegen-mcp --playwright-mcp(2026-11-01 主机只读核对: +主机 ExecStart = 仓库版 ExecStart + 这三项,别无差异)。 +仓库版滞后意味着按仓库重装会静默丢掉技能播种与两路 MCP。 + +本文件只读仓库内两个文件,不碰主机、不联网、不跑 worker: +- unit 的 worker 侧(`--` 之后)必须含全部四项 MCP/技能播种 flag; +- worker 侧所有 --flag 必须在 cloud/worker.py 里有 add_argument 定义, + 防止拼写漂移导致 worker 启动即报未知参数; +- 四项 flag 绝不能出现在 scaler 侧(`--` 之前),它们是 worker 参数。 +""" + +import os +import unittest + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname( + os.path.abspath(__file__)))) +UNIT_PATH = os.path.join(REPO_ROOT, "deploy", "autoscaler", + "codex-autoscaler.service") +WORKER_PATH = os.path.join(REPO_ROOT, "cloud", "worker.py") + +# 生产主机现行 unit 带的 worker 侧 MCP/技能播种四项。 +MCP_SEED_FLAGS = ("--websearch-mcp", "--seed-skills", + "--imagegen-mcp", "--playwright-mcp") + + +def _execstart(): + with open(UNIT_PATH, encoding="utf-8") as f: + lines = [ln.rstrip("\n") for ln in f] + starts = [ln for ln in lines if ln.startswith("ExecStart=")] + assert len(starts) == 1, "unit 应恰好有一行 ExecStart,实得 %d 行" % len(starts) + return starts[0][len("ExecStart="):] + + +def _split_sides(): + cmd = _execstart() + assert cmd.startswith("/usr/bin/python3 -m cloud.autoscaler"), \ + "unit 应以 cloud.autoscaler 启动,实得:%s" % cmd[:60] + parts = cmd.split(" -- ", 1) + assert len(parts) == 2, "ExecStart 缺少 scaler/worker 分隔符 ' -- '" + return parts[0], parts[1] + + +def _flag_tokens(side): + """side 里以 -- 开头的 token(去重前顺序保留,含 =value 形式剥离)。""" + flags = [] + for tok in side.split(): + if tok.startswith("--"): + flags.append(tok.split("=", 1)[0]) + return flags + + +class TestAutoscalerUnitSync(unittest.TestCase): + def test_worker_side_includes_all_four_mcp_seed_flags(self): + _, worker_side = _split_sides() + flags = _flag_tokens(worker_side) + for flag in MCP_SEED_FLAGS: + self.assertIn(flag, flags, + "worker 侧缺 %s,仓库 unit 已滞后于生产主机" % flag) + + def test_worker_side_flags_have_no_duplicates(self): + _, worker_side = _split_sides() + flags = _flag_tokens(worker_side) + self.assertEqual(len(flags), len(set(flags)), + "worker 侧 flag 重复:%s" % (flags,)) + + def test_worker_side_flags_are_defined_in_worker_parser(self): + _, worker_side = _split_sides() + with open(WORKER_PATH, encoding="utf-8") as f: + worker_src = f.read() + for flag in _flag_tokens(worker_side): + self.assertIn('add_argument("%s"' % flag, worker_src, + "%s 未在 cloud/worker.py 定义,worker 会报未知参数" + % flag) + + def test_mcp_seed_flags_not_on_scaler_side(self): + scaler_side, _ = _split_sides() + flags = _flag_tokens(scaler_side) + for flag in MCP_SEED_FLAGS: + self.assertNotIn(flag, flags, + "%s 不应出现在 scaler 侧(`--` 之前)" % flag) + + def test_worker_side_keeps_docker_runtime_runsc(self): + _, worker_side = _split_sides() + tokens = worker_side.split() + self.assertIn("--docker-runtime", tokens) + self.assertEqual(tokens[tokens.index("--docker-runtime") + 1], "runsc") + + +if __name__ == "__main__": + unittest.main()