fix(HEL-461): CLI/后台强制重发改为整组边界切换

eod-refresh --force 与管理后台补数不再单数据集发布,
统一走 force_republish_boundary,避免绕过 A/B 完整边界。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
总工
2026-09-05 11:26:27 +08:00
co-authored by Cursor multica-agent
parent 32f565ecb9
commit 75c2e33b68
7 changed files with 140 additions and 46 deletions
+35 -16
View File
@@ -666,17 +666,33 @@ class Pipeline:
results.update(self.run_eod_batch_b(trade_date))
return results
def run_eod_batch_a(self, trade_date: str) -> dict[str, Any]:
return self.run_release_group(EOD_A_DATASETS, trade_date, include_stocks=True)
def run_eod_batch_a(self, trade_date: str, force: bool = False) -> dict[str, Any]:
return self.run_release_group(EOD_A_DATASETS, trade_date, include_stocks=True, force=force)
def run_eod_batch_b(self, trade_date: str) -> dict[str, Any]:
return self.run_release_group(EOD_B_DATASETS, trade_date)
def run_eod_batch_b(self, trade_date: str, force: bool = False) -> dict[str, Any]:
return self.run_release_group(EOD_B_DATASETS, trade_date, force=force)
def force_republish_boundary(self, dataset: str, trade_date: str) -> dict[str, Any]:
"""Force-republish the full A/B consistency boundary that owns ``dataset``.
CLI ``eod-refresh --force`` and admin manual backfill must not publish a
single official member alone — that would mix old and new batches inside
the same trade date. Naming any A-group member (or stocks) rebuilds the
whole A group; naming ``index_daily`` rebuilds B.
"""
name = str(dataset or "").strip()
if name in EOD_A_DATASETS or name == STOCKS_DATASET:
return self.run_eod_batch_a(trade_date, force=True)
if name in EOD_B_DATASETS:
return self.run_eod_batch_b(trade_date, force=True)
raise ValueError(f"dataset is not part of an EOD release boundary: {dataset}")
def run_release_group(
self,
datasets: tuple[str, ...],
trade_date: str,
include_stocks: bool = False,
force: bool = False,
) -> dict[str, Any]:
"""One post-market publish/republish becomes one atomic visibility flip.
@@ -689,11 +705,11 @@ class Pipeline:
the previous complete official version keeps serving and the reason
is recorded on the batches and in the audit log.
Skip is all-or-nothing for the boundary: if every official member
(and stocks when required) is already published, the group is
skipped. If any official member is still missing, every official
member is re-staged — including ones that already had a publication
— so a retry never mixes old and new batches in one release.
Skip is all-or-nothing for the boundary unless ``force``: if every
official member (and stocks when required) is already published, the
group is skipped. If any official member is still missing — or
``force`` is set — every official member is re-staged, so a retry or
manual republish never mixes old and new batches in one release.
"""
day = yyyymmdd(trade_date)
results: dict[str, Any] = {}
@@ -702,7 +718,7 @@ class Pipeline:
missing_official = [dataset for dataset in datasets if self.active_batch(dataset, day) is None]
stocks_missing = include_stocks and self.active_batch(STOCKS_DATASET, day) is None
if not missing_official and not stocks_missing:
if not force and not missing_official and not stocks_missing:
for dataset in datasets:
results[dataset] = {
"dataset": dataset,
@@ -719,8 +735,8 @@ class Pipeline:
}
return results
# Incomplete boundary → restage every official member together.
pending = list(datasets) if missing_official else []
# Incomplete or forced boundary → restage every official member together.
pending = list(datasets)
for dataset in pending:
if failure is not None:
@@ -743,10 +759,12 @@ class Pipeline:
}
# Stocks join the same switch when the official boundary is being
# rebuilt, or when only the stocks snapshot is still missing.
if include_stocks and failure is None and (missing_official or stocks_missing):
# rebuilt (missing or forced), or when only the stocks snapshot is
# still missing.
rebuild_official = bool(force or missing_official)
if include_stocks and failure is None and (rebuild_official or stocks_missing):
try:
stocks_plan = self._stage_stocks_snapshot(day, force=bool(missing_official))
stocks_plan = self._stage_stocks_snapshot(day, force=rebuild_official)
except Exception as exc:
failure = f"{STOCKS_DATASET}: {exc}"
results[STOCKS_DATASET] = {
@@ -809,7 +827,8 @@ class Pipeline:
self.audit(
"pipeline", "release-group", f"eod:{day}",
json.dumps(
{"state": "ok", "switched": sorted(staged)}, ensure_ascii=False
{"state": "ok", "switched": sorted(staged), "force": bool(force)},
ensure_ascii=False,
),
)
return results