Files
MeshSate/scripts/cron_entrypoint.py
zhaolei da8496adfd infra: 自动定时任务 — cron 服务
新增:
  - scripts/cron_entrypoint.py: 纯 Python 调度器, 零依赖
    任务1: 每天 03:00 跑 archive_old_tasks.py --apply
    任务2: 每周一 03:10 VACUUM ANALYZE (维护表统计信息)
  - docker-compose.yml: 加 cron 服务 (共享 meshsafe:latest 镜像)

归档策略 (archive_old_tasks.py):
  - scan_tasks  90天前: 清 detail JSONB (省空间)
  - scan_tasks  365天前: 移到 scan_tasks_archive
  - idempotency_records:  清 7 天前 (幂等 TTL)
  - policy_versions:      只留最新 5 个

启动: docker compose up -d cron
日志: docker compose logs -f cron
docker compose up 会自动拉起 (restart: unless-stopped)
2026-10-29 17:05:41 +08:00

122 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""MeshSafe 定时任务调度器 — 零依赖,Docker 容器前台常驻。
和 api 共享同一个镜像,容器 entrypoint 换成本脚本即可。
用法: docker run meshsafe:latest python3 scripts/cron_entrypoint.py
支持的任务:
每天 03:00 归档 + 清理 DB (archive_old_tasks.py --apply)
每周一 03:10 VACUUM ANALYZE (维护表统计信息)
"""
from __future__ import annotations
import logging
import os
import subprocess
import sys
import time
from datetime import datetime, timedelta
sys.path.insert(0, "/app")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger("scheduler")
def run(cmd: list[str], label: str) -> None:
"""跑一个命令,捕获输出并记日志。"""
log.info(f"[{label}] 开始执行: {' '.join(cmd)}")
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=1800)
if proc.stdout:
for line in proc.stdout.strip().splitlines():
log.info(f"[{label}] {line}")
if proc.stderr:
for line in proc.stderr.strip().splitlines():
log.warning(f"[{label}] {line}")
if proc.returncode == 0:
log.info(f"[{label}] ✅ 完成")
else:
log.error(f"[{label}] ❌ 返回码 {proc.returncode}")
except subprocess.TimeoutExpired:
log.error(f"[{label}] ❌ 超时 (30min)")
except Exception as e:
log.error(f"[{label}] ❌ 异常: {e}")
def run_vacuum() -> None:
"""VACUUM ANALYZE — 维护表统计信息。"""
try:
from app.db import engine
from sqlalchemy import text
tables = [
"scan_tasks", "scan_tasks_archive",
"scan_findings", "idempotency_records",
"policy_versions", "audit_logs",
]
with engine.begin() as conn:
for tbl in tables:
log.info(f"[VACUUM] {tbl} ...")
conn.execute(text(f"VACUUM ANALYZE {tbl}"))
log.info("[VACUUM] ✅ 全部完成")
except Exception as e:
log.error(f"[VACUUM] ❌ 异常: {e}")
def seconds_until(target_hour: int, target_minute: int, weekday: int | None = None) -> float:
"""计算到下次目标时间的秒数。
weekday: 0=周一...6=周日, None=每天
"""
now = datetime.now()
candidate = now.replace(hour=target_hour, minute=target_minute, second=0, microsecond=0)
if weekday is not None:
# 找到本周目标日
days_ahead = weekday - now.weekday()
if days_ahead < 0 or (days_ahead == 0 and now >= candidate):
days_ahead += 7
candidate += timedelta(days=days_ahead)
else:
if candidate <= now:
candidate += timedelta(days=1)
return (candidate - now).total_seconds()
def main() -> None:
log.info("=" * 60)
log.info("MeshSafe 定时任务调度器启动")
log.info(f"PID={os.getpid()} TZ=Asia/Shanghai")
log.info("任务:")
log.info(" - 每天 03:00 archive_old_tasks.py --apply")
log.info(" - 每周一 03:10 VACUUM ANALYZE")
log.info("=" * 60)
while True:
# 计算两个任务下次执行时间,取更早的那个
secs_archive = seconds_until(3, 0, weekday=None) # 每天 03:00
secs_vacuum = seconds_until(3, 10, weekday=0) # 周一 03:10
if secs_archive <= secs_vacuum:
log.info(f"下一个任务: 归档 → {secs_archive/3600:.1f}h 后")
time.sleep(secs_archive)
run(
[sys.executable, "/app/scripts/archive_old_tasks.py", "--apply"],
"ARCHIVE",
)
# 跑完后清下一次时间 (避免在 03:00 这一刻两个任务重叠)
continue
else:
log.info(f"下一个任务: VACUUM → {secs_vacuum/3600:.1f}h 后")
time.sleep(secs_vacuum)
run_vacuum()
continue
if __name__ == "__main__":
main()