import collections import json import os import threading import tempfile import unittest import sys import time from queue import Queue from unittest.mock import MagicMock from unittest.mock import patch sys.path.insert(0, "/www/wwwroot/getDomain/domainCheck") import detect_worker as detect_worker_module # noqa: E402 from app.config import config # noqa: E402 from detect_worker import DetectWorker, control_targets_current_worker, pending_control_key # noqa: E402 from app.utils.status_codes import REGISTER_STATUS_CLIENT_HOLD, REGISTER_STATUS_SERVER_HOLD # noqa: E402 class _FakeDb: def __init__(self, method_name: str): self.method_name = method_name self.calls = 0 def _run(self, batch): self.calls += 1 return -1 if self.calls == 1 else len(batch) def mark_detect_job_items_running_batch(self, batch): return self._run(batch) def release_detect_job_items_batch(self, batch): return self._run(batch) def finalize_detect_job_items_batch(self, batch): return self._run(batch) class _FakeRedis: def __init__(self): self._store = {} self._expires = {} def _cleanup(self, key): expires_at = self._expires.get(key) if expires_at is not None and expires_at <= time.time(): self._store.pop(key, None) self._expires.pop(key, None) def get(self, key): self._cleanup(key) return self._store.get(key) def set(self, key, value, nx=False, ex=None): self._cleanup(key) if nx and key in self._store: return False self._store[key] = value if ex is not None: self._expires[key] = time.time() + float(ex) else: self._expires.pop(key, None) return True def delete(self, key): existed = key in self._store self._store.pop(key, None) self._expires.pop(key, None) return 1 if existed else 0 class DetectWorkerFlushRetryTest(unittest.TestCase): def _build_worker(self, db): worker = DetectWorker.__new__(DetectWorker) worker.db = db worker._task_local = threading.local() worker._running_mark_lock = threading.Lock() worker._pending_running_marks = collections.deque() worker._last_running_mark_flush_at = 0.0 worker._job_release_lock = threading.Lock() worker._pending_job_releases = collections.deque() worker._pending_job_release_reasons = {} worker._last_job_release_flush_at = 0.0 worker._job_finalize_lock = threading.Lock() worker._pending_job_finalizations = collections.deque() worker._last_job_finalize_flush_at = 0.0 worker.proxy_pool = [] worker.proxy_pool_lock = threading.Lock() worker.proxy_refresh_lock = threading.Lock() worker.proxy_refresh_cooldown_seconds = 2 worker.proxy_next_refresh_time = 0.0 worker._last_proxy_refresh_reason_at = {} worker.proxy_failure_lock = threading.Lock() worker.proxy_failure_counts = {} worker.proxy_quarantine_until = {} worker.proxy_source_by_key = {} worker.proxy_source_failure_counts = {} worker.proxy_source_quarantine_until = {} worker.proxy_active_leases = {} worker.proxy_config = {"proxy_enable": True, "allow_direct": True} worker.proxy_last_refresh_time = None worker.proxy_last_refresh_status = "test" worker.proxy_last_refresh_source_count = 0 worker.proxy_last_refresh_total_items = 0 worker.proxy_last_validated_count = 0 worker.proxy_last_available_count = 0 worker.proxy_last_source_stats = [] worker._last_proxy_clock_skew_warning_at = 0.0 worker.proxy_max_reuse_count = 1000 worker.proxy_step_wait_timeout_seconds = 1.5 worker.proxy_direct_fallback_grace_seconds = 0.35 worker.proxy_direct_fallback_grace_overrides = {} worker.proxy_direct_retry_wait_seconds = 1.2 worker.proxy_direct_retry_wait_overrides = {} worker.register_single_machine_mode_enabled = False worker.register_single_machine_direct_streak_attempts = 0 worker.trigger_proxy_refresh = MagicMock() worker._update_runtime_state = MagicMock() worker._sync_worker_log_event = MagicMock() worker._emit_step_trace = MagicMock() worker.runtime_settings = {} worker.thread_count = 600 worker.process_count = 1 worker.refresh_process_count_runtime = MagicMock(side_effect=lambda *args, **kwargs: worker.process_count) worker.use_redis = False worker.redis_client = None worker.detect_lock = threading.Lock() worker.running = True worker.detecting = True worker.stop_requested = False worker.detect_command_thread = None worker.detect_command_started_at = 0.0 worker.detect_command_last_activity_at = 0.0 worker._last_domain_started_at = 0.0 worker._last_domain_result_at = 0.0 worker._last_job_item_release_at = 0.0 worker._active_domain_threads = 0 worker._domain_thread_counter_lock = threading.Lock() worker.current_cycle_token = "" worker.current_job_id = None worker.current_job_code = "" worker.current_job_task_mode = "" worker._detect_session_owner = 0 worker._detect_session_seq = 0 worker._pending_restart_source = "" worker._pending_restart_payload = None worker._pending_restart_reason = "" worker._worker_started_at = time.time() worker._last_worker_activity_at = worker._worker_started_at worker._idle_recycle_requested = False worker._last_explicit_start_signal_at = 0.0 worker._last_explicit_start_payload = {} worker._last_detection_start_at = 0.0 worker._last_detection_start_source = "" worker._last_detection_start_job_id = None worker._last_detection_start_job_code = "" worker._last_detection_start_task_mode = "" worker._cached_active_detect_job = None worker._last_active_detect_job_refresh_at = 0.0 worker._explicit_claim_scope_job_id = None worker._explicit_claim_scope_job_code = "" worker._explicit_claim_scope_until = 0.0 worker._explicit_claim_scope_source = "" worker._ignored_targeted_job_id = None worker._ignored_targeted_job_code = "" worker._ignored_targeted_job_until = 0.0 worker._ignored_targeted_job_targets = "" worker._get_active_domain_threads = MagicMock(return_value=0) worker._worker_log_sync_queue = Queue(maxsize=50000) worker._last_worker_log_sync_degrade_notice_at = 0.0 worker._remote_debug_event_failure_streak = 0 worker._remote_debug_event_cooldown_until = 0.0 worker._last_remote_debug_event_notice_at = 0.0 worker._last_runtime_extra = {} worker._last_runtime_phase = "idle" worker._last_runtime_detail = "" worker._runtime_heartbeat_stop = threading.Event() worker._worker_log_sync_stop = threading.Event() worker._flush_pending_domain_status_updates = MagicMock(return_value=0) worker._flush_pending_domain_completions = MagicMock(return_value=0) worker._flush_pending_review_status_updates = MagicMock(return_value=0) worker._flush_completed_futures = MagicMock(return_value=0) worker.single_machine_site_direct_fallback_enabled = False return worker def test_running_mark_flush_requeues_batch_when_db_write_fails(self): worker = self._build_worker(_FakeDb("running")) worker._pending_running_marks.extend([(101, "token-a"), (102, "token-b")]) updated = worker._flush_pending_running_marks(force=True, batch_limit=10) self.assertEqual(0, updated) self.assertEqual([(101, "token-a"), (102, "token-b")], list(worker._pending_running_marks)) updated = worker._flush_pending_running_marks(force=True, batch_limit=10) self.assertEqual(2, updated) self.assertEqual([], list(worker._pending_running_marks)) def test_job_finalization_flush_requeues_batch_when_db_write_fails(self): worker = self._build_worker(_FakeDb("finalize")) worker._pending_job_finalizations.extend( [ {"job_item_id": 201, "claim_token": "token-a", "final_status": "completed"}, {"job_item_id": 202, "claim_token": "token-b", "final_status": "failed"}, ] ) updated = worker._flush_pending_job_finalizations(force=True, batch_limit=10) self.assertEqual(0, updated) self.assertEqual(2, len(worker._pending_job_finalizations)) updated = worker._flush_pending_job_finalizations(force=True, batch_limit=10) self.assertEqual(2, updated) self.assertEqual(0, len(worker._pending_job_finalizations)) def test_job_release_flush_requeues_batch_when_db_write_fails(self): worker = self._build_worker(_FakeDb("release")) worker._enqueue_job_release(201, "token-a", "session_replaced:7") worker._enqueue_job_release(202, "token-b", "queued_before_start") updated = worker._flush_pending_job_releases(force=True, batch_limit=10) self.assertEqual(0, updated) self.assertEqual(2, len(worker._pending_job_releases)) updated = worker._flush_pending_job_releases(force=True, batch_limit=10) self.assertEqual(2, updated) self.assertEqual(0, len(worker._pending_job_releases)) def test_detection_session_abort_reason_rejects_replaced_owner(self): worker = self._build_worker(_FakeDb("noop")) worker._detect_session_owner = 7 self.assertEqual("session_replaced:7", worker._detection_session_abort_reason(6)) self.assertEqual("", worker._detection_session_abort_reason(7)) def test_release_job_item_for_session_abort_enqueues_current_item(self): db = MagicMock() worker = self._build_worker(db) worker._detect_session_owner = 7 released = worker._release_job_item_for_session_abort( 201, "token-a", session_id=6, domain_name="example.com", detail="prepare", ) self.assertTrue(released) self.assertEqual(1, len(worker._pending_job_releases)) job_item_id, claim_token = worker._pending_job_releases[0] reason = worker._pending_job_release_reasons[(job_item_id, claim_token)] self.assertEqual(201, job_item_id) self.assertEqual("token-a", claim_token) self.assertIn("session_replaced:7", reason) self.assertIn("prepare", reason) def test_enqueue_job_release_deduplicates_same_job_item_and_claim_token(self): worker = self._build_worker(_FakeDb("release")) self.assertTrue(worker._enqueue_job_release(201, "token-a", "queued_before_start")) self.assertTrue(worker._enqueue_job_release(201, "token-a", "session_replaced:9")) self.assertEqual(1, len(worker._pending_job_releases)) self.assertEqual((201, "token-a"), worker._pending_job_releases[0]) self.assertEqual( "session_replaced:9", worker._pending_job_release_reasons[(201, "token-a")], ) def test_incoming_job_id_prefers_target_job_id_for_local_queue_scope(self): worker = self._build_worker(_FakeDb("noop")) payload = { "job_id": 26025, "job_code": "sync-overseas-26025", "target_job_id": 1, "target_job_code": "sync-overseas-26025-local", "task_mode": "domain_pipeline", } self.assertEqual(1, worker._incoming_job_id(payload)) def test_schedule_explicit_claim_scope_uses_target_job_id_when_present(self): worker = self._build_worker(_FakeDb("noop")) payload = { "job_id": 26025, "job_code": "sync-overseas-26025", "target_job_id": 1, "target_job_code": "sync-overseas-26025-local", "task_mode": "domain_pipeline", } worker._schedule_explicit_claim_scope(payload, source="sync-pull") self.assertEqual(1, worker._explicit_claim_scope_job_id) self.assertEqual("sync-overseas-26025", worker._explicit_claim_scope_job_code) self.assertEqual("sync-pull", worker._explicit_claim_scope_source) def test_detect_domain_releases_job_item_when_session_replaced(self): db = MagicMock() worker = self._build_worker(db) worker._detect_session_owner = 7 worker._set_current_task_context = MagicMock() worker._clear_current_task_context = MagicMock() worker._mark_detection_phase = MagicMock() worker._release_job_item_for_session_abort = MagicMock(return_value=True) worker.detect_domain( 101, {"id": 101, "domain": "example.com"}, { "job_item_id": 301, "job_id": 1, "claim_token": "token-x", "task_mode": "domain_pipeline", "session_id": 6, }, ) worker._release_job_item_for_session_abort.assert_called_once() args, kwargs = worker._release_job_item_for_session_abort.call_args self.assertEqual(301, args[0]) self.assertEqual("token-x", args[1]) self.assertEqual(6, kwargs["session_id"]) self.assertEqual("example.com", kwargs["domain_name"]) def test_detect_domain_step_task_writes_domain_started_event(self): db = MagicMock() worker = self._build_worker(db) worker.sensitive_words = [] worker._set_current_task_context = MagicMock() worker._clear_current_task_context = MagicMock() worker._assert_detection_session_active = MagicMock() worker._change_active_domain_threads = MagicMock(return_value=1) worker._mark_detection_phase = MagicMock() worker._enqueue_running_mark = MagicMock() worker._run_detect_register = MagicMock(return_value=True) worker._get_detect_execution_order = MagicMock(return_value=["detect_register"]) worker._build_single_step_job_result = MagicMock(return_value={"state": "completed", "message": "ok"}) worker._resolve_single_step_finalization = MagicMock(return_value=("completed", "ok")) worker._enqueue_job_finalization = MagicMock() worker.detect_domain( 101, {"id": 101, "domain": "example.com"}, { "job_item_id": 301, "job_id": 1, "job_code": "sync-overseas-26118", "claim_token": "token-x", "task_mode": "domain_pipeline", "step_code": "detect_register", "step_payload": {"step_code": "detect_register"}, "session_id": 7, }, ) db.append_detect_run_event.assert_called_once() self.assertEqual("domain_started", db.append_detect_run_event.call_args.kwargs["event_type"]) payload = db.append_detect_run_event.call_args.kwargs["payload"] self.assertEqual("domain_pipeline", payload["task_mode"]) self.assertEqual("detect_register", payload["step_code"]) def test_detect_domain_step_task_finalize_emits_domain_terminal_event(self): db = MagicMock() worker = self._build_worker(db) worker.sensitive_words = [] worker._set_current_task_context = MagicMock() worker._clear_current_task_context = MagicMock() worker._assert_detection_session_active = MagicMock() worker._change_active_domain_threads = MagicMock(return_value=1) worker._mark_detection_phase = MagicMock() worker._enqueue_running_mark = MagicMock() worker._run_detect_register = MagicMock(return_value=True) worker._get_detect_execution_order = MagicMock(return_value=["detect_register"]) worker._build_single_step_job_result = MagicMock( return_value={"state": "blocked", "message": "client hold", "field_name": "register_status"} ) worker._resolve_single_step_finalization = MagicMock(return_value=("blacklisted", "client hold")) worker._enqueue_job_finalization = MagicMock() worker.detect_domain( 101, {"id": 101, "domain": "example.com"}, { "job_item_id": 301, "job_id": 1, "job_code": "sync-overseas-26118", "claim_token": "token-x", "task_mode": "domain_pipeline", "step_code": "detect_register", "step_payload": {"step_code": "detect_register"}, "session_id": 7, }, ) self.assertTrue(worker._enqueue_job_finalization.called) self.assertEqual( "domain_blacklisted", worker._enqueue_job_finalization.call_args.kwargs["event_type"], ) event_payload = worker._enqueue_job_finalization.call_args.kwargs["event_payload"] self.assertEqual("example.com", event_payload["domain"]) self.assertEqual("blacklisted", event_payload["status"]) def test_start_detection_async_ignores_duplicate_start_for_same_job_and_cycle(self): worker = self._build_worker(_FakeDb("noop")) live_thread = MagicMock() live_thread.is_alive.return_value = True worker.detect_command_thread = live_thread worker.current_job_id = 321 worker.current_job_code = "sync-overseas-321" worker.current_cycle_token = "cycle-abc" worker.detect_command_started_at = time.time() - 75 worker.detect_command_last_activity_at = time.time() - 60 worker._last_domain_result_at = time.time() - 60 started = worker.start_detection_async( source="redis-control", control_payload={ "job_id": 321, "job_code": "sync-overseas-321", "cycle_token": "cycle-abc", }, ) self.assertTrue(started) self.assertIs(worker.detect_command_thread, live_thread) self.assertFalse(worker.stop_requested) worker._update_runtime_state.assert_called() def test_start_detection_async_does_not_coalesce_recent_duplicate_without_live_thread(self): worker = self._build_worker(_FakeDb("noop")) worker.detect_command_thread = None worker.detecting = False worker.current_job_id = 321 worker.current_job_code = "sync-overseas-321" worker.current_job_task_mode = "domain_pipeline" worker._last_detection_start_at = time.time() - 10 worker._last_detection_start_source = "auto-resume" worker._last_detection_start_job_id = 321 worker._last_detection_start_job_code = "sync-overseas-321" worker._last_detection_start_task_mode = "domain_pipeline" with patch("detect_worker.threading.Thread") as mock_thread: started = worker.start_detection_async( source="redis-control", control_payload={}, ) self.assertTrue(started) mock_thread.assert_called_once() worker._update_runtime_state.assert_called() def test_start_detection_async_does_not_coalesce_recent_duplicate_for_stale_session(self): worker = self._build_worker(_FakeDb("noop")) live_thread = MagicMock() live_thread.is_alive.return_value = True worker.detect_command_thread = live_thread worker.detecting = True worker.current_job_id = 874 worker.current_job_code = "sync-overseas-19897" worker.current_job_task_mode = "domain_pipeline" worker._last_detection_start_at = time.time() - 10 worker._last_detection_start_source = "auto-resume" worker._last_detection_start_job_id = 874 worker._last_detection_start_job_code = "sync-overseas-19897" worker._last_detection_start_task_mode = "domain_pipeline" worker._is_stale_detect_session = MagicMock(return_value=(True, "stale")) worker._should_force_takeover_stale_session = MagicMock(return_value=True) with patch("detect_worker.threading.Thread") as mock_thread: started = worker.start_detection_async( source="auto-resume", control_payload={ "job_id": 874, "job_code": "sync-overseas-19897", "task_mode": "domain_pipeline", }, ) self.assertTrue(started) mock_thread.assert_called_once() worker._update_runtime_state.assert_called() def test_run_detection_session_clears_context_before_idle_runtime_update(self): worker = self._build_worker(_FakeDb("noop")) worker.start_detection = MagicMock(return_value=None) worker._consume_pending_restart_request = MagicMock(return_value=False) worker._maybe_recycle_idle_worker = MagicMock(return_value=False) control_payload = { "job_id": 876, "job_code": "sync-overseas-19961", "task_mode": "domain_pipeline", "cycle_token": "cycle-876", } worker._run_detection_session(source="redis-control", control_payload=control_payload, session_id=1) phase, detail = worker._update_runtime_state.call_args_list[-1].args[:2] kwargs = worker._update_runtime_state.call_args_list[-1].kwargs self.assertEqual("idle", phase) self.assertEqual("检测任务结束,Worker 保持待命", detail) self.assertNotIn("job_id", kwargs) self.assertNotIn("job_code", kwargs) self.assertEqual("", worker.current_cycle_token) self.assertIsNone(worker.current_job_id) self.assertEqual("", worker.current_job_code) def test_start_detection_async_schedules_pending_restart_for_explicit_pipeline_switch(self): worker = self._build_worker(_FakeDb("noop")) live_thread = MagicMock() live_thread.is_alive.return_value = True worker.detect_command_thread = live_thread worker.current_job_id = 867 worker.current_job_code = "sync-overseas-19835" worker.current_job_task_mode = "domain_pipeline" worker._explicit_claim_scope_job_id = 884 worker._explicit_claim_scope_job_code = "sync-overseas-20554" worker._explicit_claim_scope_until = time.time() + 120 started = worker.start_detection_async( source="redis-control", control_payload={ "job_id": 884, "job_code": "sync-overseas-20554", "task_mode": "domain_pipeline", }, ) self.assertTrue(started) self.assertTrue(worker.stop_requested) self.assertEqual("redis-control", worker._pending_restart_source) self.assertEqual(884, worker._pending_restart_payload.get("job_id")) worker._update_runtime_state.assert_called() def test_start_detection_async_ignores_overlap_handoff_when_current_session_has_active_threads(self): worker = self._build_worker(_FakeDb("noop")) live_thread = MagicMock() live_thread.is_alive.return_value = True worker.detect_command_thread = live_thread worker.current_job_id = 884 worker.current_job_code = "sync-overseas-20554" worker.current_job_task_mode = "domain_pipeline" worker._explicit_claim_scope_job_id = 885 worker._explicit_claim_scope_job_code = "sync-overseas-20592" worker._explicit_claim_scope_until = time.time() + 120 worker._get_active_domain_threads.return_value = 128 started = worker.start_detection_async( source="redis-control", control_payload={ "job_id": 885, "job_code": "sync-overseas-20592", "task_mode": "domain_pipeline", "source": "overlap-handoff", "tail_handoff_candidate": True, }, ) self.assertTrue(started) self.assertFalse(worker.stop_requested) self.assertEqual("", worker._pending_restart_source) self.assertIsNone(worker._explicit_claim_scope_job_id) worker._update_runtime_state.assert_called() def test_single_machine_mode_allows_direct_fallback_for_360_and_baidu(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_config["allow_direct"] = False worker.single_machine_site_direct_fallback_enabled = True self.assertTrue(worker._allow_direct_connection_for_step("360检测")) self.assertTrue(worker._allow_direct_connection_for_step("百度site检测")) self.assertFalse(worker._allow_direct_connection_for_step("时光机检测")) def test_retry_budget_allows_one_pending_direct_fallback_after_proxy_failure(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_config["allow_direct"] = False worker.single_machine_site_direct_fallback_enabled = True worker.proxy_step_retry_budget_overrides = {"360检测": {"max_attempts": 2, "max_seconds": 5}} worker._set_step_force_direct_once("360检测", True) exhausted, reason = worker._check_proxy_step_retry_budget( "360检测", "example.com", time.perf_counter() - 15, 1, {"proxy-a"}, last_reason="proxy timeout", ) self.assertFalse(exhausted) self.assertEqual("", reason) def test_retry_budget_still_exhausts_when_no_direct_fallback_is_pending(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_config["allow_direct"] = False worker.single_machine_site_direct_fallback_enabled = True worker.proxy_step_retry_budget_overrides = {"360检测": {"max_attempts": 2, "max_seconds": 5}} exhausted, reason = worker._check_proxy_step_retry_budget( "360检测", "example.com", time.perf_counter() - 15, 1, {"proxy-a"}, last_reason="proxy timeout", ) self.assertTrue(exhausted) self.assertIn("attempts=1", reason) def test_auto_resume_skips_when_recent_explicit_start_signal_exists(self): worker = self._build_worker(MagicMock()) worker.db.get_active_detect_job = MagicMock( return_value={ "id": 1, "job_code": "sync-overseas-123", "task_mode": "domain_pipeline", "items_pending": 30, "items_claimed": 4, "items_running": 2, } ) worker.detecting = False worker.detect_command_thread = None worker._last_explicit_start_signal_at = time.time() worker.start_detection_async = MagicMock(return_value=True) started = worker._resume_active_detect_job_if_needed(reason="heartbeat_autoresume") self.assertFalse(started) worker.db.get_active_detect_job.assert_not_called() worker.start_detection_async.assert_not_called() def test_auto_resume_skips_for_non_primary_worker_on_heartbeat(self): worker = self._build_worker(MagicMock()) worker.db.get_active_detect_job = MagicMock(return_value={}) worker.detecting = False worker.detect_command_thread = None with patch.object(config, "NODE_CODE", "mainland-controller-01-b"), patch.dict( os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False, ): started = worker._resume_active_detect_job_if_needed(reason="heartbeat_autoresume") self.assertFalse(started) worker.db.get_active_detect_job.assert_called_once() def test_auto_resume_heartbeat_reuses_recent_active_job_snapshot(self): worker = self._build_worker(MagicMock()) worker.db.get_active_detect_job = MagicMock( return_value={ "id": 1, "job_code": "sync-overseas-123", "task_mode": "domain_pipeline", "items_pending": 30, "items_claimed": 4, "items_running": 2, } ) worker.detecting = False worker.detect_command_thread = None worker.start_detection_async = MagicMock(return_value=True) with patch.dict( os.environ, { "DOMAINCHECK_AUTORESUME_ACTIVE_JOB_MIN_REFRESH_SECONDS": "60", "WORKER_PARENT_NODE_CODE": "mainland-controller-01", }, clear=False, ), patch.object(config, "NODE_CODE", "mainland-controller-01-a"): first = worker._resume_active_detect_job_if_needed(reason="heartbeat_autoresume") second = worker._resume_active_detect_job_if_needed(reason="heartbeat_autoresume") self.assertTrue(first) self.assertTrue(second) worker.db.get_active_detect_job.assert_called_once() self.assertEqual(2, worker.start_detection_async.call_count) def test_auto_resume_skips_same_job_when_threads_already_active(self): worker = self._build_worker(MagicMock()) worker.db.get_active_detect_job = MagicMock( return_value={ "id": 895, "job_code": "sync-overseas-25520", "task_mode": "domain_pipeline", "items_pending": 73298, "items_claimed": 89865, "items_running": 31396, "selection_reason": "running_job_active", } ) worker.detecting = False worker.detect_command_thread = None worker.current_job_id = 895 worker.current_job_code = "sync-overseas-25520" worker.current_job_task_mode = "domain_pipeline" worker._get_active_domain_threads.return_value = 800 worker.start_detection_async = MagicMock(return_value=True) started = worker._resume_active_detect_job_if_needed(reason="heartbeat_autoresume") self.assertFalse(started) worker.db.get_active_detect_job.assert_called_once() worker.start_detection_async.assert_not_called() def test_auto_resume_skips_same_job_during_restart_handoff_window(self): worker = self._build_worker(MagicMock()) worker.db.get_active_detect_job = MagicMock( return_value={ "id": 895, "job_code": "sync-overseas-25520", "task_mode": "domain_pipeline", "items_pending": 73298, "items_claimed": 89865, "items_running": 31396, "selection_reason": "running_job_active", } ) worker.detecting = False worker.detect_command_thread = None worker.current_job_id = 895 worker.current_job_code = "sync-overseas-25520" worker.current_job_task_mode = "domain_pipeline" worker._restart_release_handoff_job_id = 895 worker._restart_release_handoff_job_code = "sync-overseas-25520" worker._restart_release_handoff_until = time.time() + 120 worker.start_detection_async = MagicMock(return_value=True) started = worker._resume_active_detect_job_if_needed(reason="heartbeat_autoresume") self.assertFalse(started) worker.db.get_active_detect_job.assert_called_once() worker.start_detection_async.assert_not_called() def test_auto_resume_skips_when_explicit_claim_scope_matches_active_job(self): worker = self._build_worker(MagicMock()) worker.db.get_active_detect_job = MagicMock( return_value={ "id": 871, "job_code": "sync-overseas-19873", "task_mode": "domain_pipeline", "items_pending": 2134, "items_claimed": 1936, "items_running": 3402, "selection_reason": "running_job_active", } ) worker.detecting = False worker.detect_command_thread = None worker._explicit_claim_scope_job_id = 871 worker._explicit_claim_scope_job_code = "sync-overseas-19873" worker._explicit_claim_scope_until = time.time() + 120 worker._explicit_claim_scope_source = "redis-control" worker.start_detection_async = MagicMock(return_value=True) started = worker._resume_active_detect_job_if_needed(reason="heartbeat_autoresume") self.assertFalse(started) worker.db.get_active_detect_job.assert_called_once() worker.start_detection_async.assert_not_called() def test_auto_resume_allows_non_primary_worker_for_tail_handoff_pending_job(self): worker = self._build_worker(MagicMock()) worker.db.get_active_detect_job = MagicMock( return_value={ "id": 885, "job_code": "sync-overseas-20592", "task_mode": "domain_pipeline", "items_pending": 64000, "items_claimed": 0, "items_running": 0, "tail_handoff_candidate": True, "selection_reason": "tail_handoff_pending", } ) worker.detecting = False worker.detect_command_thread = None worker.start_detection_async = MagicMock(return_value=True) with patch.object(config, "NODE_CODE", "mainland-controller-01-b"), patch.dict( os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False, ): started = worker._resume_active_detect_job_if_needed(reason="heartbeat_autoresume") self.assertTrue(started) worker.db.get_active_detect_job.assert_called_once() worker.start_detection_async.assert_called_once() def test_auto_resume_skips_stalled_running_job(self): worker = self._build_worker(MagicMock()) worker.db.get_active_detect_job = MagicMock( return_value={ "id": 876, "job_code": "sync-overseas-19961", "task_mode": "domain_pipeline", "items_pending": 4, "items_claimed": 235, "items_running": 266, "selection_reason": "running_job_stalled", "running_job_stalled": True, } ) worker.db.recycle_stalled_detect_job_items = MagicMock(return_value=7) worker.detecting = False worker.detect_command_thread = None worker.start_detection_async = MagicMock(return_value=True) worker._acquire_shared_job_maintenance_lock = MagicMock(return_value="lock-token") worker._release_shared_job_maintenance_lock = MagicMock() started = worker._resume_active_detect_job_if_needed(reason="heartbeat_autoresume") self.assertFalse(started) worker.db.get_active_detect_job.assert_called_once() worker.db.recycle_stalled_detect_job_items.assert_called_once_with(876, stall_seconds=900) worker._release_shared_job_maintenance_lock.assert_called_once() worker.start_detection_async.assert_not_called() def test_pull_sync_tasks_skips_when_shared_lock_not_acquired(self): worker = self._build_worker(MagicMock()) worker._acquire_shared_job_maintenance_lock = MagicMock(return_value="") worker._release_shared_job_maintenance_lock = MagicMock() with patch.object(config, "NODE_REGION", "mainland"), patch.object(config, "NODE_ROLE", "control"), patch( "detect_worker.urllib.request.urlopen" ) as mock_urlopen: domains = worker._pull_sync_tasks_until_available(thread_limit=1000) self.assertEqual([], domains) mock_urlopen.assert_not_called() worker._release_shared_job_maintenance_lock.assert_not_called() def test_process_pipeline_skips_when_shared_lock_not_acquired(self): worker = self._build_worker(MagicMock()) worker._acquire_shared_job_maintenance_lock = MagicMock(return_value="") worker._release_shared_job_maintenance_lock = MagicMock() with patch.object(config, "NODE_REGION", "mainland"), patch.object(config, "NODE_ROLE", "control"), patch( "detect_worker.urllib.request.urlopen" ) as mock_urlopen: domains = worker._process_pipeline_tasks_until_available(thread_limit=1000) self.assertEqual([], domains) mock_urlopen.assert_not_called() worker._release_shared_job_maintenance_lock.assert_not_called() def test_runtime_heartbeat_does_not_fake_current_load_when_detecting_but_idle(self): class _SpyRedis: def __init__(self): self.calls = [] def set(self, *args, **kwargs): self.calls.append((args, kwargs)) return True db = MagicMock() worker = self._build_worker(db) worker.use_redis = True worker.redis_client = _SpyRedis() worker.detecting = True worker.running = True worker.stop_requested = False worker.proxy_last_refresh_status = "ok" worker.proxy_last_refresh_time = None worker.proxy_last_source_stats = [] worker._active_domain_threads = 0 worker._last_runtime_state_push_phase = "" worker._last_runtime_state_push_at = 0.0 DetectWorker._update_runtime_state(worker, "running", "等待线程派发") register_calls = [call for call in db.mock_calls if call[0] == "register_cluster_node"] self.assertEqual(1, len(register_calls)) _, args, kwargs = register_calls[0] self.assertEqual(config.NODE_CODE, args[0]) self.assertEqual("busy", kwargs["status"]) self.assertEqual(0, kwargs["current_load"]) self.assertTrue(kwargs["metadata"]["detecting"]) self.assertTrue(kwargs["metadata"]["waiting_for_dispatch"]) self.assertEqual(0, kwargs["metadata"]["active_threads"]) def test_release_node_job_items_is_throttled_between_detection_restarts(self): db = MagicMock() db.release_detect_job_items_for_node.return_value = 7 worker = self._build_worker(db) first = worker._maybe_release_node_job_items(reason="test", min_interval_seconds=300) second = worker._maybe_release_node_job_items(reason="test", min_interval_seconds=300) self.assertEqual(7, first) self.assertEqual(0, second) db.release_detect_job_items_for_node.assert_called_once_with(config.NODE_CODE) def test_control_node_can_release_node_job_items(self): db = MagicMock() db.release_detect_job_items_for_node.return_value = 7 worker = self._build_worker(db) with patch.object(config, "NODE_ROLE", "control"): released = worker._maybe_release_node_job_items(reason="control_start") self.assertEqual(7, released) db.release_detect_job_items_for_node.assert_called_once_with(config.NODE_CODE) def test_release_node_job_items_records_restart_release_handoff_for_current_job(self): db = MagicMock() db.release_detect_job_items_for_node.return_value = 7 worker = self._build_worker(db) worker.current_job_id = 880 worker.current_job_code = "sync-overseas-20249" released = worker._maybe_release_node_job_items(reason="start_detection", min_interval_seconds=0) self.assertEqual(7, released) self.assertEqual(880, worker._restart_release_handoff_job_id) self.assertEqual("sync-overseas-20249", worker._restart_release_handoff_job_code) self.assertGreater(worker._restart_release_handoff_until, time.time()) def test_claim_detect_job_items_prioritizes_restart_released_handoff_before_global_queue(self): db = MagicMock() db.claim_restart_released_detect_job_items.return_value = [{"job_item_id": 1}] db.claim_detect_job_items.return_value = [{"job_item_id": 2}] worker = self._build_worker(db) worker.current_job_id = 880 worker.current_job_code = "sync-overseas-20249" worker.current_job_task_mode = "domain_pipeline" worker._restart_release_handoff_job_id = 880 worker._restart_release_handoff_job_code = "sync-overseas-20249" worker._restart_release_handoff_until = time.time() + 120 worker._restart_release_handoff_reason = "start_detection" with patch.object(config, "NODE_CODE", "mainland-controller-01-a"), patch.dict( os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False, ): rows = worker._claim_detect_job_items(limit=320, lease_seconds=900, job_id=None) self.assertEqual([{"job_item_id": 1}, {"job_item_id": 2}], rows) db.claim_restart_released_detect_job_items.assert_called_once_with( "mainland-controller-01-a", 880, limit=128, lease_seconds=900, ) db.claim_detect_job_items.assert_called_once_with( "mainland-controller-01-a", limit=319, lease_seconds=900, job_id=None, prefer_recent_jobs=False, preferred_recent_job_limit=8, preferred_recent_job_window_hours=24, ) def test_claim_detect_job_items_skips_restart_handoff_query_for_non_primary_worker(self): db = MagicMock() worker = self._build_worker(db) worker.current_job_id = 880 worker.current_job_code = "sync-overseas-20249" worker.current_job_task_mode = "domain_pipeline" worker._restart_release_handoff_job_id = 880 worker._restart_release_handoff_job_code = "sync-overseas-20249" worker._restart_release_handoff_until = time.time() + 120 worker._restart_release_handoff_reason = "start_detection" with patch.object(config, "NODE_CODE", "mainland-controller-01-b"), patch.dict( os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False, ): rows = worker._claim_detect_job_items(limit=320, lease_seconds=900, job_id=None) self.assertEqual([], rows) db.claim_restart_released_detect_job_items.assert_not_called() db.claim_detect_job_items.assert_not_called() def test_mark_explicit_start_signal_records_pipeline_claim_scope(self): db = MagicMock() worker = self._build_worker(db) worker._mark_explicit_start_signal( { "job_id": 884, "job_code": "sync-overseas-20554", "task_mode": "domain_pipeline", }, source="redis-control", ) self.assertEqual(884, worker._explicit_claim_scope_job_id) self.assertEqual("sync-overseas-20554", worker._explicit_claim_scope_job_code) self.assertEqual("redis-control", worker._explicit_claim_scope_source) self.assertGreater(worker._explicit_claim_scope_until, time.time()) def test_current_scoped_claim_job_id_prefers_explicit_pipeline_scope(self): db = MagicMock() worker = self._build_worker(db) worker.current_job_id = 867 worker.current_job_code = "sync-overseas-19835" worker.current_job_task_mode = "domain_pipeline" worker._explicit_claim_scope_job_id = 884 worker._explicit_claim_scope_job_code = "sync-overseas-20554" worker._explicit_claim_scope_until = time.time() + 120 self.assertEqual(884, worker._current_scoped_claim_job_id()) def test_explicit_claim_scope_window_expires(self): db = MagicMock() worker = self._build_worker(db) worker._explicit_claim_scope_job_id = 884 worker._explicit_claim_scope_job_code = "sync-overseas-20554" worker._explicit_claim_scope_until = time.time() - 1 self.assertIsNone(worker._explicit_claim_scope_job_id_active()) self.assertIsNone(worker._explicit_claim_scope_job_id) def test_claim_detect_job_items_honors_explicit_pipeline_scope(self): db = MagicMock() db.claim_detect_job_items.return_value = [{"job_item_id": 9}] worker = self._build_worker(db) worker._explicit_claim_scope_job_id = 884 worker._explicit_claim_scope_job_code = "sync-overseas-20554" worker._explicit_claim_scope_until = time.time() + 120 with patch.object(config, "NODE_CODE", "mainland-controller-01-da"): rows = worker._claim_detect_job_items(limit=250, lease_seconds=900, job_id=None) self.assertEqual([{"job_item_id": 9}], rows) db.claim_detect_job_items.assert_called_once_with( "mainland-controller-01-da", limit=250, lease_seconds=900, job_id=884, prefer_recent_jobs=False, preferred_recent_job_limit=8, preferred_recent_job_window_hours=24, ) def test_shared_job_maintenance_lock_uses_single_worker_with_same_scope(self): redis_client = _FakeRedis() db = MagicMock() worker_a = self._build_worker(db) worker_a.use_redis = True worker_a.redis_client = redis_client worker_b = self._build_worker(db) worker_b.use_redis = True worker_b.redis_client = redis_client with patch.object(config, "NODE_CODE", "mainland-controller-01-a"), patch.dict( os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False, ): token_a = worker_a._acquire_shared_job_maintenance_lock("recycle-expired-job-items", ttl_seconds=10) with patch.object(config, "NODE_CODE", "mainland-controller-01-b"), patch.dict( os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False, ): token_b = worker_b._acquire_shared_job_maintenance_lock("recycle-expired-job-items", ttl_seconds=10) self.assertTrue(token_a) self.assertEqual("", token_b) def test_control_node_skips_local_auto_resume(self): worker = self._build_worker(MagicMock()) worker.db.get_active_detect_job = MagicMock(return_value={"id": 1, "job_code": "sync-overseas-123"}) worker.detecting = False worker.detect_command_thread = None with patch.object(config, "NODE_ROLE", "control"): started = worker._resume_active_detect_job_if_needed(reason="service_runtime_bootstrap") self.assertFalse(started) worker.db.get_active_detect_job.assert_not_called() def test_control_node_can_auto_resume_when_override_enabled(self): worker = self._build_worker(MagicMock()) worker.runtime_settings = {"control_node_autoresume_enabled": True} worker.db.get_active_detect_job = MagicMock( return_value={ "id": 1, "job_code": "sync-overseas-123", "task_mode": "domain_pipeline", "items_pending": 30, "items_claimed": 4, "items_running": 2, } ) worker.detecting = False worker.detect_command_thread = None worker.start_detection_async = MagicMock(return_value=True) with patch.object(config, "NODE_ROLE", "control"): started = worker._resume_active_detect_job_if_needed(reason="service_runtime_bootstrap") self.assertTrue(started) worker.db.get_active_detect_job.assert_called_once() worker.start_detection_async.assert_called_once() def test_bootstrap_autoresume_lock_allows_only_one_worker_for_same_job(self): redis_client = _FakeRedis() db = MagicMock() active_job = { "id": 895, "job_code": "sync-overseas-25520", "task_mode": "domain_pipeline", "items_pending": 73298, "items_claimed": 89865, "items_running": 31396, "selection_reason": "running_job_active", } worker_a = self._build_worker(db) worker_a.use_redis = True worker_a.redis_client = redis_client worker_a.runtime_settings = {"control_node_autoresume_enabled": True} worker_a.db.get_active_detect_job = MagicMock(return_value=active_job) worker_a.detecting = False worker_a.start_detection_async = MagicMock(return_value=True) worker_b = self._build_worker(db) worker_b.use_redis = True worker_b.redis_client = redis_client worker_b.runtime_settings = {"control_node_autoresume_enabled": True} worker_b.db.get_active_detect_job = MagicMock(return_value=active_job) worker_b.detecting = False worker_b.start_detection_async = MagicMock(return_value=True) with patch.object(config, "NODE_ROLE", "control"), patch.object(config, "NODE_CODE", "mainland-controller-01-a"), patch.dict( os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False, ): started_a = worker_a._resume_active_detect_job_if_needed(reason="service_runtime_bootstrap") with patch.object(config, "NODE_ROLE", "control"), patch.object(config, "NODE_CODE", "mainland-controller-01-b"), patch.dict( os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False, ): started_b = worker_b._resume_active_detect_job_if_needed(reason="service_runtime_bootstrap") self.assertTrue(started_a) self.assertFalse(started_b) worker_a.start_detection_async.assert_called_once() worker_b.start_detection_async.assert_not_called() def test_same_job_recent_start_skip_detail_never_goes_negative(self): worker = self._build_worker(MagicMock()) worker.current_job_id = 895 worker.current_job_code = "sync-overseas-25520" worker._last_detection_start_at = time.time() + 900 should_skip, detail = worker._should_skip_same_job_autoresume( active_job={"id": 895, "job_code": "sync-overseas-25520"}, now_ts=time.time(), ) self.assertTrue(should_skip) self.assertEqual("same_job_recent_start:0s", detail) def test_claim_detect_job_items_prefers_recent_jobs_when_runtime_enabled(self): db = MagicMock() db.claim_detect_job_items.return_value = [] worker = self._build_worker(db) worker.runtime_settings = { "claim_recent_jobs_first": True, "claim_recent_jobs_limit": 6, "claim_recent_jobs_window_hours": 72, } worker._claim_detect_job_items(limit=320, lease_seconds=900, job_id=None) db.claim_detect_job_items.assert_called_once_with( config.NODE_CODE, limit=320, lease_seconds=900, job_id=None, prefer_recent_jobs=True, preferred_recent_job_limit=6, preferred_recent_job_window_hours=72, ) def test_claim_detect_job_items_does_not_enable_recent_priority_for_scoped_job(self): db = MagicMock() db.claim_detect_job_items.return_value = [] worker = self._build_worker(db) worker.runtime_settings = { "claim_recent_jobs_first": True, "claim_recent_jobs_limit": 6, "claim_recent_jobs_window_hours": 72, } worker._claim_detect_job_items(limit=320, lease_seconds=900, job_id=99) db.claim_detect_job_items.assert_called_once_with( config.NODE_CODE, limit=320, lease_seconds=900, job_id=99, prefer_recent_jobs=False, preferred_recent_job_limit=6, preferred_recent_job_window_hours=72, ) def test_resolve_sync_pull_limit_scales_with_process_pool_capacity(self): worker = self._build_worker(MagicMock()) worker.process_count = 60 worker.refresh_process_count_runtime = MagicMock(return_value=60) pull_limit = worker._resolve_sync_pull_limit(1000) self.assertEqual(120000, pull_limit) def test_proxy_auth_failure_quarantines_whole_source_group(self): worker = self._build_worker(_FakeDb("noop")) bad_proxy = {"http": "http://bad-proxy", "https": "http://bad-proxy"} good_proxy = {"http": "http://good-proxy", "https": "http://good-proxy"} worker.proxy_pool = [ {"proxy": bad_proxy, "usage_count": 0, "source_tag": "D"}, {"proxy": good_proxy, "usage_count": 0, "source_tag": "E"}, ] worker.proxy_source_by_key[worker._proxy_key(bad_proxy)] = "D" worker.proxy_source_by_key[worker._proxy_key(good_proxy)] = "E" source_tag, failure_count, cooldown_seconds, removed = worker._mark_proxy_source_failure( bad_proxy, "Tunnel connection failed: 407 Proxy Authentication Required", ) self.assertEqual("D", source_tag) self.assertEqual(1, failure_count) self.assertGreaterEqual(cooldown_seconds, 90) self.assertEqual(1, removed) self.assertTrue(worker._is_proxy_source_quarantined("D")) self.assertEqual( [{"proxy": good_proxy, "usage_count": 0, "source_tag": "E"}], worker.proxy_pool, ) def test_get_proxies_skips_quarantined_entries_without_dropping_pool_items(self): worker = self._build_worker(_FakeDb("noop")) bad_proxy = {"http": "http://bad-proxy", "https": "http://bad-proxy"} good_proxy = {"http": "http://good-proxy", "https": "http://good-proxy"} worker.proxy_pool = [ {"proxy": bad_proxy, "usage_count": 0, "source_tag": "B"}, {"proxy": good_proxy, "usage_count": 0, "source_tag": "C"}, ] worker.proxy_quarantine_until[worker._proxy_key(bad_proxy)] = time.time() + 60 worker._schedule_proxy_refresh_if_needed = MagicMock() selected = worker.get_proxies() self.assertEqual(good_proxy, selected) self.assertEqual(2, len(worker.proxy_pool)) self.assertEqual( [entry["proxy"] for entry in worker.proxy_pool], [bad_proxy, good_proxy], ) self.assertEqual(1, worker.proxy_pool[1]["usage_count"]) def test_get_proxies_scans_multiple_batches_until_it_finds_usable_proxy(self): worker = self._build_worker(_FakeDb("noop")) worker._proxy_pool_scan_batch_size = MagicMock(return_value=2) proxies = [ {"http": f"http://proxy-{idx}", "https": f"http://proxy-{idx}"} for idx in range(5) ] worker.proxy_pool = [ {"proxy": proxy, "usage_count": 0, "source_tag": f"S{idx}"} for idx, proxy in enumerate(proxies) ] excluded_keys = {worker._proxy_key(proxy) for proxy in proxies[:4]} worker._schedule_proxy_refresh_if_needed = MagicMock() selected = worker.get_proxies(excluded_proxy_keys=excluded_keys) self.assertEqual(proxies[4], selected) self.assertEqual(5, len(worker.proxy_pool)) self.assertEqual(1, worker.proxy_pool[-1]["usage_count"]) def test_prepare_proxy_fetch_plan_respects_pool_shortage_instead_of_overfetching(self): worker = self._build_worker(_FakeDb("noop")) plan, meta = worker._prepare_proxy_fetch_plan( [ "http://provider.local/get?group=B", "http://provider.local/get?group=C", "http://provider.local/get?group=D", "http://provider.local/get?group=E", "http://provider.local/get?group=F", "http://provider.local/get?group=G", ], current_pool_size=1000, ) self.assertEqual(1, meta["rounds"]) self.assertGreaterEqual(meta["target_total"], 840) self.assertLessEqual(meta["target_total"], 1200) self.assertEqual(6, len(plan)) def test_prepare_proxy_fetch_plan_caps_active_rounds(self): worker = self._build_worker(_FakeDb("noop")) plan, meta = worker._prepare_proxy_fetch_plan( [ "http://provider.local/get?group=B", "http://provider.local/get?group=C", "http://provider.local/get?group=D", "http://provider.local/get?group=E", "http://provider.local/get?group=F", "http://provider.local/get?group=G", ], current_pool_size=0, ) self.assertLessEqual(meta["rounds"], 2) self.assertEqual(meta["rounds"] * 6, len(plan)) def test_prepare_proxy_fetch_plan_scales_down_target_when_process_count_is_high(self): worker = self._build_worker(_FakeDb("noop")) worker.thread_count = 1000 worker.process_count = 80 worker.refresh_process_count_runtime = MagicMock(return_value=80) plan, meta = worker._prepare_proxy_fetch_plan( [ "http://provider.local/get?group=B", "http://provider.local/get?group=C", "http://provider.local/get?group=D", "http://provider.local/get?group=E", "http://provider.local/get?group=F", "http://provider.local/get?group=G", ], current_pool_size=150, ) self.assertEqual(80, meta["process_count"]) self.assertLessEqual(meta["target_total"], 360) self.assertLessEqual(meta["batch_size"], 60) self.assertEqual(6, len(plan)) def test_refresh_proxy_pool_reuses_shared_snapshot_when_refresh_lock_is_busy(self): worker = self._build_worker(_FakeDb("noop")) worker.use_redis = True worker.redis_client = _FakeRedis() worker.process_count = 80 worker.refresh_process_count_runtime = MagicMock(return_value=80) worker.proxy_config = { "proxy_enable": True, "allow_direct": True, "proxy_urls": ["http://provider.local/get?group=B"], } local_proxy = {"http": "http://local-proxy", "https": "http://local-proxy"} shared_proxy = {"http": "http://shared-proxy", "https": "http://shared-proxy"} worker.proxy_pool = [ {"proxy": local_proxy, "usage_count": 0, "source_tag": "L", "expire_at_ts": time.time() + 120} ] with patch.object(config, "NODE_CODE", "mainland-controller-01-u"): with patch.dict(os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False): worker.redis_client.set( worker._shared_proxy_pool_key(), json.dumps( { "node_code": "mainland-controller-01", "owner": "mainland-controller-01-k", "refreshed_at_ts": time.time(), "source_count": 6, "raw_items": 180, "available_count": 1, "proxy_pool": [ { "proxy": shared_proxy, "usage_count": 0, "source_tag": "S", "expire_at_ts": time.time() + 120, } ], }, ensure_ascii=False, ), ex=30, ) worker.redis_client.set(worker._shared_proxy_refresh_lock_key(), "other-owner", ex=10) worker.refresh_proxy_pool() self.assertEqual(1, len(worker.proxy_pool)) self.assertEqual(shared_proxy, worker.proxy_pool[0]["proxy"]) self.assertIn("共享代理快照", worker.proxy_last_refresh_status) def test_refresh_proxy_pool_publishes_shared_snapshot_after_success(self): worker = self._build_worker(_FakeDb("noop")) worker.use_redis = True worker.redis_client = _FakeRedis() worker.process_count = 80 worker.refresh_process_count_runtime = MagicMock(return_value=80) worker.proxy_config = { "proxy_enable": True, "allow_direct": True, "proxy_urls": ["http://provider.local/get?group=B"], } class _FakeResponse: status_code = 200 def json(self): return { "list": [ { "ip": "1.1.1.1", "port": "8080", "expire_at": time.time() + 120, } ] } with patch.object(config, "NODE_CODE", "mainland-controller-01-u"): with patch.dict(os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False): with patch("requests.get", return_value=_FakeResponse()): worker.refresh_proxy_pool() raw_payload = worker.redis_client.get(worker._shared_proxy_pool_key()) self.assertTrue(raw_payload) payload = json.loads(raw_payload) self.assertEqual("mainland-controller-01-u", payload["owner"]) self.assertEqual(1, payload["available_count"]) def test_refresh_proxy_pool_prefers_shared_snapshot_when_local_pool_lags(self): worker = self._build_worker(_FakeDb("noop")) worker.use_redis = True worker.redis_client = _FakeRedis() worker.process_count = 80 worker.refresh_process_count_runtime = MagicMock(return_value=80) worker.thread_count = 1000 worker.proxy_config = { "proxy_enable": True, "allow_direct": True, "proxy_urls": ["http://provider.local/get?group=B"], } worker.proxy_pool = [ {"proxy": {"http": f"http://local-{idx}", "https": f"http://local-{idx}"}, "usage_count": 0, "source_tag": "L", "expire_at_ts": time.time() + 120} for idx in range(20) ] shared_entries = [ {"proxy": {"http": f"http://shared-{idx}", "https": f"http://shared-{idx}"}, "usage_count": 0, "source_tag": "S", "expire_at_ts": time.time() + 120} for idx in range(80) ] with patch.object(config, "NODE_CODE", "mainland-controller-01-u"): with patch.dict(os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False): worker.redis_client.set( worker._shared_proxy_pool_key(), json.dumps( { "node_code": "mainland-controller-01", "owner": "mainland-controller-01-k", "refreshed_at_ts": time.time(), "source_count": 6, "raw_items": 120, "available_count": len(shared_entries), "proxy_pool": shared_entries, }, ensure_ascii=False, ), ex=30, ) worker.refresh_proxy_pool() self.assertEqual(len(shared_entries), len(worker.proxy_pool)) self.assertIn("共享代理快照", worker.proxy_last_refresh_status) def test_schedule_proxy_refresh_skips_empty_pool_during_cooldown(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_next_refresh_time = time.time() + 10 with patch("detect_worker.threading.Thread") as mock_thread: worker._schedule_proxy_refresh_if_needed(0) mock_thread.assert_not_called() def test_schedule_proxy_refresh_skips_low_water_pool_during_cooldown(self): worker = self._build_worker(_FakeDb("noop")) worker.process_count = 80 worker.refresh_process_count_runtime = MagicMock(return_value=80) worker.proxy_next_refresh_time = time.time() + 10 with patch("detect_worker.threading.Thread") as mock_thread: worker._schedule_proxy_refresh_if_needed(150) mock_thread.assert_not_called() def test_proxy_refresh_threshold_scales_down_for_multi_process_node(self): worker = self._build_worker(_FakeDb("noop")) worker.thread_count = 1000 worker._get_active_domain_threads = MagicMock(return_value=900) self.assertEqual(2700, worker._proxy_refresh_threshold()) worker.process_count = 80 worker.refresh_process_count_runtime = MagicMock(return_value=80) self.assertLess(worker._proxy_refresh_threshold(), 400) def test_trigger_proxy_refresh_respects_cooldown_without_reset(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_config["proxy_urls"] = ["http://provider.local/get?group=B&count=1"] worker.proxy_next_refresh_time = time.time() + 10 worker.trigger_proxy_refresh = DetectWorker.trigger_proxy_refresh.__get__(worker, DetectWorker) with patch("detect_worker.threading.Thread") as mock_thread: scheduled = worker.trigger_proxy_refresh(reason="cooldown-check") self.assertFalse(scheduled) mock_thread.assert_not_called() def test_trigger_proxy_refresh_debounces_config_update_burst(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_config["proxy_urls"] = ["http://provider.local/get?group=B&count=1"] worker.trigger_proxy_refresh = DetectWorker.trigger_proxy_refresh.__get__(worker, DetectWorker) with patch("detect_worker.threading.Thread") as mock_thread: first = worker.trigger_proxy_refresh(reason="config_update:thread_count") second = worker.trigger_proxy_refresh(reason="config_update:runtime_settings") self.assertTrue(first) self.assertFalse(second) mock_thread.assert_called_once() def test_config_update_proxy_refresh_ignores_runtime_settings_only_changes(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_config = { "proxy_enable": True, "allow_direct": True, "proxy_urls": ["http://provider.local/get?group=B"], } worker.runtime_settings = {"worker_log_sync_mode": "key"} worker.proxy_pool = [ {"proxy": {"http": "http://cached", "https": "http://cached"}, "usage_count": 0, "source_tag": "L", "expire_at_ts": time.time() + 120} for _ in range(160) ] should_refresh, reset_cooldown = worker._should_trigger_proxy_refresh_for_config_update( config_type="runtime_settings", previous_proxy_config=dict(worker.proxy_config), previous_thread_count=worker.thread_count, current_thread_count=worker.thread_count, ) self.assertFalse(should_refresh) self.assertFalse(reset_cooldown) def test_proxy_item_expiry_respects_recent_grace_window(self): worker = self._build_worker(_FakeDb("noop")) expired_recently = {"expire_at": time.time() - 30} self.assertTrue(worker._is_proxy_item_expired(expired_recently)) self.assertFalse(worker._is_proxy_item_expired(expired_recently, grace_seconds=90)) self.assertTrue(worker._is_proxy_item_expired(expired_recently, grace_seconds=10)) def test_proxy_expire_clock_skew_hint_detects_large_positive_offset(self): worker = self._build_worker(_FakeDb("noop")) base_now = 1_700_000_000.0 proxy_items = [ {"ip": "1.1.1.1", "port": "8001", "expire_at": base_now - 46000}, {"ip": "1.1.1.2", "port": "8002", "expire_at": base_now - 45990}, {"ip": "1.1.1.3", "port": "8003", "expire_at": base_now - 45980}, {"ip": "1.1.1.4", "port": "8004", "expire_at": base_now - 45970}, {"ip": "1.1.1.5", "port": "8005", "expire_at": base_now - 45960}, {"ip": "1.1.1.6", "port": "8006", "expire_at": base_now - 45950}, {"ip": "1.1.1.7", "port": "8007", "expire_at": base_now - 45940}, {"ip": "1.1.1.8", "port": "8008", "expire_at": base_now - 45930}, {"ip": "1.1.1.9", "port": "8009", "expire_at": base_now - 45920}, {"ip": "1.1.1.10", "port": "8010", "expire_at": base_now - 45910}, {"ip": "1.1.1.11", "port": "8011", "expire_at": base_now - 45900}, {"ip": "1.1.1.12", "port": "8012", "expire_at": base_now - 45890}, ] hint = worker._summarize_proxy_expire_clock_skew(proxy_items, now_ts=base_now) self.assertIsNotNone(hint) self.assertEqual(12, hint["timed_proxy_count"]) self.assertEqual(12, hint["expired_timed_count"]) self.assertGreater(hint["clock_ahead_seconds"], 45_000) def test_proxy_expire_clock_skew_hint_ignores_normally_fresh_entries(self): worker = self._build_worker(_FakeDb("noop")) base_now = 1_700_000_000.0 proxy_items = [ {"ip": "2.2.2.1", "port": "9001", "expire_at": base_now + 120}, {"ip": "2.2.2.2", "port": "9002", "expire_at": base_now + 180}, {"ip": "2.2.2.3", "port": "9003", "expire_at": base_now + 240}, {"ip": "2.2.2.4", "port": "9004", "expire_at": base_now + 300}, {"ip": "2.2.2.5", "port": "9005", "expire_at": base_now + 360}, {"ip": "2.2.2.6", "port": "9006", "expire_at": base_now + 420}, {"ip": "2.2.2.7", "port": "9007", "expire_at": base_now + 480}, {"ip": "2.2.2.8", "port": "9008", "expire_at": base_now + 540}, {"ip": "2.2.2.9", "port": "9009", "expire_at": base_now + 600}, {"ip": "2.2.2.10", "port": "9010", "expire_at": base_now + 660}, {"ip": "2.2.2.11", "port": "9011", "expire_at": base_now + 720}, {"ip": "2.2.2.12", "port": "9012", "expire_at": base_now + 780}, ] hint = worker._summarize_proxy_expire_clock_skew(proxy_items, now_ts=base_now) self.assertIsNone(hint) def test_recently_expired_backfill_only_kicks_in_when_pool_is_too_small(self): worker = self._build_worker(_FakeDb("noop")) worker.thread_count = 2000 worker._get_active_domain_threads = MagicMock(return_value=1800) stale_entries = [ { "proxy": {"http": f"http://stale-{idx}", "https": f"http://stale-{idx}"}, "usage_count": 0, "source_tag": f"S{idx % 3}", } for idx in range(200) ] merged_entries, added_count, floor = worker._backfill_recently_expired_proxy_entries( [ { "proxy": {"http": "http://fresh-1", "https": "http://fresh-1"}, "usage_count": 0, "source_tag": "F", } ], stale_entries, ) self.assertGreater(floor, 1) self.assertEqual(floor, len(merged_entries)) self.assertEqual(floor - 1, added_count) healthy_pool = [ { "proxy": {"http": f"http://fresh-{idx}", "https": f"http://fresh-{idx}"}, "usage_count": 0, "source_tag": "F", } for idx in range(floor + 5) ] merged_entries, added_count, reused_floor = worker._backfill_recently_expired_proxy_entries( healthy_pool, stale_entries, ) self.assertEqual(0, added_count) self.assertEqual(reused_floor, floor) self.assertEqual(len(healthy_pool), len(merged_entries)) def test_refresh_proxy_pool_can_reuse_recently_expired_entries_without_unbound_local_error(self): worker = self._build_worker(_FakeDb("noop")) worker.thread_count = 1200 worker.proxy_config = { "proxy_enable": True, "allow_direct": True, "proxy_urls": ["http://provider.local/get?group=B&count=1"], } class _Response: status_code = 200 @staticmethod def json(): return [ { "ip": "1.1.1.1", "port": "8080", "expire_at": time.time() - 30, } ] with patch("requests.get", return_value=_Response()): worker.refresh_proxy_pool() def test_pending_control_key_scopes_to_current_node_code(self): with patch.object(config, "NODE_CODE", "mainland-controller-01-a"): self.assertEqual( "domain_tool:worker_pending_command:mainland-controller-01-a", pending_control_key(), ) def test_control_targets_current_worker_rejects_other_instance(self): with patch.object(config, "NODE_CODE", "mainland-controller-01-a"): self.assertTrue(control_targets_current_worker({"action": "start_detection"})) self.assertTrue(control_targets_current_worker({"target_node_code": "mainland-controller-01-a"})) self.assertFalse(control_targets_current_worker({"target_node_code": "mainland-controller-01-b"})) def test_consume_pending_control_command_prefers_instance_key(self): worker = DetectWorker.__new__(DetectWorker) worker.use_redis = True worker._handle_control_message = MagicMock() class _FakeRedis: def __init__(self): self.values = { "domain_tool:worker_pending_command:mainland-controller-01-a": '{"action":"start_detection","target_node_code":"mainland-controller-01-a"}', "domain_tool:worker_pending_command": '{"action":"start_detection"}', } def getdel(self, key): return self.values.pop(key, None) def get(self, key): return self.values.get(key) def delete(self, key): self.values.pop(key, None) worker.redis_client = _FakeRedis() with patch.object(config, "NODE_CODE", "mainland-controller-01-a"): worker._consume_pending_control_command() worker._handle_control_message.assert_called_once_with( '{"action":"start_detection","target_node_code":"mainland-controller-01-a"}' ) def test_handle_control_message_ignores_command_for_other_instance(self): worker = DetectWorker.__new__(DetectWorker) worker.use_redis = False worker.redis_client = None worker._acknowledge_pending_control_command = MagicMock() worker._remember_ignored_targeted_control = MagicMock() worker.start_detection_async = MagicMock() worker.request_stop_detection = MagicMock() with patch.object(config, "NODE_CODE", "mainland-controller-01-a"): worker._handle_control_message( '{"action":"start_detection","target_node_code":"mainland-controller-01-b"}' ) worker._acknowledge_pending_control_command.assert_not_called() worker._remember_ignored_targeted_control.assert_called_once() worker.start_detection_async.assert_not_called() def test_handle_control_message_records_ignored_targeted_job_for_other_instance(self): worker = self._build_worker(MagicMock()) with patch.object(config, "NODE_CODE", "mainland-controller-01-a"): worker._handle_control_message( { "action": "start_detection", "job_id": 870, "job_code": "sync-overseas-19868", "target_node_codes": ["mainland-controller-01-do", "mainland-controller-01-cz"], } ) self.assertEqual(870, worker._ignored_targeted_job_id) self.assertEqual("sync-overseas-19868", worker._ignored_targeted_job_code) self.assertEqual( "mainland-controller-01-do,mainland-controller-01-cz", worker._ignored_targeted_job_targets, ) def test_auto_resume_skips_when_active_job_matches_recently_ignored_targeted_job(self): worker = self._build_worker(MagicMock()) worker._load_active_detect_job_snapshot = MagicMock( return_value={ "id": 870, "job_code": "sync-overseas-19868", "task_mode": "domain_pipeline", "items_pending": 1200, "items_claimed": 949, "items_running": 6315, "selection_reason": "running_job_active", } ) worker.start_detection_async = MagicMock(return_value=True) worker._ignored_targeted_job_id = 870 worker._ignored_targeted_job_code = "sync-overseas-19868" worker._ignored_targeted_job_until = time.time() + 120 worker._ignored_targeted_job_targets = "mainland-controller-01-do,mainland-controller-01-cz" started = worker._resume_active_detect_job_if_needed(reason="heartbeat_autoresume") self.assertFalse(started) worker.start_detection_async.assert_not_called() def test_proxy_direct_fallback_grace_shrinks_for_shortage_steps(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_direct_fallback_grace_overrides = { "注册状态检测": 0.08, "360检测": 0.12, "站长之家检测": 0.12, } worker.proxy_last_refresh_status = "代理源暂时冷却中,稍后继续补货" self.assertEqual(0.05, worker._proxy_direct_fallback_grace_for_step("注册状态检测")) self.assertEqual(0.05, worker._proxy_direct_fallback_grace_for_step("360检测")) self.assertEqual(0.05, worker._proxy_direct_fallback_grace_for_step("站长之家检测")) def test_run_detect_register_prefers_direct_when_proxy_source_is_short(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_last_refresh_status = "代理源暂时冷却中,稍后继续补货" worker._check_proxy_step_retry_budget = MagicMock(return_value=(False, "")) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=2.0) worker._is_current_single_step_task = MagicMock(return_value=False) worker._clear_proxy_failure = MagicMock() worker._clear_proxy_source_failure = MagicMock() worker._get_proxy_for_step = MagicMock(return_value={"http": "http://proxy", "https": "http://proxy"}) worker.db.update_domain_register_result = MagicMock() with patch("detect_worker.register.check_register", return_value=(3, "")) as mock_check: result = worker._run_detect_register(1, {"source_type": 0}, "example.com") self.assertTrue(result) worker._get_proxy_for_step.assert_not_called() self.assertIsNone(mock_check.call_args.args[2]) def test_run_detect_register_single_machine_mode_allows_direct_even_when_global_direct_disabled(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_config = {"proxy_enable": True, "allow_direct": False} worker.register_single_machine_mode_enabled = True worker.register_single_machine_direct_streak_attempts = 2 worker._check_proxy_step_retry_budget = MagicMock(return_value=(False, "")) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=2.0) worker._is_current_single_step_task = MagicMock(return_value=False) worker._clear_proxy_failure = MagicMock() worker._clear_proxy_source_failure = MagicMock() worker._get_proxy_for_step = MagicMock(return_value={"http": "http://proxy", "https": "http://proxy"}) worker.db.update_domain_register_result = MagicMock() with patch("detect_worker.register.check_register", return_value=(3, "")) as mock_check: result = worker._run_detect_register(1, {"source_type": 0}, "example.com") self.assertTrue(result) worker._get_proxy_for_step.assert_not_called() self.assertIsNone(mock_check.call_args.args[2]) def test_run_detect_register_single_machine_mode_retries_direct_before_proxy(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_config = {"proxy_enable": True, "allow_direct": False} worker.register_single_machine_mode_enabled = True worker.register_single_machine_direct_streak_attempts = 2 worker._check_proxy_step_retry_budget = MagicMock(return_value=(False, "")) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=2.0) worker._is_current_single_step_task = MagicMock(return_value=False) worker._clear_proxy_failure = MagicMock() worker._clear_proxy_source_failure = MagicMock() worker._get_proxy_for_step = MagicMock(return_value={"http": "http://proxy", "https": "http://proxy"}) worker.db.update_domain_register_result = MagicMock() with patch( "detect_worker.register.check_register", side_effect=[RuntimeError("direct timeout"), (3, "")], ) as mock_check: result = worker._run_detect_register(1, {"source_type": 0}, "example.com") self.assertTrue(result) self.assertEqual(2, mock_check.call_count) self.assertTrue(all(call.args[2] is None for call in mock_check.call_args_list)) worker._get_proxy_for_step.assert_not_called() def test_run_detect_register_direct_error_keeps_fast_direct_retry_during_shortage(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_last_refresh_status = "代理源暂时冷却中,稍后继续补货" worker._check_proxy_step_retry_budget = MagicMock(side_effect=[(False, ""), (True, "budget exhausted")]) worker._get_proxy_for_step = MagicMock(return_value=None) worker._mark_detection_failed = MagicMock() with patch("detect_worker.register.check_register", side_effect=RuntimeError("direct timeout")): result = worker._run_detect_register(1, {"source_type": 0}, "example.com") self.assertFalse(result) self.assertTrue(worker._consume_step_force_direct_once("注册状态检测")) self.assertAlmostEqual(0.05, worker._consume_step_proxy_retry_wait("注册状态检测")) worker._mark_detection_failed.assert_called_once() def test_run_detect_register_blacklists_client_hold_for_single_step_task(self): worker = self._build_worker(_FakeDb("noop")) worker._check_proxy_step_retry_budget = MagicMock(return_value=(False, "")) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=2.0) worker._is_current_single_step_task = MagicMock(return_value=True) worker._cache_single_step_result_payload = MagicMock() worker._mark_blacklisted = MagicMock() worker._clear_proxy_failure = MagicMock() worker._clear_proxy_source_failure = MagicMock() worker._get_proxy_for_step = MagicMock(return_value={"http": "http://proxy", "https": "http://proxy"}) worker.db.update_domain_register_result = MagicMock() with patch("detect_worker.register.check_register", return_value=(REGISTER_STATUS_CLIENT_HOLD, "2026-05-01")): result = worker._run_detect_register(1, {"source_type": 0}, "example.com") self.assertFalse(result) worker._cache_single_step_result_payload.assert_called_once() payload = worker._cache_single_step_result_payload.call_args.args[2] self.assertEqual("blacklisted", payload["state"]) self.assertEqual(REGISTER_STATUS_CLIENT_HOLD, payload["register_status"]) worker._mark_blacklisted.assert_called_once() worker.db.update_domain_register_result.assert_not_called() def test_run_detect_register_blacklists_server_hold_for_domain_task(self): worker = self._build_worker(_FakeDb("noop")) worker._check_proxy_step_retry_budget = MagicMock(return_value=(False, "")) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=2.0) worker._is_current_single_step_task = MagicMock(return_value=False) worker._mark_blacklisted = MagicMock() worker._clear_proxy_failure = MagicMock() worker._clear_proxy_source_failure = MagicMock() worker._get_proxy_for_step = MagicMock(return_value={"http": "http://proxy", "https": "http://proxy"}) worker.db.update_domain_register_result = MagicMock() domain = {"source_type": 0} with patch("detect_worker.register.check_register", return_value=(REGISTER_STATUS_SERVER_HOLD, "2026-05-01")): result = worker._run_detect_register(1, domain, "example.com") self.assertFalse(result) worker.db.update_domain_register_result.assert_called_once() self.assertEqual(REGISTER_STATUS_SERVER_HOLD, domain["register_status"]) worker._mark_blacklisted.assert_called_once() def test_get_proxy_for_step_fast_direct_fallback_when_proxy_source_is_cooling_down(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_last_refresh_status = "代理源暂时冷却中,稍后继续补货" worker.proxy_direct_fallback_grace_overrides = {"360检测": 0.12} worker.get_proxies = MagicMock(return_value=None) with patch("detect_worker.time.time", side_effect=[100.0, 100.2]), patch("detect_worker.time.sleep") as mock_sleep: proxy = worker._get_proxy_for_step(1, "example.com", "360检测") self.assertIsNone(proxy) mock_sleep.assert_not_called() def test_get_proxy_for_step_register_single_machine_mode_skips_proxy_wait(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_config = {"proxy_enable": True, "allow_direct": False} worker.register_single_machine_mode_enabled = True worker.register_single_machine_direct_streak_attempts = 2 worker.get_proxies = MagicMock(return_value=None) with patch("detect_worker.time.time", side_effect=[100.0, 100.0]), patch("detect_worker.time.sleep") as mock_sleep: proxy = worker._get_proxy_for_step(1, "example.com", "注册状态检测") self.assertEqual(0.0, worker._proxy_direct_fallback_grace_for_step("注册状态检测")) self.assertEqual(0.0, worker._proxy_direct_retry_wait_for_step("注册状态检测")) self.assertIsNone(proxy) mock_sleep.assert_not_called() def test_external_issue_retry_wait_uses_step_specific_short_window(self): worker = self._build_worker(_FakeDb("noop")) worker._set_current_task_context(domain_id=1, is_step_task=True, task_mode="single_step") worker.proxy_direct_retry_wait_overrides = {"360检测": 0.25} worker.proxy_last_refresh_status = "代理源暂时冷却中,稍后继续补货" should_retry = worker._should_retry_external_issue( None, "360搜索请求超时", "360检测", "example.com", ) self.assertTrue(should_retry) self.assertAlmostEqual(0.25, worker._consume_step_proxy_retry_wait("360检测")) def test_stale_detect_session_holds_during_proxy_shortage_for_same_job(self): worker = self._build_worker(_FakeDb("noop")) worker.detect_command_thread = MagicMock() worker.detect_command_thread.is_alive.return_value = True worker.current_job_code = "sync-overseas-51" worker.proxy_last_refresh_status = "代理源暂时冷却中,稍后继续补货" worker.proxy_pool = [] with patch("detect_worker.time.time", return_value=200.0): worker.detect_command_started_at = 160.0 worker.detect_command_last_activity_at = 165.0 worker._last_domain_started_at = 165.0 worker._last_domain_result_at = 165.0 stale_detect, stale_reason = worker._is_stale_detect_session({"job_code": "sync-overseas-51"}) self.assertFalse(stale_detect) self.assertIn("同任务同周期重复唤起已忽略", stale_reason) def test_stale_detect_session_still_recovers_when_new_job_arrives(self): worker = self._build_worker(_FakeDb("noop")) worker.detect_command_thread = MagicMock() worker.detect_command_thread.is_alive.return_value = True worker.current_job_code = "sync-overseas-51" worker.current_job_task_mode = "single_step" worker.proxy_last_refresh_status = "代理源暂时冷却中,稍后继续补货" worker.proxy_pool = [] with patch("detect_worker.time.time", return_value=200.0): worker.detect_command_started_at = 150.0 worker.detect_command_last_activity_at = 170.0 worker._last_domain_started_at = 170.0 worker._last_domain_result_at = 170.0 stale_detect, stale_reason = worker._is_stale_detect_session({"job_code": "sync-overseas-99"}) self.assertTrue(stale_detect) self.assertIn("新任务", stale_reason) def test_stale_detect_session_does_not_replace_busy_domain_pipeline_for_new_job(self): worker = self._build_worker(_FakeDb("noop")) worker.detect_command_thread = MagicMock() worker.detect_command_thread.is_alive.return_value = True worker.current_job_code = "sync-overseas-51" worker.current_job_task_mode = "domain_pipeline" worker.proxy_last_refresh_status = "代理正常" worker.proxy_pool = [{}] worker._active_domain_threads = 6 with patch("detect_worker.time.time", return_value=200.0): worker.detect_command_started_at = 150.0 worker.detect_command_last_activity_at = 170.0 worker._last_domain_started_at = 170.0 worker._last_domain_result_at = 170.0 stale_detect, stale_reason = worker._is_stale_detect_session( {"job_code": "sync-overseas-99", "task_mode": "domain_pipeline"} ) self.assertFalse(stale_detect) self.assertEqual("", stale_reason) def test_submit_backlog_limit_defaults_to_thread_count_window(self): worker = self._build_worker(_FakeDb("noop")) self.assertEqual(2000, worker._resolve_submit_backlog_limit(2000, 0)) self.assertEqual(2000, worker._resolve_submit_backlog_limit(2000, 1200)) def test_dispatch_capacity_can_ramp_to_full_thread_limit(self): worker = self._build_worker(_FakeDb("noop")) self.assertEqual(2000, worker._resolve_dispatch_capacity(2000, 0, 2000)) self.assertEqual(2000, worker._resolve_dispatch_capacity(2000, 1000, 1000)) def test_claim_batch_size_defaults_to_thread_count_window(self): worker = self._build_worker(_FakeDb("noop")) self.assertEqual(2000, worker._resolve_claim_batch_size(2000, 2000, live_active=0)) self.assertEqual(1500, worker._resolve_claim_batch_size(2000, 1500, live_active=800)) def test_runtime_settings_can_shrink_claim_and_backlog_windows(self): worker = self._build_worker(_FakeDb("noop")) worker.runtime_settings = { "claim_batch_floor": 200, "claim_batch_ceil": 800, "submit_backlog_floor": 500, "submit_backlog_ceil": 1500, "dispatch_cap_multiplier": 2, "pending_buffer_cap_multiplier": 2, } self.assertEqual(500, worker._resolve_submit_backlog_limit(2000, 0)) self.assertEqual(500, worker._resolve_claim_batch_size(2000, 2000, live_active=0)) self.assertEqual(1500, worker._resolve_dispatch_capacity(2000, 0, 1500)) self.assertEqual(2, worker._runtime_positive_int_override("pending_buffer_cap_multiplier", "DOMAINCHECK_PENDING_BUFFER_CAP_MULTIPLIER")) def test_runtime_settings_override_environment_claim_and_backlog_windows(self): worker = self._build_worker(_FakeDb("noop")) worker.runtime_settings = { "claim_batch_floor": 200, "claim_batch_ceil": 800, "submit_backlog_floor": 500, "submit_backlog_ceil": 1500, "dispatch_cap_multiplier": 2, } with patch.dict( os.environ, { "DOMAINCHECK_CLAIM_BATCH_FLOOR": "4000", "DOMAINCHECK_CLAIM_BATCH_CEIL": "10000", "DOMAINCHECK_SUBMIT_BACKLOG_FLOOR": "5000", "DOMAINCHECK_SUBMIT_BACKLOG_CEIL": "10000", "DOMAINCHECK_DISPATCH_CAP_MULTIPLIER": "5", }, clear=False, ): self.assertEqual(500, worker._resolve_submit_backlog_limit(2000, 0)) self.assertEqual(500, worker._resolve_claim_batch_size(2000, 2000, live_active=0)) self.assertEqual(1500, worker._resolve_dispatch_capacity(2000, 0, 1500)) def test_pipeline_topup_triggers_when_partial_claim_leaves_large_gap(self): worker = self._build_worker(_FakeDb("noop")) self.assertTrue(worker._should_proactively_top_up_pipeline(280, 1800, 2000)) self.assertFalse(worker._should_proactively_top_up_pipeline(1700, 1800, 2000)) def test_sync_prefetch_triggers_when_local_work_falls_below_threshold(self): worker = self._build_worker(_FakeDb("noop")) self.assertTrue(worker._should_prefetch_sync_tasks(24, 18, 1000)) self.assertFalse(worker._should_prefetch_sync_tasks(420, 180, 1000)) def test_pull_sync_tasks_can_prefetch_without_claiming_after_pull(self): worker = self._build_worker(_FakeDb("noop")) worker._acquire_shared_job_maintenance_lock = MagicMock(return_value="lock-owner") worker._release_shared_job_maintenance_lock = MagicMock() worker._claim_detect_job_items = MagicMock(return_value=[{"domain_name": "should-not-claim.com"}]) class _FakeResponse: def __enter__(self_inner): return self_inner def __exit__(self_inner, exc_type, exc, tb): return False @staticmethod def read(): return json.dumps( { "code": 0, "message": "ok", "data": {"pull_state": "queued", "queued_count": 500}, }, ensure_ascii=False, ).encode("utf-8") with patch.object(config, "NODE_REGION", "mainland"), patch.object(config, "NODE_ROLE", "control"), patch( "detect_worker.urllib.request.urlopen", return_value=_FakeResponse(), ): domains = worker._pull_sync_tasks_until_available(thread_limit=1000, claim_after_pull=False) self.assertEqual([], domains) worker._claim_detect_job_items.assert_not_called() worker._release_shared_job_maintenance_lock.assert_called_once() def test_get_proxy_for_step_can_force_single_direct_fallback_once(self): worker = self._build_worker(_FakeDb("noop")) worker._set_current_task_context(domain_id=1, is_step_task=True, task_mode="single_step") worker._set_step_force_direct_once("百度site检测", True) proxy = worker._get_proxy_for_step(1, "example.com", "百度site检测") self.assertIsNone(proxy) self.assertFalse(worker._consume_step_force_direct_once("百度site检测")) def test_load_thread_count_can_inherit_parent_node_override(self): worker = DetectWorker.__new__(DetectWorker) worker.use_redis = True class _FakeRedis: def get(self, key): if key == "domain_tool:node_thread_counts": return json.dumps({"mainland-controller-01": 1200}, ensure_ascii=False) if key == "domain_tool:thread_count": return "1000" return None worker.redis_client = _FakeRedis() with patch.object(config, "NODE_CODE", "mainland-controller-01-a"), patch.dict( os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False, ): thread_count = DetectWorker.load_thread_count(worker) self.assertEqual(1200, thread_count) def test_load_process_count_can_inherit_parent_node_override_from_local_file_root(self): worker = DetectWorker.__new__(DetectWorker) worker.use_redis = False with patch.object(config, "NODE_CODE", "mainland-controller-01-u"), patch.dict( os.environ, {"WORKER_PARENT_NODE_CODE": "mainland-controller-01"}, clear=False, ), patch( "detect_worker._read_worker_local_json_config", side_effect=lambda name: ( {"mainland-controller-01": 80} if name == "node_process_counts.json" else ({"process_count": "80"} if name == "process_count.json" else None) ), ): process_count = DetectWorker.load_process_count(worker) self.assertEqual(80, process_count) def test_get_proxies_with_lease_holds_entry_until_release(self): worker = self._build_worker(_FakeDb("noop")) good_proxy = {"http": "http://good-proxy", "https": "http://good-proxy"} worker.proxy_pool = [ { "proxy": good_proxy, "usage_count": 0, "source_tag": "C", "expire_at_ts": time.time() + 60, } ] worker._schedule_proxy_refresh_if_needed = MagicMock() selected = worker.get_proxies(step_name="百度site检测", lease_selected=True) self.assertEqual(good_proxy, selected) self.assertEqual(0, len(worker.proxy_pool)) self.assertIn(worker._proxy_key(good_proxy), worker.proxy_active_leases) reused = worker.release_proxy(good_proxy, step_name="百度site检测") self.assertTrue(reused) self.assertEqual(1, len(worker.proxy_pool)) self.assertNotIn(worker._proxy_key(good_proxy), worker.proxy_active_leases) def test_release_proxy_discards_entry_when_remaining_ttl_is_too_low(self): worker = self._build_worker(_FakeDb("noop")) proxy = {"http": "http://soon-expire", "https": "http://soon-expire"} worker.proxy_active_leases = { worker._proxy_key(proxy): { "proxy": proxy, "usage_count": 1, "source_tag": "D", "expire_at_ts": time.time() + 2, "leased_step_name": "百度site检测", } } worker._schedule_proxy_refresh_if_needed = MagicMock() reused = worker.release_proxy(proxy, step_name="百度site检测") self.assertFalse(reused) self.assertEqual([], worker.proxy_pool) self.assertNotIn(worker._proxy_key(proxy), worker.proxy_active_leases) def test_retry_external_issue_with_proxy_forces_next_attempt_direct_when_allowed(self): worker = self._build_worker(_FakeDb("noop")) worker._set_current_task_context(domain_id=1, is_step_task=True, task_mode="single_step") worker.remove_proxy = MagicMock() proxy = {"http": "http://bad-proxy", "https": "http://bad-proxy"} should_retry = worker._should_retry_external_issue( proxy, "curl: (28) Connection timed out after 552 milliseconds", "百度site检测", "example.com", ) self.assertTrue(should_retry) worker.remove_proxy.assert_called_once_with(proxy) worker.trigger_proxy_refresh.assert_called_once() self.assertTrue(worker._consume_step_force_direct_once("百度site检测")) def test_external_dependency_issue_recognizes_common_chinese_timeout_messages(self): worker = self._build_worker(_FakeDb("noop")) self.assertTrue(worker._is_external_dependency_issue("360搜索请求超时")) self.assertTrue(worker._is_external_dependency_issue("百度site检测失败: 连接超时")) self.assertTrue(worker._is_external_dependency_issue("站长之家检测失败: 网络异常")) def test_run_detect_aizhan_passes_remaining_budget_to_detector(self): worker = self._build_worker(_FakeDb("noop")) worker._check_proxy_step_retry_budget = MagicMock(return_value=(False, "")) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=1.7) worker._get_proxy_for_step = MagicMock(return_value=None) worker._mark_detection_degraded = MagicMock() worker._should_blacklist_result = MagicMock(return_value=False) worker._record_step_result = MagicMock() worker._should_retry_external_issue = MagicMock(return_value=False) worker._is_external_dependency_issue = MagicMock(return_value=False) worker._mark_detection_rejected = MagicMock() worker._mark_detection_failed = MagicMock() with patch("detect_worker.aizhan.check_aizhan", return_value=(True, "")) as mock_check: result = worker._run_detect_aizhan(1, "example.com", []) self.assertTrue(result) self.assertEqual(1.7, mock_check.call_args.kwargs["budget_seconds"]) def test_run_detect_aizhan_uses_external_issue_retry_path(self): worker = self._build_worker(_FakeDb("noop")) worker._check_proxy_step_retry_budget = MagicMock(side_effect=[(False, ""), (False, ""), (True, "budget exhausted")]) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=2.0) worker._get_proxy_for_step = MagicMock(side_effect=[ {"http": "http://proxy-1", "https": "http://proxy-1"}, None, ]) worker._mark_detection_degraded = MagicMock() worker._should_blacklist_result = MagicMock(return_value=False) worker._record_step_result = MagicMock() worker._should_retry_external_issue = MagicMock(side_effect=[True, False]) worker._is_external_dependency_issue = MagicMock(return_value=False) worker._mark_detection_rejected = MagicMock() worker._mark_detection_failed = MagicMock() worker.release_proxy = MagicMock() with patch( "detect_worker.aizhan.check_aizhan", side_effect=[(False, "proxy timeout"), (True, "ok")], ) as mock_check: result = worker._run_detect_aizhan(1, "example.com", []) self.assertTrue(result) self.assertEqual(2, mock_check.call_count) worker._should_retry_external_issue.assert_called_with( {"http": "http://proxy-1", "https": "http://proxy-1"}, "proxy timeout", "爱站网检测", "example.com", ) def test_single_machine_aizhan_direct_first_enabled_only_on_first_attempt(self): worker = self._build_worker(_FakeDb("noop")) worker.single_machine_aizhan_direct_first_enabled = True worker.single_machine_site_direct_fallback_enabled = True worker.allow_direct_connection = MagicMock(return_value=False) self.assertTrue(worker._should_force_direct_first_attempt("爱站网检测", 0)) self.assertFalse(worker._should_force_direct_first_attempt("爱站网检测", 1)) self.assertFalse(worker._should_force_direct_first_attempt("百度site检测", 0)) def test_single_machine_baidu_direct_first_enabled_only_on_first_attempt(self): worker = self._build_worker(_FakeDb("noop")) worker.single_machine_baidu_direct_first_enabled = True worker.single_machine_site_direct_fallback_enabled = True worker.allow_direct_connection = MagicMock(return_value=False) self.assertTrue(worker._should_force_direct_first_attempt("百度site检测", 0)) self.assertFalse(worker._should_force_direct_first_attempt("百度site检测", 1)) self.assertFalse(worker._should_force_direct_first_attempt("爱站网检测", 0)) def test_single_machine_chinaz_direct_first_enabled_only_on_first_attempt(self): worker = self._build_worker(_FakeDb("noop")) worker.single_machine_chinaz_direct_first_enabled = True worker.single_machine_site_direct_fallback_enabled = True worker.allow_direct_connection = MagicMock(return_value=False) self.assertTrue(worker._should_force_direct_first_attempt("站长之家检测", 0)) self.assertFalse(worker._should_force_direct_first_attempt("站长之家检测", 1)) self.assertFalse(worker._should_force_direct_first_attempt("百度site检测", 0)) def test_run_detect_aizhan_forces_direct_first_when_single_machine_flag_enabled(self): worker = self._build_worker(_FakeDb("noop")) worker.single_machine_aizhan_direct_first_enabled = True worker.single_machine_site_direct_fallback_enabled = True worker.allow_direct_connection = MagicMock(return_value=False) worker._check_proxy_step_retry_budget = MagicMock(return_value=(False, "")) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=2.0) worker._get_proxy_for_step = MagicMock(return_value=None) worker._mark_detection_degraded = MagicMock() worker._should_blacklist_result = MagicMock(return_value=False) worker._record_step_result = MagicMock() worker._should_retry_external_issue = MagicMock(return_value=False) worker._is_external_dependency_issue = MagicMock(return_value=False) worker._mark_detection_rejected = MagicMock() worker._mark_detection_failed = MagicMock() worker._set_step_force_direct_once = MagicMock() with patch("detect_worker.aizhan.check_aizhan", return_value=(True, "")): result = worker._run_detect_aizhan(1, "example.com", []) self.assertTrue(result) worker._set_step_force_direct_once.assert_called_once_with("爱站网检测", True) def test_run_detect_baidu_forces_direct_first_when_single_machine_flag_enabled(self): worker = self._build_worker(_FakeDb("noop")) worker.single_machine_baidu_direct_first_enabled = True worker.single_machine_site_direct_fallback_enabled = True worker.allow_direct_connection = MagicMock(return_value=False) worker._check_proxy_step_retry_budget = MagicMock(return_value=(False, "")) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=2.0) worker._get_proxy_for_step = MagicMock(return_value=None) worker._mark_detection_degraded = MagicMock() worker._should_blacklist_result = MagicMock(return_value=False) worker._record_step_result = MagicMock() worker._should_retry_external_issue = MagicMock(return_value=False) worker._is_external_dependency_issue = MagicMock(return_value=False) worker._mark_detection_rejected = MagicMock() worker._mark_detection_failed = MagicMock() worker._set_step_force_direct_once = MagicMock() with patch("detect_worker.baidu.check_site", return_value=(True, "")): result = worker._run_detect_baidu(1, "example.com", []) self.assertTrue(result) worker._set_step_force_direct_once.assert_called_once_with("百度site检测", True) def test_run_detect_chinaz_forces_direct_first_when_single_machine_flag_enabled(self): worker = self._build_worker(_FakeDb("noop")) worker.single_machine_chinaz_direct_first_enabled = True worker.single_machine_site_direct_fallback_enabled = True worker.allow_direct_connection = MagicMock(return_value=False) worker._check_proxy_step_retry_budget = MagicMock(return_value=(False, "")) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=2.0) worker._get_proxy_for_step = MagicMock(return_value={"http": "http://proxy-1", "https": "http://proxy-1"}) worker._mark_detection_degraded = MagicMock() worker._should_blacklist_result = MagicMock(return_value=False) worker._record_step_result = MagicMock() worker._should_rotate_proxy_on_error = MagicMock(return_value=False) worker._is_external_dependency_issue = MagicMock(return_value=False) worker._mark_detection_rejected = MagicMock() worker._mark_detection_failed = MagicMock() worker._emit_step_trace = MagicMock() with patch("detect_worker.chinaz.check_title", return_value=(True, "", {})) as mock_check_title: result = worker._run_detect_chinaz(1, "example.com", []) self.assertTrue(result) worker._get_proxy_for_step.assert_not_called() worker._emit_step_trace.assert_called_once() self.assertIsNone(mock_check_title.call_args.args[2]) def test_run_detect_aizhan_degrades_remote_disconnected_early_when_flag_enabled(self): worker = self._build_worker(_FakeDb("noop")) worker.aizhan_remote_disconnect_degrade_enabled = True worker._check_proxy_step_retry_budget = MagicMock(return_value=(False, "")) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=2.0) proxy = {"http": "http://proxy-1", "https": "http://proxy-1"} worker._get_proxy_for_step = MagicMock(return_value=proxy) worker._mark_detection_degraded = MagicMock() worker._should_blacklist_result = MagicMock(return_value=False) worker._record_step_result = MagicMock() worker._should_retry_external_issue = MagicMock(return_value=True) worker._is_external_dependency_issue = MagicMock(return_value=True) worker._mark_detection_rejected = MagicMock() worker._mark_detection_failed = MagicMock() worker.release_proxy = MagicMock() with patch( "detect_worker.aizhan.check_aizhan", return_value=(False, "('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))"), ): result = worker._run_detect_aizhan(1, "example.com", []) self.assertTrue(result) worker._should_retry_external_issue.assert_not_called() worker.release_proxy.assert_called_once() worker._mark_detection_degraded.assert_called_once() def test_run_detect_aizhan_degrades_external_issue_early_when_fast_flag_enabled(self): worker = self._build_worker(_FakeDb("noop")) worker.aizhan_external_fast_degrade_enabled = True worker._check_proxy_step_retry_budget = MagicMock(return_value=(False, "")) worker._remaining_proxy_step_retry_budget_seconds = MagicMock(return_value=2.0) proxy = {"http": "http://proxy-1", "https": "http://proxy-1"} worker._get_proxy_for_step = MagicMock(return_value=proxy) worker._mark_detection_degraded = MagicMock() worker._should_blacklist_result = MagicMock(return_value=False) worker._record_step_result = MagicMock() worker._should_retry_external_issue = MagicMock(return_value=True) worker._is_external_dependency_issue = MagicMock(return_value=True) worker._mark_detection_rejected = MagicMock() worker._mark_detection_failed = MagicMock() worker.release_proxy = MagicMock() with patch( "detect_worker.aizhan.check_aizhan", return_value=(False, "Read timed out. (read timeout=1.8)"), ): result = worker._run_detect_aizhan(1, "example.com", []) self.assertTrue(result) worker._should_retry_external_issue.assert_not_called() worker.release_proxy.assert_called_once() worker._mark_detection_degraded.assert_called_once() def test_resolve_single_step_finalization_keeps_degraded_as_completed(self): status, message = DetectWorker._resolve_single_step_finalization( { "state": "degraded", "message": "外部依赖异常,建议重试", "retry_recommended": True, } ) self.assertEqual("completed", status) self.assertEqual("外部依赖异常,建议重试", message) def test_worker_log_sync_mode_keeps_full_when_load_is_small(self): worker = self._build_worker(_FakeDb("noop")) worker.runtime_settings = {"worker_log_sync_enabled": True, "worker_log_sync_mode": "full"} worker.thread_count = 1200 worker.detecting = True worker._get_active_domain_threads = MagicMock(return_value=80) self.assertEqual("full", worker._worker_log_sync_mode()) def test_worker_log_sync_mode_downgrades_full_when_threads_are_busy(self): worker = self._build_worker(_FakeDb("noop")) worker.runtime_settings = {"worker_log_sync_enabled": True, "worker_log_sync_mode": "full"} worker.thread_count = 1200 worker.detecting = True worker._get_active_domain_threads = MagicMock(return_value=650) self.assertEqual("key", worker._worker_log_sync_mode()) def test_worker_log_sync_mode_downgrades_full_when_queue_backlog_is_high(self): worker = self._build_worker(_FakeDb("noop")) worker.runtime_settings = {"worker_log_sync_enabled": True, "worker_log_sync_mode": "full"} worker.thread_count = 1200 worker.detecting = False worker._worker_log_sync_queue = MagicMock() worker._worker_log_sync_queue.maxsize = 50000 worker._worker_log_sync_queue.qsize.return_value = 12000 self.assertEqual("key", worker._worker_log_sync_mode()) def test_push_remote_debug_event_enters_cooldown_after_failure(self): worker = self._build_worker(_FakeDb("noop")) with patch.object(worker, "_remote_debug_ingest_url", return_value="http://example.invalid/api/v1/runtime/debug-ingest"): with patch("detect_worker.urllib.request.urlopen", side_effect=TimeoutError("timed out")) as mock_urlopen: worker._push_remote_debug_event(message="test") self.assertEqual(1, worker._remote_debug_event_failure_streak) self.assertGreater(worker._remote_debug_event_cooldown_until, time.time()) self.assertEqual(1, mock_urlopen.call_count) worker._push_remote_debug_event(message="test") self.assertEqual(1, mock_urlopen.call_count) def test_push_remote_debug_event_resets_failure_streak_after_success(self): worker = self._build_worker(_FakeDb("noop")) worker._remote_debug_event_failure_streak = 3 class _FakeResponse: def __enter__(self_inner): return self_inner def __exit__(self_inner, exc_type, exc, tb): return False @staticmethod def read(): return b'{"code":0}' with patch.object(worker, "_remote_debug_ingest_url", return_value="http://example.invalid/api/v1/runtime/debug-ingest"): with patch("detect_worker.urllib.request.urlopen", return_value=_FakeResponse()): worker._push_remote_debug_event(message="test") self.assertEqual(0, worker._remote_debug_event_failure_streak) self.assertEqual(0.0, worker._remote_debug_event_cooldown_until) def test_idle_worker_recycle_skips_parent_control_worker(self): worker = self._build_worker(_FakeDb("noop")) worker.running = True worker.detecting = True worker._get_active_domain_threads = MagicMock(return_value=0) worker._last_runtime_extra = {"waiting_for_dispatch": True} worker._last_worker_activity_at = time.time() - 600 worker._worker_started_at = time.time() - 900 with patch.dict( os.environ, { "WORKER_MODE": "linux-systemd", "WORKER_PARENT_NODE_CODE": "mainland-controller-01", "DOMAINCHECK_IDLE_WORKER_RECYCLE_ENABLED": "1", }, clear=False, ): with patch.object(config, "NODE_CODE", "mainland-controller-01"): with patch.object(worker, "_trigger_idle_worker_recycle") as mock_recycle: recycled = worker._maybe_recycle_idle_worker(trigger_reason="heartbeat") self.assertFalse(recycled) mock_recycle.assert_not_called() def test_configure_worker_thread_stack_size_uses_linux_systemd_default(self): state = {"size": 0} def fake_stack_size(value=None): if value is None: return state["size"] state["size"] = int(value) return 0 with patch.dict(os.environ, {"WORKER_MODE": "linux-systemd"}, clear=False): with patch.object(detect_worker_module.threading, "stack_size", side_effect=fake_stack_size): applied = detect_worker_module._configure_worker_thread_stack_size() self.assertEqual(1024 * 1024, applied) self.assertEqual(1024 * 1024, state["size"]) def test_configure_worker_thread_stack_size_skips_non_service_default(self): state = {"size": 0} def fake_stack_size(value=None): if value is None: return state["size"] state["size"] = int(value) return 0 with patch.dict(os.environ, {"WORKER_MODE": "desktop"}, clear=False): with patch.object(detect_worker_module.threading, "stack_size", side_effect=fake_stack_size): applied = detect_worker_module._configure_worker_thread_stack_size() self.assertEqual(0, applied) self.assertEqual(0, state["size"]) def test_idle_worker_recycle_triggers_for_idle_child_worker(self): worker = self._build_worker(_FakeDb("noop")) worker.running = True worker.detecting = True worker._get_active_domain_threads = MagicMock(return_value=0) worker._last_runtime_extra = {"waiting_for_dispatch": True} worker._last_worker_activity_at = time.time() - 600 worker._worker_started_at = time.time() - 900 with patch.dict( os.environ, { "WORKER_MODE": "linux-systemd", "WORKER_PARENT_NODE_CODE": "mainland-controller-01", "DOMAINCHECK_IDLE_WORKER_RECYCLE_ENABLED": "1", "DOMAINCHECK_IDLE_WORKER_RECYCLE_AFTER_SECONDS": "120", "DOMAINCHECK_IDLE_WORKER_RECYCLE_MIN_UPTIME_SECONDS": "180", "DOMAINCHECK_IDLE_WORKER_RECYCLE_JITTER_SECONDS": "0", }, clear=False, ): with patch.object(config, "NODE_CODE", "mainland-controller-01-b"): with patch.object(worker, "_trigger_idle_worker_recycle", return_value=True) as mock_recycle: recycled = worker._maybe_recycle_idle_worker(trigger_reason="heartbeat") self.assertTrue(recycled) mock_recycle.assert_called_once() def test_idle_worker_recycle_respects_recent_explicit_start_grace(self): worker = self._build_worker(_FakeDb("noop")) worker.running = True worker.detecting = True worker._get_active_domain_threads = MagicMock(return_value=0) worker._last_runtime_extra = {"waiting_for_dispatch": True} worker._last_worker_activity_at = time.time() - 600 worker._worker_started_at = time.time() - 900 worker._last_explicit_start_signal_at = time.time() - 10 with patch.dict( os.environ, { "WORKER_MODE": "linux-systemd", "WORKER_PARENT_NODE_CODE": "mainland-controller-01", "DOMAINCHECK_IDLE_WORKER_RECYCLE_ENABLED": "1", "DOMAINCHECK_IDLE_WORKER_RECYCLE_AFTER_SECONDS": "120", "DOMAINCHECK_IDLE_WORKER_RECYCLE_MIN_UPTIME_SECONDS": "180", "DOMAINCHECK_IDLE_WORKER_RECYCLE_START_GRACE_SECONDS": "90", "DOMAINCHECK_IDLE_WORKER_RECYCLE_JITTER_SECONDS": "0", }, clear=False, ): with patch.object(config, "NODE_CODE", "mainland-controller-01-c"): with patch.object(worker, "_trigger_idle_worker_recycle") as mock_recycle: recycled = worker._maybe_recycle_idle_worker(trigger_reason="heartbeat") self.assertFalse(recycled) mock_recycle.assert_not_called() def test_prepare_proxy_pool_for_detection_start_reuses_local_pool(self): worker = self._build_worker(_FakeDb("noop")) worker.proxy_pool = [{"proxy": "http://1.1.1.1:8080"}] worker._schedule_proxy_refresh_if_needed = MagicMock() ready, count, reason = worker._prepare_proxy_pool_for_detection_start() self.assertTrue(ready) self.assertEqual(1, count) self.assertEqual("local_pool_ready", reason) worker._schedule_proxy_refresh_if_needed.assert_called_once_with(1) def test_prepare_proxy_pool_for_detection_start_reuses_shared_snapshot(self): worker = self._build_worker(_FakeDb("noop")) worker._schedule_proxy_refresh_if_needed = MagicMock() worker._proxy_should_coordinate_shared_refresh = MagicMock(return_value=True) worker._load_shared_proxy_snapshot_payload = MagicMock( return_value={"proxy_pool": [{"proxy": "http://2.2.2.2:8080"}]} ) def apply_snapshot(payload, status_prefix=""): worker.proxy_pool = list(payload.get("proxy_pool") or []) return True worker._apply_shared_proxy_snapshot = MagicMock(side_effect=apply_snapshot) ready, count, reason = worker._prepare_proxy_pool_for_detection_start() self.assertTrue(ready) self.assertEqual(1, count) self.assertEqual("shared_snapshot_ready", reason) worker._apply_shared_proxy_snapshot.assert_called_once() worker._schedule_proxy_refresh_if_needed.assert_called_once_with(1) if __name__ == "__main__": unittest.main()