mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
* ci(#2131): 给 backend-gate offline pytest 加 --timeout=120 + faulthandler_timeout=300
issue #2131 报告 backend-gate 在执行 `Offline test suite` 步骤时两次
在同一提交 `e6abcef17fbc5d655c1c49429182079f02d6552e`(PR #2123)
上间歇性无 traceback 卡住:
workflow run 30551305640(2026-07-30):
- Offline test suite 开始于 14:23:13 UTC
- 最后一条测试输出 14:23:58 UTC(AlphaSift hotspot PASSED [11%])
- job 在 22 分 38 秒无输出后被取消
workflow run 30553352307(2026-07-30):同样在 11% 位置卡住
GitHub Actions runner 在被取消前没有 pytest 进度的任何信号,也没
有 traceback。本地 16 个 AlphaSift hotspot 测试都能稳过,意味着
问题是 CI-only 的测试执行顺序、进程级全局状态、线程/事件循环
清理或依赖行为 — 没有稳定的 assertion failure 可调试。
issue #2131 已经列出推荐排查方向之一:给 pytest 加单测试超时与
卡住时的线程栈 dump(`pytest-timeout`、`faulthandler_timeout`)。
但当前 `scripts/ci_gate.sh` 的 `offline_test_suite()` 仅运行
`python -m pytest -m "not network"`,没有任何超时或 watchdog。
本 PR 实施 issue #2131 推荐的 watchdog 改造,目标不是修复根因
(那需要 issue #2131 的等线程栈 dump 复现才能定位),而是让任何
未来 CI hang 都会留下可定位的失败信息或 post-mortem 栈,而不是
静默消亡到 GitHub Actions workflow timeout 才被取消。
改动:
1. `.github/requirements-ci.txt`:新增 `pytest-timeout>=2.3.0`
依赖。CI 的 `setup-python` + `pip install -r` 步骤会自动拉取。
2. `scripts/ci_gate.sh` 的 `offline_test_suite()`:
python -m pytest -m "not network" \
--timeout=120 -o timeout_method=thread \
-o faulthandler_timeout=300
- `--timeout=120`:单个测试如果执行超过 2 分钟直接 fail,
生成 pytest-timeout 的 traceback 指出是哪个 case。
- `-o timeout_method=thread`:pytest-timeout 用 watcher 线程
而非 signal 方法,对吞了 SIGINT/SIGTERM 的测试更可靠
(yfinance、AlphaSift 这类的 Threads/eventloop 都
有可能 swallow signal)。
- `-o faulthandler_timeout=300`:pytest 内置 faulthandler
的 watchdog,整体 pytest 5 分钟无任何输出(最末一个测试
结束到下一个测试开始之间的「沉默期」超过 300 秒)就 dump
全部 Python 线程栈到 stderr。这是定位卡住位置的关键信号。
3. `docs/CHANGELOG.md`:在 [Unreleased] 段加一条 [修复] entry
描述本次改动并指明 issue #2131。
本地验证:
$ pip install pytest-timeout
$ bash -n scripts/ci_gate.sh && echo syntax ok
$ python -m pytest -m "not network" --timeout=120 \
-o timeout_method=thread -o faulthandler_timeout=300 \
tests/test_stock_list_parser.py tests/test_stock_code_utils.py -q
============================== 98 passed in 2.00s ==============================
$ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
0
行为不变项:
- 本地开发 `python -m pytest ...` 不带 `--timeout` 仍可任意长跑调试
(`setup.cfg` 没在 `[tool:pytest]` 全局加 timeout — 仅 CI 路径加)。
- `scripts/ci_gate.sh` 的 syntax/flake8/deterministic phases 不受影响。
- `pytest-timeout` 只在 `scripts/ci_gate.sh` 的 offline_test_suite 被
调用时生效,不会污染本地开发流程。
注:本 PR 不修复 issue #2131 的根本 hang 原因(仍需 thread dump 复
现定位),而是把未来的 hang 转成可定位的失败。reviewer 在 issue
#2131 上对类似方向说过「即使再次复现,定位价值很低」—— 但本 PR
的 watchdog 至少把「无信息消亡」转成「带 stacktrace 的 fail」,
当 hang 再次发生时能立刻看到卡在哪个测试的哪一行。
* ci(#2131): close PR #2140 review blocker OR-COM-cc22d635 + OR-COM-c76d8eff — docker-publish.yml 对齐 backend-gate 依赖安装
按 reviewer 在 head `e01a1835` 上的 OpenReview Bot 复核反馈,关闭以下 2 个
高置信度 compatibility blocker:
- OR-COM-cc22d635: Docker Release Publish workflow 的 Install backend gate
dependencies 仍按旧依赖集合执行 ./scripts/ci_gate.sh,新加的
pytest-timeout 没覆盖到发布入口
- OR-COM-c76d8eff: 与 OR-COM-cc22d635 同源,cache-dependency-path 也
缺失 .github/requirements-ci.txt
## 改了什么
.github/workflows/docker-publish.yml:
- setup-python cache-dependency-path 对齐 ci.yml backend-gate:
加入 requirements.txt + .github/requirements-ci.txt 两个文件
作为 pip cache key,命中缓存
- Install backend gate dependencies 改用与 ci.yml 完全相同的
pattern:retry loop (3 attempts, 15s backoff) + 单一
pip install -r .github/requirements-ci.txt(该文件已 -r 递归拉
requirements.txt,所以无需重复 pip install -r requirements.txt)
- 加注释说明 issue #2131 引入 pytest-timeout 的关联
docs/CHANGELOG.md:
- [Unreleased] #2131 entry 末尾补一句:同步修正 docker-publish.yml
的 install 与 cache-dependency-path 对齐
## 为什么这么改
issue #2131 让 scripts/ci_gate.sh 的 offline_test_suite 用
`--timeout=120 -o timeout_method=thread -o faulthandler_timeout=300`,
这要求 pytest-timeout>=2.3.0 插件。
ci.yml 的 backend-gate 已通过 .github/requirements-ci.txt 安装该
插件,但 docker-publish.yml 仍用旧的 `pip install flake8 pytest`,
没有 pytest-timeout,发布前 gate 跑 ./scripts/ci_gate.sh 会直接
报 unrecognized --timeout=120 fail,阻断镜像发布。
reviewer OR-COM-cc22d635 / OR-COM-c76d8eff 都指这是 PR 引入的
compatibility regression(不是已有旧债),需要 PR 同步修。
ci.yml 的 install pattern 是 retry 3 次带 backoff,对齐到
docker-publish 让两个工作流完全统一,未来新加 CI-only 依赖只需改
.github/requirements-ci.txt。
## 验证情况
已本地验证:
- python -c "import yaml; yaml.safe_load(open('.github/workflows/
docker-publish.yml'))" — YAML 语法 OK
- 对比 ci.yml 的 backend-gate install 步骤,pattern 完全一致
已 CI 验证(待 push 后跑):
- 本 PR 触发 backend-gate / docker-build / ai-governance / Change
Detection,但 docker-publish.yml 只在 v*.*.* tag 或 workflow_dispatch
触发,本 PR CI 不会真跑该 workflow。reviewer 复核时会做静态比对
未验证 / 风险点:
- 真实发布流程跑不通:需要 maintainer 推 v*.*.* tag 或手动
workflow_dispatch 触发,才能验证发布前 gate 真的工作
- 但 install pattern 与 ci.yml 完全对齐,ci.yml 那边过则该边也
应该过;问题概率很低
## 风险点与回滚
回滚:
1. revert 本 commit
2. docker-publish.yml install step 改回 pip install -r requirements.txt
+ pip install flake8 pytest
3. cache-dependency-path 删除新增两行
4. CHANGELOG.md entry 末尾去掉补充句
潜在风险:
- .github/requirements-ci.txt 递归 -r requirements.txt,release
runner 之前装过 requirements.txt;retry loop + cache 应该吸收掉
任何 pip 网络抖动,但首次 release 可能比之前略慢(多一次冗余
install)。Trade-off 可接受:与 ci.yml 完全统一比省一次冗余
install 更重要。
* docs(#2131): 收窄 PR #2140 faulthandler_timeout 措辞 + 补 PR 描述 Refs/回滚
OpenReview Bot 在 head 14416440 上已给「可以直接合入」结论,剩 2 个非阻断建议:
1. CHANGELOG 与 PR 描述里 -o faulthandler_timeout=300 写成「整体超过 5 分钟无输出 watchdog」与 pytest 实际语义不一致,应改为「单个测试(含其 teardown)超过 5 分钟时 dump 线程栈」
2. 把 issue 关联补成显式 Refs #2131 + 加最小回滚方案
本 commit 同步两条:
- docs/CHANGELOG.md:把 faulthandler_timeout 描述从「整体超过 5 分钟无输出时 dump 全部线程栈」改为「单个测试(含其 teardown)超过 5 分钟时 dump 全部线程栈」,与 python -m pytest --help 中 faulthandler_timeout 的语义对齐
- PR #2140 描述:在末尾追加 "## 关联 issue Refs #2131" 与 "## 最小回滚方案" 两段(用 gh pr edit --body-file 更新),按 .github/PULL_REQUEST_TEMPLATE.md 模板完整化
无代码逻辑改动,仅文案同步。
---------
Co-authored-by: xxiaoxiong <xxiaoxiong@nicholasxiong.cn>
72 lines
1.9 KiB
Bash
Executable File
72 lines
1.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
syntax_check() {
|
|
echo "==> backend-gate: Python syntax check"
|
|
python -m py_compile main.py src/config.py src/auth.py src/analyzer.py src/notification.py
|
|
python -m py_compile src/storage.py src/scheduler.py src/search_service.py
|
|
python -m py_compile src/market_analyzer.py src/stock_analyzer.py
|
|
python -m py_compile data_provider/*.py
|
|
}
|
|
|
|
flake8_checks() {
|
|
echo "==> backend-gate: flake8 critical checks"
|
|
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
}
|
|
|
|
deterministic_checks() {
|
|
echo "==> backend-gate: local deterministic checks"
|
|
./scripts/test.sh code
|
|
./scripts/test.sh yfinance
|
|
}
|
|
|
|
offline_test_suite() {
|
|
echo "==> backend-gate: offline test suite"
|
|
# ``--timeout=120`` hard-fails any single test that runs longer than two
|
|
# minutes (issue #2131: backend-gate previously hung indefinitely around
|
|
# AlphaSift hotspot cases without leaving any traceback). ``-o
|
|
# timeout_method=thread`` makes pytest-timeout use a watcher thread that
|
|
# is reliable even when the test has swallowed Ctrl-C / signal handling
|
|
# (yfinance, AlphaSift). ``-o faulthandler_timeout=300`` dumps all
|
|
# thread + interpreter stacks to stderr after five minutes of total
|
|
# test silence, giving us a post-mortem root cause for any future
|
|
# CI hang instead of ``backend-gate`` being silently cancelled by the
|
|
# workflow timeout.
|
|
python -m pytest -m "not network" \
|
|
--timeout=120 -o timeout_method=thread \
|
|
-o faulthandler_timeout=300
|
|
}
|
|
|
|
run_all() {
|
|
syntax_check
|
|
flake8_checks
|
|
deterministic_checks
|
|
offline_test_suite
|
|
echo "==> backend-gate: all checks passed"
|
|
}
|
|
|
|
phase="${1:-all}"
|
|
|
|
case "$phase" in
|
|
all)
|
|
run_all
|
|
;;
|
|
syntax)
|
|
syntax_check
|
|
;;
|
|
flake8)
|
|
flake8_checks
|
|
;;
|
|
deterministic)
|
|
deterministic_checks
|
|
;;
|
|
offline-tests)
|
|
offline_test_suite
|
|
;;
|
|
*)
|
|
echo "Usage: $0 [all|syntax|flake8|deterministic|offline-tests]" >&2
|
|
exit 2
|
|
;;
|
|
esac
|