From 0b8bbebf6a6a03d6b83db520cdb8f182fb1588c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=95=E6=96=B0=E9=9B=A8?= Date: Mon, 9 Feb 2026 11:52:11 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E5=A2=9E=E5=8A=A0=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E6=8E=A8=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/Dockerfile | 8 +++- server/docker-entrypoint.sh | 85 +++++++++++++++++++++++++++++++++++++ server/run.sh | 68 +++++++++++++++++++++++++++-- 3 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 server/docker-entrypoint.sh diff --git a/server/Dockerfile b/server/Dockerfile index 4da3e8a..e70ecd7 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -20,6 +20,9 @@ RUN python -m pip install -U pip \ COPY app /app/app COPY alembic /app/alembic COPY alembic.ini /app/alembic.ini +COPY docker-entrypoint.sh /app/docker-entrypoint.sh + +RUN chmod +x /app/docker-entrypoint.sh EXPOSE 8000 @@ -29,4 +32,7 @@ EXPOSE 8000 # - 参考文档:server/README.md # 生产镜像默认不开启 reload -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +# 默认行为:只启动 API(与之前一致) +# 如需同时启动定时推送相关进程(Celery Worker/Beat),可在运行时注入: +# -e START_ALL=1 +ENTRYPOINT ["/app/docker-entrypoint.sh"] diff --git a/server/docker-entrypoint.sh b/server/docker-entrypoint.sh new file mode 100644 index 0000000..4471832 --- /dev/null +++ b/server/docker-entrypoint.sh @@ -0,0 +1,85 @@ +#!/bin/sh +set -eu + +# 容器入口: +# - 默认只启动 API(与原 Dockerfile 行为一致) +# - 如需测试“定时推送”,可额外启动 Celery Worker + Beat +# +# 环境变量: +# - START_API=1|0(默认 1) +# - START_WORKER=1|0(默认 0) +# - START_BEAT=1|0(默认 0) +# - START_ALL=1(等价于 START_WORKER=1 + START_BEAT=1) +# - HOST / PORT(API 监听地址,默认 0.0.0.0:8000) + +log() { + echo "[entrypoint] $*" +} + +START_API="${START_API:-1}" +START_WORKER="${START_WORKER:-0}" +START_BEAT="${START_BEAT:-0}" + +if [ "${START_ALL:-0}" = "1" ]; then + START_WORKER="1" + START_BEAT="1" +fi + +# Beat 只负责“投递任务到队列”,真正执行仍需要 Worker;避免误配。 +if [ "$START_BEAT" = "1" ] && [ "$START_WORKER" != "1" ]; then + log "提示:已启用 START_BEAT=1,自动同时启用 START_WORKER=1(否则队列无人执行)。" + START_WORKER="1" +fi + +PIDS="" + +stop_children() { + # 温和退出 + for pid in $PIDS; do + kill -TERM "$pid" >/dev/null 2>&1 || true + done +} + +on_term() { + log "收到退出信号,正在停止子进程..." + stop_children + # 等待子进程退出,避免残留 + wait >/dev/null 2>&1 || true + exit 0 +} + +trap on_term INT TERM + +if [ "$START_WORKER" = "1" ]; then + log "启动 Celery Worker:celery -A app.worker:celery_app worker -l info" + celery -A app.worker:celery_app worker -l info & + PIDS="$PIDS $!" +fi + +if [ "$START_BEAT" = "1" ]; then + log "启动 Celery Beat:celery -A app.worker:celery_app beat -l info" + celery -A app.worker:celery_app beat -l info & + PIDS="$PIDS $!" +fi + +if [ "$START_API" = "1" ]; then + HOST="${HOST:-0.0.0.0}" + PORT="${PORT:-8000}" + log "启动 API:uvicorn app.main:app --host $HOST --port $PORT" + uvicorn app.main:app --host "$HOST" --port "$PORT" & + API_PID="$!" + PIDS="$PIDS $API_PID" + + # 以 API 生命周期为准:API 退出则容器退出,并清理其他进程 + wait "$API_PID" + CODE="$?" + log "API 已退出(code=$CODE),正在停止其他进程..." + stop_children + wait >/dev/null 2>&1 || true + exit "$CODE" +fi + +# 未启动 API:就阻塞等待其他进程(一般用于仅跑 worker/beat 的容器) +log "未启动 API,等待后台进程..." +wait + diff --git a/server/run.sh b/server/run.sh index 86905fd..e859f6b 100755 --- a/server/run.sh +++ b/server/run.sh @@ -11,24 +11,28 @@ set -euo pipefail # ./run.sh --env prod # 使用 .env.prod(若存在且可被 source) # ./run.sh --port 9000 # 改端口 # ./run.sh --no-reload # 关闭热更新 +# ./run.sh --with-worker --with-beat # 同时启动 Celery Worker + Beat(用于定时推送) # ./run.sh --install-only # 只安装依赖,不启动 usage() { cat <<'EOF' 用法: - ./run.sh [--env dev|prod] [--host 0.0.0.0] [--port 8000] [--no-reload] [--skip-install] [--install-only] + ./run.sh [--env dev|prod] [--host 0.0.0.0] [--port 8000] [--no-reload] [--with-worker] [--with-beat] [--skip-install] [--install-only] 参数: --env dev|prod 优先尝试加载 .env.dev 或 .env.prod(如果存在)。 --host uvicorn host(默认 0.0.0.0) --port uvicorn port(默认 8000) --no-reload 关闭 uvicorn --reload + --with-worker 同时启动 Celery Worker(处理异步/ETA 任务) + --with-beat 同时启动 Celery Beat(定时调度,例如每日生成推送排程) --skip-install 跳过依赖安装(默认会安装/更新 requirements.txt) --install-only 只安装依赖,不启动服务 -h, --help 显示帮助 说明: - 若你的 .env.* 不是 shell 可 source 的格式(例如包含空格/特殊字符未加引号),建议改成 KEY=value 形式。 + - 仅启动 API 并不会生成 `push_send_log`;要测试“定时推送”,需要 Beat 调度 `tasks.push.generate_daily_schedule`,并由 Worker 执行后续 ETA 任务。 - 启动后访问: /healthz 健康检查 /docs OpenAPI 文档 @@ -45,6 +49,8 @@ PORT="8000" RELOAD="1" SKIP_INSTALL="0" INSTALL_ONLY="0" +WITH_WORKER="0" +WITH_BEAT="0" while [[ $# -gt 0 ]]; do case "$1" in @@ -64,6 +70,14 @@ while [[ $# -gt 0 ]]; do RELOAD="0" shift 1 ;; + --with-worker) + WITH_WORKER="1" + shift 1 + ;; + --with-beat) + WITH_BEAT="1" + shift 1 + ;; --skip-install) SKIP_INSTALL="1" shift 1 @@ -99,6 +113,15 @@ if [[ -f "$ENV_FILE" ]]; then set +a fi +# 让 API/Celery 统一使用同一个 APP_ENV(影响 Redis key 前缀、定时任务配置等) +export APP_ENV="${APP_ENV:-$ENV_NAME}" + +# Beat 只负责“投递任务到队列”,真正执行仍需要 Worker;这里自动补齐,避免误用。 +if [[ "$WITH_BEAT" == "1" && "$WITH_WORKER" == "0" ]]; then + echo "提示:已启用 --with-beat,自动同时启用 --with-worker(否则队列无人执行)。" + WITH_WORKER="1" +fi + # 选择 python 命令(优先 python3) PY_BIN="" if command -v python3 >/dev/null 2>&1; then @@ -140,6 +163,45 @@ if [[ "$RELOAD" == "1" ]]; then UVICORN_ARGS+=(--reload) fi -echo "启动服务:uvicorn ${UVICORN_ARGS[*]}" -exec uvicorn "${UVICORN_ARGS[@]}" +if [[ "$WITH_WORKER" == "0" && "$WITH_BEAT" == "0" ]]; then + echo "启动服务:uvicorn ${UVICORN_ARGS[*]}" + exec uvicorn "${UVICORN_ARGS[@]}" +fi + +PIDS=() + +cleanup() { + # 避免重复清理导致脚本退出码被覆盖 + set +e + if [[ ${#PIDS[@]} -gt 0 ]]; then + echo "" + echo "正在停止后台进程..." + # 先尝试温和退出 + for pid in "${PIDS[@]}"; do + kill -TERM "$pid" >/dev/null 2>&1 || true + done + # 等待一点时间,再强制杀掉仍存活的(防止残留) + sleep 1 + for pid in "${PIDS[@]}"; do + kill -KILL "$pid" >/dev/null 2>&1 || true + done + fi +} + +trap cleanup EXIT INT TERM + +if [[ "$WITH_WORKER" == "1" ]]; then + echo "启动 Celery Worker:celery -A app.worker:celery_app worker -l info" + celery -A app.worker:celery_app worker -l info & + PIDS+=("$!") +fi + +if [[ "$WITH_BEAT" == "1" ]]; then + echo "启动 Celery Beat:celery -A app.worker:celery_app beat -l info" + celery -A app.worker:celery_app beat -l info & + PIDS+=("$!") +fi + +echo "启动服务:uvicorn ${UVICORN_ARGS[*]}" +uvicorn "${UVICORN_ARGS[@]}"