From 5578cf137d9196f546d5367692da3fbb823501a1 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Mon, 19 Oct 2026 13:23:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=8D=E6=A0=B8=20R-b4=EF=BC=88prod.sh?= =?UTF-8?q?=E3=80=81Docker=20=E5=85=A5=E5=8F=A3=E8=AF=BB=E4=B8=8D=E5=88=B0?= =?UTF-8?q?=20.env=20=E9=87=8C=E7=9A=84=E9=85=8D=E7=BD=AE=EF=BC=89?= =?UTF-8?q?=EF=BC=9A=E6=96=B0=E5=A2=9E=20python=20-m=20app.cluster=20worke?= =?UTF-8?q?rs=E2=80=94=E2=80=94=E7=85=A7=E5=90=8E=E7=AB=AF=E8=87=AA?= =?UTF-8?q?=E5=B7=B1=E7=9A=84=E9=85=8D=E7=BD=AE=EF=BC=88=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F=E5=92=8C=20.env=20=E4=B8=80=E8=B5=B7?= =?UTF-8?q?=E8=AF=BB=EF=BC=89=E8=AF=B4=E8=B5=B7=E5=87=A0=E4=B8=AA=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E8=BF=9B=E7=A8=8B=EF=BC=8CSQLite=20=E8=80=8C=E5=A4=A7?= =?UTF-8?q?=E4=BA=8E=201=20=E6=97=B6=E7=85=A7=E5=AE=9E=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E3=80=81=E9=80=80=E5=87=BA=E7=A0=81=202=EF=BC=9Bprod.sh=20?= =?UTF-8?q?=E5=8F=AA=E5=9C=A8=E5=91=BD=E4=BB=A4=E8=A1=8C=E7=BB=99=E4=BA=86?= =?UTF-8?q?=20--workers=20=E6=97=B6=E5=86=99=E8=BF=9B=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F=EF=BC=8C=E8=BF=9B=E7=A8=8B=E6=95=B0=E4=B8=80?= =?UTF-8?q?=E5=BE=8B=E9=97=AE=E5=90=8E=E7=AB=AF=EF=BC=8CDocker=20=E7=9A=84?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E5=90=8C=E6=A0=B7=E9=97=AE=E5=AE=83=E3=80=82?= =?UTF-8?q?=E8=A1=A5=E6=B5=8B=E8=AF=95=EF=BC=9ASQLite=20=E8=80=8C=E5=A4=A7?= =?UTF-8?q?=E4=BA=8E=201=20=E6=8B=92=E7=BB=9D=E3=80=81PostgreSQL=20?= =?UTF-8?q?=E7=85=A7=E7=BB=99=E7=9A=84=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 3 ++- backend/app/cluster/__main__.py | 24 ++++++++++++++++++++++++ backend/tests/test_cluster.py | 19 +++++++++++++++++++ scripts/prod.sh | 16 ++++++++-------- 4 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 backend/app/cluster/__main__.py diff --git a/Dockerfile b/Dockerfile index 53bf9fbc..c3bd9ba7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -102,4 +102,5 @@ LABEL org.opencontainers.image.title="AgentLab" \ # 工作进程数照 AGENTLAB_WORKERS(缺省 1)。大于 1 要应用库是 PostgreSQL(AGENTLAB_DATABASE_URL):SQLite 只支持单进程, # 服务启动时拒绝第二个进程(app/cluster/guard.py)。exec:uvicorn 直接收 SIGTERM,走完整的优雅退出 -CMD ["sh", "-c", "exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers \"${AGENTLAB_WORKERS:-1}\""] +# 进程数问后端(python -m app.cluster workers,环境变量和 .env 一起读;SQLite 而大于 1 时照实报错、不起) +CMD ["sh", "-c", "n=\"$(python -m app.cluster workers)\" || exit 2; exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers \"$n\""] diff --git a/backend/app/cluster/__main__.py b/backend/app/cluster/__main__.py new file mode 100644 index 00000000..04824c79 --- /dev/null +++ b/backend/app/cluster/__main__.py @@ -0,0 +1,24 @@ +"""python -m app.cluster workers:照后端自己的配置(环境变量和 .env 一起读)说起几个工作进程,scripts/prod.sh 和 Docker 的入口按它给 +uvicorn --workers(复核 R-b4:写在 .env 里的 AGENTLAB_DATABASE_URL、AGENTLAB_WORKERS,shell 脚本自己读不到)。应用库是 SQLite 而进程数 +大于 1 时照实报错、退出码 2(SQLite 只支持单进程)。""" +from __future__ import annotations + +import sys + + +def main(argv: list[str]) -> int: + if argv[:1] != ["workers"]: + print("用法:python -m app.cluster workers", file=sys.stderr) + return 64 + from app.cluster.guard import SINGLE_PROCESS + from app.core.config import settings + + if settings.db_dialect == "sqlite" and settings.workers > 1: + print(SINGLE_PROCESS.format(dir=settings.data_dir, hint=f"AGENTLAB_WORKERS={settings.workers}"), file=sys.stderr) + return 2 + print(settings.workers) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/backend/tests/test_cluster.py b/backend/tests/test_cluster.py index 870742a9..c347f658 100644 --- a/backend/tests/test_cluster.py +++ b/backend/tests/test_cluster.py @@ -481,3 +481,22 @@ async def test_an_idle_single_process_writes_nothing_for_coordination(monkeypatc assert cluster.leading(), "只有本进程:直接当班" finally: await cluster.stop() + + +def test_the_worker_count_comes_from_the_backend_settings(): + """复核 R-b4:prod.sh、Docker 的入口按 python -m app.cluster workers 起几个进程(后端自己读环境变量和 .env);SQLite 而大于 1 时 + 照实报错、退出码 2。""" + backend = Path(__file__).resolve().parents[1] + base = {k: v for k, v in os.environ.items() if not k.startswith("AGENTLAB_")} + base.update(PYTHONPATH=str(backend), AGENTLAB_DATA_DIR=os.environ["AGENTLAB_DATA_DIR"], + AGENTLAB_SECRET_KEY="test-only-not-a-real-key") + + def ask(**env: str) -> subprocess.CompletedProcess: + return subprocess.run([sys.executable, "-m", "app.cluster", "workers"], cwd=str(backend), env={**base, **env}, + capture_output=True, text=True, timeout=60) + + sqlite = ask(AGENTLAB_DATABASE_URL="", AGENTLAB_WORKERS="3") + assert sqlite.returncode == 2 and "只支持单进程" in sqlite.stderr, sqlite.stderr[-1000:] + assert ask(AGENTLAB_DATABASE_URL="", AGENTLAB_WORKERS="1").stdout.strip() == "1" + pg = ask(AGENTLAB_DATABASE_URL="postgresql://u:p@db.internal:5432/agentlab", AGENTLAB_WORKERS="3") + assert pg.returncode == 0 and pg.stdout.strip() == "3", pg.stderr[-1000:] diff --git a/scripts/prod.sh b/scripts/prod.sh index fe688b39..c3e9be81 100755 --- a/scripts/prod.sh +++ b/scripts/prod.sh @@ -26,6 +26,7 @@ USAGE HOST="127.0.0.1" PORT="${AGENTLAB_PORT:-8000}" WORKERS="${AGENTLAB_WORKERS:-1}" +WORKERS_CLI="" BUILD="auto" while [ $# -gt 0 ]; do case "$1" in @@ -36,8 +37,8 @@ while [ $# -gt 0 ]; do --port=*) PORT="${1#--port=}" ;; --workers) [ $# -ge 2 ] || { echo "--workers 后面要跟进程数"; echo; usage; exit 2; } - WORKERS="$2"; shift ;; - --workers=*) WORKERS="${1#--workers=}" ;; + WORKERS="$2"; WORKERS_CLI=1; shift ;; + --workers=*) WORKERS="${1#--workers=}"; WORKERS_CLI=1 ;; --build) BUILD="always" ;; --no-build) BUILD="never" ;; -h|--help) usage; exit 0 ;; @@ -57,12 +58,7 @@ esac if [ "$WORKERS" -lt 1 ]; then echo "进程数至少是 1:$WORKERS"; exit 2 fi -# SQLite 只支持单进程(服务自己也会拒绝启动第二个进程,app/cluster/guard.py):在这里先说清楚,免得 uvicorn 一遍遍重起被拒的进程 -if [ "$WORKERS" -gt 1 ] && [ -z "${AGENTLAB_DATABASE_URL:-}" ]; then - echo "应用库是 SQLite 时只支持单进程:要 --workers $WORKERS,先设 AGENTLAB_DATABASE_URL 换成 PostgreSQL(README「用 PostgreSQL」「多进程」)" - exit 2 -fi -export AGENTLAB_WORKERS="$WORKERS" +# 命令行给了 --workers 才写进环境变量(见下面问后端的那一句);没给的话照后端自己的配置(环境变量和 .env) ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" DIST="$ROOT/frontend/dist" @@ -169,6 +165,10 @@ fi echo "==> 启动 $URLS" cd "$ROOT/backend" +# 起几个进程问后端(python -m app.cluster workers):它照环境变量和 .env 一起读应用库、进程数——写在 .env 里的 AGENTLAB_DATABASE_URL、 +# AGENTLAB_WORKERS 这里也算数;应用库是 SQLite 而进程数大于 1 时它照实报错(SQLite 只支持单进程),这里不起 +[ -n "$WORKERS_CLI" ] && export AGENTLAB_WORKERS="$WORKERS" +WORKERS="$("$PY_BIN" -m app.cluster workers)" || exit 2 # exec:让 uvicorn 直接收 Ctrl-C / SIGTERM,一个信号就走完整的优雅退出(收尾运行、关沙箱)。 # 要是包一层 shell 再转发,终端的 Ctrl-C 会让 uvicorn 收到两次信号,第二次直接强退、跳过收尾 # 多进程(--workers 大于 1,PostgreSQL):uvicorn 起几个工作进程共用这个端口,各自在库里登记、续租约(app/cluster)