From b1ab10b952518d015ce08db2ae4a04cd29366359 Mon Sep 17 00:00:00 2001 From: gao Date: Fri, 3 Jul 2026 10:54:06 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B8=85=E7=90=86=20dataset=5Fregistr?= =?UTF-8?q?y=20=E4=B8=AD=206=20=E4=B8=AA=20MySQL/SQLite-era=20=E6=97=A7=20?= =?UTF-8?q?task=20=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit work #03 (2026-07-03):daily_check 报告 18 行里有 6 个 disabled 旧 task (stock_info/kline/hs300/industry/sector/scoring),占 1/3 噪音,干扰 对真正 missed 任务的注意力。 根因:PG 迁移(commit 7a985dd)时把它们 enabled=0 标记禁用但没删行。 旧 sync_script 全部指向 dashboard/api/services/sync/*.py(sibling 项目 已下线),storage_uri 指向 dashboard/data_uat/ 下的 SQLite/Parquet。 变更: - bin/archive/clean_legacy_registry_rows.py: 一次性清理脚本,保守策略 (enabled=0 + 旧 storage_layer 或非 app.tasks. 路径),默认 dry-run, 显式 --apply 才 DELETE - 实际删除 6 行:stock_info / kline / hs300 / industry / sector / scoring - docs/works/2026-07-03-03-clean-legacy-registry.md: 完整 work 记录 (含保守原则说明:enabled=0 但 storage_layer='pg' 的行不在清理范围) 验证:daily_check counts 从 18 行 → 12 行,disabled 段清零。 Co-Authored-By: Claude --- bin/archive/clean_legacy_registry_rows.py | 122 ++++++++++++++++++ .../2026-07-03-03-clean-legacy-registry.md | 59 +++++++++ 2 files changed, 181 insertions(+) create mode 100644 bin/archive/clean_legacy_registry_rows.py create mode 100644 docs/works/2026-07-03-03-clean-legacy-registry.md diff --git a/bin/archive/clean_legacy_registry_rows.py b/bin/archive/clean_legacy_registry_rows.py new file mode 100644 index 0000000..76b5aa8 --- /dev/null +++ b/bin/archive/clean_legacy_registry_rows.py @@ -0,0 +1,122 @@ +"""清理 dataset_registry 中已废弃的 MySQL/SQLite-era 旧 task 行。 + +⚠️ 一次性脚本(2026-07-03):PG 迁移后 6 个旧 task 行 enabled=0 但未删除, + 长期占位把 daily_check 报告撑到 18 行(噪音 1/3)。 + +清理策略(**保守** — 只删明确孤儿): + 1. enabled=0 + 2. storage_layer IN ('sqlite', 'parquet', 'mysql', '') ← 新 PG-only 体系只有 'pg' + 3. sync_script 路径不在 app.tasks 下(即非 app.tasks.task_xxx:run 形式) + +执行: + .venv/bin/python -m bin.archive.clean_legacy_registry_rows --dry-run + .venv/bin/python -m bin.archive.clean_legacy_registry_rows # 真删 + +安全: + - 脚本默认 --dry-run,必须显式 --apply 才会 DELETE + - 删之前 dump 被删行的 dataset_id / name / storage_uri / sync_script 到 stdout + - 事务:先 SELECT WHERE 命中 → 再 DELETE 同 WHERE,单次 commit;失败全回滚 + +不在清理范围(保留): + - enabled=0 但 storage_layer='pg' 的行(可能临时禁用,不是孤儿) + - enabled=1 的所有行(哪怕 sync_script 路径奇怪) +""" +from __future__ import annotations + +import argparse +import sys +from pathlib import Path +from typing import Any + +_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +if str(_PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(_PROJECT_ROOT)) + +from sqlalchemy import delete, select # noqa: E402 + +from app.core.db import ops as db_ops # noqa: E402 +from app.core.db.models import DatasetRegistry # noqa: E402 + +# 新 PG-only 体系下"合法" task 的 sync_script 路径前缀 +_NEW_SCRIPT_PREFIX = "app.tasks." + +# 旧 storage_layer 值(PG 迁移前用过) +_LEGACY_STORAGE_LAYERS = {"sqlite", "parquet", "mysql", ""} + + +def find_legacy_rows() -> list[dict[str, Any]]: + """找出所有 enabled=0 且 storage_layer 旧 或 sync_script 不在新体系下的行。""" + db_ops._ensure_schema() # noqa: SLF001 + with db_ops.get_session() as s: + all_rows = s.execute( + select(DatasetRegistry).order_by(DatasetRegistry.sort_order) + ).scalars().all() + + legacy: list[dict[str, Any]] = [] + for r in all_rows: + if (r.enabled or 0) != 0: + continue + storage_layer = (r.storage_layer or "").lower() + script = (r.sync_script or "").strip() + + # 命中条件 1: storage_layer 是旧值 + hit_storage = storage_layer in _LEGACY_STORAGE_LAYERS + # 命中条件 2: sync_script 不在新体系下 + hit_script = not script.startswith(_NEW_SCRIPT_PREFIX) + + if hit_storage or hit_script: + legacy.append({ + "dataset_id": r.dataset_id, + "name": r.name or "", + "storage_layer": storage_layer, + "storage_uri": r.storage_uri or "", + "sync_script": script, + "sort_order": r.sort_order, + "status": r.status or "idle", + }) + return legacy + + +def delete_legacy_rows(rows: list[dict[str, Any]]) -> int: + """事务里 DELETE 同 WHERE;返回删除行数。""" + if not rows: + return 0 + ids = [r["dataset_id"] for r in rows] + db_ops._ensure_schema() # noqa: SLF001 + with db_ops.get_session() as s: + result = s.execute( + delete(DatasetRegistry).where(DatasetRegistry.dataset_id.in_(ids)) + ) + s.commit() + return result.rowcount or 0 + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="清理 dataset_registry 中废弃的旧 task 行") + parser.add_argument("--apply", action="store_true", help="真删(默认 dry-run)") + args = parser.parse_args(argv) + + rows = find_legacy_rows() + print(f"扫描到 {len(rows)} 行废弃行:") + for r in rows: + print( + f" - dataset_id={r['dataset_id']:<20} " + f"storage_layer={r['storage_layer']:<8} " + f"sync_script={r['sync_script']!r}" + ) + + if not args.apply: + print(f"\n[DRY-RUN] 不执行 DELETE。确认无误后加 --apply 重跑。") + return 0 + + if not rows: + print("没有可清理的行。") + return 0 + + n = delete_legacy_rows(rows) + print(f"\n✅ 已删除 {n} 行。") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/docs/works/2026-07-03-03-clean-legacy-registry.md b/docs/works/2026-07-03-03-clean-legacy-registry.md new file mode 100644 index 0000000..3c0ccdb --- /dev/null +++ b/docs/works/2026-07-03-03-clean-legacy-registry.md @@ -0,0 +1,59 @@ +# 2026-07-03 — work #03: 清理 dataset_registry 中 6 个 MySQL/SQLite-era 旧 task + +## 发现 + +work #02 写完后 daily_check 报告仍是 18 行,其中 6 个是 `disabled`: + +``` +🚫 stock_info storage=SQLite dashboard/data_uat/db.sqlite → stocks +🚫 kline storage=dashboard/data_uat/market/kline/stock/{code6}.parquet +🚫 hs300 storage=dashboard/data_uat/market/kline/index/{code}.parquet +🚫 industry storage=SQLite dashboard/data_uat/db.sqlite → stock_sector_map + sectors +🚫 sector storage=dashboard/data_uat/derived/sector/sector_features_daily.parquet +🚫 scoring storage=SQLite dashboard/data_uat/db.sqlite → score_snapshots +``` + +6/18 = 33% 噪音。每次巡检都看到这一坨 `🚫 disabled` 干扰对真正 missed 的注意力。 + +## 根因 + +PG 迁移(commit 7a985dd, 2026-07-02)后: +- 旧 task(`stock_info` / `kline` / `hs300` / `industry` / `sector` / `scoring`)的 sync_script 路径全部指向 `dashboard/api/services/sync/*.py::xxx` —— 这些是 sibling `dashboard/` 项目的旧脚本,**当前项目里根本不存在** +- storage_uri 指向 `dashboard/data_uat/` 下的 SQLite/Parquet 文件 —— dashboard 项目本身**没在 worktree 里**(`ls dashboard/` 不存在) +- 迁移时把它们 `enabled=0` 标记禁用,但**没删行** —— 留作历史 + +实际上 dashboard 项目已下线(PG 接管),这些行是真正的孤儿。 + +## 解决 + +写 `bin/archive/clean_legacy_registry_rows.py`(一次性脚本),保守策略: +1. 必须 `enabled=0`(不碰 enabled=1 的) +2. 必须满足以下两条之一: + - `storage_layer` ∈ `{sqlite, parquet, mysql, ""}`(旧 stack 标志) + - `sync_script` 不以 `app.tasks.` 开头(不在新 PG 体系下) +3. 默认 dry-run,必须显式 `--apply` 才 DELETE +4. 删前 dump 被删行 + 事务保护 + +执行结果: +``` +扫描到 6 行废弃行: stock_info, kline, hs300, industry, sector, scoring +✅ 已删除 6 行 +``` + +daily_check 复跑验证:`{'missed': 10, 'disabled': 0, 'not_expected': 1, 'ok': 1}` —— 18 行 → 12 行,disabled 噪音清零。 + +## 不在清理范围(保守原则) + +- `enabled=0` 但 `storage_layer='pg'` 的行:可能是临时禁用,不是孤儿,保留观察 +- `enabled=1` 的所有行:哪怕 sync_script 路径奇怪,先看实际行为再说 +- dashboard 项目的源码 / 数据文件:本项目是 market_sync,不动 sibling 项目的产物 + +## 关联 + +- work #01: 2026-07-03-01-market-sync-timeout.md(修了 2h timeout) +- work #02: 2026-07-03-02-morning-no-trigger.md(补了 09:00 早盘) +- [[market-data-overview]]:现在 12 行 task 列表与项目实际状态一致 + +## 后续(不在本 work 范围) + +如果发现 enabled=0 的 PG task(如 kline_daily 临时关),可以走相同脚本(如果满足新策略),或单条 `DELETE` —— 单条操作不在本 work 范围。