alphax/docker/scheduler.py
2026-05-13 22:32:50 +08:00

107 lines
3.3 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
script: 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, str(ROOT / job.script), *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", "event_driven_screener.py", 60, ("--once",), initial_delay=5),
Job("tracker", "price_tracker.py", 180, initial_delay=20),
Job("confirm", "altcoin_confirm.py", 600, initial_delay=40),
Job("screener", "altcoin_screener.py", 900, initial_delay=80),
Job("sentiment", "sentiment_monitor.py", 1800, ("--collect",), initial_delay=120),
Job("review", "review_engine.py", 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()