Files
FinDataHub f43b442bf1 fix: 修连接/句柄泄漏 + 修脚本版本契约 + 去掉 stk_factor 假启用
P1-13 连接池位泄漏(Scripts/bin/backfill.py)
---------------------------------------------
三处 `get_conn()` 都把 `put_conn(conn)` 放在**成功路径**,异常时连接永不归还:
  get_source_map / get_existing_ids / insert_news
连接池有容量上限,异常路径不归还即永久占住池位,反复失败会耗尽池,
表现为后续所有取连接的操作阻塞(与 db_pool 取连接超时修复是同一类问题)。
修法:全部改为 try/finally 无条件归还。
**自查发现并修正一处残留缺陷**:`finally` 里写成 `cur.close(); put_conn(conn)`,
若 close 本身抛错(连接已断时常见)仍会跳过归还 → 改为嵌套 try/finally,
归还作为无条件最后一步。回归用例正是靠这一点抓出来的。

P1-13 后端 stderr 句柄泄漏(Scripts/bin/newsflash_collect.py)
--------------------------------------------------------------
`stderr=open(BACKEND_STDERR_LOG, "w")` 把 open() 返回值直接丢给 Popen,
父进程这份句柄没有任何引用可关 → 每启动一次后端泄漏一个 fd。
改为先存局部变量、Popen 后立即 close(子进程已持有独立 fd 副本,写入不受影响)。

P2-16 bundle-python.sh 版本契约冲突
------------------------------------
`uv venv --python 3.14` 与 .python-version(=3.12) 及所有 pyproject 的
requires-python=">=3.12,<3.13" 冲突。改为读取 .python-version 作为单一事实来源。
同时 `set -e` → `set -eo pipefail`:原先 `uv venv ... | tail -3` 会吞掉失败退出码,
与文件头注释声称的"确保失败中止"相矛盾。
并注明该脚本**当前不在构建链路内**(build-app.sh 未调用,全仓仅文档提及),
避免后人误以为它是打包的一个阶段。

stk_factor / stk_factor_pro 假启用
----------------------------------
config.yaml 与 config/default.yaml 写 `true`,但 fetcher.py:306 对该表显式跳过
(接口限 5 次/天,无法批量)。改为 false 并注明原因。
另外查明:`tables:` 开关只在「未显式指定表」时才生效(fetcher.py:239-243),
调度脚本总是显式传参 → **不能以 config 为准判断某项是否在跑**。

回归
----
Scripts/bin 213 passed(新增 6 条连接归还用例,已证伪:
旧写法下 `test_get_source_map_returns_conn_on_error` 报 assert 0 == 1)、
core 121、tushare 174、akshare 183、kuaixun 407 —— 合计 1098 passed。

本次一并完成的非代码清理
------------------------
- P2-17 陈旧可写 venv:`~/Library/Application Support/FinDataHub/venvs/` 下
  akshare_fetcher(3.14.6, 308M) 与 kuaixun-service(3.14.5, 156M) 两个副本
  来自 v1.4.7 时期,与 bundle 的 3.12 不符、且每次启动刷 WARN。
  已移出(464M → 12K,保留 venv_versions.json)。应用日志本身提示
  "如需 pip install,请在设置中重建可写副本",故可安全清理。
- P2-21 构建残留:findatahub-core/build、findatahub_core.egg-info、
  findatahub_client.egg-info 已移出(移出前先 mv 到 /tmp 再后台删除,规避沙箱批量删除限制)。
2026-09-19 00:27:21 +08:00

114 lines
4.4 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ==============================================================
# bundle-python.sh — 将 Python 运行时 + akshare-fetcher 打包进 .app
#
# ⚠️ 当前**不在构建链路内**:`Scripts/build-app.sh` 未调用本脚本,
# 全仓仅 Docs/DEVLOG 提到它。保留作为独立打包工具的参考实现。
# 若要重新启用,请先确认它与 build-app.sh 的分工,避免产物重复。
# ==============================================================
# pipefail:否则 `uv venv ... | tail -3` 这类管道会吞掉失败退出码,
# 与本文件头部注释声称的"确保失败中止"相矛盾。
set -eo pipefail
APP_DIR="${1:-.build/FinDataHub.app}"
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# 数据源项目路径可通过环境变量覆盖
AKSHARE_SRC="${AKSHARE_SRC:-${PROJECT_ROOT}/Services/akshare_fetcher}"
FRAMEWORKS_DIR="${APP_DIR}/Contents/Frameworks"
RESOURCES_DIR="${APP_DIR}/Contents/Resources"
VENV_DIR="${FRAMEWORKS_DIR}/akshare-venv"
AKSHARE_DST="${RESOURCES_DIR}/akshare_fetcher"
echo "=== 打包 Python 运行时 ==="
echo " .app: ${APP_DIR}"
echo ""
# ---------- 1. 创建 venv ----------
if [ -d "${VENV_DIR}/bin" ]; then
echo "[1/3] 虚拟环境已存在,跳过创建"
else
echo "[1/3] 创建 Python 虚拟环境..."
mkdir -p "${FRAMEWORKS_DIR}"
cd "${PROJECT_ROOT}"
# uv venv 创建独立虚拟环境(与 bundle 内实际运行时保持一致)
# 版本必须取 .python-version(=3.12):此前这里写死 3.14,
# 与 .python-version 及所有 pyproject 的 requires-python=">=3.12,<3.13" 冲突,
# 会造出一个与 bundle 内 3.12 不一致的运行时(详见项目记忆"全项目锁定 3.12")。
PY_VER="$(tr -d '[:space:]' < "${PROJECT_ROOT}/.python-version")"
uv venv --python "${PY_VER}" "${VENV_DIR}" 2>&1 | tail -3
echo " ✅ 虚拟环境已创建 (Python ${PY_VER})"
fi
source "${VENV_DIR}/bin/activate"
echo " Python: $(python3 --version)"
# ---------- 2. 安装依赖 ----------
echo "[2/3] 安装 akshare-fetcher 依赖..."
cd "${AKSHARE_SRC}"
# 从 pyproject.toml 安装依赖(版本约束由 pyproject.toml 管理,避免无版本锁定)
pip install --quiet --upgrade pip 2>/dev/null || true
install_time=$(date +%s)
# 以可编辑模式安装项目,读取 pyproject.toml 中声明的依赖及版本约束
pip install --quiet -e "${AKSHARE_SRC}" 2>&1 | tail -3
# 验证
python3 -c "import akshare; print(f' ✅ akshare {akshare.__version__} 可导入')" 2>&1
python3 -c "import pandas; print(f' ✅ pandas {pandas.__version__}')" 2>&1
python3 -c "import numpy; print(f' ✅ numpy {numpy.__version__}')" 2>&1
du -sh "${VENV_DIR}"
echo ""
# ---------- 3. 复制项目代码 ----------
echo "[3/3] 部署 akshare-fetcher 项目代码..."
mkdir -p "${AKSHARE_DST}"
rsync -a --delete \
--exclude='.venv' \
--exclude='.git' \
--exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='*.pyo' \
--exclude='.DS_Store' \
--exclude='uv.lock' \
"${AKSHARE_SRC}/" "${AKSHARE_DST}/"
echo " ✅ 项目代码已部署 (${AKSHARE_DST})"
# ---------- 4. 创建入口脚本 ----------
echo " 创建入口脚本..."
mkdir -p "${RESOURCES_DIR}/bin"
cat > "${RESOURCES_DIR}/bin/akshare-fetcher.sh" << 'SCRIPT'
#!/bin/bash
# FinDataHub 内置 akshare-fetcher 入口
# 使用捆绑的虚拟环境
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
VENV_PYTHON="${APP_DIR}/Contents/Frameworks/akshare-venv/bin/python3"
PROJECT_DIR="${APP_DIR}/Contents/Resources/akshare_fetcher"
export PYTHONUNBUFFERED=1
export AKSHARE_HOME="${PROJECT_DIR}"
export PYTHONPATH="${PROJECT_DIR}/src"
cd "${PROJECT_DIR}"
exec "${VENV_PYTHON}" -m akshare_fetcher "$@"
SCRIPT
chmod +x "${RESOURCES_DIR}/bin/akshare-fetcher.sh"
# 同时创建快捷入口: akshare-fetcher → 直接调项目源码
cat > "${AKSHARE_DST}/run_bundled.sh" << 'SCRIPT'
#!/bin/bash
# 捆绑环境入口 — 使用 FinDataHub 内建的 venv
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VENV_PYTHON="${SCRIPT_DIR}/../../Frameworks/akshare-venv/bin/python3"
export PYTHONPATH="${SCRIPT_DIR}/src"
cd "${SCRIPT_DIR}"
exec "${VENV_PYTHON}" -m akshare_fetcher "$@"
SCRIPT
chmod +x "${AKSHARE_DST}/run_bundled.sh"
echo ""
echo "=== Python 运行时打包完成 ==="
du -sh "${VENV_DIR}" "${AKSHARE_DST}" 2>/dev/null
echo ""
echo "总大小:"
du -sh "${FRAMEWORKS_DIR}" "${RESOURCES_DIR}" 2>/dev/null