From f4fb4000e18b0390ecb658fb17c695ff6dd512a5 Mon Sep 17 00:00:00 2001 From: host Date: Fri, 30 Oct 2026 01:38:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(worker):=20=E7=BB=88=E6=80=81=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E5=B8=A6=E4=B8=8A=20error=EF=BC=8C=E5=A4=8D=E5=8F=91?= =?UTF-8?q?=E5=8D=A1=E6=AD=BB=E5=8F=AF=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-10-29 任务 A:认领后 61 秒 interrupted,但终态审计只记 task_id、自动续跑又把 error_text 覆盖掉,事后无法区分取消、 配额、host 返回 interrupted 还是其它失败;容器 --rm 后日志 也没了,根因至今无法从留存证据证明(不是 sweep:阈值 300 秒;不是外部取消:该时段无取消审计;是 worker 自己写回 interrupted,但第一轮的 error 已被第二轮 failed 覆盖)。 本批不谎称找到根因,只补可定位性:run_one 终态审计在 interrupted/failed 时 detail 带 error(截 500 字、去换行), 完成态仍只记 task_id;分段审计 worker_exec_start/error/done 已在现 main,复发时最后一段 + 终态 error 即能定位。 单测 test_final_audit_detail_carries_error:失败派发后 worker_failed 审计 detail 以 'A:' 开头且含 error 文本。 --- cloud/tests/test_worker.py | 25 +++++++++++++++++++++++++ cloud/worker.py | 12 +++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/cloud/tests/test_worker.py b/cloud/tests/test_worker.py index c9c7f80..4293781 100644 --- a/cloud/tests/test_worker.py +++ b/cloud/tests/test_worker.py @@ -481,6 +481,31 @@ class WorkerTestCase(unittest.TestCase): self.assertEqual(result["status"], "interrupted") self.assertEqual(_status(self.db_path, "A"), "interrupted") + def test_final_audit_detail_carries_error(self): + # 2026-10-29 任务 A 教训:终态审计只记 task_id 时, + # interrupted/failed 的原因事后无法区分,且自动续跑 + # 会把 error_text 覆盖掉。非完成终态的审计 detail + # 必须带上 error,完成态仍只记 task_id。 + _insert_task(self.db_path, "A") + + def failing(cmd): + return SimpleNamespace(returncode=1, stdout="", + stderr="dispatch boom xyz") + + worker = self._worker(runner=failing) + result = worker.run_one() + self.assertEqual(result["status"], "failed") + from cloud.auth import AuthStore + store = AuthStore(self.db_path) + try: + entries = [e for e in store.list_audit("p1") + if e["action"] == "worker_failed"] + finally: + store.conn.close() + self.assertTrue(entries) + self.assertTrue(entries[0]["detail"].startswith("A:")) + self.assertIn("boom", entries[0]["detail"]) + # -- 缺 key 门 -------------------------------------------------------- def test_missing_key_fails_clearly_and_does_not_run(self): _insert_task(self.db_path, "A") diff --git a/cloud/worker.py b/cloud/worker.py index 9fc8fd5..0d292bd 100644 --- a/cloud/worker.py +++ b/cloud/worker.py @@ -5025,12 +5025,22 @@ class Worker: if final_written: # 审计名须与终态一致:interrupted 任务记 worker_failed # 会把“可续跑的中断”误读成失败,误导后续排查。 + # 终态审计细节(2026-10-29 任务 A 教训):interrupted / + # failed 时只记 task_id,事后完全无法区分是取消、 + # 配额、host 返回 interrupted 还是其它失败——且自动 + # 续跑会把 error_text 覆盖掉,第一轮的原因就此丢失。 + # 因此非完成终态的审计 detail 带上 error(截 500 字、 + # 去换行,不含 prompt/key),完成态仍只记 task_id。 + _final_detail = task_id + if final in ("interrupted", "failed") and error: + _final_detail = "%s: %s" % ( + task_id, str(error)[:500].replace("\n", " ")) self._audit(task["project_id"], "worker_quota_stop" if quota_stop else ("worker_completed" if final == "completed" else "worker_interrupted" if final == "interrupted" - else "worker_failed"), task_id) + else "worker_failed"), _final_detail) log_event(self.logger, "worker_quota_stop" if quota_stop else ("worker_completed" if final == "completed"