From 1c740a9d4824049d66911e488d1fabe1fb34bf59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=80=BB=E5=B7=A5?= Date: Sat, 5 Sep 2026 15:56:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(HEL-461):=20=E5=90=8E=E5=8F=B0=E6=95=B4?= =?UTF-8?q?=E7=BB=84=E5=88=87=E6=8D=A2=E5=BC=82=E5=B8=B8=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=B8=BA=20FAILED=5FPRECONDITION?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 管理后台补数在切换事务中断时不再抛出原始异常, 统一映射为 ApiError FAILED_PRECONDITION,并保留旧完整版本。 Co-authored-by: Cursor Co-authored-by: multica-agent --- xiaobai-datahub/datahub/admin_api.py | 16 ++++++++++---- xiaobai-datahub/tests/test_atomic_release.py | 23 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/xiaobai-datahub/datahub/admin_api.py b/xiaobai-datahub/datahub/admin_api.py index fb284f5..41f162b 100644 --- a/xiaobai-datahub/datahub/admin_api.py +++ b/xiaobai-datahub/datahub/admin_api.py @@ -145,10 +145,18 @@ class AdminAPI: result = self.pipeline.ingest_reference(day) elif dataset in OFFICIAL_DATASETS or dataset == STOCKS_DATASET: # Manual same-day republish must rebuild the full A/B boundary. - result = self.pipeline.force_republish_boundary(dataset, day) - failures = self.pipeline.eod_failures(result) - if failures: - raise ApiError("FAILED_PRECONDITION", "; ".join(failures)) + # Gate failures and mid-switch exceptions both surface as + # FAILED_PRECONDITION so the admin API never leaks raw + # transaction errors to the client. + try: + result = self.pipeline.force_republish_boundary(dataset, day) + failures = self.pipeline.eod_failures(result) + if failures: + raise ApiError("FAILED_PRECONDITION", "; ".join(failures)) + except ApiError: + raise + except Exception as exc: + raise ApiError("FAILED_PRECONDITION", str(exc)) from exc else: raise ApiError("INVALID_ARGUMENT", f"unsupported backfill dataset: {dataset}") self.pipeline.audit(actor, "backfill", f"{dataset}:{day}", json.dumps({"ok": True})) diff --git a/xiaobai-datahub/tests/test_atomic_release.py b/xiaobai-datahub/tests/test_atomic_release.py index 49a1ffc..8bccf7d 100644 --- a/xiaobai-datahub/tests/test_atomic_release.py +++ b/xiaobai-datahub/tests/test_atomic_release.py @@ -367,6 +367,29 @@ class ForceBoundaryEntryTests(unittest.TestCase): with self.assertRaises(ApiError): admin.backfill("daily", TRADE_DATE, "wrong", f"daily:{TRADE_DATE}", "tester") + def test_admin_backfill_switch_crash_is_failed_precondition(self) -> None: + from datahub.admin_api import AdminAPI + from datahub.auth import AuthService + from datahub.crypto import SecretVault + from datahub.scheduler import Scheduler + from datahub.serving import ApiError + + vault = SecretVault(self.pipe.settings.encryption_key) + auth = AuthService(self.db, vault, self.pipe.settings.api_token, "StartPass1") + admin = AdminAPI(self.db, self.pipe, Scheduler(self.db, self.pipe), auth) + before = publications_map(self.db, TRADE_DATE) + + def explode() -> None: + raise RuntimeError("killed mid-switch") + + self.pipe.before_commit = explode + with self.assertRaises(ApiError) as ctx: + admin.backfill("valuation", TRADE_DATE, "StartPass1", f"valuation:{TRADE_DATE}", "tester") + self.assertEqual(ctx.exception.code, "FAILED_PRECONDITION") + self.assertIn("killed mid-switch", ctx.exception.message) + # previous complete A/B versions keep serving + self.assertEqual(publications_map(self.db, TRADE_DATE), before) + if __name__ == "__main__": unittest.main()