fix(dev): idempotent Alembic chain for squashed 0001 + clearer dev scripts
- Make migrations 0002–0008 skip schema changes already applied when 0001_initial creates current ORM (rename segments column, timeline FK, memoir phase flags, drop content_tsv, eval_* tables). - development.sh / internal-eval.sh: surface Alembic stderr, warn on docker-style DB hosts, TCP port checks without lsof, verify Uvicorn listens before claiming started.
This commit is contained in:
@@ -163,6 +163,122 @@ wait_postgres_ready() {
|
||||
return 1
|
||||
}
|
||||
|
||||
get_effective_database_url() {
|
||||
if [[ -n "${DATABASE_URL:-}" ]]; then
|
||||
printf '%s\n' "${DATABASE_URL}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -f "${ROOT_DIR}/.env" ]]; then
|
||||
local line
|
||||
line="$(sed -n 's/^DATABASE_URL=//p' "${ROOT_DIR}/.env" | sed -n '1p')"
|
||||
line="${line%\"}"
|
||||
line="${line#\"}"
|
||||
line="${line%\'}"
|
||||
line="${line#\'}"
|
||||
if [[ -n "${line}" ]]; then
|
||||
printf '%s\n' "${line}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
warn_database_url_host_pitfall() {
|
||||
local database_url
|
||||
local host
|
||||
|
||||
if ! database_url="$(get_effective_database_url)"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "${database_url}" =~ @([^:/?#]+) ]]; then
|
||||
host="${BASH_REMATCH[1]}"
|
||||
case "${host}" in
|
||||
postgres|db|postgres-dev|postgresql)
|
||||
print_warn "检测到 DATABASE_URL 主机为 ${host};在宿主机执行 Alembic/uvicorn 时通常应使用 localhost"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
print_alembic_failure_hint() {
|
||||
local log_file="$1"
|
||||
local log_output
|
||||
|
||||
log_output="$(sed -n '1,200p' "${log_file}")"
|
||||
if [[ "${log_output}" == *'could not translate host name "postgres"'* ]] || [[ "${log_output}" == *"Name or service not known"* ]]; then
|
||||
print_warn "看起来 DATABASE_URL 指向了容器内主机名;在宿主机运行时请改用 localhost:5432"
|
||||
elif [[ "${log_output}" == *"Connection refused"* ]] || [[ "${log_output}" == *"could not connect to server"* ]]; then
|
||||
print_warn "PostgreSQL 连接被拒绝;请确认容器已启动且 DATABASE_URL 与 docker-compose.dev.yml 暴露端口一致"
|
||||
elif [[ "${log_output}" == *"password authentication failed"* ]]; then
|
||||
print_warn "PostgreSQL 用户名或密码不匹配;请核对 .env.development 中的 DATABASE_URL"
|
||||
elif [[ "${log_output}" == *"No such file or directory"* ]] || [[ "${log_output}" == *"can't open file"* ]]; then
|
||||
print_warn "Alembic 依赖的文件或工作目录可能不正确;请确认在 api/ 目录运行脚本"
|
||||
fi
|
||||
}
|
||||
|
||||
is_port_listening() {
|
||||
local port="$1"
|
||||
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
if [[ -x "${PYTHON_BIN}" ]]; then
|
||||
"${PYTHON_BIN}" - "${port}" <<'PY' >/dev/null 2>&1
|
||||
import socket
|
||||
import sys
|
||||
|
||||
sock = socket.socket()
|
||||
sock.settimeout(0.2)
|
||||
try:
|
||||
sock.connect(("127.0.0.1", int(sys.argv[1])))
|
||||
except OSError:
|
||||
raise SystemExit(1)
|
||||
finally:
|
||||
sock.close()
|
||||
raise SystemExit(0)
|
||||
PY
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_tcp_listener() {
|
||||
local pid="$1"
|
||||
local port="$2"
|
||||
local timeout="${3:-8}"
|
||||
local waited=0
|
||||
|
||||
while (( waited < timeout )); do
|
||||
if is_port_listening "${port}"; then
|
||||
return 0
|
||||
fi
|
||||
if ! is_pid_alive "${pid}"; then
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
ensure_background_process_alive() {
|
||||
local name="$1"
|
||||
local pid="$2"
|
||||
|
||||
sleep 1
|
||||
if ! is_pid_alive "${pid}"; then
|
||||
print_err "${name} 启动后立即退出,请查看上方日志"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_venv() {
|
||||
print_header "检查 Python 虚拟环境"
|
||||
|
||||
@@ -199,16 +315,25 @@ check_env_file() {
|
||||
print_warn "未找到 .env,应用可能因缺少配置启动失败"
|
||||
else
|
||||
print_ok "检测到 .env"
|
||||
warn_database_url_host_pitfall
|
||||
fi
|
||||
}
|
||||
|
||||
run_migrations() {
|
||||
print_header "执行数据库迁移"
|
||||
cd "${ROOT_DIR}"
|
||||
if uv run alembic upgrade head 2>/dev/null; then
|
||||
local log_file
|
||||
log_file="$(mktemp -t life-echo-alembic.XXXXXX.log)"
|
||||
|
||||
if uv run alembic upgrade head >"${log_file}" 2>&1; then
|
||||
print_ok "Alembic 迁移已就绪"
|
||||
rm -f "${log_file}"
|
||||
else
|
||||
print_warn "Alembic 迁移失败(可能数据库未启动或 DATABASE_URL 未配置),应用启动可能失败"
|
||||
print_alembic_failure_hint "${log_file}"
|
||||
print_warn "Alembic 输出(最近 40 行):"
|
||||
tail -n 40 "${log_file}"
|
||||
rm -f "${log_file}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -216,12 +341,10 @@ start_services() {
|
||||
print_header "启动 FastAPI 和 Celery"
|
||||
cd "${ROOT_DIR}"
|
||||
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
if lsof -nP -iTCP:"${API_PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
|
||||
print_err "端口 ${API_PORT} 已被占用,无法启动新的 Uvicorn。"
|
||||
print_err "请先结束占用进程,例如: lsof -nP -iTCP:${API_PORT} -sTCP:LISTEN"
|
||||
exit 1
|
||||
fi
|
||||
if is_port_listening "${API_PORT}"; then
|
||||
print_err "端口 ${API_PORT} 已被占用,无法启动新的 Uvicorn。"
|
||||
print_err "请先结束占用进程,例如: lsof -nP -iTCP:${API_PORT} -sTCP:LISTEN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 迁移由 main.py 在启动时执行;排除 alembic 目录与 alembic.ini,避免编辑迁移时触发整进程重载
|
||||
@@ -230,10 +353,30 @@ start_services() {
|
||||
--reload-exclude 'alembic.ini' \
|
||||
--host "${API_HOST}" --port "${API_PORT}" &
|
||||
API_PID=$!
|
||||
print_ok "FastAPI 已启动 (PID: ${API_PID})"
|
||||
local api_start_status=0
|
||||
if wait_for_tcp_listener "${API_PID}" "${API_PORT}" 8; then
|
||||
api_start_status=0
|
||||
else
|
||||
api_start_status=$?
|
||||
fi
|
||||
|
||||
case "${api_start_status}" in
|
||||
0)
|
||||
print_ok "FastAPI 已启动 (PID: ${API_PID})"
|
||||
;;
|
||||
1)
|
||||
print_err "FastAPI 启动失败,进程已退出;请查看上方 Uvicorn 日志"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
print_err "FastAPI 进程仍存活,但端口 ${API_PORT} 未在预期时间内开始监听"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
"${CELERY_BIN}" -A app.tasks.celery_app worker --loglevel=info --pool="${CELERY_POOL}" &
|
||||
CELERY_PID=$!
|
||||
ensure_background_process_alive "Celery" "${CELERY_PID}"
|
||||
print_ok "Celery 已启动 (PID: ${CELERY_PID})"
|
||||
|
||||
echo
|
||||
|
||||
Reference in New Issue
Block a user