112 lines
3.6 KiB
Python
112 lines
3.6 KiB
Python
"""Entrypoint for the scheduler process.
|
||
|
||
This module starts the APScheduler scheduler and ensures a graceful
|
||
shutdown on SIGINT/SIGTERM. It also improves job error logging to
|
||
include exception tracebacks for easier debugging.
|
||
"""
|
||
|
||
import datetime
|
||
import signal
|
||
import sys
|
||
import traceback
|
||
from functools import partial
|
||
from typing import Any
|
||
|
||
from apscheduler.events import EVENT_JOB_ERROR
|
||
from apscheduler.schedulers.blocking import BlockingScheduler
|
||
|
||
from config import config
|
||
from log.log_manager import log, logger
|
||
from task.manager_task import manager_task
|
||
|
||
|
||
def _format_exception(exc: BaseException) -> str:
|
||
"""Return a nicely formatted exception with traceback."""
|
||
return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
||
|
||
|
||
def job_error_listener(event: Any) -> None:
|
||
"""Listener for job errors that logs the exception and traceback.
|
||
|
||
Uses the APScheduler event object. This is defensive: if an exception
|
||
is present it logs its string representation and the full traceback to
|
||
the configured logger. Keep this lightweight to avoid raising inside
|
||
the listener.
|
||
"""
|
||
try:
|
||
if getattr(event, "exception", None):
|
||
exc = event.exception
|
||
logger.error(f"Job {getattr(event, 'job_id', '<unknown>')} crashed: {exc}")
|
||
logger.error(_format_exception(exc))
|
||
# 可添加邮件告警逻辑(如果需要且已配置)
|
||
except Exception:
|
||
# We must not let the listener raise — log and continue.
|
||
logger.exception("Exception in job_error_listener")
|
||
|
||
|
||
def _validate_interval(value: Any) -> int:
|
||
"""Return a valid positive integer interval in seconds.
|
||
|
||
If the provided value is invalid, returns a safe default (300).
|
||
"""
|
||
DEFAULT = 300
|
||
try:
|
||
iv = int(value)
|
||
if iv <= 0:
|
||
raise ValueError("interval must be > 0")
|
||
return iv
|
||
except Exception:
|
||
logger.warning(
|
||
"Invalid config.scheduler_interval=%r, falling back to %s seconds",
|
||
value,
|
||
DEFAULT,
|
||
)
|
||
return DEFAULT
|
||
|
||
|
||
def main() -> None:
|
||
"""Create and start the scheduler with graceful shutdown handling."""
|
||
scheduler = BlockingScheduler()
|
||
|
||
interval_seconds = _validate_interval(getattr(config, "scheduler_interval", None))
|
||
|
||
# 每隔 interval_seconds 秒执行一次任务,同时设定第一次执行在程序启动10秒后执行
|
||
scheduler.add_job(
|
||
partial(manager_task, scheduler),
|
||
"interval",
|
||
seconds=interval_seconds,
|
||
jitter=30,
|
||
next_run_time=datetime.datetime.now() + datetime.timedelta(seconds=10),
|
||
)
|
||
|
||
# 添加任务错误监听器
|
||
scheduler.add_listener(job_error_listener, EVENT_JOB_ERROR)
|
||
|
||
# Graceful shutdown handlers
|
||
def _shutdown(signum, frame):
|
||
log(f"Received signal {signum}. Shutting down scheduler...")
|
||
try:
|
||
scheduler.shutdown(wait=False)
|
||
except Exception:
|
||
logger.exception("Error while shutting down scheduler")
|
||
# Ensure process exits after scheduler shutdown
|
||
# sys.exit(0)
|
||
|
||
signal.signal(signal.SIGINT, _shutdown)
|
||
signal.signal(signal.SIGTERM, _shutdown)
|
||
|
||
try:
|
||
log("started successfully.")
|
||
scheduler.start() # 阻塞运行
|
||
except (KeyboardInterrupt, SystemExit):
|
||
log("Shutting down ...")
|
||
if scheduler.state == 1:
|
||
try:
|
||
scheduler.shutdown(wait=False)
|
||
except Exception:
|
||
logger.exception("Error while shutting down scheduler")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|