diff --git a/backend/tests/fake_docker.py b/backend/tests/fake_docker.py index cbe2db54..8dc02df4 100644 --- a/backend/tests/fake_docker.py +++ b/backend/tests/fake_docker.py @@ -11,6 +11,7 @@ FAKE_COPY_FAIL 成功或失败。 FAKE_PSQL_FAIL=1 docker exec … psql 失败 FAKE_UNHEALTHY_ENV=<变量名> 带这个环境变量(-e)起的容器健康检查不过 FAKE_UNHEALTHY_IMAGE=<镜像> 用这个镜像起的容器健康检查不过 + FAKE_RESTARTING_IMAGE=<镜像> 用这个镜像起的容器一直在重启(restarting) """ from __future__ import annotations @@ -272,14 +273,29 @@ def main(argv: list[str]) -> int: fail(f'docker: Error response from daemon: Conflict. The container name "/{name}" is already in use.', 125) for vol in opts["volumes"]: state["volumes"].setdefault(vol, {}) + flapping = os.environ.get("FAKE_RESTARTING_IMAGE") == image state["containers"][name] = { - "image": image, "state": "running", "env": opts["env"], "labels": opts["labels"], - "mounts": opts["mounts"], "ports": opts["ports"], "network": network or "bridge", + "image": image, "state": "restarting" if flapping else "running", "env": opts["env"], + "labels": opts["labels"], "mounts": opts["mounts"], "ports": opts["ports"], "network": network or "bridge", + "restart": opts.get("--restart", "no"), } save(state) print("f" * 64) return 0 + if cmd == "update": + name = args[-1] + c = state["containers"].get(name) + if c is None: + fail(f"Error response from daemon: No such container: {name}") + for i, a in enumerate(args[:-1]): + if a.startswith("--restart="): + c["restart"] = a.split("=", 1)[1] + elif a == "--restart": + c["restart"] = args[i + 1] + save(state) + return 0 + if cmd in ("start", "stop", "restart"): names = [a for a in args if not a.startswith("-") and not a.isdigit()] for name in names: diff --git a/backend/tests/test_deploy_script.py b/backend/tests/test_deploy_script.py index a7f5e306..a179f475 100644 --- a/backend/tests/test_deploy_script.py +++ b/backend/tests/test_deploy_script.py @@ -560,6 +560,30 @@ def test_a_failed_copy_leaves_the_sqlite_service_as_it_was_and_rerunning_works(d assert deploy.mode() == "builtin" +def test_a_database_that_keeps_restarting_is_stopped_and_left_without_a_restart_policy(deploy): + """复核 R-d2:库的镜像起不来、容器一遍遍重启(restarting)时,回退要真停下它、关掉重启策略;再起时恢复。""" + _sqlite_deployment(deploy) + (deploy.pkg / "postgres-image.tar").write_text(f"alpine:latest \n{PG_IMAGE} \n") + out = deploy("migrate-to-postgres", ok=False, DEPLOY_POSTGRES_IMAGE="alpine:latest", FAKE_RESTARTING_IMAGE="alpine:latest") + assert "没有切换,仍用 SQLite" in out.stderr + pg = deploy.containers()[PG] + assert pg["state"] == "exited" and pg["restart"] == "no", "停下了、不会再被拉起来" + calls = deploy.calls() + assert index_of(calls, lambda a: a == ["update", "--restart=no", PG]) < index_of(calls, is_cmd("stop", PG)) + # 换回能起的镜像重跑:库的容器按新镜像重建,重启策略照常 + deploy("migrate-to-postgres") + assert deploy.containers()[PG]["restart"] == "unless-stopped" and deploy.mode() == "builtin" + # ./deploy.sh stop 同一个函数:在重启的也停 + state = deploy.state() + state["containers"][PG]["state"] = "restarting" + deploy.state_file.write_text(json.dumps(state)) + deploy("stop") + assert deploy.containers()[PG]["state"] == "exited" + deploy("start") + calls = deploy.calls() + assert index_of(calls, lambda a: a == ["update", "--restart", "unless-stopped", PG]) < index_of(calls, is_cmd("start", PG)) + + def test_a_stopped_service_stays_stopped_after_a_failed_migration(deploy): _sqlite_deployment(deploy) deploy("stop") diff --git a/deploy/deploy.sh b/deploy/deploy.sh index a921c824..41d4497d 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -873,6 +873,8 @@ start_pg() { fi if [ "$(container_state "$POSTGRES_CONTAINER")" != running ]; then say "启动内置 PostgreSQL(${POSTGRES_CONTAINER})" + # 回退时关掉过重启策略(stop_pg --no-restart),起的时候恢复 + docker update --restart unless-stopped "$POSTGRES_CONTAINER" >/dev/null 2>&1 || true docker start "$POSTGRES_CONTAINER" >/dev/null || die "内置 PostgreSQL 的容器起不来,见上面 docker 的报错" fi else @@ -885,9 +887,15 @@ start_pg() { fi } +# 在跑的、在反复重启的(镜像起不来、--restart unless-stopped 一遍遍拉)都停。--no-restart:再关掉重启策略(回退时用: +# 一个起不来的库容器不该在后台一直重启),下次 start_pg 起它时恢复 stop_pg() { container_exists "$POSTGRES_CONTAINER" || return 0 - [ "$(container_state "$POSTGRES_CONTAINER")" = running ] || return 0 + [ "${1:-}" != --no-restart ] || docker update --restart=no "$POSTGRES_CONTAINER" >/dev/null 2>&1 || true + case "$(container_state "$POSTGRES_CONTAINER")" in + running|restarting) ;; + *) return 0 ;; + esac say "停止内置 PostgreSQL(${POSTGRES_CONTAINER},数据保留)" docker stop "$POSTGRES_CONTAINER" >/dev/null } @@ -1040,7 +1048,7 @@ migrate_rollback() { docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true fi fi - stop_pg + stop_pg --no-restart die "没有切换,仍用 SQLite(数据目录里的 agentlab.db 没动过)。照上面的报错处理好之后重新运行 ./deploy.sh migrate-to-postgres, 重复执行是安全的:内置库里这次导了一半的会先清空再导" }