Run account payment flows at configured local times.
| 20 | |
| 21 | |
| 22 | class SchedulerService: |
| 23 | """Run account payment flows at configured local times.""" |
| 24 | |
| 25 | def __init__(self) -> None: |
| 26 | self.state_service = get_account_state_service() |
| 27 | self.payment_service = get_payment_service() |
| 28 | self.runtime_logs = get_runtime_log_service() |
| 29 | self._stop_event = threading.Event() |
| 30 | self._thread: threading.Thread | None = None |
| 31 | self._running_accounts: set[str] = set() |
| 32 | self._pause_requested: set[str] = set() |
| 33 | self._lock = threading.Lock() |
| 34 | |
| 35 | def start(self) -> None: |
| 36 | if self._thread and self._thread.is_alive(): |
| 37 | return |
| 38 | self._stop_event.clear() |
| 39 | self._thread = threading.Thread(target=self._run_loop, name="glm-desk-scheduler", daemon=True) |
| 40 | self._thread.start() |
| 41 | threading.Thread(target=self.check_cached_accounts_once, name="glm-desk-account-check", daemon=True).start() |
| 42 | self.runtime_logs.log_system_event( |
| 43 | stage="scheduler", |
| 44 | status="started", |
| 45 | message="调度器已启动", |
| 46 | ) |
| 47 | |
| 48 | def stop(self) -> None: |
| 49 | self._stop_event.set() |
| 50 | if self._thread and self._thread.is_alive(): |
| 51 | self._thread.join(timeout=2) |
| 52 | self.runtime_logs.log_system_event( |
| 53 | stage="scheduler", |
| 54 | status="stopped", |
| 55 | message="调度器已停止", |
| 56 | ) |
| 57 | |
| 58 | def _run_loop(self) -> None: |
| 59 | while not self._stop_event.is_set(): |
| 60 | try: |
| 61 | self.poll_once() |
| 62 | except Exception as exc: # pragma: no cover - defensive scheduler guard |
| 63 | logger.exception("scheduler poll failed: %s", exc) |
| 64 | self.runtime_logs.log_system_event( |
| 65 | stage="scheduler_poll", |
| 66 | status="failed", |
| 67 | message=f"调度器轮询失败:{exc}", |
| 68 | details={"error": exc.__class__.__name__}, |
| 69 | level=logging.ERROR, |
| 70 | ) |
| 71 | self._stop_event.wait(1) |
| 72 | |
| 73 | def poll_once(self) -> None: |
| 74 | now = datetime.now(SCHEDULER_TZ) if SCHEDULER_TZ is not None else datetime.now().astimezone() |
| 75 | current_hms = now.strftime("%H:%M:%S") |
| 76 | current_date = now.strftime("%Y-%m-%d") |
| 77 | for public_account in self.state_service.list_accounts(): |
| 78 | if not public_account.schedule_enabled or not public_account.scheduled_start_time: |
| 79 | continue |
no outgoing calls
no test coverage detected