Files
getDomain/domain-api/tests/test_detect_job_service.py
Your Name 7cbde2aa78 d
2026-04-22 14:13:21 +08:00

793 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import unittest
from unittest.mock import MagicMock, patch
from psycopg2 import errors
from app.services.detect_job_service import (
append_detect_job_event,
_build_step_payload,
_classify_pipeline_item_outcome,
_classify_runtime_debug_event,
_enrich_active_job_summary_with_runtime,
_build_display_summary,
_build_runtime_display_bucket,
normalize_detect_step_code,
get_detect_queue_health,
process_detect_pipeline_now,
resolve_initial_domain_pipeline_item,
resolve_domain_pipeline_step,
resolve_detect_job_definition,
)
class DetectJobServiceTests(unittest.TestCase):
def test_append_detect_job_event_skips_missing_job_fk(self) -> None:
class FakeCursor:
def __init__(self) -> None:
self.exec_calls = []
self.selects = 0
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def execute(self, sql, params=None):
self.exec_calls.append((sql, params))
if "INSERT INTO detect_run_events" in sql:
raise AssertionError("should not insert detect_run_events when job row is missing")
if "SELECT 1 FROM detect_jobs" in sql:
self.selects += 1
def fetchone(self):
return None
class FakeConn:
def __init__(self) -> None:
self.cursor_obj = FakeCursor()
self.commit_calls = 0
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def cursor(self):
return self.cursor_obj
def commit(self):
self.commit_calls += 1
fake_conn = FakeConn()
with patch("app.services.detect_job_service.get_db", return_value=fake_conn):
with patch("app.services.detect_job_service.push_debug_event") as mock_push:
append_detect_job_event(
5,
event_type="job_dispatch_requested",
message="控制面已发送检测启动请求",
)
self.assertEqual(1, fake_conn.cursor_obj.selects)
self.assertEqual(0, fake_conn.commit_calls)
mock_push.assert_called_once()
def test_process_detect_pipeline_now_retries_deadlock_once(self) -> None:
calls = {"count": 0}
def _run_pipeline(*, limit, job_id):
calls["count"] += 1
if calls["count"] == 1:
raise errors.DeadlockDetected()
return {
"processed_items": 7,
"advanced_items": 3,
"retried_items": 1,
}
with patch("app.services.detect_job_service.process_detect_pipeline", side_effect=_run_pipeline):
with patch("app.services.detect_job_service.time.sleep") as mock_sleep:
ok, message, data = process_detect_pipeline_now(limit=123, job_id=45)
self.assertTrue(ok)
self.assertIn("deadlock 自动重试 1 次后成功", message)
self.assertEqual(1, data["retry_attempts"])
self.assertEqual(7, data["processed_items"])
self.assertEqual(2, calls["count"])
mock_sleep.assert_called_once()
def test_classify_runtime_debug_event_maps_single_step_finalized_completed(self) -> None:
classified = _classify_runtime_debug_event(
event_type="worker_log",
message="检测步骤跟踪: domain=example.com | step=注册状态检测 | stage=single_step_finalized | elapsed_ms=2700 | ok=1 | detect_key=detect_register | final_status=completed | result_state=passed",
payload={"job_code": "sync-overseas-27456", "node_code": "mainland-worker-01"},
)
self.assertEqual("sync-overseas-27456", classified["job_code"])
self.assertTrue(classified["terminal"])
self.assertEqual("completed", classified["terminal_status"])
self.assertEqual("detect_register", classified["step_code"])
def test_build_runtime_display_bucket_prefers_real_active_threads_over_raw_current_load(self) -> None:
bucket = _build_runtime_display_bucket(
(
"mainland-worker-01",
"mainland",
"worker",
"busy",
753,
{
"job_items_total": 1000,
"job_items_claimed": 0,
"job_items_running": 0,
"job_items_completed": 342,
"active_threads": 61,
"max_threads": 400,
"detect_participating": True,
},
None,
)
)
self.assertIsNotNone(bucket)
self.assertEqual(61, bucket["current_load"])
self.assertEqual(61, bucket["display_running"])
self.assertEqual(61, bucket["active_threads"])
def test_build_runtime_display_bucket_skips_local_overseas_control_plane_load(self) -> None:
with patch("app.services.detect_job_service.settings.node_region", "overseas"):
with patch("app.services.detect_job_service.settings.node_role", "control"):
with patch("app.services.detect_job_service.settings.node_code", "overseas-control-01"):
bucket = _build_runtime_display_bucket(
(
"overseas-control-01",
"overseas",
"control",
"busy",
371,
{
"active_threads": 371,
"max_threads": 3200,
"detect_participating": False,
},
None,
)
)
self.assertIsNone(bucket)
def test_build_runtime_display_bucket_skips_any_control_node_without_worker_support(self) -> None:
bucket = _build_runtime_display_bucket(
(
"overseas-control-01",
"overseas",
"control",
"busy",
378,
{
"active_threads": 378,
"max_threads": 3200,
"detect_participating": False,
"worker_online": False,
},
None,
)
)
self.assertIsNone(bucket)
def test_build_display_summary_ignores_inflated_raw_current_load(self) -> None:
summary = _build_display_summary(
[
{
"node_code": "mainland-controller-01",
"items_claimed": 0,
"items_running": 0,
"display_running": 729,
"current_load": 729,
"active_threads": 729,
"max_threads": 800,
"items_completed": 196,
"items_failed": 0,
},
{
"node_code": "mainland-worker-01",
"items_claimed": 0,
"items_running": 0,
"display_running": 753,
"current_load": 753,
"active_threads": 61,
"max_threads": 400,
"items_completed": 146,
"items_failed": 0,
},
]
)
self.assertEqual(790, summary["display_running"])
self.assertEqual(790, summary["current_load"])
self.assertEqual(790, summary["active_threads"])
def test_enrich_active_job_summary_with_runtime_adds_runtime_job_code_and_recent_events(self) -> None:
summary = _enrich_active_job_summary_with_runtime(
{
"job_id": 275,
"job_code": "sync-overseas-5297",
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
"recent_domain_events": [],
},
event_limit=20,
window_minutes=15,
runtime_activity={
"focus_job_code": "sync-overseas-9506",
"job_codes": ["sync-overseas-9506", "sync-overseas-27456"],
"processed_recent": 120,
"completed_recent": 118,
"failed_recent": 1,
"blacklisted_recent": 1,
},
runtime_snapshot={
"job": {
"job_id": 1835,
"job_code": "sync-overseas-9506",
"progress_percent": 44.2,
},
"queue_health": {
"queue": {
"items_total": 1000,
"pending": 176,
"claimed": 223,
"running": 159,
"completed": 442,
"display_running": 220,
}
},
},
recent_domain_events=[
{
"event_type": "domain_completed",
"message": "域名检测完成: example.com",
"created_at": "2026-04-21 04:10:00",
}
],
)
self.assertEqual("sync-overseas-9506", summary["runtime_job_code"])
self.assertEqual(["sync-overseas-9506", "sync-overseas-27456"], summary["runtime_job_codes"])
self.assertEqual(120, summary["processed_recent"])
self.assertEqual(8.0, summary["processed_per_minute"])
self.assertEqual(118, summary["completed_recent"])
self.assertEqual(1, summary["failed_recent"])
self.assertEqual(1, summary["blacklisted_recent"])
self.assertEqual(1, len(summary["recent_domain_events"]))
self.assertEqual("sync-overseas-9506", summary["runtime_snapshot_job_code"])
self.assertEqual(1000, summary["runtime_snapshot_queue"]["items_total"])
self.assertEqual(1000, summary["items_total"])
self.assertEqual(176, summary["items_pending"])
self.assertEqual(223, summary["items_claimed"])
self.assertEqual(159, summary["items_running"])
self.assertEqual(442, summary["items_completed"])
self.assertEqual(220, summary["display_items_running"])
def test_enrich_active_job_summary_with_runtime_prefers_snapshot_identity_and_events(self) -> None:
summary = _enrich_active_job_summary_with_runtime(
{
"job_id": 275,
"job_code": "sync-overseas-5297",
"status": "running",
"recent_events": [
{
"node_code": "mainland-worker-01",
"event_type": "domain_started",
"message": "开始检测域名: stale.com",
"payload": {"job_code": "sync-overseas-5297"},
"created_at": "2026-04-21 11:58:00",
}
],
"current_cycle_events": [],
"latest_event": None,
"display_items_running": 0,
"display_current_load": 0,
"display_active_threads": 0,
"display_max_threads": 0,
"display_active_node_codes": [],
},
event_limit=10,
window_minutes=15,
runtime_activity={
"focus_job_code": "sync-overseas-31437",
"job_codes": ["sync-overseas-31437"],
"processed_recent": 10,
"completed_recent": 10,
"failed_recent": 0,
"blacklisted_recent": 0,
},
runtime_snapshot={
"job": {
"job_id": 1902,
"job_code": "sync-overseas-31437",
"status": "running",
"progress_percent": 34.3,
"node_stats": [
{
"node_code": "mainland-controller-01",
"items_claimed": 230,
"items_running": 67,
"items_completed": 343,
"items_failed": 0,
"active_threads": 170,
"max_threads": 2000,
},
{
"node_code": "mainland-worker-01",
"items_claimed": 75,
"items_running": 0,
"items_completed": 0,
"items_failed": 0,
"active_threads": 19,
"max_threads": 1200,
},
],
},
"queue_health": {
"queue": {
"items_total": 1000,
"pending": 285,
"claimed": 305,
"running": 67,
"completed": 343,
"display_running": 189,
}
},
"recent_events": [
{
"job_id": 2137,
"node_code": "mainland-controller-01",
"event_type": "job_created",
"message": "同步拉取待检测批次 sync-overseas-35461共 1000 个任务项",
"payload": {"source_record_id": 35461},
"created_at": "2026-04-22 01:21:29",
},
{
"job_id": 1902,
"node_code": "mainland-controller-01",
"event_type": "worker_log",
"message": "从任务队列获取到 125 个需要检测的域名",
"payload": {"job_code": "sync-overseas-31437"},
"created_at": "2026-04-22 01:19:25",
},
],
},
)
self.assertEqual(1902, summary["job_id"])
self.assertEqual("sync-overseas-31437", summary["job_code"])
self.assertEqual("sync-overseas-31437", summary["runtime_job_code"])
self.assertEqual(189, summary["display_items_running"])
self.assertEqual(189, summary["display_current_load"])
self.assertEqual(189, summary["display_active_threads"])
self.assertEqual(3200, summary["display_max_threads"])
self.assertEqual(["mainland-controller-01", "mainland-worker-01"], summary["display_active_node_codes"])
self.assertEqual(1, len(summary["recent_events"]))
self.assertEqual("worker_log", summary["latest_event"]["event_type"])
self.assertEqual("sync-overseas-31437", summary["recent_events"][0]["payload"]["job_code"])
def test_get_detect_queue_health_preserves_runtime_node_throughput_after_snapshot_override(self) -> None:
lease_row = (None, None, 0, 0)
throughput_rows = []
step_throughput_rows = []
runtime_display_rows = [
(
"mainland-controller-01",
"mainland",
"control",
"busy",
170,
{
"job_items_total": 265,
"job_items_claimed": 85,
"job_items_running": 0,
"job_items_completed": 105,
"active_threads": 170,
"max_threads": 2000,
"detect_participating": True,
},
None,
),
(
"mainland-worker-01",
"mainland",
"worker",
"busy",
19,
{
"job_items_total": 139,
"job_items_claimed": 64,
"job_items_running": 0,
"job_items_completed": 4,
"active_threads": 19,
"max_threads": 1200,
"detect_participating": True,
},
None,
),
(
"overseas-control-01",
"overseas",
"control",
"busy",
371,
{
"active_threads": 371,
"max_threads": 3200,
"detect_participating": False,
},
None,
),
]
conn = MagicMock()
cursor_cm = MagicMock()
cursor = MagicMock()
conn.cursor.return_value = cursor_cm
cursor_cm.__enter__.return_value = cursor
db_cm = MagicMock()
db_cm.__enter__.return_value = conn
cursor.fetchone.return_value = lease_row
cursor.fetchall.side_effect = [throughput_rows, step_throughput_rows, runtime_display_rows]
active_job = {
"job_id": 1937,
"job_code": "sync-overseas-31987",
"status": "running",
"items_total": 1000,
"items_pending": 596,
"items_claimed": 149,
"items_running": 146,
"items_completed": 109,
"items_blacklisted": 0,
"items_failed": 0,
"progress_percent": 10.9,
"node_stats": [],
"distributed_node_stats": [
{
"node_code": "mainland-controller-01",
"items_total": 265,
"items_pending": 0,
"items_claimed": 85,
"items_running": 75,
"items_completed": 105,
"items_blacklisted": 0,
"items_failed": 0,
"metrics_source": "runtime",
},
{
"node_code": "mainland-worker-01",
"items_total": 139,
"items_pending": 0,
"items_claimed": 64,
"items_running": 71,
"items_completed": 4,
"items_blacklisted": 0,
"items_failed": 0,
"metrics_source": "runtime",
},
],
"step_stats": [
{
"step_code": "detect_register",
"items_total": 1000,
"items_pending": 449,
"items_claimed": 131,
"items_running": 273,
"items_completed": 147,
"items_blacklisted": 0,
"items_failed": 0,
}
],
}
runtime_activity = {
"processed_recent": 133,
"completed_recent": 113,
"failed_recent": 20,
"blacklisted_recent": 0,
"focus_job_code": "sync-overseas-31563",
"job_codes": ["sync-overseas-31563"],
"step_code": "detect_register",
"nodes": {
"mainland-controller-01": {
"node_code": "mainland-controller-01",
"processed_recent": 74,
"completed_recent": 54,
"failed_recent": 20,
"blacklisted_recent": 0,
},
"mainland-worker-01": {
"node_code": "mainland-worker-01",
"processed_recent": 59,
"completed_recent": 59,
"failed_recent": 0,
"blacklisted_recent": 0,
},
},
}
runtime_snapshot = {
"job": {
"job_id": 1937,
"job_code": "sync-overseas-31987",
"progress_percent": 10.9,
},
"queue_health": {
"queue": {
"items_total": 1000,
"pending": 596,
"claimed": 149,
"running": 146,
"completed": 109,
"blacklisted": 0,
"failed": 0,
"display_claimed": 149,
"display_running": 403,
},
"nodes": [
{
"node_code": "mainland-controller-01",
"items_total": 265,
"items_pending": 0,
"items_claimed": 85,
"items_running": 75,
"items_completed": 105,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
"metrics_source": "runtime",
},
{
"node_code": "mainland-worker-01",
"items_total": 139,
"items_pending": 0,
"items_claimed": 64,
"items_running": 71,
"items_completed": 4,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
"metrics_source": "runtime",
},
],
"steps": [],
},
}
with patch("app.services.detect_job_service.settings.node_region", "overseas"):
with patch("app.services.detect_job_service.settings.node_role", "control"):
with patch("app.services.detect_job_service.settings.node_code", "overseas-control-01"):
with patch("app.services.detect_job_service.get_active_detect_job_summary", return_value=active_job):
with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value=runtime_activity):
with patch("app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value=runtime_snapshot):
with patch("app.services.detect_job_service.get_db", return_value=db_cm):
health = get_detect_queue_health(window_minutes=15)
node_map = {item["node_code"]: item for item in health["nodes"]}
self.assertEqual(74, node_map["mainland-controller-01"]["processed_recent"])
self.assertEqual(54, node_map["mainland-controller-01"]["completed_recent"])
self.assertEqual(20, node_map["mainland-controller-01"]["failed_recent"])
self.assertEqual(170, node_map["mainland-controller-01"]["items_running"])
self.assertEqual(59, node_map["mainland-worker-01"]["processed_recent"])
self.assertEqual(59, node_map["mainland-worker-01"]["completed_recent"])
self.assertEqual(19, node_map["mainland-worker-01"]["items_running"])
self.assertEqual(113, health["throughput"]["completed_recent"])
self.assertEqual(189, health["queue"]["display_running"])
self.assertNotIn("overseas-control-01", node_map)
def test_normalize_detect_step_code_accepts_supported_single_step(self) -> None:
self.assertEqual("detect_baidu_site", normalize_detect_step_code("detect_baidu_site"))
self.assertEqual("detect_wayback", normalize_detect_step_code("detect_wayback"))
def test_normalize_detect_step_code_rejects_unknown_step(self) -> None:
self.assertEqual("", normalize_detect_step_code("detect_unknown"))
def test_resolve_detect_job_definition_builds_single_step_job(self) -> None:
definition = resolve_detect_job_definition("detect_baidu_site")
self.assertTrue(definition["is_single_step"])
self.assertEqual("single_step", definition["task_mode"])
self.assertEqual("detect_baidu_site", definition["step_code"])
def test_resolve_detect_job_definition_builds_wayback_single_step_job(self) -> None:
definition = resolve_detect_job_definition("detect_wayback")
self.assertTrue(definition["is_single_step"])
self.assertEqual("single_step", definition["task_mode"])
self.assertEqual("detect_wayback", definition["step_code"])
def test_build_step_payload_adds_wayback_recent_years_strategy(self) -> None:
payload = _build_step_payload(
step_code="detect_wayback",
domain_snapshot={"domain": "example.com", "source_type": 2},
settings_payload={"detect_options": {"detect_wayback": True}},
)
self.assertEqual("detect_wayback", payload["step_code"])
self.assertEqual("recent_years", payload["wayback_strategy"])
self.assertEqual(5, payload["wayback_recent_years"])
self.assertTrue(payload["wayback_stop_on_first_hit"])
def test_resolve_detect_job_definition_defaults_to_domain_pipeline(self) -> None:
definition = resolve_detect_job_definition(None)
self.assertFalse(definition["is_single_step"])
self.assertEqual("domain_pipeline", definition["task_mode"])
self.assertEqual("", definition["step_code"])
def test_resolve_domain_pipeline_step_skips_yikoujia_register(self) -> None:
step_code = resolve_domain_pipeline_step(
{
"source_type": 1,
"register_status": 0,
"baidu_site": {},
"qihu360_site": {},
"chinaz_info": {},
"aizhan_info": {},
"wayback_info": {},
"jucha_info": {},
"juziseo_info": {},
},
settings_payload={
"detect_options": {
"detect_register": True,
"detect_baidu_site": True,
"detect_360_site": False,
"detect_chinaz": False,
"detect_aizhan": False,
"detect_wayback": False,
"detect_jucha": False,
"detect_juziseo": False,
"detect_order": ["detect_register", "detect_baidu_site"],
}
},
)
self.assertEqual("detect_baidu_site", step_code)
def test_resolve_domain_pipeline_step_moves_to_next_incomplete_step(self) -> None:
step_code = resolve_domain_pipeline_step(
{
"source_type": 2,
"register_status": 3,
"baidu_site": {"state": "passed"},
"qihu360_site": {},
"chinaz_info": {},
"aizhan_info": {},
"wayback_info": {},
"jucha_info": {},
"juziseo_info": {},
},
settings_payload={
"detect_options": {
"detect_register": True,
"detect_baidu_site": True,
"detect_360_site": True,
"detect_chinaz": False,
"detect_aizhan": False,
"detect_wayback": False,
"detect_jucha": False,
"detect_juziseo": False,
"detect_order": ["detect_register", "detect_baidu_site", "detect_360_site"],
}
},
after_step_code="detect_baidu_site",
)
self.assertEqual("detect_360_site", step_code)
def test_resolve_initial_domain_pipeline_item_builds_first_step_payload(self) -> None:
step_code, payload = resolve_initial_domain_pipeline_item(
{
"id": 10,
"domain": "example.com",
"source_type": 2,
"register_status": 0,
"baidu_site": {},
"qihu360_site": {},
"chinaz_info": {},
"aizhan_info": {},
"wayback_info": {},
"jucha_info": {},
"juziseo_info": {},
},
settings_payload={
"detect_options": {
"detect_register": True,
"detect_baidu_site": True,
"detect_order": ["detect_register", "detect_baidu_site"],
}
},
)
self.assertEqual("detect_register", step_code)
self.assertIsNotNone(payload)
self.assertEqual("detect_register", payload["step_code"])
self.assertEqual("example.com", payload["domain"])
def test_resolve_initial_domain_pipeline_item_returns_empty_when_pipeline_already_done(self) -> None:
step_code, payload = resolve_initial_domain_pipeline_item(
{
"id": 11,
"domain": "done.com",
"source_type": 2,
"register_status": 3,
"baidu_site": {"state": "passed"},
"qihu360_site": {},
"chinaz_info": {},
"aizhan_info": {},
"wayback_info": {},
"jucha_info": {},
"juziseo_info": {},
},
settings_payload={
"detect_options": {
"detect_register": True,
"detect_baidu_site": True,
"detect_order": ["detect_register", "detect_baidu_site"],
}
},
)
self.assertEqual("", step_code)
self.assertIsNone(payload)
def test_classify_pipeline_item_outcome_retries_external_failure(self) -> None:
outcome = _classify_pipeline_item_outcome(
item_status="failed",
result_payload={"state": "degraded", "message": "timeout", "retry_recommended": True},
step_code="detect_baidu_site",
attempt_count=0,
)
self.assertEqual("retry", outcome["action"])
self.assertTrue(outcome["should_retry"])
self.assertEqual("external_retry", outcome["reason_code"])
def test_classify_pipeline_item_outcome_rejects_business_failure(self) -> None:
outcome = _classify_pipeline_item_outcome(
item_status="failed",
result_payload={"state": "rejected", "message": "title contains forbidden keyword"},
step_code="detect_chinaz",
attempt_count=0,
)
self.assertEqual("reject", outcome["action"])
self.assertFalse(outcome["should_retry"])
self.assertEqual("business_reject", outcome["reason_code"])
def test_classify_pipeline_item_outcome_marks_blacklisted_terminal(self) -> None:
outcome = _classify_pipeline_item_outcome(
item_status="blacklisted",
result_payload={"state": "blacklisted", "message": "risk hit"},
step_code="detect_baidu_site",
attempt_count=0,
)
self.assertEqual("black_hit", outcome["action"])
self.assertFalse(outcome["should_retry"])
self.assertEqual("blacklisted", outcome["reason_code"])
if __name__ == "__main__":
unittest.main()