#!/usr/bin/env python3 """AlphaX 容器内轻量调度器。 设计目标: - 替代宿主机 crontab; - 单进程串行执行,避免 SQLite 并发写锁; - 默认 DRY_RUN=1,不影响线上,也不会真的跑任务; - 部署验证通过后再把 ALPHAX_SCHEDULER_DRY_RUN=0 打开。 """ from __future__ import annotations import os import subprocess import sys import time from dataclasses import dataclass from datetime import datetime from pathlib import Path ROOT = Path(__file__).resolve().parents[1] PYTHON = sys.executable DRY_RUN = os.getenv("ALPHAX_SCHEDULER_DRY_RUN", "1").strip() not in {"0", "false", "False", "no", "NO"} @dataclass class Job: name: str module: str every_seconds: int args: tuple[str, ...] = () initial_delay: int = 0 next_run: float = 0.0 def now_str() -> str: return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def env_for_child() -> dict[str, str]: env = os.environ.copy() env.setdefault("PYTHONUNBUFFERED", "1") return env def run_job(job: Job) -> None: cmd = [PYTHON, "-m", job.module, *job.args] print(f"[{now_str()}] [scheduler] start {job.name}: {' '.join(cmd)}", flush=True) if DRY_RUN: print(f"[{now_str()}] [scheduler] DRY_RUN=1 skip {job.name}", flush=True) return started = time.time() try: proc = subprocess.run( cmd, cwd=ROOT, env=env_for_child(), text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=max(job.every_seconds * 2, 600), ) duration = time.time() - started out = (proc.stdout or "").strip() if len(out) > 8000: out = out[-8000:] print(f"[{now_str()}] [scheduler] done {job.name} exit={proc.returncode} duration={duration:.1f}s", flush=True) if out: print(out, flush=True) except subprocess.TimeoutExpired as e: print(f"[{now_str()}] [scheduler] timeout {job.name}: {e}", flush=True) except Exception as e: print(f"[{now_str()}] [scheduler] error {job.name}: {e}", flush=True) def build_jobs() -> list[Job]: # 与当前宿主机 crontab 对齐,但串行执行。 return [ Job("event", "app.services.event_driven_screener", 60, ("--once",), initial_delay=5), Job("tracker", "app.services.price_tracker", 180, initial_delay=20), Job("confirm", "app.services.altcoin_confirm", 600, initial_delay=40), Job("screener", "app.services.altcoin_screener", 900, initial_delay=80), Job("sentiment", "app.services.sentiment_monitor", 1800, ("--collect",), initial_delay=120), Job("review", "app.services.review_engine", 24 * 3600, initial_delay=300), ] def main() -> None: jobs = build_jobs() base = time.time() for job in jobs: job.next_run = base + job.initial_delay print(f"[{now_str()}] [scheduler] started jobs={len(jobs)} dry_run={DRY_RUN}", flush=True) while True: now = time.time() due = [j for j in jobs if now >= j.next_run] if not due: time.sleep(1) continue # 串行执行;一个 job 跑完才跑下一个,避免 SQLite 写锁。 for job in sorted(due, key=lambda j: j.next_run): run_job(job) job.next_run = time.time() + job.every_seconds if __name__ == "__main__": main()