feat: 支持持续自动获取任务 (#45)

This commit is contained in:
chengma
2026-08-09 10:54:18 +08:00
parent 4022feee43
commit 5fd71981e0
4 changed files with 317 additions and 30 deletions
+141 -6
View File
@@ -12,7 +12,7 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt5.QtWidgets import QApplication
from src.pdd_ui import PDDTaskPage
from src.admin_gateway import AdminTask, SubmissionReceipt
from src.admin_gateway import AdminGatewayError, AdminTask, SubmissionReceipt
from src.pdd_ui_event import (
PDDTaskPageEvent,
admin_task_to_new_claimed_task,
@@ -78,6 +78,35 @@ class SlowClaimGateway(RecordingClaimGateway):
return super().claim_next(client, capabilities)
class SequenceClaimGateway(RecordingClaimGateway):
"""依次返回多条任务,并记录是否出现并发领取。"""
def __init__(self, responses):
super().__init__()
self.responses = list(responses)
self.active_calls = 0
self.max_active_calls = 0
def claim_next(self, client, capabilities):
self.active_calls += 1
self.max_active_calls = max(self.max_active_calls, self.active_calls)
try:
time.sleep(0.01)
response = self.responses.pop(0) if self.responses else None
self.response = response
return super().claim_next(client, capabilities)
finally:
self.active_calls -= 1
class RetryableClaimGateway(RecordingClaimGateway):
"""模拟 Admin 暂时不可用。"""
def claim_next(self, client, capabilities):
self.calls.append((client, capabilities))
raise AdminGatewayError("ADMIN_UNAVAILABLE", "Admin 暂时不可用", True)
class FakeCollectResult:
def to_pdd_data(self):
return {
@@ -331,7 +360,7 @@ class PDDTaskPageEventTest(unittest.TestCase):
"737116531267",
)
def test_click_claims_one_collect_task_saves_and_refreshes_table(self):
def test_start_claims_task_and_keeps_auto_fetch_running(self):
gateway = RecordingClaimGateway(collect_admin_task())
page = PDDTaskPage()
events = PDDTaskPageEvent(
@@ -357,12 +386,12 @@ class PDDTaskPageEventTest(unittest.TestCase):
self.assertEqual(self.repository.count_tasks(), 1)
self.assertEqual(page.taskModel.data_row_count(), 1)
self.assertEqual(page.taskModel.row_at(0).remote_task_id, "COL-001")
self.assertEqual(page.autoFetchButton.text(), "获取任务")
self.assertEqual(page.autoFetchButton.text(), "停止自动获取")
self.assertIn("COL-001", page.statusLabel.text())
events.shutdown()
page.deleteLater()
def test_204_is_neutral_and_each_click_only_calls_once(self):
def test_no_task_schedules_next_claim_and_stop_cancels_timer(self):
gateway = RecordingClaimGateway(None)
page = PDDTaskPage()
events = PDDTaskPageEvent(
@@ -377,12 +406,18 @@ class PDDTaskPageEventTest(unittest.TestCase):
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
self.assertEqual(len(gateway.calls), 1)
self.assertEqual(page.statusLabel.text(), "暂无可领取的采集任务")
self.assertIn("5 秒后再次领取", page.statusLabel.text())
self.assertTrue(events._next_cycle_timer.isActive())
self.assertEqual(self.repository.count_tasks(), 0)
page.autoFetchRequested.emit()
self.assertFalse(events._next_cycle_timer.isActive())
self.assertEqual(page.autoFetchButton.text(), "开始自动获取")
events.shutdown()
page.deleteLater()
def test_repeated_click_while_claiming_does_not_start_second_request(self):
def test_second_click_while_claiming_requests_safe_stop(self):
gateway = SlowClaimGateway(None)
page = PDDTaskPage()
events = PDDTaskPageEvent(
@@ -394,11 +429,86 @@ class PDDTaskPageEventTest(unittest.TestCase):
)
page.autoFetchRequested.emit()
self.assertTrue(wait_until(self.app, gateway.started.is_set))
page.autoFetchRequested.emit()
self.assertFalse(page.autoFetchButton.isEnabled())
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
self.assertEqual(len(gateway.calls), 1)
self.assertEqual(page.autoFetchButton.text(), "开始自动获取")
events.shutdown()
page.deleteLater()
def test_one_start_processes_multiple_tasks_strictly_in_sequence(self):
gateway = SequenceClaimGateway(
[collect_admin_task("COL-001"), collect_admin_task("COL-002")]
)
page = PDDTaskPage()
events = PDDTaskPageEvent(
page,
self.repository,
claim_gateway=gateway,
settings_repository=self._saved_settings(),
collect_service_factory=fake_collect_factory,
next_task_delay_ms=10,
no_task_delay_ms=1_000,
)
page.autoFetchRequested.emit()
self.assertTrue(
wait_until(self.app, lambda: self.repository.count_tasks() == 2)
)
self.assertEqual(gateway.max_active_calls, 1)
self.assertEqual(page.autoFetchButton.text(), "停止自动获取")
page.autoFetchRequested.emit()
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
self.assertEqual(page.autoFetchButton.text(), "开始自动获取")
events.shutdown()
page.deleteLater()
def test_retryable_admin_error_schedules_backoff(self):
gateway = RetryableClaimGateway()
page = PDDTaskPage()
events = PDDTaskPageEvent(
page,
self.repository,
claim_gateway=gateway,
settings_repository=self._saved_settings(),
retry_delays_ms=(20, 40, 80, 100),
)
page.autoFetchRequested.emit()
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
self.assertEqual(events._retry_count, 1)
self.assertEqual(events._next_cycle_timer.interval(), 20)
self.assertIn("退避等待", page.statusLabel.text())
page.autoFetchRequested.emit()
self.assertFalse(events._next_cycle_timer.isActive())
events.shutdown()
page.deleteLater()
def test_retry_delay_grows_to_cap_and_success_resets_it(self):
page = PDDTaskPage()
events = PDDTaskPageEvent(
page,
self.repository,
claim_gateway=RecordingClaimGateway(None),
settings_repository=self._saved_settings(),
retry_delays_ms=(5_000, 10_000, 20_000, 30_000),
)
observed = []
for _ in range(5):
events._on_claim_retryable_failed("Admin 暂时不可用")
observed.append(events._cycle_next_delay_ms)
self.assertEqual(observed, [5_000, 10_000, 20_000, 30_000, 30_000])
events._on_collect_outcome("succeeded", "任务完成", "COL-001")
self.assertEqual(events._retry_count, 0)
events.shutdown()
page.deleteLater()
@@ -459,9 +569,34 @@ class PDDTaskPageEventTest(unittest.TestCase):
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
self.assertEqual(gateway.calls, [])
self.assertIn("设置页", page.statusLabel.text())
self.assertFalse(events._auto_fetch_running)
self.assertEqual(page.autoFetchButton.text(), "开始自动获取")
events.shutdown()
page.deleteLater()
def test_shutdown_cancels_scheduled_next_claim(self):
gateway = RecordingClaimGateway(None)
page = PDDTaskPage()
events = PDDTaskPageEvent(
page,
self.repository,
claim_gateway=gateway,
settings_repository=self._saved_settings(),
no_task_delay_ms=20,
)
page.autoFetchRequested.emit()
self.assertTrue(wait_until(self.app, lambda: not events._claim_busy))
self.assertTrue(events._next_cycle_timer.isActive())
events.shutdown()
deadline = time.monotonic() + 0.08
while time.monotonic() < deadline:
self.app.processEvents()
time.sleep(0.005)
self.assertEqual(len(gateway.calls), 1)
page.deleteLater()
def test_shutdown_after_claim_started_still_saves_task_without_ui_callback(self):
gateway = SlowClaimGateway(collect_admin_task("COL-CLOSE"))
page = PDDTaskPage()