You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					276 lines
				
				10 KiB
			
		
		
			
		
	
	
					276 lines
				
				10 KiB
			| 
											1 year ago
										 | import json | ||
| 
											1 year ago
										 | from apscheduler.events import EVENT_ALL | ||
| 
											12 months ago
										 | from apscheduler.executors.asyncio import AsyncIOExecutor | ||
| 
											12 months ago
										 | from apscheduler.executors.pool import ProcessPoolExecutor | ||
| 
											2 years ago
										 | from apscheduler.jobstores.memory import MemoryJobStore | ||
|  | from apscheduler.jobstores.redis import RedisJobStore | ||
| 
											1 year ago
										 | from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore | ||
| 
											12 months ago
										 | from apscheduler.schedulers.asyncio import AsyncIOScheduler | ||
| 
											2 years ago
										 | from apscheduler.triggers.cron import CronTrigger | ||
| 
											12 months ago
										 | from asyncio import iscoroutinefunction | ||
| 
											1 year ago
										 | from datetime import datetime, timedelta | ||
| 
											1 year ago
										 | from sqlalchemy.engine import create_engine | ||
|  | from sqlalchemy.orm import sessionmaker | ||
| 
											1 year ago
										 | from typing import Union | ||
| 
											1 year ago
										 | from config.database import AsyncSessionLocal, quote_plus | ||
| 
											1 year ago
										 | from config.env import DataBaseConfig, RedisConfig | ||
|  | from module_admin.dao.job_dao import JobDao | ||
| 
											1 year ago
										 | from module_admin.entity.vo.job_vo import JobLogModel, JobModel | ||
| 
											1 year ago
										 | from module_admin.service.job_log_service import JobLogService | ||
| 
											2 years ago
										 | from utils.log_util import logger | ||
| 
											1 year ago
										 | import module_task  # noqa: F401 | ||
| 
											2 years ago
										 | 
 | ||
|  | 
 | ||
|  | # 重写Cron定时 | ||
|  | class MyCronTrigger(CronTrigger): | ||
|  |     @classmethod | ||
| 
											1 year ago
										 |     def from_crontab(cls, expr: str, timezone=None): | ||
| 
											2 years ago
										 |         values = expr.split() | ||
|  |         if len(values) != 6 and len(values) != 7: | ||
|  |             raise ValueError('Wrong number of fields; got {}, expected 6 or 7'.format(len(values))) | ||
|  | 
 | ||
|  |         second = values[0] | ||
|  |         minute = values[1] | ||
|  |         hour = values[2] | ||
|  |         if '?' in values[3]: | ||
|  |             day = None | ||
|  |         elif 'L' in values[5]: | ||
|  |             day = f"last {values[5].replace('L', '')}" | ||
|  |         elif 'W' in values[3]: | ||
|  |             day = cls.__find_recent_workday(int(values[3].split('W')[0])) | ||
|  |         else: | ||
|  |             day = values[3].replace('L', 'last') | ||
|  |         month = values[4] | ||
|  |         if '?' in values[5] or 'L' in values[5]: | ||
|  |             week = None | ||
|  |         elif '#' in values[5]: | ||
|  |             week = int(values[5].split('#')[1]) | ||
|  |         else: | ||
|  |             week = values[5] | ||
|  |         if '#' in values[5]: | ||
|  |             day_of_week = int(values[5].split('#')[0]) - 1 | ||
|  |         else: | ||
|  |             day_of_week = None | ||
|  |         year = values[6] if len(values) == 7 else None | ||
| 
											1 year ago
										 |         return cls( | ||
|  |             second=second, | ||
|  |             minute=minute, | ||
|  |             hour=hour, | ||
|  |             day=day, | ||
|  |             month=month, | ||
|  |             week=week, | ||
|  |             day_of_week=day_of_week, | ||
|  |             year=year, | ||
|  |             timezone=timezone, | ||
|  |         ) | ||
| 
											2 years ago
										 | 
 | ||
|  |     @classmethod | ||
| 
											1 year ago
										 |     def __find_recent_workday(cls, day: int): | ||
| 
											2 years ago
										 |         now = datetime.now() | ||
|  |         date = datetime(now.year, now.month, day) | ||
|  |         if date.weekday() < 5: | ||
|  |             return date.day | ||
|  |         else: | ||
|  |             diff = 1 | ||
|  |             while True: | ||
|  |                 previous_day = date - timedelta(days=diff) | ||
|  |                 if previous_day.weekday() < 5: | ||
|  |                     return previous_day.day | ||
|  |                 else: | ||
|  |                     diff += 1 | ||
|  | 
 | ||
|  | 
 | ||
| 
											1 year ago
										 | SQLALCHEMY_DATABASE_URL = ( | ||
|  |     f'mysql+pymysql://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@' | ||
|  |     f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}' | ||
|  | ) | ||
| 
											1 year ago
										 | if DataBaseConfig.db_type == 'postgresql': | ||
|  |     SQLALCHEMY_DATABASE_URL = ( | ||
|  |         f'postgresql+psycopg2://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@' | ||
|  |         f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}' | ||
|  |     ) | ||
| 
											1 year ago
										 | engine = create_engine( | ||
|  |     SQLALCHEMY_DATABASE_URL, | ||
|  |     echo=DataBaseConfig.db_echo, | ||
|  |     max_overflow=DataBaseConfig.db_max_overflow, | ||
|  |     pool_size=DataBaseConfig.db_pool_size, | ||
|  |     pool_recycle=DataBaseConfig.db_pool_recycle, | ||
| 
											1 year ago
										 |     pool_timeout=DataBaseConfig.db_pool_timeout, | ||
| 
											1 year ago
										 | ) | ||
|  | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | ||
| 
											2 years ago
										 | job_stores = { | ||
|  |     'default': MemoryJobStore(), | ||
|  |     'sqlalchemy': SQLAlchemyJobStore(url=SQLALCHEMY_DATABASE_URL, engine=engine), | ||
|  |     'redis': RedisJobStore( | ||
|  |         **dict( | ||
| 
											2 years ago
										 |             host=RedisConfig.redis_host, | ||
|  |             port=RedisConfig.redis_port, | ||
|  |             username=RedisConfig.redis_username, | ||
|  |             password=RedisConfig.redis_password, | ||
| 
											1 year ago
										 |             db=RedisConfig.redis_database, | ||
| 
											2 years ago
										 |         ) | ||
| 
											1 year ago
										 |     ), | ||
| 
											2 years ago
										 | } | ||
| 
											12 months ago
										 | executors = {'default': AsyncIOExecutor(), 'processpool': ProcessPoolExecutor(5)} | ||
| 
											1 year ago
										 | job_defaults = {'coalesce': False, 'max_instance': 1} | ||
| 
											12 months ago
										 | scheduler = AsyncIOScheduler() | ||
| 
											2 years ago
										 | scheduler.configure(jobstores=job_stores, executors=executors, job_defaults=job_defaults) | ||
|  | 
 | ||
|  | 
 | ||
|  | class SchedulerUtil: | ||
|  |     """
 | ||
|  |     定时任务相关方法 | ||
|  |     """
 | ||
|  | 
 | ||
|  |     @classmethod | ||
| 
											1 year ago
										 |     async def init_system_scheduler(cls): | ||
| 
											2 years ago
										 |         """
 | ||
|  |         应用启动时初始化定时任务 | ||
| 
											1 year ago
										 | 
 | ||
| 
											2 years ago
										 |         :return: | ||
|  |         """
 | ||
| 
											1 year ago
										 |         logger.info('开始启动定时任务...') | ||
| 
											2 years ago
										 |         scheduler.start() | ||
| 
											1 year ago
										 |         async with AsyncSessionLocal() as session: | ||
|  |             job_list = await JobDao.get_job_list_for_scheduler(session) | ||
|  |             for item in job_list: | ||
| 
											12 months ago
										 |                 cls.remove_scheduler_job(job_id=str(item.job_id)) | ||
| 
											1 year ago
										 |                 cls.add_scheduler_job(item) | ||
| 
											2 years ago
										 |         scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL) | ||
| 
											1 year ago
										 |         logger.info('系统初始定时任务加载成功') | ||
| 
											2 years ago
										 | 
 | ||
|  |     @classmethod | ||
|  |     async def close_system_scheduler(cls): | ||
|  |         """
 | ||
|  |         应用关闭时关闭定时任务 | ||
| 
											1 year ago
										 | 
 | ||
| 
											2 years ago
										 |         :return: | ||
|  |         """
 | ||
|  |         scheduler.shutdown() | ||
| 
											1 year ago
										 |         logger.info('关闭定时任务成功') | ||
| 
											2 years ago
										 | 
 | ||
|  |     @classmethod | ||
| 
											1 year ago
										 |     def get_scheduler_job(cls, job_id: Union[str, int]): | ||
| 
											2 years ago
										 |         """
 | ||
|  |         根据任务id获取任务对象 | ||
| 
											1 year ago
										 | 
 | ||
| 
											2 years ago
										 |         :param job_id: 任务id | ||
|  |         :return: 任务对象 | ||
|  |         """
 | ||
| 
											12 months ago
										 |         query_job = scheduler.get_job(job_id=str(job_id)) | ||
| 
											2 years ago
										 | 
 | ||
|  |         return query_job | ||
|  | 
 | ||
|  |     @classmethod | ||
| 
											1 year ago
										 |     def add_scheduler_job(cls, job_info: JobModel): | ||
| 
											2 years ago
										 |         """
 | ||
|  |         根据输入的任务对象信息添加任务 | ||
| 
											1 year ago
										 | 
 | ||
| 
											2 years ago
										 |         :param job_info: 任务对象信息 | ||
|  |         :return: | ||
|  |         """
 | ||
| 
											12 months ago
										 |         job_func = eval(job_info.invoke_target) | ||
| 
											12 months ago
										 |         job_executor = job_info.job_executor | ||
|  |         if iscoroutinefunction(job_func): | ||
|  |             job_executor = 'default' | ||
|  |         scheduler.add_job( | ||
|  |             func=eval(job_info.invoke_target), | ||
| 
											2 years ago
										 |             trigger=MyCronTrigger.from_crontab(job_info.cron_expression), | ||
|  |             args=job_info.job_args.split(',') if job_info.job_args else None, | ||
|  |             kwargs=json.loads(job_info.job_kwargs) if job_info.job_kwargs else None, | ||
|  |             id=str(job_info.job_id), | ||
|  |             name=job_info.job_name, | ||
|  |             misfire_grace_time=1000000000000 if job_info.misfire_policy == '3' else None, | ||
|  |             coalesce=True if job_info.misfire_policy == '2' else False, | ||
|  |             max_instances=3 if job_info.concurrent == '0' else 1, | ||
|  |             jobstore=job_info.job_group, | ||
| 
											12 months ago
										 |             executor=job_executor, | ||
| 
											2 years ago
										 |         ) | ||
|  | 
 | ||
|  |     @classmethod | ||
| 
											1 year ago
										 |     def execute_scheduler_job_once(cls, job_info: JobModel): | ||
| 
											2 years ago
										 |         """
 | ||
|  |         根据输入的任务对象执行一次任务 | ||
| 
											1 year ago
										 | 
 | ||
| 
											2 years ago
										 |         :param job_info: 任务对象信息 | ||
|  |         :return: | ||
|  |         """
 | ||
| 
											12 months ago
										 |         job_func = eval(job_info.invoke_target) | ||
| 
											12 months ago
										 |         job_executor = job_info.job_executor | ||
|  |         if iscoroutinefunction(job_func): | ||
|  |             job_executor = 'default' | ||
|  |         scheduler.add_job( | ||
|  |             func=eval(job_info.invoke_target), | ||
| 
											2 years ago
										 |             trigger='date', | ||
|  |             run_date=datetime.now() + timedelta(seconds=1), | ||
|  |             args=job_info.job_args.split(',') if job_info.job_args else None, | ||
|  |             kwargs=json.loads(job_info.job_kwargs) if job_info.job_kwargs else None, | ||
|  |             id=str(job_info.job_id), | ||
|  |             name=job_info.job_name, | ||
|  |             misfire_grace_time=1000000000000 if job_info.misfire_policy == '3' else None, | ||
|  |             coalesce=True if job_info.misfire_policy == '2' else False, | ||
|  |             max_instances=3 if job_info.concurrent == '0' else 1, | ||
|  |             jobstore=job_info.job_group, | ||
| 
											12 months ago
										 |             executor=job_executor, | ||
| 
											2 years ago
										 |         ) | ||
|  | 
 | ||
|  |     @classmethod | ||
| 
											1 year ago
										 |     def remove_scheduler_job(cls, job_id: Union[str, int]): | ||
| 
											2 years ago
										 |         """
 | ||
|  |         根据任务id移除任务 | ||
| 
											1 year ago
										 | 
 | ||
| 
											2 years ago
										 |         :param job_id: 任务id | ||
|  |         :return: | ||
|  |         """
 | ||
| 
											12 months ago
										 |         query_job = cls.get_scheduler_job(job_id=job_id) | ||
|  |         if query_job: | ||
| 
											12 months ago
										 |             scheduler.remove_job(job_id=str(job_id)) | ||
| 
											2 years ago
										 | 
 | ||
|  |     @classmethod | ||
|  |     def scheduler_event_listener(cls, event): | ||
|  |         # 获取事件类型和任务ID | ||
|  |         event_type = event.__class__.__name__ | ||
|  |         # 获取任务执行异常信息 | ||
|  |         status = '0' | ||
|  |         exception_info = '' | ||
|  |         if event_type == 'JobExecutionEvent' and event.exception: | ||
|  |             exception_info = str(event.exception) | ||
|  |             status = '1' | ||
| 
											1 year ago
										 |         if hasattr(event, 'job_id'): | ||
|  |             job_id = event.job_id | ||
|  |             query_job = cls.get_scheduler_job(job_id=job_id) | ||
|  |             if query_job: | ||
|  |                 query_job_info = query_job.__getstate__() | ||
|  |                 # 获取任务名称 | ||
|  |                 job_name = query_job_info.get('name') | ||
|  |                 # 获取任务组名 | ||
|  |                 job_group = query_job._jobstore_alias | ||
|  |                 # 获取任务执行器 | ||
|  |                 job_executor = query_job_info.get('executor') | ||
|  |                 # 获取调用目标字符串 | ||
|  |                 invoke_target = query_job_info.get('func') | ||
|  |                 # 获取调用函数位置参数 | ||
|  |                 job_args = ','.join(query_job_info.get('args')) | ||
|  |                 # 获取调用函数关键字参数 | ||
|  |                 job_kwargs = json.dumps(query_job_info.get('kwargs')) | ||
|  |                 # 获取任务触发器 | ||
|  |                 job_trigger = str(query_job_info.get('trigger')) | ||
|  |                 # 构造日志消息 | ||
|  |                 job_message = f"事件类型: {event_type}, 任务ID: {job_id}, 任务名称: {job_name}, 执行于{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" | ||
|  |                 job_log = JobLogModel( | ||
|  |                     jobName=job_name, | ||
|  |                     jobGroup=job_group, | ||
|  |                     jobExecutor=job_executor, | ||
|  |                     invokeTarget=invoke_target, | ||
|  |                     jobArgs=job_args, | ||
|  |                     jobKwargs=job_kwargs, | ||
|  |                     jobTrigger=job_trigger, | ||
|  |                     jobMessage=job_message, | ||
|  |                     status=status, | ||
|  |                     exceptionInfo=exception_info, | ||
|  |                     createTime=datetime.now(), | ||
|  |                 ) | ||
|  |                 session = SessionLocal() | ||
|  |                 JobLogService.add_job_log_services(session, job_log) | ||
|  |                 session.close() |