""" 系统服务,用于读取服务配置文件,启动或停止相关的服务。 """ import asyncio import datetime import logging from asyncio import AbstractEventLoop, Task from enum import Enum from typing import Callable, Awaitable, Optional from dateutil.relativedelta import relativedelta from paste.core import aio_pool from paste.core.logging import echo_log from paste.db.baseadapter import BaseAdapter from paste.service.daemonize import DaemonizeService class PeriodType(Enum): WEEKLY = "weekly" MONTHLY = "monthly" YEARLY = "yearly" QUARTERLY = "quarterly" class TaskService: """ 任务服务,专用于创建或停止服务。 """ task_event_loop: Optional[AbstractEventLoop] = None """ 任务件循环对象。 """ def __init__(self, service_name: str = None, pid_file: str = None): """ 构造函数。 :param service_name: 服务名称 :param pid_file: 进程 ID 文件路径 """ self.service_name = service_name """ 服务名称。 """ if self.service_name is None: self.service_name = '未命名服务' self.pid_file = pid_file """ PID 文件路径。 """ if self.pid_file is None: _now = datetime.datetime.now() self.pid_file = f'/tmp/task_service_{_now.strftime("%Y%m%d%H%M%S%f")}.pid' self.task_list: list[Task] = [] """ 任务列表。 """ self._create_task_params: list[dict] = [] """ 创建任务的参数列表。 """ self.is_running = True """ 是否允许运行。 """ self.log_next_time = True """ 是否记录下次执行时间。 """ def event_loop(self): """ 在需要调用的时间点取得事件循环对象。 :return: 事件循环对象 """ self.task_event_loop = aio_pool.get_aio_loop() return self.task_event_loop def create_delay_task(self, fn: Callable = None, delay: int = 60, **kwargs): """ 创建延时任务工厂,每次任务完成后,将等待固定时长后继续执行。 :param fn: 要执行的任务函数 :param delay: 延时长度,单位:秒 :param kwargs fn 函数的参数 """ def log_next(log_next_time: bool): if log_next_time and self.log_next_time: echo_log(f"距下次执行:{fn.__name__} 还有:{delay} 秒.") async def task_warp(): """ 任务包装器。 """ if fn is not None: try: _result = fn(**kwargs) if isinstance(_result, Awaitable): await _result except Exception as e: echo_log(msg=e, level=logging.ERROR, is_log_exc=True) async def task_loop(): """ 任务循环。 """ if fn is None: return _log_next_time = True _next_time = None while self.is_running: _delta_seconds = relativedelta(datetime.datetime.now(), _next_time).seconds if _next_time else 1 if _delta_seconds > 0: await task_warp() # 执行服务后,更新日期值 _next_time = datetime.datetime.now() + relativedelta(seconds=delay) _log_next_time = True else: log_next(_log_next_time) _log_next_time = False await asyncio.sleep(0.5) continue _loop = self.event_loop() _tsk: Task = _loop.create_task(task_loop()) return _tsk def create_daily_task(self, fn: Callable = None, run_on_start=False, year: int = None, month: int = None, day: int = None, hour: int = None, minute: int = None, **kwargs): """ 日常任务工厂,每次任务完成后,在第二天的固定时间继续执行。若设置的时间小于当前时间,则自动加一天。 :param fn: 要执行的任务函数 :param run_on_start: 是否在启动时立即运行一次,默认不运行 :param year: 年 :param month: 月 :param day: 日 :param hour: 时 :param minute: 分 :param kwargs fn 函数的参数 """ _now = datetime.datetime.now() year = _now.year if year is None else year month = _now.month if month is None else month day = _now.day if day is None else day hour = _now.hour if hour is None else hour minute = _now.minute if minute is None else minute _next_time = datetime.datetime(year, month, day, hour, minute, 0) if relativedelta(_next_time, datetime.datetime.now()).seconds < 0: # 小于当前时间的,自动加一天 _next_time = _next_time + relativedelta(days=1) def log_next(log_next_time: bool, next_time: datetime.datetime): if log_next_time and self.log_next_time: _delta = relativedelta(next_time, datetime.datetime.now()) _d, _h, _m, _s = _delta.days, _delta.hours, _delta.minutes, _delta.seconds echo_log(f"距下次执行:{fn.__name__} 还有:{_d} 天 {_h} 时 {_m} 分 {_s} 秒.") async def task_warp(): """ 任务包装器。 """ if fn is not None: try: _result = fn(**kwargs) if isinstance(_result, Awaitable): await _result except Exception as e: echo_log(msg=e, level=logging.ERROR, is_log_exc=True) async def task_loop(next_time: datetime.datetime): """ 任务循环。 :param next_time: 下次执行时间 """ if fn is None: return _log_next_time = True _run_on_start = run_on_start while self.is_running: _delta_seconds = relativedelta(datetime.datetime.now(), next_time).seconds if _run_on_start or _delta_seconds > 0: await task_warp() _run_on_start = False # 执行服务后,更新日期值 next_time = next_time + relativedelta(days=1) _log_next_time = True else: log_next(_log_next_time, next_time) _log_next_time = False await asyncio.sleep(0.5) continue _loop = self.event_loop() _tsk: Task = _loop.create_task(task_loop(next_time=_next_time)) return _tsk def create_weekly_task(self, fn: Callable = None, weekday: int = 0, hour: int = 0, minute: int = 0, run_on_start: bool = False, **kwargs): """每周某天固定时间执行(周一=0,周日=6)""" return self.create_periodic_task( fn, PeriodType.WEEKLY, run_on_start=run_on_start, hour=hour, minute=minute, weekday=weekday, **kwargs ) def create_monthly_task(self, fn: Callable = None, day_of_month: int = 1, hour: int = 0, minute: int = 0, run_on_start: bool = False, **kwargs): """每月固定日期执行""" return self.create_periodic_task( fn, PeriodType.MONTHLY, run_on_start=run_on_start, hour=hour, minute=minute, day_of_month=day_of_month, **kwargs ) def create_yearly_task(self, fn: Callable = None, month: int = 1, day_of_month: int = 1, hour: int = 0, minute: int = 0, run_on_start: bool = False, **kwargs): """每年固定日期执行""" return self.create_periodic_task( fn, PeriodType.YEARLY, run_on_start=run_on_start, hour=hour, minute=minute, month=month, day_of_month=day_of_month, **kwargs ) def create_quarterly_task(self, fn: Callable = None, start_month: int = 1, day_of_month: int = 1, hour: int = 0, minute: int = 0, run_on_start: bool = False, **kwargs): """每季度固定日期执行(start_month: 1,4,7,10)""" return self.create_periodic_task( fn, PeriodType.QUARTERLY, run_on_start=run_on_start, hour=hour, minute=minute, month=start_month, day_of_month=day_of_month, **kwargs ) def create_periodic_task(self, fn: Callable = None, period_type: PeriodType = PeriodType.WEEKLY, run_on_start: bool = False, hour: int = 0, minute: int = 0, weekday: Optional[int] = None, # 0=周一, 6=周日,仅 weekly 有效 day_of_month: Optional[int] = None, # 1-31,仅 monthly/quarterly/yearly 有效 month: Optional[int] = None, # 1-12,仅 quarterly/yearly 有效 **kwargs ): """ 通用周期任务工厂。 :param fn: 任务函数 :param period_type: 周期类型 (weekly/monthly/yearly/quarterly) :param run_on_start: 启动时是否立即运行一次 :param hour: 时 (0-23) :param minute: 分 (0-59) :param weekday: 星期几 (0=周一, 6=周日),仅 period_type=weekly 时使用 :param day_of_month: 每月几号 (1-31),仅 monthly/quarterly/yearly 时使用 :param month: 月份 (1-12),仅 quarterly/yearly 时使用(quarterly 时表示起始季度月份) """ def get_next_run_time(now: datetime.datetime) -> Optional[datetime.datetime]: """根据规则计算下一次执行时间""" if period_type == PeriodType.WEEKLY: if weekday is None: raise ValueError("weekly 模式需要指定 weekday") # 计算目标星期 days_ahead = (weekday - now.weekday()) % 7 # 如果今天就是目标星期 if days_ahead == 0: target_time = now.replace(hour=hour, minute=minute, second=0, microsecond=0) # 如果今天的目标时间已过,则推迟到下周 if target_time <= now and not run_on_start: days_ahead = 7 else: return target_time next_date = now + relativedelta(days=days_ahead) return datetime.datetime( next_date.year, next_date.month, next_date.day, hour, minute, 0 ) elif period_type == PeriodType.MONTHLY: if day_of_month is None: raise ValueError("monthly 模式需要指定 day_of_month") # 获取当前月份的最后一天 last_day = (now.replace(day=1) + relativedelta(months=1) - relativedelta(days=1)).day target_day = min(day_of_month, last_day) candidate = now.replace(day=target_day, hour=hour, minute=minute, second=0, microsecond=0) # 如果候选时间已过,则下个月 if candidate <= now and not run_on_start: next_month = now + relativedelta(months=1) last_day_next = (next_month.replace(day=1) + relativedelta(months=1) - relativedelta(days=1)).day target_day_next = min(day_of_month, last_day_next) candidate = next_month.replace(day=target_day_next, hour=hour, minute=minute, second=0, microsecond=0) return candidate elif period_type == PeriodType.QUARTERLY: if day_of_month is None or month is None: raise ValueError("quarterly 模式需要指定 day_of_month 和 month(起始季度月份)") # 修正:计算季度的月份 q_months = [] for i in range(4): qm = month + i * 3 if qm > 12: qm -= 12 q_months.append(qm) # 查找下一个季度月 target_month = None target_year = now.year for qm in sorted(q_months): if qm > now.month: target_month = qm break if target_month is None: target_month = q_months[0] target_year += 1 # 处理日期有效性 last_day = (datetime.datetime(target_year, target_month, 1) + relativedelta(months=1) - relativedelta(days=1)).day target_day = min(day_of_month, last_day) candidate = datetime.datetime(target_year, target_month, target_day, hour, minute, 0) if candidate <= now and not run_on_start: # 跳到下个季度 candidate = candidate + relativedelta(months=3) return candidate elif period_type == PeriodType.YEARLY: if day_of_month is None or month is None: raise ValueError("yearly 模式需要指定 day_of_month 和 month") # 检查年份 try: candidate = datetime.datetime(now.year, month, day_of_month, hour, minute, 0) except ValueError: # 日期无效(如2月30日),取当月最后一天 last_day = (datetime.datetime(now.year, month, 1) + relativedelta(months=1) - relativedelta(days=1)).day candidate = datetime.datetime(now.year, month, last_day, hour, minute, 0) if candidate <= now and not run_on_start: try: candidate = datetime.datetime(now.year + 1, month, day_of_month, hour, minute, 0) except ValueError: last_day = (datetime.datetime(now.year + 1, month, 1) + relativedelta(months=1) - relativedelta(days=1)).day candidate = datetime.datetime(now.year + 1, month, last_day, hour, minute, 0) return candidate async def task_warp(): if fn is not None: try: _result = fn(**kwargs) if isinstance(_result, Awaitable): await _result except Exception as e: echo_log(msg=e, level=logging.ERROR, is_log_exc=True) async def task_loop(): if fn is None: return _log_next_time = True _run_on_start = run_on_start next_time = get_next_run_time(datetime.datetime.now()) while self.is_running: now = datetime.datetime.now() if _run_on_start or now >= next_time: await task_warp() _run_on_start = False # 执行完后,基于当前时间重新计算下一次 next_time = get_next_run_time(now) _log_next_time = True else: if _log_next_time and self.log_next_time: delta = relativedelta(next_time, now) echo_log( f"距下次执行:{fn.__name__} 还有:{delta.days}天 {delta.hours}时 {delta.minutes}分 {delta.seconds}秒") _log_next_time = False await asyncio.sleep(1) _loop = self.event_loop() return _loop.create_task(task_loop()) def add_task(self, creator: Callable, fn: Callable, **kwargs): """ 添加任务。注意:这里只是存储创建参数,直到任务启动前,才实际把任务创建出来。 :param creator: 任务工厂,对应:应延时任务工厂、日常任务工厂 :param fn: 任务函数,即任务对应的实际功能函数 :param kwargs: 任务函数的参数 """ _d = { 'creator': creator, # 创建器,对应延时任务和日常任务 'fn': fn, 'kwargs': kwargs # 任务函数及其参数 } self._create_task_params.append(_d) def rebuild_task_list(self): """ 重建任务列表。 :return: 任务列表 """ self.task_list.clear() # 遍历创建器列表,创建任务 for _ctp in self._create_task_params: _creator: Callable = _ctp.get('creator') _fn: Callable = _ctp.get('fn') _kwargs: dict = _ctp.get('kwargs') _task = _creator(_fn, **_kwargs) self.task_list.append(_task) return self.task_list async def run_tasks(self): """ 执行任务。支持多任务同时启动,如:预统计服务、设备数据同步服务等。 """ try: self.rebuild_task_list() echo_log(f'{self.service_name}启动成功.') await asyncio.gather(*self.task_list) except Exception as e: echo_log(msg=e, level=logging.ERROR, is_log_exc=True) def start_service(self, env_check: bool = True): """ 以控制台服务方式启动服务。 """ echo_log(f'正在启动{self.service_name}...') try: if env_check: # 检测 MySQL 服务是否正常 echo_log('检测 Database 服务...') BaseAdapter.ping() # 注意,这里是取得协程 _future = self.run_tasks() # 开始执行任务事件循环 _loop = self.event_loop() _loop.run_until_complete(_future) except KeyboardInterrupt: echo_log(msg='KeyboardInterrupt') self.stop_service() except Exception as e: echo_log(msg=e, level=logging.ERROR, is_log_exc=True) def stop_service(self): """ 停止服务。 """ self.is_running = False echo_log(f'{self.service_name}已停止.') def start(self, env_check: bool = True): """ 以驻内存服务方式启动服务。 """ ds = DaemonizeService(pid_file=self.pid_file, name=f'{self.service_name}') ds.set_start_callback(self.start_service, env_check=env_check) ds.set_term_callback(self.stop_service) ds.start() def stop(self, env_check: bool = True): """ 停止驻内存服务。 """ ds = DaemonizeService(pid_file=self.pid_file, name=f'{self.service_name}') ds.set_start_callback(self.start_service, env_check=env_check) ds.set_term_callback(self.stop_service) ds.stop()