fix(bms): #335 低频采集两类失败 —— bms_order OOM 循环 + 导出异常请求诊断
Some checks failed
gitleaks / gitleaks (push) Failing after 10s
backend-test / commit-message-lint (push) Successful in 3s
backend-test / analytics-ods-isolation (push) Failing after 2s
backend-test / backend-lint-test (push) Failing after 14m3s
backend-test / frontend-lint-test (push) Failing after 36m8s

- _fetch.py 两处全量导出读取(openpyxl→calamine): bms_order 导出被 BMS
  硬截断 10 万行, openpyxl DOM 全量加载峰值 RSS>4GiB 触发 memcg OOM
  (dmesg 实证 anon-rss 4161640kB), gunicorn 被杀→重启→僵尸清理标 failed,
  2026-10-13 起每小时循环; calamine 内存降一个量级(data_parser 同款先例)
- docker-compose.yml backend 内存 4G→8G 双保险(主机 23G 余量充足)
- _browser.py 导出 JSON 异常时记录请求 url/method/post_data(诊断):
  定位 return_tracking 页面把日期序列化为 ISO-UTC(BMS 10-11 收紧只认
  yyyy-MM-dd HH:mm:ss)的实际端点与 payload, 为下一步 route 改写供事实
- ruff 全绿; bms/fetch/browser 相关测试 EXIT=0

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
This commit is contained in:
2026-10-19 10:01:27 +08:00
committed by trae-solo-agent
parent ad8ed8661c
commit 38f8042cf7
3 changed files with 25 additions and 3 deletions

View File

@@ -522,6 +522,20 @@ async def _download_with_retry(
try:
body = await resp.text()
export_rate_limited["msg"] = body[:200]
# #335: 导出异常时记录请求端点与 payload ——
# 定位 BMS 前端实际发送的参数形态(如 return_tracking
# 日期被页面序列化为 ISO-UTC 触发 400), 为 route
# 改写提供事实依据; 正常下载不经过此路径。
try:
req = resp.request
logger_obj.warning(
"[%s] 导出异常请求诊断: url=%s method=%s post_data=%r",
data_type, resp.url,
req.method if req else "?",
(req.post_data or "")[:500] if req else "",
)
except Exception:
pass
except Exception:
export_rate_limited["msg"] = "(body 读取失败)"
# 用 create_task 挂后台,避免同步回调里直接 await 协程;

View File

@@ -1976,7 +1976,11 @@ class BmsFetcher:
import pandas as pd
suffix_l = str(dest_path).lower()
if suffix_l.endswith(".xlsx"):
df = pd.read_excel(dest_path, engine="openpyxl", dtype=str)
# #335: 大文件读取换 calamine(Rust 流式) —— openpyxl 全量 DOM 加载,
# bms_order 导出被 BMS 硬截断 10 万行时峰值 RSS > 4GiB 触发 memcg OOM
# (gunicorn 被杀 → 服务重启 → 僵尸清理标 failed, 2026-10-13 起每小时循环)。
# calamine 内存降一个量级; data_parser 同款引擎(其 514 行先例)。
df = pd.read_excel(dest_path, engine="calamine", dtype=str)
elif suffix_l.endswith(".xls"):
try:
df = pd.read_excel(dest_path, dtype=str)
@@ -2096,7 +2100,8 @@ class BmsFetcher:
import pandas as pd
suffix_l = str(dest_path).lower()
if suffix_l.endswith(".xlsx"):
df = pd.read_excel(dest_path, engine="openpyxl", dtype=str)
# #335: 同上 —— 全量导出(10万行硬截断)读取换 calamine, 防 memcg OOM
df = pd.read_excel(dest_path, engine="calamine", dtype=str)
elif suffix_l.endswith(".xls"):
try:
df = pd.read_excel(dest_path, dtype=str)

View File

@@ -147,11 +147,14 @@ services:
start_period: 30s # 预留迁移/种子/调度器启动时间
# 上限(#279): 4G 给 Playwright chromium + ddddocr onnx 留余量(采集期单容器峰值
# 观测约 1.5-2.5G); 2 CPU 防止采集/导出任务饿死健康检查响应。
# #335: bms_order 全量导出(10万行硬截断)采集期峰值破 4G → memcg OOM 杀 gunicorn
# (2026-10-13 起每小时循环, 详见 #335); _fetch 读取已换 calamine 降一个量级,
# 内存再放宽到 8G 双保险(主机 23G, 余量充足)。
deploy:
resources:
limits:
cpus: '2.0'
memory: 4G
memory: 8G
depends_on:
db:
condition: service_healthy