fix(data): 分区改多 part 追加写+批量提交,根治 day-1 O(n²) 劣化 [nas]
CI/CD / test (push) Successful in 25s
CI/CD / nas-deploy (push) Successful in 10s
CI/CD / nas-verify (push) Successful in 15s

09-07 首跑实锤: append_parquet 每 unit 整读整写当日唯一 part-0, news 段
13万+行后 5.3s/股(应1.15s)且随行数恶化,10:30 墙钟必跑不完且逐日更糟。
修(两层):
- DayBuffer: 当日新行内存缓冲→flush 落新 part-N.parquet(只写新行,
  旧数据永不重写);id 去重职责收敛到调用方(账本+近窗双层,原就存在)
- Ctx 批量提交: unit_done 延后 marker 入批,每 FLUSH_EVERY unit 一次
  commit=数据→账本→marker 依次落盘(崩溃=整批重拉,绝不产生
  marker-done-但数据缺的洞);run_lane 段首清缓冲/finally 兜底 commit
- ③全文回补 known 检查补近窗兜底(账本 flush 崩溃窗防重)
- readers 全改 part-* glob(RecentIndex/_iter_partitions/verify_corpus)
- 测试 54:新增 多part语义/flush崩溃一致性/批量提交次序 三钉
This commit is contained in:
2026-09-07 07:12:48 +08:00
parent c5db293b46
commit efcdc8d8e0
3 changed files with 156 additions and 106 deletions
+41 -15
View File
@@ -165,33 +165,59 @@ def test_recent_index_reads_partitions(root):
# ---------- parquet 追加(append-only + 原子) ----------
def test_append_parquet_dedup_and_atomic(root):
def test_append_parquet_buffers_then_flush_batch(root):
"""多 part 语义(09-07 首跑 O(n²) 修复): append 进内存缓冲, flush 落新 part
文件只写新行; id 去重职责在调用方(账本+近窗)。"""
import pandas as pd
today = dt.date.today().isoformat()
rows1 = [{"announcement_id": "a1", "title": "x"}, {"announcement_id": "a2", "title": "y"}]
cd.append_parquet("ann_meta", rows1, id_col="announcement_id")
part = root / "ann_meta" / f"dt={today}" / "part-0.parquet"
df = pd.read_parquet(part)
assert len(df) == 2
# 同日追加含重复 id → 只进新 id
rows2 = [{"announcement_id": "a2", "title": "y2"}, {"announcement_id": "a3", "title": "z"}]
cd.append_parquet("ann_meta", rows2, id_col="announcement_id")
df = pd.read_parquet(part)
assert sorted(df["announcement_id"]) == ["a1", "a2", "a3"]
assert not list((root / "ann_meta" / f"dt={today}").glob("*.tmp")) # 无残留 tmp
cd._DAY_BUFFERS.clear()
cd.append_parquet("ann_meta", [{"announcement_id": "a1"}, {"announcement_id": "a2"}])
d = root / "ann_meta" / f"dt={today}"
assert not list(d.glob("part-*.parquet")) # 未 flush 无文件
cd.flush_domains()
assert sorted(p.name for p in d.glob("part-*.parquet")) == ["part-0.parquet"]
# 第二批 → 新 part-1(旧 part-0 永不重写)
cd.append_parquet("ann_meta", [{"announcement_id": "a3"}])
cd.flush_domains()
df0 = pd.read_parquet(d / "part-0.parquet")
df1 = pd.read_parquet(d / "part-1.parquet")
assert sorted(df0["announcement_id"]) == ["a1", "a2"] and len(df1) == 1
assert not list(d.glob("*.tmp"))
def test_append_parquet_crash_leaves_no_partial(root):
"""写中途抛错 → 不留 tmp/半截文件(下次重拉幂等前提)。"""
"""flush 写中途抛错 → 不留 tmp/半截文件(下次重拉幂等前提)。"""
import pandas as pd
cd._DAY_BUFFERS.clear()
cd.append_parquet("ann_meta", [{"announcement_id": "a1"}])
with patch.object(pd.DataFrame, "to_parquet", side_effect=RuntimeError("disk")):
with pytest.raises(RuntimeError):
cd.append_parquet("ann_meta", [{"announcement_id": "a1"}],
id_col="announcement_id")
cd.flush_domains()
today = dt.date.today().isoformat()
assert not list((root / "ann_meta" / f"dt={today}").glob("*"))
def test_commit_batches_data_before_markers(root, monkeypatch):
"""批量提交次序=数据→账本→marker: unit_done 只入批,commit 才双双落盘
(崩溃时宁可整批重拉,不产生 marker-done-但数据缺的洞)。"""
import pandas as pd
ctx = cd.Ctx(lane="daily")
ledgers = {"ann_meta": cd.IdLedger("ann_meta")}
ctx.set_stores(ledgers)
cd._DAY_BUFFERS.clear()
cd.append_parquet("ann_meta", [{"announcement_id": "m1", "title": "x"}])
ledgers["ann_meta"].add(["m1"]) # 真实流程 _absorb_new 负责
ctx.unit_done("ann", "000001")
today = dt.date.today().isoformat()
d = root / "ann_meta" / f"dt={today}"
assert not d.exists() or not list(d.glob("part-*.parquet"))
assert not cd.is_done("daily", "ann", "000001")
ctx.commit()
assert (d / "part-0.parquet").exists()
assert cd.is_done("daily", "ann", "000001")
assert cd.IdLedger("ann_meta").has_any(["m1"]) == [True]
# ---------- unit marker ----------
def test_marker_done_and_skip(root):