Files
Nexus/server/background/schedule_runner.py
T
Your Name c9a99f4fb3 fix: 全项目文档对齐 + 代码清理
代码修复:
- web/agent/agent.py: datetime.utcnow() → datetime.now(timezone.utc)
- requirements.txt: 移除 paramiko==3.5.0 (已无活跃引用)
- 删除 server/infrastructure/ssh/pool.py (DEPRECATED, 无引用)

连接池三层对齐 (config.py/.env.example/session.py):
- DB_POOL_SIZE 100→160, DB_MAX_OVERFLOW 100→120
- 基于 MySQL max_connections=400, install.php 公式

文档修复 (21项):
- P0: 硬编码域名/IP替换为配置变量占位符
- P1: 10个过时设计文档加归档标注 (引用旧文件结构)
- P2: step-3-webssh报告 paramiko引用修正
- 6份审查报告连接池参数勘误 100/100→160/120
- ECC安全报告 EC5 datetime.utcnow 标记已修复
- docs/README.md 文档索引重写
- docs/memory/mem_nexus_overview.md 移除硬编码凭证
- docs/project/tech-stack-inventory.md paramiko标记已移除

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 08:19:56 +08:00

138 lines
5.3 KiB
Python

"""Nexus — Schedule Runner Background Task
Every 60 seconds: check PushSchedule cron expressions, trigger due schedules.
"""
import asyncio
import json
import logging
from datetime import datetime, timezone
from server.infrastructure.database.session import AsyncSessionLocal
from server.infrastructure.redis.client import get_redis_sync
from server.domain.models import PushSchedule, SyncLog, AuditLog
logger = logging.getLogger("nexus.schedule_runner")
SCHEDULE_CHECK_INTERVAL = 60 # seconds
def _cron_match(cron_expr: str, now: datetime) -> bool:
"""Check if current time matches a 5-field cron expression (min hour dom month dow).
Supports: * (any), */N (step), specific values, comma-separated lists.
"""
parts = cron_expr.strip().split()
if len(parts) != 5:
return False
fields = [
(now.minute, range(0, 60)),
(now.hour, range(0, 24)),
(now.day, range(1, 32)),
(now.month, range(1, 13)),
((now.weekday() + 1) % 7, range(0, 7)), # cron: 0=Sunday, Python weekday(): 0=Monday
]
for (current_val, _), field_expr in zip(fields, parts):
if not _field_match(field_expr, current_val):
return False
return True
def _field_match(expr: str, value: int) -> bool:
"""Match a single cron field expression against a value."""
for part in expr.split(","):
if part == "*":
return True
if part.startswith("*/"):
step = int(part[2:])
return value % step == 0
if "-" in part:
low, high = part.split("-", 1)
if int(low) <= value <= int(high):
return True
else:
# cron: 7 also means Sunday (=0)
if value == int(part) or (int(part) == 7 and value == 0):
return True
return False
async def schedule_runner_loop():
"""Background task: check schedules every 60s, trigger due cron jobs."""
logger.info("Schedule runner loop started (interval: 60s)")
while True:
await asyncio.sleep(SCHEDULE_CHECK_INTERVAL)
try:
async with AsyncSessionLocal() as session:
from sqlalchemy import select
result = await session.execute(
select(PushSchedule).where(PushSchedule.enabled == True)
)
schedules = result.scalars().all()
now = datetime.now(timezone.utc)
triggered = 0
for schedule in schedules:
try:
if not _cron_match(schedule.cron_expr, now):
continue
# Don't re-trigger if already ran this minute
if schedule.last_run_at:
seconds_since = (now - schedule.last_run_at).total_seconds()
if seconds_since < SCHEDULE_CHECK_INTERVAL:
continue
# Parse server_ids from JSON
try:
server_ids = json.loads(schedule.server_ids)
except (json.JSONDecodeError, TypeError):
logger.error(f"Schedule {schedule.id}: invalid server_ids JSON")
continue
if not server_ids:
continue
# Execute sync via SyncEngineV2
from server.application.services.sync_engine_v2 import SyncEngineV2
from server.infrastructure.database.server_repo import ServerRepositoryImpl
from server.infrastructure.database.sync_log_repo import SyncLogRepositoryImpl
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
from server.infrastructure.database.push_schedule_repo import PushRetryJobRepositoryImpl
engine = SyncEngineV2(
server_repo=ServerRepositoryImpl(session),
sync_log_repo=SyncLogRepositoryImpl(session),
audit_repo=AuditLogRepositoryImpl(session),
retry_repo=PushRetryJobRepositoryImpl(session),
)
result = await engine.sync_files(
server_ids=server_ids,
source_path=schedule.source_path,
trigger_type="schedule",
operator=f"schedule:{schedule.name}",
)
# Update last_run_at
schedule.last_run_at = now
await session.commit()
triggered += 1
logger.info(
f"Schedule '{schedule.name}' triggered: "
f"{result['completed']}/{result['total']} succeeded"
)
except Exception as e:
logger.error(f"Schedule {schedule.id} execution failed: {e}")
if triggered:
logger.info(f"Schedule runner: {triggered} schedules triggered")
except Exception as e:
logger.error(f"Schedule runner check failed: {e}")