fix: 允许等待重试任务手动重新执行 (#70)
This commit is contained in:
@@ -608,7 +608,7 @@ class PDDTaskPageEvent(QObject):
|
|||||||
self._stop_after_current(content)
|
self._stop_after_current(content)
|
||||||
|
|
||||||
@pyqtSlot(str, str, str)
|
@pyqtSlot(str, str, str)
|
||||||
def _on_collect_outcome(self, kind: str, message: str, _task_id: str) -> None:
|
def _on_collect_outcome(self, kind: str, message: str, task_id: str) -> None:
|
||||||
if self._closing:
|
if self._closing:
|
||||||
return
|
return
|
||||||
self._reload()
|
self._reload()
|
||||||
@@ -622,6 +622,13 @@ class PDDTaskPageEvent(QObject):
|
|||||||
)
|
)
|
||||||
elif kind == "result_pending":
|
elif kind == "result_pending":
|
||||||
self._on_claim_retryable_failed(message)
|
self._on_claim_retryable_failed(message)
|
||||||
|
elif kind == "failed" and self._is_retry_wait_task(task_id):
|
||||||
|
content = (
|
||||||
|
f"自动获取:已停止 · 任务 {task_id} 重试已暂停;"
|
||||||
|
"请选择该任务点击“重新执行”,或重新启动获取任务"
|
||||||
|
)
|
||||||
|
self._show_retry_paused(content)
|
||||||
|
self._stop_after_current(content)
|
||||||
elif kind in {"manual_review", "failed"}:
|
elif kind in {"manual_review", "failed"}:
|
||||||
self._show_claim_error("采集任务需要处理", message)
|
self._show_claim_error("采集任务需要处理", message)
|
||||||
self._stop_after_current(message)
|
self._stop_after_current(message)
|
||||||
@@ -632,6 +639,29 @@ class PDDTaskPageEvent(QObject):
|
|||||||
self._show_claim_error("自动获取已停止", content)
|
self._show_claim_error("自动获取已停止", content)
|
||||||
self._stop_after_current(content)
|
self._stop_after_current(content)
|
||||||
|
|
||||||
|
def _is_retry_wait_task(self, task_id: str) -> bool:
|
||||||
|
"""判断失败结果对应的任务是否仍可在本地重试。"""
|
||||||
|
|
||||||
|
if not task_id:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
task = self._repository.get_task(task_id)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return task is not None and task.status is TaskStatus.RETRY_WAIT
|
||||||
|
|
||||||
|
def _show_retry_paused(self, content: str) -> None:
|
||||||
|
"""用持久警告说明任务不会自行倒计时重试。"""
|
||||||
|
|
||||||
|
InfoBar.warning(
|
||||||
|
title="重试已暂停",
|
||||||
|
content=content,
|
||||||
|
isClosable=True,
|
||||||
|
duration=-1,
|
||||||
|
position=InfoBarPosition.TOP_RIGHT,
|
||||||
|
parent=self._page,
|
||||||
|
)
|
||||||
|
|
||||||
def _show_claim_error(self, title: str, content: str) -> None:
|
def _show_claim_error(self, title: str, content: str) -> None:
|
||||||
"""显示不会自动消失的可恢复错误,同时保留底部状态文字。"""
|
"""显示不会自动消失的可恢复错误,同时保留底部状态文字。"""
|
||||||
|
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ class TaskRepository:
|
|||||||
if row["task_type"] != TaskType.COLLECT.value:
|
if row["task_type"] != TaskType.COLLECT.value:
|
||||||
raise CollectRerunError("采购任务不能重新执行,以免重复下单")
|
raise CollectRerunError("采购任务不能重新执行,以免重复下单")
|
||||||
allowed_statuses = {
|
allowed_statuses = {
|
||||||
|
TaskStatus.RETRY_WAIT.value,
|
||||||
TaskStatus.SUCCEEDED.value,
|
TaskStatus.SUCCEEDED.value,
|
||||||
TaskStatus.FAILED.value,
|
TaskStatus.FAILED.value,
|
||||||
TaskStatus.CANCELLED.value,
|
TaskStatus.CANCELLED.value,
|
||||||
@@ -272,17 +273,20 @@ class TaskRepository:
|
|||||||
TaskStatus.CLAIMED.value: "待执行",
|
TaskStatus.CLAIMED.value: "待执行",
|
||||||
TaskStatus.RUNNING.value: "执行中",
|
TaskStatus.RUNNING.value: "执行中",
|
||||||
TaskStatus.RESULT_PENDING.value: "结果待提交",
|
TaskStatus.RESULT_PENDING.value: "结果待提交",
|
||||||
TaskStatus.RETRY_WAIT.value: "等待重试",
|
|
||||||
}.get(row["status"], row["status"])
|
}.get(row["status"], row["status"])
|
||||||
raise CollectRerunError(f"任务当前为“{status_name}”,不能重新采集")
|
raise CollectRerunError(f"任务当前为“{status_name}”,不能重新采集")
|
||||||
unsent_count = int(
|
unsent = connection.execute(
|
||||||
connection.execute(
|
"SELECT status, last_error FROM outbox_events"
|
||||||
"SELECT COUNT(*) FROM outbox_events"
|
" WHERE task_id = ? AND status != 'sent'"
|
||||||
" WHERE task_id = ? AND status != 'sent'",
|
" ORDER BY id DESC LIMIT 1",
|
||||||
(row["id"],),
|
(row["id"],),
|
||||||
).fetchone()[0]
|
).fetchone()
|
||||||
)
|
if unsent is not None and unsent["status"] == OutboxStatus.FAILED.value:
|
||||||
if unsent_count:
|
reason = unsent["last_error"] or "Admin 未接收上次结果"
|
||||||
|
raise CollectRerunError(
|
||||||
|
f"任务上次结果提交失败:{reason};请先处理后再重新采集"
|
||||||
|
)
|
||||||
|
if unsent is not None:
|
||||||
raise CollectRerunError("任务仍有未发送的结果,请先完成提交")
|
raise CollectRerunError("任务仍有未发送的结果,请先完成提交")
|
||||||
|
|
||||||
def start_collect_run(
|
def start_collect_run(
|
||||||
|
|||||||
@@ -563,6 +563,65 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
|||||||
events.shutdown()
|
events.shutdown()
|
||||||
page.deleteLater()
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_retry_wait_failure_shows_paused_recovery_actions(self):
|
||||||
|
self._add_task(1)
|
||||||
|
started = self.repository.start_collect_run("PDD-001", "USB-001")
|
||||||
|
event = self.repository.save_collect_failure(
|
||||||
|
"PDD-001",
|
||||||
|
started.attempt_id,
|
||||||
|
TaskStatus.RETRY_WAIT,
|
||||||
|
"DEVICE_OFFLINE",
|
||||||
|
"设备离线",
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.repository.mark_outbox_sent(event.id)
|
||||||
|
page = PDDTaskPage()
|
||||||
|
events = PDDTaskPageEvent(
|
||||||
|
page,
|
||||||
|
self.repository,
|
||||||
|
claim_gateway=RecordingClaimGateway(None),
|
||||||
|
settings_repository=self._saved_settings(),
|
||||||
|
)
|
||||||
|
|
||||||
|
events._on_collect_outcome(
|
||||||
|
"failed",
|
||||||
|
"任务 PDD-001 采集未完成",
|
||||||
|
"PDD-001",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("重试已暂停", events._stop_status)
|
||||||
|
self.assertIn("重新执行", events._stop_status)
|
||||||
|
self.assertIn("重新启动获取任务", events._stop_status)
|
||||||
|
events.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_failed_task_does_not_show_retry_paused_status(self):
|
||||||
|
self._add_task(1)
|
||||||
|
started = self.repository.start_collect_run("PDD-001", "USB-001")
|
||||||
|
event = self.repository.save_collect_failure(
|
||||||
|
"PDD-001",
|
||||||
|
started.attempt_id,
|
||||||
|
TaskStatus.FAILED,
|
||||||
|
"GOODS_OFF_SHELF",
|
||||||
|
"商品已下架",
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
self.repository.mark_outbox_sent(event.id)
|
||||||
|
page = PDDTaskPage()
|
||||||
|
events = PDDTaskPageEvent(
|
||||||
|
page,
|
||||||
|
self.repository,
|
||||||
|
claim_gateway=RecordingClaimGateway(None),
|
||||||
|
settings_repository=self._saved_settings(),
|
||||||
|
)
|
||||||
|
|
||||||
|
events._on_collect_outcome("failed", "商品已下架", "PDD-001")
|
||||||
|
|
||||||
|
self.assertEqual(events._stop_status, "商品已下架")
|
||||||
|
self.assertNotIn("重试已暂停", events._stop_status)
|
||||||
|
events.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
def test_duplicate_task_is_normal_status(self):
|
def test_duplicate_task_is_normal_status(self):
|
||||||
task = collect_admin_task()
|
task = collect_admin_task()
|
||||||
self.repository.add_claimed_task(admin_task_to_new_claimed_task(task))
|
self.repository.add_claimed_task(admin_task_to_new_claimed_task(task))
|
||||||
|
|||||||
@@ -267,6 +267,48 @@ class TaskRepositoryTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(CollectRerunError, "未发送"):
|
with self.assertRaisesRegex(CollectRerunError, "未发送"):
|
||||||
self.repository.validate_collect_rerun("UNSENT-RERUN")
|
self.repository.validate_collect_rerun("UNSENT-RERUN")
|
||||||
|
|
||||||
|
def test_retry_wait_can_rerun_only_after_failure_outbox_is_sent(self):
|
||||||
|
self.repository.add_claimed_task(self._task("RETRY-RERUN"))
|
||||||
|
first = self.repository.start_collect_run("RETRY-RERUN", "USB-001")
|
||||||
|
event = self.repository.save_collect_failure(
|
||||||
|
"RETRY-RERUN",
|
||||||
|
first.attempt_id,
|
||||||
|
TaskStatus.RETRY_WAIT,
|
||||||
|
"DEVICE_OFFLINE",
|
||||||
|
"设备离线",
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(CollectRerunError, "未发送"):
|
||||||
|
self.repository.validate_collect_rerun("RETRY-RERUN")
|
||||||
|
|
||||||
|
self.repository.mark_outbox_sent(event.id)
|
||||||
|
prepared = self.repository.prepare_collect_rerun("RETRY-RERUN")
|
||||||
|
second = self.repository.start_collect_run("RETRY-RERUN", "USB-001")
|
||||||
|
|
||||||
|
self.assertEqual(prepared.status, TaskStatus.CLAIMED)
|
||||||
|
self.assertEqual(second.attempt_no, 2)
|
||||||
|
|
||||||
|
def test_retry_wait_explains_permanently_failed_outbox(self):
|
||||||
|
self.repository.add_claimed_task(self._task("RETRY-REJECTED"))
|
||||||
|
started = self.repository.start_collect_run(
|
||||||
|
"RETRY-REJECTED", "USB-001"
|
||||||
|
)
|
||||||
|
event = self.repository.save_collect_failure(
|
||||||
|
"RETRY-REJECTED",
|
||||||
|
started.attempt_id,
|
||||||
|
TaskStatus.RETRY_WAIT,
|
||||||
|
"DEVICE_OFFLINE",
|
||||||
|
"设备离线",
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.repository.mark_outbox_failed(event.id, "任务不存在")
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
CollectRerunError, "上次结果提交失败:任务不存在"
|
||||||
|
):
|
||||||
|
self.repository.validate_collect_rerun("RETRY-REJECTED")
|
||||||
|
|
||||||
def test_recovery_restores_sending_and_interrupted_running(self):
|
def test_recovery_restores_sending_and_interrupted_running(self):
|
||||||
self.repository.add_claimed_task(self._task("TASK-RECOVER"))
|
self.repository.add_claimed_task(self._task("TASK-RECOVER"))
|
||||||
started = self.repository.start_collect_run("TASK-RECOVER", "USB-001")
|
started = self.repository.start_collect_run("TASK-RECOVER", "USB-001")
|
||||||
|
|||||||
@@ -425,13 +425,15 @@ CREATE TABLE app_settings (
|
|||||||
| `running` | `cancelled` | 用户停止,且已到安全点、未进入不可逆阶段 | 任务协调器 |
|
| `running` | `cancelled` | 用户停止,且已到安全点、未进入不可逆阶段 | 任务协调器 |
|
||||||
| `result_pending` 结果待提交 | `succeeded` | Admin 返回 `accepted: true` | 结果提交服务 |
|
| `result_pending` 结果待提交 | `succeeded` | Admin 返回 `accepted: true` | 结果提交服务 |
|
||||||
| `result_pending` | `manual_review` | 重试次数超上限仍提交不上去 | 结果提交服务 |
|
| `result_pending` | `manual_review` | 重试次数超上限仍提交不上去 | 结果提交服务 |
|
||||||
| `retry_wait` 重试等待 | `running` | 退避时间到,**本地直接重跑**,新建 `task_runs` 记录(`attempt_no` 加 1) | 任务协调器 |
|
| `retry_wait` 重试等待 | `running` | 重新启动获取任务,或用户确认“重新执行”;**本地直接重跑**并新建 `task_runs` 记录 | 任务协调器 / 人 |
|
||||||
| `retry_wait` | `failed` | 超过最大重试次数,且从未进入不可逆阶段 | 任务协调器 |
|
| `retry_wait` | `failed` | 超过最大重试次数,且从未进入不可逆阶段 | 任务协调器 |
|
||||||
| `retry_wait` | `manual_review` | 超过最大重试次数,但**曾经进入过不可逆阶段** | 任务协调器 |
|
| `retry_wait` | `manual_review` | 超过最大重试次数,但**曾经进入过不可逆阶段** | 任务协调器 |
|
||||||
| `manual_review` / `succeeded` / `failed` / `cancelled` | `claimed` | 用户在 Client 明确确认重新采集;仅限采集任务,且没有未发送 Outbox | 人 |
|
| `manual_review` / `succeeded` / `failed` / `cancelled` | `claimed` | 用户在 Client 明确确认重新采集;仅限采集任务,且没有未发送 Outbox | 人 |
|
||||||
|
|
||||||
**注意 `retry_wait` → `running` 是纯本地操作。** 任务已经在本地库里了,重试直接重跑就行,
|
**注意 `retry_wait` → `running` 是纯本地操作。** 任务已经在本地库里了,重试直接重跑就行,
|
||||||
**不需要再向 Admin 要一次**。没有租约,也就没有"重新获取执行权"这回事。
|
**不需要再向 Admin 要一次**。没有租约,也就没有"重新获取执行权"这回事。
|
||||||
|
当前 `retry_wait` 不包含倒计时。自动获取因可恢复采集错误停止后,界面必须显示
|
||||||
|
“重试已暂停”;用户可以选择任务点击“重新执行”,或重新启动获取任务。
|
||||||
|
|
||||||
### 7.2 补充规则
|
### 7.2 补充规则
|
||||||
|
|
||||||
|
|||||||
@@ -82,10 +82,12 @@
|
|||||||
- “刷新”只重新读取本地任务列表,不请求 Admin,也不操作手机。
|
- “刷新”只重新读取本地任务列表,不请求 Admin,也不操作手机。
|
||||||
- “重新执行”位于刷新右侧;没有选中任务时禁用。
|
- “重新执行”位于刷新右侧;没有选中任务时禁用。
|
||||||
- 点击后先校验任务,再显示明确的“重新采集”确认弹窗。弹窗显示任务编号、商品标题,并说明新结果会覆盖 Client 和 Admin 的当前采集数据。
|
- 点击后先校验任务,再显示明确的“重新采集”确认弹窗。弹窗显示任务编号、商品标题,并说明新结果会覆盖 Client 和 Admin 的当前采集数据。
|
||||||
- 只允许重新采集已经结束的采集任务。采购、执行中、结果待提交、等待重试、仍有未发送 Outbox 或自动获取忙碌时必须阻止,并用中文说明原因。
|
- 允许重新采集已经结束或处于“等待重试”的采集任务。采购、执行中、结果待提交、仍有未发送 Outbox 或自动获取忙碌时必须阻止,并用中文说明原因。
|
||||||
- 确认后只执行选中的稳定任务编号,不领取新任务,不先处理其他任务或 Outbox。
|
- 确认后只执行选中的稳定任务编号,不领取新任务,不先处理其他任务或 Outbox。
|
||||||
- 重新采集在工作线程运行。执行期间禁用“获取任务”和“重新执行”,完成后刷新列表。
|
- 重新采集在工作线程运行。执行期间禁用“获取任务”和“重新执行”,完成后刷新列表。
|
||||||
- 每次重新采集创建新的执行记录和幂等键;当前结果更新,旧结果保存在历史执行记录中。
|
- 每次重新采集创建新的执行记录和幂等键;当前结果更新,旧结果保存在历史执行记录中。
|
||||||
|
- “等待重试”当前没有倒计时。自动获取因可恢复采集错误停止时,底部状态显示
|
||||||
|
“重试已暂停”,并提示选择任务点击“重新执行”或重新启动获取任务。
|
||||||
|
|
||||||
## 5. 任务表格
|
## 5. 任务表格
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user