fix: host_deploy stops/starts and health-checks server-2 too (#56)
Co-authored-by: host <host@test.local>
This commit is contained in:
1 parent
84ff968111
commit
c6e633d162
3 files changed
+75
-13
No files matched your search
+2
-1
@@ -85,7 +85,8 @@ deploy/deploy.sh <host> [user] [--dry-run]
|
|||||||
- 备份保留策略:每次部署前在主机上把当前 `/opt/codex-app-server` 打包到
|
- 备份保留策略:每次部署前在主机上把当前 `/opt/codex-app-server` 打包到
|
||||||
`/opt/codex-app-server-backups/codex-app-server-<时间戳>.tar.gz`(排除
|
`/opt/codex-app-server-backups/codex-app-server-<时间戳>.tar.gz`(排除
|
||||||
work/ 目录以免过大),只留最近 3 份,多余的自动删掉。
|
work/ 目录以免过大),只留最近 3 份,多余的自动删掉。
|
||||||
- 健康门:重启后循环 curl `localhost:8765/health` 最多 60 秒,要求
|
- 健康门:重启后循环 curl `localhost:8765/health` 与
|
||||||
|
`localhost:8766/health`(server-2 存在时)最多 60 秒,要求
|
||||||
`ok=true` 且 `backend=postgres` 且 `db_ok=true`;失败时自动恢复刚打的
|
`ok=true` 且 `backend=postgres` 且 `db_ok=true`;失败时自动恢复刚打的
|
||||||
备份包并重启,再验一次。回滚后 health 恢复也算部署失败(退出非零),
|
备份包并重启,再验一次。回滚后 health 恢复也算部署失败(退出非零),
|
||||||
因为新代码没上去;回滚后 health 仍失败则退出非零并明说
|
因为新代码没上去;回滚后 health 仍失败则退出非零并明说
|
||||||
|
|||||||
+27
-9
@@ -20,12 +20,14 @@
|
|||||||
# 4. 备份当前 live 代码(备份名含旧 commit + 时间戳,留最近 5 份;
|
# 4. 备份当前 live 代码(备份名含旧 commit + 时间戳,留最近 5 份;
|
||||||
# 备份 tar 遇运行中文件变化返回 1 时不直接 abort,只要备份文件
|
# 备份 tar 遇运行中文件变化返回 1 时不直接 abort,只要备份文件
|
||||||
# 非空就继续,见 PR #46 教训)。
|
# 非空就继续,见 PR #46 教训)。
|
||||||
# 5. 停 server + worker/autoscaler,再换目录:live -> old,
|
# 5. 停 server(s) + worker/autoscaler,再换目录:live -> old,
|
||||||
# staging -> live,work/ 与 host-config-real.json(如有)原样搬回。
|
# staging -> live,work/ 与 host-config-real.json(如有)原样搬回。
|
||||||
# 换目录代替“原地 rm + 解包”,中途失败不会留半解包的 live。
|
# 换目录代替“原地 rm + 解包”,中途失败不会留半解包的 live。
|
||||||
# 6. 启动并验 health 60 秒:ok=true 且 backend=postgres 且 db_ok=true,
|
# 6. 启动并验 health 60 秒:ok=true 且 backend=postgres 且 db_ok=true,
|
||||||
# 且 live 的 DEPLOYED_COMMIT 等于 expected_commit;health 里
|
# 且 live 的 DEPLOYED_COMMIT 等于 expected_commit;health 里
|
||||||
# 若有 commit 字段且非空,也必须等于 expected_commit。
|
# 若有 commit 字段且非空,也必须等于 expected_commit。
|
||||||
|
# server-2(HA 阶段 2 加的 8766)若存在必须同批停/起、同批验
|
||||||
|
# health,否则 LB 会混跑新旧内存代码(2026-10-27 首推教训)。
|
||||||
# 7. 成功打印 RELEASE_OK commit=... sha=... backup=...,删掉 old 目录
|
# 7. 成功打印 RELEASE_OK commit=... sha=... backup=...,删掉 old 目录
|
||||||
# (备份 tar 已留档);失败则把 old 换回、重启、再验 health,
|
# (备份 tar 已留档);失败则把 old 换回、重启、再验 health,
|
||||||
# 回滚后 health 恢复也算部署失败(新代码未上线),退出非零。
|
# 回滚后 health 恢复也算部署失败(新代码未上线),退出非零。
|
||||||
@@ -87,8 +89,10 @@ python3 -m compileall -q "$STAGING_DIR" || {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_health() {
|
check_health() {
|
||||||
OUT=$(curl -s --max-time 5 http://127.0.0.1:8765/health || true)
|
for port in $HEALTH_PORTS; do
|
||||||
[ -n "$OUT" ] && printf '%s' "$OUT" | EXPECTED_COMMIT="$EXPECTED_COMMIT" python3 -c '
|
OUT=$(curl -s --max-time 5 "http://127.0.0.1:$port/health" || true)
|
||||||
|
if [ -z "$OUT" ]; then return 1; fi
|
||||||
|
if ! printf '%s' "$OUT" | EXPECTED_COMMIT="$EXPECTED_COMMIT" python3 -c '
|
||||||
import json, os, sys
|
import json, os, sys
|
||||||
try:
|
try:
|
||||||
d = json.load(sys.stdin)
|
d = json.load(sys.stdin)
|
||||||
@@ -101,7 +105,11 @@ commit = d.get("commit")
|
|||||||
if commit is not None and commit != os.environ.get("EXPECTED_COMMIT"):
|
if commit is not None and commit != os.environ.get("EXPECTED_COMMIT"):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
'
|
'; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_health() {
|
wait_health() {
|
||||||
@@ -122,6 +130,16 @@ worker_unit() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# server-2(8766)是 HA 阶段 2 加的;unit 存在才纳入停/起与 health
|
||||||
|
# 检查,老主机没有它时退回单机行为。2026-10-27 首推教训:脚本只动
|
||||||
|
# server-1 时,LB 上游的 server-2 仍跑旧内存代码,新旧混跑。
|
||||||
|
SERVER_UNITS="codex-cloud-server"
|
||||||
|
HEALTH_PORTS="8765"
|
||||||
|
if systemctl cat codex-cloud-server-2 >/dev/null 2>&1; then
|
||||||
|
SERVER_UNITS="$SERVER_UNITS codex-cloud-server-2"
|
||||||
|
HEALTH_PORTS="$HEALTH_PORTS 8766"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- 4. 备份 live(名含旧 commit,留 5 份) ------------------------------
|
# --- 4. 备份 live(名含旧 commit,留 5 份) ------------------------------
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
PREV_COMMIT=unknown
|
PREV_COMMIT=unknown
|
||||||
@@ -144,8 +162,8 @@ ls -t "$BACKUP_DIR"/codex-app-server-*.tar.gz 2>/dev/null \
|
|||||||
| tail -n +6 | xargs -r rm -f
|
| tail -n +6 | xargs -r rm -f
|
||||||
|
|
||||||
WORKER_UNIT=$(worker_unit)
|
WORKER_UNIT=$(worker_unit)
|
||||||
echo "停服务并换目录:worker_unit=$WORKER_UNIT"
|
echo "停服务并换目录:worker_unit=$WORKER_UNIT server_units=$SERVER_UNITS"
|
||||||
systemctl stop "$WORKER_UNIT" codex-cloud-server || true
|
systemctl stop "$WORKER_UNIT" $SERVER_UNITS || true
|
||||||
|
|
||||||
# --- 5. 换目录:live -> old,staging -> live,保留文件搬回 --------------
|
# --- 5. 换目录:live -> old,staging -> live,保留文件搬回 --------------
|
||||||
rm -rf "$OLD_DIR" "$PRESERVE_DIR"
|
rm -rf "$OLD_DIR" "$PRESERVE_DIR"
|
||||||
@@ -169,7 +187,7 @@ fi
|
|||||||
|
|
||||||
rollback() {
|
rollback() {
|
||||||
echo "新代码 health 未通过,开始回滚到备份:$BACKUP_FILE" >&2
|
echo "新代码 health 未通过,开始回滚到备份:$BACKUP_FILE" >&2
|
||||||
systemctl stop "$WORKER_UNIT" codex-cloud-server || true
|
systemctl stop "$WORKER_UNIT" $SERVER_UNITS || true
|
||||||
rm -rf "$FAILED_DIR" "$PRESERVE_DIR"
|
rm -rf "$FAILED_DIR" "$PRESERVE_DIR"
|
||||||
mkdir -p "$PRESERVE_DIR"
|
mkdir -p "$PRESERVE_DIR"
|
||||||
for name in work host-config-real.json; do
|
for name in work host-config-real.json; do
|
||||||
@@ -188,7 +206,7 @@ rollback() {
|
|||||||
if id codexsvc >/dev/null 2>&1; then
|
if id codexsvc >/dev/null 2>&1; then
|
||||||
chown -R codexsvc:codexsvc "$APP_DIR"
|
chown -R codexsvc:codexsvc "$APP_DIR"
|
||||||
fi
|
fi
|
||||||
systemctl start codex-cloud-server "$WORKER_UNIT" || true
|
systemctl start $SERVER_UNITS "$WORKER_UNIT" || true
|
||||||
if wait_health; then
|
if wait_health; then
|
||||||
echo "部署失败,已回滚到备份,health 已恢复(新代码未上线)" >&2
|
echo "部署失败,已回滚到备份,health 已恢复(新代码未上线)" >&2
|
||||||
else
|
else
|
||||||
@@ -198,7 +216,7 @@ rollback() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- 6. 启动 + 验 health + 验上线 commit -------------------------------
|
# --- 6. 启动 + 验 health + 验上线 commit -------------------------------
|
||||||
systemctl start codex-cloud-server "$WORKER_UNIT" || true
|
systemctl start $SERVER_UNITS "$WORKER_UNIT" || true
|
||||||
if ! wait_health; then
|
if ! wait_health; then
|
||||||
rollback
|
rollback
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -39,12 +39,17 @@ class HostDeploySim(unittest.TestCase):
|
|||||||
f.write("keep\n")
|
f.write("keep\n")
|
||||||
with open(os.path.join(self.app, "old.py"), "w") as f:
|
with open(os.path.join(self.app, "old.py"), "w") as f:
|
||||||
f.write("x = 0\n")
|
f.write("x = 0\n")
|
||||||
# 假 systemctl:is-active 一律失败(走 cloud-worker 分支),其余成功。
|
# 假 systemctl:is-active 一律失败(走 cloud-worker 分支),其余成功;
|
||||||
|
# cat codex-cloud-server-2 按 FAKE_SERVER2_PRESENT 决定 unit 是否存在。
|
||||||
fake_bin = os.path.join(base, "fakebin")
|
fake_bin = os.path.join(base, "fakebin")
|
||||||
os.makedirs(fake_bin)
|
os.makedirs(fake_bin)
|
||||||
with open(os.path.join(fake_bin, "systemctl"), "w") as f:
|
with open(os.path.join(fake_bin, "systemctl"), "w") as f:
|
||||||
f.write("#!/bin/sh\n"
|
f.write("#!/bin/sh\n"
|
||||||
"echo \"$@\" >> \"$FAKE_SYSTEMCTL_LOG\"\n"
|
"echo \"$@\" >> \"$FAKE_SYSTEMCTL_LOG\"\n"
|
||||||
|
"if [ \"$1\" = \"cat\" ] && [ \"$2\" = \"codex-cloud-server-2\" ]; then\n"
|
||||||
|
" if [ \"$FAKE_SERVER2_PRESENT\" = \"0\" ]; then exit 1; fi\n"
|
||||||
|
" exit 0\n"
|
||||||
|
"fi\n"
|
||||||
"if [ \"$1\" = \"is-active\" ]; then exit 1; fi\n"
|
"if [ \"$1\" = \"is-active\" ]; then exit 1; fi\n"
|
||||||
"exit 0\n")
|
"exit 0\n")
|
||||||
with open(os.path.join(fake_bin, "curl"), "w") as f:
|
with open(os.path.join(fake_bin, "curl"), "w") as f:
|
||||||
@@ -67,6 +72,7 @@ class HostDeploySim(unittest.TestCase):
|
|||||||
self.env["CODEX_DEPLOY_HEALTH_ATTEMPTS"] = "3"
|
self.env["CODEX_DEPLOY_HEALTH_ATTEMPTS"] = "3"
|
||||||
self.env["FAKE_HEALTH_JSON"] = self.health_path
|
self.env["FAKE_HEALTH_JSON"] = self.health_path
|
||||||
self.env["FAKE_SYSTEMCTL_LOG"] = self.log_path
|
self.env["FAKE_SYSTEMCTL_LOG"] = self.log_path
|
||||||
|
self.env["FAKE_SERVER2_PRESENT"] = "1"
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.tmp.cleanup()
|
self.tmp.cleanup()
|
||||||
@@ -86,10 +92,17 @@ class HostDeploySim(unittest.TestCase):
|
|||||||
sha = make_tarball(src, tarball)
|
sha = make_tarball(src, tarball)
|
||||||
return tarball, sha
|
return tarball, sha
|
||||||
|
|
||||||
def run_host(self, tarball, sha, commit):
|
def run_host(self, tarball, sha, commit, extra_env=None):
|
||||||
|
env = dict(self.env)
|
||||||
|
if extra_env:
|
||||||
|
env.update(extra_env)
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
["sh", HOST_SCRIPT, tarball, sha, commit],
|
["sh", HOST_SCRIPT, tarball, sha, commit],
|
||||||
capture_output=True, text=True, env=self.env)
|
capture_output=True, text=True, env=env)
|
||||||
|
|
||||||
|
def systemctl_log(self):
|
||||||
|
with open(self.log_path) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
def live_commit(self):
|
def live_commit(self):
|
||||||
with open(os.path.join(self.app, "DEPLOYED_COMMIT")) as f:
|
with open(os.path.join(self.app, "DEPLOYED_COMMIT")) as f:
|
||||||
@@ -142,6 +155,36 @@ class HostDeploySim(unittest.TestCase):
|
|||||||
with open(os.path.join(self.app, "work", "keep.txt")) as f:
|
with open(os.path.join(self.app, "work", "keep.txt")) as f:
|
||||||
self.assertEqual(f.read().strip(), "keep")
|
self.assertEqual(f.read().strip(), "keep")
|
||||||
|
|
||||||
|
def test_server2_stopped_started_when_present(self):
|
||||||
|
# 2026-10-27 首推教训:server-2 存在时必须同批停/起,
|
||||||
|
# 否则 LB 混跑新旧内存代码。
|
||||||
|
self.set_health({"ok": True, "backend": "postgres", "db_ok": True,
|
||||||
|
"commit": "newcommit"})
|
||||||
|
tarball, sha = self.build_new_tarball()
|
||||||
|
r = self.run_host(tarball, sha, "newcommit")
|
||||||
|
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
||||||
|
log = self.systemctl_log()
|
||||||
|
self.assertIn(
|
||||||
|
"stop codex-cloud-worker codex-cloud-server codex-cloud-server-2",
|
||||||
|
log)
|
||||||
|
self.assertIn(
|
||||||
|
"start codex-cloud-server codex-cloud-server-2 codex-cloud-worker",
|
||||||
|
log)
|
||||||
|
|
||||||
|
def test_server2_skipped_when_unit_absent(self):
|
||||||
|
# 老主机没有 server-2 unit 时退回单机行为,不报错、不提它。
|
||||||
|
self.set_health({"ok": True, "backend": "postgres", "db_ok": True,
|
||||||
|
"commit": "newcommit"})
|
||||||
|
tarball, sha = self.build_new_tarball()
|
||||||
|
r = self.run_host(tarball, sha, "newcommit",
|
||||||
|
extra_env={"FAKE_SERVER2_PRESENT": "0"})
|
||||||
|
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
||||||
|
# 探测行 cat 本身会提到 server-2(探测失败才判定不存在),
|
||||||
|
# 所以断言停/起行里没有它,而不是全日志没有这个词。
|
||||||
|
log = self.systemctl_log()
|
||||||
|
self.assertIn("stop codex-cloud-worker codex-cloud-server\n", log)
|
||||||
|
self.assertIn("start codex-cloud-server codex-cloud-worker\n", log)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user