首次提交
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user