Merge commit '47296980495f8bbfc9493e93de85dd62de6fa6b9' as 'paste-framework'

This commit is contained in:
zwf
2026-06-02 19:09:22 +08:00
107 changed files with 21484 additions and 0 deletions
@@ -0,0 +1,496 @@
"""
系统服务,用于读取服务配置文件,启动或停止相关的服务。
"""
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()