kimi-v1
This commit is contained in:
parent
2579ce9f18
commit
f5cd4aa34c
12 changed files with 1648 additions and 0 deletions
106
examples/scheduler_example.py
Normal file
106
examples/scheduler_example.py
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
"""
|
||||||
|
Flask Scheduler 使用示例
|
||||||
|
"""
|
||||||
|
from datetime import timedelta, datetime
|
||||||
|
from flask import Flask, jsonify
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 创建Flask应用
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# 配置调度器
|
||||||
|
app.config.update({
|
||||||
|
'SCHEDULER_ENABLED': True, # 启用调度器
|
||||||
|
'SCHEDULER_AUTOSTART': True, # 自动启动调度器
|
||||||
|
'SCHEDULER_TICK_INTERVAL': 1.0, # 检查间隔(秒)
|
||||||
|
'SCHEDULER_MAX_WORKERS': 4, # 最大工作线程数
|
||||||
|
'SCHEDULER_STORAGE_PATH': 'scheduler_data.json' # 存储路径
|
||||||
|
})
|
||||||
|
|
||||||
|
# 初始化调度器
|
||||||
|
from flask.scheduler import Scheduler
|
||||||
|
scheduler = Scheduler(app)
|
||||||
|
|
||||||
|
|
||||||
|
# 定义任务
|
||||||
|
from flask.scheduler.decorators import interval_task, delay_task, cron_task
|
||||||
|
|
||||||
|
@interval_task(interval=timedelta(seconds=10), description="每10秒执行一次的示例任务")
|
||||||
|
def my_interval_task():
|
||||||
|
"""每10秒执行一次的示例任务"""
|
||||||
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
logger.info(f"[间隔任务] 执行时间: {current_time}")
|
||||||
|
return f"间隔任务执行成功: {current_time}"
|
||||||
|
|
||||||
|
|
||||||
|
@delay_task(delay=timedelta(seconds=5), description="延迟5秒后执行的示例任务")
|
||||||
|
def my_delay_task():
|
||||||
|
"""延迟5秒后执行的示例任务"""
|
||||||
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
logger.info(f"[延迟任务] 执行时间: {current_time}")
|
||||||
|
return f"延迟任务执行成功: {current_time}"
|
||||||
|
|
||||||
|
|
||||||
|
@cron_task(cron_expression="*/2 * * * *", description="每2分钟执行一次的cron任务")
|
||||||
|
def my_cron_task():
|
||||||
|
"""每2分钟执行一次的cron任务"""
|
||||||
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
logger.info(f"[Cron任务] 执行时间: {current_time}")
|
||||||
|
return f"Cron任务执行成功: {current_time}"
|
||||||
|
|
||||||
|
|
||||||
|
# 创建示例路由
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
"""首页"""
|
||||||
|
return jsonify({
|
||||||
|
'message': 'Flask Scheduler 示例应用',
|
||||||
|
'scheduler_running': scheduler.is_running(),
|
||||||
|
'endpoints': {
|
||||||
|
'metrics': '/_internal/metrics',
|
||||||
|
'tasks': '/_internal/tasks',
|
||||||
|
'scheduler_status': '/_internal/scheduler/status',
|
||||||
|
'health': '/_internal/health'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/run-task/<task_name>', methods=['POST'])
|
||||||
|
def run_task(task_name):
|
||||||
|
"""手动运行任务"""
|
||||||
|
success = scheduler.run_task(task_name)
|
||||||
|
if success:
|
||||||
|
return jsonify({'status': 'success', 'message': f'Task {task_name} started'})
|
||||||
|
else:
|
||||||
|
return jsonify({'status': 'error', 'message': f'Failed to start task {task_name}'}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/scheduler-info')
|
||||||
|
def scheduler_info():
|
||||||
|
"""获取调度器信息"""
|
||||||
|
tasks = scheduler.get_all_tasks()
|
||||||
|
metrics = scheduler.get_metrics()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'scheduler_running': scheduler.is_running(),
|
||||||
|
'total_tasks': len(tasks),
|
||||||
|
'tasks': [task.name for task in tasks],
|
||||||
|
'metrics': metrics
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# 注册管理蓝图
|
||||||
|
from flask.scheduler.blueprint import create_scheduler_blueprint
|
||||||
|
app.register_blueprint(create_scheduler_blueprint(scheduler, name='scheduler_admin'))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 导入示例任务(确保它们被注册)
|
||||||
|
from flask.scheduler import examples # 这会触发任务注册
|
||||||
|
|
||||||
|
logger.info("启动Flask应用和调度器...")
|
||||||
|
app.run(debug=True, port=5000)
|
||||||
74
scheduler_data.json
Normal file
74
scheduler_data.json
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"my_interval_task": {
|
||||||
|
"name": "my_interval_task",
|
||||||
|
"task_type": "interval",
|
||||||
|
"description": "每10秒执行一次的示例任务",
|
||||||
|
"enabled": true,
|
||||||
|
"max_retries": 0,
|
||||||
|
"status": "running",
|
||||||
|
"next_run_at": "2025-11-29T15:41:23.195414",
|
||||||
|
"last_run_at": "2025-11-29T15:41:13.195410",
|
||||||
|
"current_run_id": "2b8af199-73c5-491a-a8b2-a945e35687d5",
|
||||||
|
"last_error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"metrics": {
|
||||||
|
"total_runs": 38,
|
||||||
|
"successful_runs": 38,
|
||||||
|
"failed_runs": 0,
|
||||||
|
"last_run_at": "2025-11-29T15:41:13.201351",
|
||||||
|
"last_success_at": "2025-11-29T15:41:13.201355",
|
||||||
|
"last_failure_at": null,
|
||||||
|
"average_duration": 0.016269001867437103,
|
||||||
|
"last_error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"my_delay_task": {
|
||||||
|
"name": "my_delay_task",
|
||||||
|
"task_type": "delay",
|
||||||
|
"description": "延迟5秒后执行的示例任务",
|
||||||
|
"enabled": false,
|
||||||
|
"max_retries": 0,
|
||||||
|
"status": "success",
|
||||||
|
"next_run_at": null,
|
||||||
|
"last_run_at": "2025-11-29T15:35:06.924327",
|
||||||
|
"current_run_id": null,
|
||||||
|
"last_error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"metrics": {
|
||||||
|
"total_runs": 1,
|
||||||
|
"successful_runs": 1,
|
||||||
|
"failed_runs": 0,
|
||||||
|
"last_run_at": "2025-11-29T15:35:06.926838",
|
||||||
|
"last_success_at": "2025-11-29T15:35:06.926846",
|
||||||
|
"last_failure_at": null,
|
||||||
|
"average_duration": 0.001039,
|
||||||
|
"last_error": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"my_cron_task": {
|
||||||
|
"name": "my_cron_task",
|
||||||
|
"task_type": "cron",
|
||||||
|
"description": "每2分钟执行一次的cron任务",
|
||||||
|
"enabled": true,
|
||||||
|
"max_retries": 0,
|
||||||
|
"status": "success",
|
||||||
|
"next_run_at": "2025-11-29T15:42:00",
|
||||||
|
"last_run_at": "2025-11-29T15:40:59.069031",
|
||||||
|
"current_run_id": null,
|
||||||
|
"last_error": null,
|
||||||
|
"retry_count": 0,
|
||||||
|
"metrics": {
|
||||||
|
"total_runs": 176,
|
||||||
|
"successful_runs": 176,
|
||||||
|
"failed_runs": 0,
|
||||||
|
"last_run_at": "2025-11-29T15:40:59.070000",
|
||||||
|
"last_success_at": "2025-11-29T15:40:59.070004",
|
||||||
|
"last_failure_at": null,
|
||||||
|
"average_duration": 0.00045138152762598126,
|
||||||
|
"last_error": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"last_updated": "2025-11-29T15:41:13.204217"
|
||||||
|
}
|
||||||
|
|
@ -37,3 +37,20 @@ from .templating import stream_template as stream_template
|
||||||
from .templating import stream_template_string as stream_template_string
|
from .templating import stream_template_string as stream_template_string
|
||||||
from .wrappers import Request as Request
|
from .wrappers import Request as Request
|
||||||
from .wrappers import Response as Response
|
from .wrappers import Response as Response
|
||||||
|
|
||||||
|
# Flask Scheduler Extension
|
||||||
|
try:
|
||||||
|
from .scheduler import Scheduler as Scheduler
|
||||||
|
from .scheduler import Task as Task
|
||||||
|
from .scheduler import TaskStatus as TaskStatus
|
||||||
|
from .scheduler import TaskType as TaskType
|
||||||
|
from .scheduler import interval_task as interval_task
|
||||||
|
from .scheduler import delay_task as delay_task
|
||||||
|
from .scheduler import cron_task as cron_task
|
||||||
|
from .scheduler import TaskStorage as TaskStorage
|
||||||
|
from .scheduler import SchedulerError as SchedulerError
|
||||||
|
from .scheduler import TaskError as TaskError
|
||||||
|
from .scheduler import CronParseError as CronParseError
|
||||||
|
except ImportError:
|
||||||
|
# 如果scheduler模块不可用,静默处理
|
||||||
|
pass
|
||||||
|
|
|
||||||
30
src/flask/scheduler/__init__.py
Normal file
30
src/flask/scheduler/__init__.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""
|
||||||
|
Flask Scheduler Extension
|
||||||
|
|
||||||
|
A comprehensive task scheduling extension for Flask applications with support for:
|
||||||
|
- Interval tasks
|
||||||
|
- Delayed tasks
|
||||||
|
- Cron tasks
|
||||||
|
- Task management and monitoring
|
||||||
|
- Metrics collection
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .scheduler import Scheduler
|
||||||
|
from .tasks import Task, TaskStatus, TaskType
|
||||||
|
from .storage import TaskStorage
|
||||||
|
from .decorators import interval_task, delay_task, cron_task
|
||||||
|
from .exceptions import SchedulerError, TaskError, CronParseError
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Scheduler',
|
||||||
|
'Task',
|
||||||
|
'TaskStatus',
|
||||||
|
'TaskType',
|
||||||
|
'TaskStorage',
|
||||||
|
'interval_task',
|
||||||
|
'delay_task',
|
||||||
|
'cron_task',
|
||||||
|
'SchedulerError',
|
||||||
|
'TaskError',
|
||||||
|
'CronParseError'
|
||||||
|
]
|
||||||
288
src/flask/scheduler/blueprint.py
Normal file
288
src/flask/scheduler/blueprint.py
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
"""
|
||||||
|
Scheduler management blueprint
|
||||||
|
"""
|
||||||
|
from flask import Blueprint, jsonify, request, current_app
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
def create_scheduler_blueprint(scheduler, name='scheduler'):
|
||||||
|
"""创建调度器管理蓝图"""
|
||||||
|
bp = Blueprint(name, __name__, url_prefix='/_internal')
|
||||||
|
|
||||||
|
@bp.route('/metrics', methods=['GET'])
|
||||||
|
def get_metrics():
|
||||||
|
"""获取调度器指标"""
|
||||||
|
try:
|
||||||
|
metrics = scheduler.get_metrics()
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'data': metrics
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/tasks', methods=['GET'])
|
||||||
|
def list_tasks():
|
||||||
|
"""获取所有任务列表"""
|
||||||
|
try:
|
||||||
|
tasks = scheduler.get_all_tasks()
|
||||||
|
tasks_data = []
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
task_data = task.to_dict()
|
||||||
|
# 添加运行状态
|
||||||
|
task_data['is_running'] = task.status.value == 'running'
|
||||||
|
task_data['scheduler_status'] = 'running' if scheduler.is_running() else 'stopped'
|
||||||
|
tasks_data.append(task_data)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'data': {
|
||||||
|
'tasks': tasks_data,
|
||||||
|
'scheduler_running': scheduler.is_running(),
|
||||||
|
'total_tasks': len(tasks_data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/tasks/<task_name>', methods=['GET'])
|
||||||
|
def get_task(task_name: str):
|
||||||
|
"""获取特定任务详情"""
|
||||||
|
try:
|
||||||
|
task = scheduler.get_task(task_name)
|
||||||
|
if not task:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': f'Task "{task_name}" not found'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
task_data = task.to_dict()
|
||||||
|
task_data['is_running'] = task.status.value == 'running'
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'data': task_data
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/tasks/<task_name>/run', methods=['POST'])
|
||||||
|
def run_task(task_name: str):
|
||||||
|
"""手动运行任务"""
|
||||||
|
try:
|
||||||
|
success = scheduler.run_task(task_name)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'message': f'Task "{task_name}" started successfully'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': f'Failed to start task "{task_name}". Task may be disabled, already running, or not found.'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/tasks/<task_name>/enable', methods=['POST'])
|
||||||
|
def enable_task(task_name: str):
|
||||||
|
"""启用任务"""
|
||||||
|
try:
|
||||||
|
task = scheduler.get_task(task_name)
|
||||||
|
if not task:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': f'Task "{task_name}" not found'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
task.enabled = True
|
||||||
|
scheduler.storage.add_task(task)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'message': f'Task "{task_name}" enabled successfully'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/tasks/<task_name>/disable', methods=['POST'])
|
||||||
|
def disable_task(task_name: str):
|
||||||
|
"""禁用任务"""
|
||||||
|
try:
|
||||||
|
task = scheduler.get_task(task_name)
|
||||||
|
if not task:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': f'Task "{task_name}" not found'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
task.enabled = False
|
||||||
|
scheduler.storage.add_task(task)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'message': f'Task "{task_name}" disabled successfully'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/scheduler/start', methods=['POST'])
|
||||||
|
def start_scheduler():
|
||||||
|
"""启动调度器"""
|
||||||
|
try:
|
||||||
|
if scheduler.is_running():
|
||||||
|
return jsonify({
|
||||||
|
'status': 'warning',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'message': 'Scheduler is already running'
|
||||||
|
})
|
||||||
|
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'message': 'Scheduler started successfully'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/scheduler/stop', methods=['POST'])
|
||||||
|
def stop_scheduler():
|
||||||
|
"""停止调度器"""
|
||||||
|
try:
|
||||||
|
if not scheduler.is_running():
|
||||||
|
return jsonify({
|
||||||
|
'status': 'warning',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'message': 'Scheduler is not running'
|
||||||
|
})
|
||||||
|
|
||||||
|
scheduler.stop()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'message': 'Scheduler stopped successfully'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/scheduler/status', methods=['GET'])
|
||||||
|
def get_scheduler_status():
|
||||||
|
"""获取调度器状态"""
|
||||||
|
try:
|
||||||
|
is_running = scheduler.is_running()
|
||||||
|
tasks = scheduler.get_all_tasks()
|
||||||
|
|
||||||
|
running_tasks = sum(1 for task in tasks if task.status.value == 'running')
|
||||||
|
enabled_tasks = sum(1 for task in tasks if task.enabled)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'data': {
|
||||||
|
'scheduler_running': is_running,
|
||||||
|
'total_tasks': len(tasks),
|
||||||
|
'enabled_tasks': enabled_tasks,
|
||||||
|
'disabled_tasks': len(tasks) - enabled_tasks,
|
||||||
|
'running_tasks': running_tasks,
|
||||||
|
'tick_interval': scheduler._tick_interval,
|
||||||
|
'max_workers': scheduler._max_workers
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/reload', methods=['POST'])
|
||||||
|
def reload_scheduler():
|
||||||
|
"""重新加载调度器配置"""
|
||||||
|
try:
|
||||||
|
scheduler.reload()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'message': 'Scheduler configuration reloaded successfully'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/health', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
"""健康检查"""
|
||||||
|
try:
|
||||||
|
is_running = scheduler.is_running()
|
||||||
|
tasks = scheduler.get_all_tasks()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'healthy' if is_running else 'degraded',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'data': {
|
||||||
|
'scheduler_running': is_running,
|
||||||
|
'total_tasks': len(tasks),
|
||||||
|
'enabled_tasks': sum(1 for task in tasks if task.enabled)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'unhealthy',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'error': str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
return bp
|
||||||
160
src/flask/scheduler/cron.py
Normal file
160
src/flask/scheduler/cron.py
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
"""
|
||||||
|
Cron expression parser for scheduling tasks
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Set
|
||||||
|
from .exceptions import CronParseError
|
||||||
|
|
||||||
|
|
||||||
|
class CronParser:
|
||||||
|
"""Parse and validate cron expressions"""
|
||||||
|
|
||||||
|
# Cron字段: 分 时 日 月 周
|
||||||
|
FIELD_NAMES = ['minute', 'hour', 'day', 'month', 'weekday']
|
||||||
|
FIELD_RANGES = {
|
||||||
|
'minute': (0, 59),
|
||||||
|
'hour': (0, 23),
|
||||||
|
'day': (1, 31),
|
||||||
|
'month': (1, 12),
|
||||||
|
'weekday': (0, 6) # 0=Monday, 6=Sunday
|
||||||
|
}
|
||||||
|
|
||||||
|
MONTH_NAMES = {
|
||||||
|
'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
|
||||||
|
'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12
|
||||||
|
}
|
||||||
|
|
||||||
|
WEEKDAY_NAMES = {
|
||||||
|
'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3,
|
||||||
|
'fri': 4, 'sat': 5, 'sun': 6
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, expression: str):
|
||||||
|
"""Initialize cron parser with expression"""
|
||||||
|
self.expression = expression.strip()
|
||||||
|
self.fields = {}
|
||||||
|
self._parse()
|
||||||
|
|
||||||
|
def _parse(self):
|
||||||
|
"""Parse cron expression"""
|
||||||
|
parts = self.expression.split()
|
||||||
|
|
||||||
|
if len(parts) != 5:
|
||||||
|
raise CronParseError(f"Invalid cron expression '{self.expression}': expected 5 fields, got {len(parts)}")
|
||||||
|
|
||||||
|
for i, (field_name, part) in enumerate(zip(self.FIELD_NAMES, parts)):
|
||||||
|
self.fields[field_name] = self._parse_field(field_name, part)
|
||||||
|
|
||||||
|
def _parse_field(self, field_name: str, field_value: str) -> Set[int]:
|
||||||
|
"""Parse individual cron field"""
|
||||||
|
min_val, max_val = self.FIELD_RANGES[field_name]
|
||||||
|
result = set()
|
||||||
|
|
||||||
|
# 处理逗号分隔的多个值
|
||||||
|
for part in field_value.split(','):
|
||||||
|
result.update(self._parse_field_part(field_name, part.strip(), min_val, max_val))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _parse_field_part(self, field_name: str, part: str, min_val: int, max_val: int) -> Set[int]:
|
||||||
|
"""Parse field part (handles ranges, steps, wildcards)"""
|
||||||
|
# 处理通配符
|
||||||
|
if part == '*':
|
||||||
|
return set(range(min_val, max_val + 1))
|
||||||
|
|
||||||
|
# 处理步长 (*/5, 1-10/2)
|
||||||
|
if '/' in part:
|
||||||
|
base, step = part.split('/', 1)
|
||||||
|
try:
|
||||||
|
step = int(step)
|
||||||
|
if step <= 0:
|
||||||
|
raise ValueError("Step must be positive")
|
||||||
|
except ValueError as e:
|
||||||
|
raise CronParseError(f"Invalid step value in '{part}': {e}")
|
||||||
|
|
||||||
|
if base == '*':
|
||||||
|
values = set(range(min_val, max_val + 1))
|
||||||
|
else:
|
||||||
|
values = self._parse_field_part(field_name, base, min_val, max_val)
|
||||||
|
|
||||||
|
return {v for v in values if (v - min_val) % step == 0}
|
||||||
|
|
||||||
|
# 处理范围 (1-5)
|
||||||
|
if '-' in part:
|
||||||
|
start, end = part.split('-', 1)
|
||||||
|
try:
|
||||||
|
start_val = self._parse_single_value(field_name, start, min_val, max_val)
|
||||||
|
end_val = self._parse_single_value(field_name, end, min_val, max_val)
|
||||||
|
if start_val > end_val:
|
||||||
|
raise ValueError("Range start must be <= end")
|
||||||
|
return set(range(start_val, end_val + 1))
|
||||||
|
except ValueError as e:
|
||||||
|
raise CronParseError(f"Invalid range in '{part}': {e}")
|
||||||
|
|
||||||
|
# 处理单个值
|
||||||
|
try:
|
||||||
|
return {self._parse_single_value(field_name, part, min_val, max_val)}
|
||||||
|
except ValueError as e:
|
||||||
|
raise CronParseError(f"Invalid value in '{part}': {e}")
|
||||||
|
|
||||||
|
def _parse_single_value(self, field_name: str, value: str, min_val: int, max_val: int) -> int:
|
||||||
|
"""Parse single value (handles numeric and named values)"""
|
||||||
|
# 尝试解析数字
|
||||||
|
try:
|
||||||
|
num_val = int(value)
|
||||||
|
if min_val <= num_val <= max_val:
|
||||||
|
return num_val
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Value {num_val} out of range [{min_val}, {max_val}]")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 尝试解析名称 (月份或星期)
|
||||||
|
if field_name == 'month':
|
||||||
|
lower_value = value.lower()
|
||||||
|
if lower_value in self.MONTH_NAMES:
|
||||||
|
return self.MONTH_NAMES[lower_value]
|
||||||
|
elif field_name == 'weekday':
|
||||||
|
lower_value = value.lower()
|
||||||
|
if lower_value in self.WEEKDAY_NAMES:
|
||||||
|
return self.WEEKDAY_NAMES[lower_value]
|
||||||
|
|
||||||
|
raise ValueError(f"Invalid value '{value}' for field {field_name}")
|
||||||
|
|
||||||
|
def get_next_run_time(self, from_time: Optional[datetime] = None) -> datetime:
|
||||||
|
"""Get next execution time after from_time"""
|
||||||
|
if from_time is None:
|
||||||
|
from_time = datetime.now()
|
||||||
|
|
||||||
|
# 从下一秒开始检查
|
||||||
|
next_time = from_time.replace(microsecond=0) + timedelta(seconds=1)
|
||||||
|
|
||||||
|
# 限制最大搜索时间(避免无限循环)
|
||||||
|
max_iterations = 366 * 24 * 60 # 一年内的分钟数
|
||||||
|
iterations = 0
|
||||||
|
|
||||||
|
while iterations < max_iterations:
|
||||||
|
if self._matches_time(next_time):
|
||||||
|
return next_time
|
||||||
|
|
||||||
|
next_time += timedelta(minutes=1)
|
||||||
|
iterations += 1
|
||||||
|
|
||||||
|
raise CronParseError("Could not find next run time within reasonable timeframe")
|
||||||
|
|
||||||
|
def _matches_time(self, dt: datetime) -> bool:
|
||||||
|
"""Check if datetime matches cron expression"""
|
||||||
|
# 转换星期 (datetime weekday: 0=Monday, cron weekday: 0=Monday)
|
||||||
|
weekday = dt.weekday()
|
||||||
|
|
||||||
|
return (
|
||||||
|
dt.minute in self.fields['minute'] and
|
||||||
|
dt.hour in self.fields['hour'] and
|
||||||
|
dt.day in self.fields['day'] and
|
||||||
|
dt.month in self.fields['month'] and
|
||||||
|
weekday in self.fields['weekday']
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.expression
|
||||||
165
src/flask/scheduler/decorators.py
Normal file
165
src/flask/scheduler/decorators.py
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
"""
|
||||||
|
Task decorators for easy task definition
|
||||||
|
"""
|
||||||
|
import functools
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Callable, Optional
|
||||||
|
from .tasks import Task, TaskType
|
||||||
|
|
||||||
|
# 延迟导入避免循环依赖
|
||||||
|
_scheduler = None
|
||||||
|
|
||||||
|
def get_scheduler():
|
||||||
|
"""获取调度器实例"""
|
||||||
|
global _scheduler
|
||||||
|
if _scheduler is None:
|
||||||
|
try:
|
||||||
|
from .scheduler import Scheduler
|
||||||
|
_scheduler = Scheduler.get_instance()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
return _scheduler
|
||||||
|
|
||||||
|
|
||||||
|
def interval_task(interval: timedelta, name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None, enabled: bool = True,
|
||||||
|
max_retries: int = 0, timeout: Optional[timedelta] = None):
|
||||||
|
"""
|
||||||
|
Decorator for interval-based tasks
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interval: Task execution interval
|
||||||
|
name: Task name (default: function name)
|
||||||
|
description: Task description
|
||||||
|
enabled: Whether task is enabled
|
||||||
|
max_retries: Maximum retry attempts
|
||||||
|
timeout: Task timeout
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
task_name = name or func.__name__
|
||||||
|
|
||||||
|
# 创建任务定义
|
||||||
|
task = Task(
|
||||||
|
name=task_name,
|
||||||
|
func=func,
|
||||||
|
task_type=TaskType.INTERVAL,
|
||||||
|
interval=interval,
|
||||||
|
description=description or func.__doc__,
|
||||||
|
enabled=enabled,
|
||||||
|
max_retries=max_retries,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
# 注册到调度器
|
||||||
|
scheduler = get_scheduler()
|
||||||
|
if scheduler:
|
||||||
|
scheduler.add_task(task)
|
||||||
|
|
||||||
|
# 保持原始函数可用
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
# 附加任务信息
|
||||||
|
wrapper._scheduler_task = task
|
||||||
|
wrapper._scheduler_task_name = task_name
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def delay_task(delay: timedelta, name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None, enabled: bool = True,
|
||||||
|
max_retries: int = 0, timeout: Optional[timedelta] = None):
|
||||||
|
"""
|
||||||
|
Decorator for delayed tasks
|
||||||
|
|
||||||
|
Args:
|
||||||
|
delay: Delay before execution
|
||||||
|
name: Task name (default: function name)
|
||||||
|
description: Task description
|
||||||
|
enabled: Whether task is enabled
|
||||||
|
max_retries: Maximum retry attempts
|
||||||
|
timeout: Task timeout
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
task_name = name or func.__name__
|
||||||
|
|
||||||
|
# 创建任务定义
|
||||||
|
task = Task(
|
||||||
|
name=task_name,
|
||||||
|
func=func,
|
||||||
|
task_type=TaskType.DELAY,
|
||||||
|
delay=delay,
|
||||||
|
description=description or func.__doc__,
|
||||||
|
enabled=enabled,
|
||||||
|
max_retries=max_retries,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
# 注册到调度器
|
||||||
|
scheduler = get_scheduler()
|
||||||
|
if scheduler:
|
||||||
|
scheduler.add_task(task)
|
||||||
|
|
||||||
|
# 保持原始函数可用
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
# 附加任务信息
|
||||||
|
wrapper._scheduler_task = task
|
||||||
|
wrapper._scheduler_task_name = task_name
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def cron_task(cron_expression: str, name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None, enabled: bool = True,
|
||||||
|
max_retries: int = 0, timeout: Optional[timedelta] = None):
|
||||||
|
"""
|
||||||
|
Decorator for cron-based tasks
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cron_expression: Cron expression (5 fields: minute hour day month weekday)
|
||||||
|
name: Task name (default: function name)
|
||||||
|
description: Task description
|
||||||
|
enabled: Whether task is enabled
|
||||||
|
max_retries: Maximum retry attempts
|
||||||
|
timeout: Task timeout
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
task_name = name or func.__name__
|
||||||
|
|
||||||
|
# 创建任务定义
|
||||||
|
task = Task(
|
||||||
|
name=task_name,
|
||||||
|
func=func,
|
||||||
|
task_type=TaskType.CRON,
|
||||||
|
cron_expression=cron_expression,
|
||||||
|
description=description or func.__doc__,
|
||||||
|
enabled=enabled,
|
||||||
|
max_retries=max_retries,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
# 注册到调度器
|
||||||
|
scheduler = get_scheduler()
|
||||||
|
if scheduler:
|
||||||
|
scheduler.add_task(task)
|
||||||
|
|
||||||
|
# 保持原始函数可用
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
# 附加任务信息
|
||||||
|
wrapper._scheduler_task = task
|
||||||
|
wrapper._scheduler_task_name = task_name
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
149
src/flask/scheduler/examples.py
Normal file
149
src/flask/scheduler/examples.py
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
"""
|
||||||
|
示例任务 - 展示Flask Scheduler的功能
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
from flask import current_app
|
||||||
|
from .decorators import interval_task, delay_task, cron_task
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@interval_task(interval=timedelta(seconds=10), description="每10秒执行一次的示例任务")
|
||||||
|
def example_interval_task():
|
||||||
|
"""每10秒执行一次的示例任务"""
|
||||||
|
try:
|
||||||
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
logger.info(f"[间隔任务] 当前时间: {current_time}")
|
||||||
|
|
||||||
|
# 模拟一些工作
|
||||||
|
import time
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
logger.info(f"[间隔任务] 任务执行完成")
|
||||||
|
return f"间隔任务执行成功: {current_time}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[间隔任务] 执行失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@delay_task(delay=timedelta(seconds=5), description="延迟5秒后执行的示例任务")
|
||||||
|
def example_delay_task():
|
||||||
|
"""延迟5秒后执行的示例任务"""
|
||||||
|
try:
|
||||||
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
logger.info(f"[延迟任务] 开始执行,当前时间: {current_time}")
|
||||||
|
|
||||||
|
# 模拟一些初始化工作
|
||||||
|
import time
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
logger.info(f"[延迟任务] 执行完成")
|
||||||
|
return f"延迟任务执行成功: {current_time}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[延迟任务] 执行失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@cron_task(cron_expression="*/2 * * * *", description="每2分钟执行一次的cron任务")
|
||||||
|
def example_cron_task():
|
||||||
|
"""每2分钟执行一次的cron任务"""
|
||||||
|
try:
|
||||||
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
logger.info(f"[Cron任务] 执行时间: {current_time}")
|
||||||
|
|
||||||
|
# 模拟定期维护工作
|
||||||
|
import random
|
||||||
|
work_duration = random.uniform(0.5, 2.0)
|
||||||
|
import time
|
||||||
|
time.sleep(work_duration)
|
||||||
|
|
||||||
|
logger.info(f"[Cron任务] 定期维护完成,耗时: {work_duration:.2f}秒")
|
||||||
|
return f"Cron任务执行成功: {current_time} (耗时: {work_duration:.2f}秒)"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Cron任务] 执行失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# 额外的示例任务
|
||||||
|
|
||||||
|
@interval_task(interval=timedelta(minutes=1), description="每分钟执行的健康检查任务")
|
||||||
|
def health_check_task():
|
||||||
|
"""每分钟执行的健康检查任务"""
|
||||||
|
try:
|
||||||
|
# 检查应用状态
|
||||||
|
if current_app:
|
||||||
|
config_count = len(current_app.config)
|
||||||
|
logger.info(f"[健康检查] 应用配置项数量: {config_count}")
|
||||||
|
|
||||||
|
# 检查内存使用情况(简化版)
|
||||||
|
import psutil
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
memory_usage = memory.percent
|
||||||
|
|
||||||
|
if memory_usage > 90:
|
||||||
|
logger.warning(f"[健康检查] 内存使用率过高: {memory_usage}%")
|
||||||
|
else:
|
||||||
|
logger.info(f"[健康检查] 内存使用率正常: {memory_usage}%")
|
||||||
|
|
||||||
|
return f"健康检查通过,内存使用率: {memory_usage}%"
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
logger.info("[健康检查] psutil模块未安装,跳过内存检查")
|
||||||
|
return "健康检查通过(基础检查)"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[健康检查] 执行失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@cron_task(cron_expression="0 9 * * *", description="每天早上9点执行的数据清理任务")
|
||||||
|
def daily_cleanup_task():
|
||||||
|
"""每天早上9点执行的数据清理任务"""
|
||||||
|
try:
|
||||||
|
current_date = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
logger.info(f"[数据清理] 开始执行每日清理任务: {current_date}")
|
||||||
|
|
||||||
|
# 模拟数据清理工作
|
||||||
|
import time
|
||||||
|
cleanup_duration = 3.0 # 模拟3秒的清理工作
|
||||||
|
time.sleep(cleanup_duration)
|
||||||
|
|
||||||
|
logger.info(f"[数据清理] 完成每日数据清理")
|
||||||
|
return f"每日数据清理完成: {current_date}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[数据清理] 执行失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@delay_task(delay=timedelta(seconds=30), description="30秒后执行的初始化任务")
|
||||||
|
def initialization_task():
|
||||||
|
"""30秒后执行的初始化任务"""
|
||||||
|
try:
|
||||||
|
logger.info("[初始化任务] 开始执行应用初始化")
|
||||||
|
|
||||||
|
# 模拟初始化工作
|
||||||
|
import time
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# 检查调度器状态
|
||||||
|
from .scheduler import Scheduler
|
||||||
|
scheduler = Scheduler.get_instance()
|
||||||
|
|
||||||
|
if scheduler and scheduler.is_running():
|
||||||
|
logger.info("[初始化任务] 调度器运行正常")
|
||||||
|
status = "调度器运行正常"
|
||||||
|
else:
|
||||||
|
logger.warning("[初始化任务] 调度器未运行")
|
||||||
|
status = "调度器未运行"
|
||||||
|
|
||||||
|
logger.info(f"[初始化任务] 初始化完成: {status}")
|
||||||
|
return f"应用初始化完成: {status}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[初始化任务] 执行失败: {e}")
|
||||||
|
raise
|
||||||
23
src/flask/scheduler/exceptions.py
Normal file
23
src/flask/scheduler/exceptions.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
"""
|
||||||
|
Scheduler exceptions
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SchedulerError(Exception):
|
||||||
|
"""Base exception for scheduler errors"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TaskError(SchedulerError):
|
||||||
|
"""Task-related errors"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CronParseError(SchedulerError):
|
||||||
|
"""Cron expression parsing errors"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class StorageError(SchedulerError):
|
||||||
|
"""Task storage errors"""
|
||||||
|
pass
|
||||||
367
src/flask/scheduler/scheduler.py
Normal file
367
src/flask/scheduler/scheduler.py
Normal file
|
|
@ -0,0 +1,367 @@
|
||||||
|
"""
|
||||||
|
Flask Scheduler - Main scheduler implementation
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Dict, Any, Callable
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, Future
|
||||||
|
|
||||||
|
from flask import Flask, current_app
|
||||||
|
from .tasks import Task, TaskType, TaskStatus
|
||||||
|
from .storage import TaskStorage
|
||||||
|
from .cron import CronParser
|
||||||
|
from .exceptions import SchedulerError, TaskError
|
||||||
|
from .blueprint import create_scheduler_blueprint
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Scheduler:
|
||||||
|
"""Flask任务调度器"""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
_instance_lock = threading.Lock()
|
||||||
|
|
||||||
|
def __init__(self, app: Optional[Flask] = None, storage_path: Optional[str] = None):
|
||||||
|
"""初始化调度器"""
|
||||||
|
self.app = app
|
||||||
|
self.storage = TaskStorage(storage_path)
|
||||||
|
self._thread: Optional[threading.Thread] = None
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._executor: Optional[ThreadPoolExecutor] = None
|
||||||
|
self._running_tasks: Dict[str, Future] = {}
|
||||||
|
self._tick_interval = 1.0 # 默认1秒检查间隔
|
||||||
|
self._max_workers = 4
|
||||||
|
self._enabled = True
|
||||||
|
self._autostart = True
|
||||||
|
|
||||||
|
if app is not None:
|
||||||
|
self.init_app(app)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_instance(cls) -> Optional['Scheduler']:
|
||||||
|
"""获取调度器实例"""
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def init_app(self, app: Flask) -> None:
|
||||||
|
"""初始化Flask应用"""
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
# 配置项
|
||||||
|
app.config.setdefault('SCHEDULER_ENABLED', True)
|
||||||
|
app.config.setdefault('SCHEDULER_AUTOSTART', True)
|
||||||
|
app.config.setdefault('SCHEDULER_TICK_INTERVAL', 1.0)
|
||||||
|
app.config.setdefault('SCHEDULER_MAX_WORKERS', 4)
|
||||||
|
app.config.setdefault('SCHEDULER_STORAGE_PATH', None)
|
||||||
|
|
||||||
|
# 应用配置
|
||||||
|
self._enabled = app.config['SCHEDULER_ENABLED']
|
||||||
|
self._autostart = app.config['SCHEDULER_AUTOSTART']
|
||||||
|
self._tick_interval = app.config['SCHEDULER_TICK_INTERVAL']
|
||||||
|
self._max_workers = app.config['SCHEDULER_MAX_WORKERS']
|
||||||
|
|
||||||
|
# 重新初始化存储
|
||||||
|
storage_path = app.config['SCHEDULER_STORAGE_PATH']
|
||||||
|
if storage_path:
|
||||||
|
self.storage = TaskStorage(storage_path)
|
||||||
|
|
||||||
|
# 注册蓝图
|
||||||
|
blueprint = create_scheduler_blueprint(self)
|
||||||
|
app.register_blueprint(blueprint, url_prefix='/_internal')
|
||||||
|
|
||||||
|
# 设置实例
|
||||||
|
with self._instance_lock:
|
||||||
|
Scheduler._instance = self
|
||||||
|
|
||||||
|
# 启动调度器
|
||||||
|
if self._enabled and self._autostart:
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
logger.info(f"Flask Scheduler initialized (enabled={self._enabled}, autostart={self._autostart})")
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""启动调度器"""
|
||||||
|
if not self._enabled:
|
||||||
|
logger.warning("Scheduler is disabled, cannot start")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.is_running():
|
||||||
|
logger.warning("Scheduler is already running")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._stop_event.clear()
|
||||||
|
self._executor = ThreadPoolExecutor(max_workers=self._max_workers)
|
||||||
|
self._thread = threading.Thread(target=self._run_scheduler, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
logger.info("Flask Scheduler started")
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""停止调度器"""
|
||||||
|
if not self.is_running():
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Stopping Flask Scheduler...")
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
# 等待调度线程结束
|
||||||
|
if self._thread and self._thread.is_alive():
|
||||||
|
self._thread.join(timeout=5)
|
||||||
|
|
||||||
|
# 关闭执行器
|
||||||
|
if self._executor:
|
||||||
|
self._executor.shutdown(wait=True)
|
||||||
|
|
||||||
|
# 取消所有运行中的任务
|
||||||
|
for future in self._running_tasks.values():
|
||||||
|
if not future.done():
|
||||||
|
future.cancel()
|
||||||
|
self._running_tasks.clear()
|
||||||
|
|
||||||
|
logger.info("Flask Scheduler stopped")
|
||||||
|
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""检查调度器是否运行中"""
|
||||||
|
return (self._thread is not None and
|
||||||
|
self._thread.is_alive() and
|
||||||
|
not self._stop_event.is_set())
|
||||||
|
|
||||||
|
def add_task(self, task: Task) -> None:
|
||||||
|
"""添加任务"""
|
||||||
|
self.storage.add_task(task)
|
||||||
|
logger.debug(f"Task '{task.name}' added to scheduler")
|
||||||
|
|
||||||
|
def remove_task(self, name: str) -> bool:
|
||||||
|
"""移除任务"""
|
||||||
|
# 取消运行中的任务
|
||||||
|
if name in self._running_tasks:
|
||||||
|
future = self._running_tasks[name]
|
||||||
|
if not future.done():
|
||||||
|
future.cancel()
|
||||||
|
del self._running_tasks[name]
|
||||||
|
|
||||||
|
return self.storage.remove_task(name)
|
||||||
|
|
||||||
|
def get_task(self, name: str) -> Optional[Task]:
|
||||||
|
"""获取任务"""
|
||||||
|
return self.storage.get_task(name)
|
||||||
|
|
||||||
|
def get_all_tasks(self) -> list:
|
||||||
|
"""获取所有任务"""
|
||||||
|
return self.storage.get_all_tasks()
|
||||||
|
|
||||||
|
def run_task(self, name: str) -> bool:
|
||||||
|
"""手动运行任务"""
|
||||||
|
task = self.get_task(name)
|
||||||
|
if not task:
|
||||||
|
logger.error(f"Task '{name}' not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not task.enabled:
|
||||||
|
logger.warning(f"Task '{name}' is disabled")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if task.status == TaskStatus.RUNNING:
|
||||||
|
logger.warning(f"Task '{name}' is already running")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 提交任务到线程池
|
||||||
|
self._submit_task(task, force_run=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _run_scheduler(self) -> None:
|
||||||
|
"""调度器主循环"""
|
||||||
|
logger.info("Scheduler thread started")
|
||||||
|
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
self._check_and_run_tasks()
|
||||||
|
time.sleep(self._tick_interval)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in scheduler loop: {e}")
|
||||||
|
time.sleep(self._tick_interval)
|
||||||
|
|
||||||
|
logger.info("Scheduler thread stopped")
|
||||||
|
|
||||||
|
def _check_and_run_tasks(self) -> None:
|
||||||
|
"""检查并运行到期任务"""
|
||||||
|
now = datetime.now()
|
||||||
|
tasks = self.storage.get_all_tasks()
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
if not task.enabled:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if task.status == TaskStatus.RUNNING:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self._should_run_task(task, now):
|
||||||
|
self._submit_task(task)
|
||||||
|
|
||||||
|
def _should_run_task(self, task: Task, now: datetime) -> bool:
|
||||||
|
"""检查任务是否应该运行"""
|
||||||
|
# 延迟任务
|
||||||
|
if task.task_type == TaskType.DELAY:
|
||||||
|
if task.next_run_at is None:
|
||||||
|
# 首次运行
|
||||||
|
task.next_run_at = now + task.delay
|
||||||
|
return False
|
||||||
|
return now >= task.next_run_at
|
||||||
|
|
||||||
|
# 间隔任务
|
||||||
|
if task.task_type == TaskType.INTERVAL:
|
||||||
|
if task.next_run_at is None:
|
||||||
|
# 首次运行
|
||||||
|
task.next_run_at = now
|
||||||
|
return True
|
||||||
|
return now >= task.next_run_at
|
||||||
|
|
||||||
|
# Cron任务
|
||||||
|
if task.task_type == TaskType.CRON:
|
||||||
|
try:
|
||||||
|
parser = CronParser(task.cron_expression)
|
||||||
|
if task.next_run_at is None:
|
||||||
|
# 首次运行
|
||||||
|
task.next_run_at = parser.get_next_run_time(now)
|
||||||
|
return False
|
||||||
|
return now >= task.next_run_at
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing cron for task '{task.name}': {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _submit_task(self, task: Task, force_run: bool = False) -> None:
|
||||||
|
"""提交任务到线程池"""
|
||||||
|
if not self._executor:
|
||||||
|
logger.error("Task executor not available")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 生成运行ID
|
||||||
|
run_id = str(uuid.uuid4())
|
||||||
|
task.current_run_id = run_id
|
||||||
|
task.status = TaskStatus.RUNNING
|
||||||
|
task.last_run_at = datetime.now()
|
||||||
|
|
||||||
|
# 更新下次运行时间
|
||||||
|
if not force_run:
|
||||||
|
self._update_next_run_time(task)
|
||||||
|
|
||||||
|
# 提交到线程池
|
||||||
|
future = self._executor.submit(self._execute_task, task, run_id)
|
||||||
|
self._running_tasks[task.name] = future
|
||||||
|
|
||||||
|
# 添加完成回调
|
||||||
|
future.add_done_callback(
|
||||||
|
lambda f: self._task_completed(task.name, run_id, f)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"Task '{task.name}' submitted (run_id: {run_id})")
|
||||||
|
|
||||||
|
def _execute_task(self, task: Task, run_id: str) -> Any:
|
||||||
|
"""执行任务"""
|
||||||
|
logger.info(f"Executing task '{task.name}' (run_id: {run_id})")
|
||||||
|
start_time = datetime.now()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 执行任务函数
|
||||||
|
if self.app:
|
||||||
|
with self.app.app_context():
|
||||||
|
result = task.func()
|
||||||
|
else:
|
||||||
|
result = task.func()
|
||||||
|
|
||||||
|
duration = (datetime.now() - start_time).total_seconds()
|
||||||
|
|
||||||
|
# 更新指标
|
||||||
|
self.storage.update_task_metrics(
|
||||||
|
task.name, duration, success=True
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Task '{task.name}' completed successfully in {duration:.2f}s")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
duration = (datetime.now() - start_time).total_seconds()
|
||||||
|
error_msg = str(e)
|
||||||
|
|
||||||
|
# 更新指标
|
||||||
|
self.storage.update_task_metrics(
|
||||||
|
task.name, duration, success=False, error=error_msg
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.error(f"Task '{task.name}' failed after {duration:.2f}s: {error_msg}")
|
||||||
|
raise TaskError(f"Task execution failed: {error_msg}") from e
|
||||||
|
|
||||||
|
def _task_completed(self, task_name: str, run_id: str, future: Future) -> None:
|
||||||
|
"""任务完成回调"""
|
||||||
|
try:
|
||||||
|
# 移除运行记录
|
||||||
|
if task_name in self._running_tasks:
|
||||||
|
del self._running_tasks[task_name]
|
||||||
|
|
||||||
|
# 获取任务
|
||||||
|
task = self.get_task(task_name)
|
||||||
|
if not task:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 更新状态
|
||||||
|
if future.exception():
|
||||||
|
task.status = TaskStatus.FAILED
|
||||||
|
task.last_error = str(future.exception())
|
||||||
|
logger.error(f"Task '{task_name}' failed: {future.exception()}")
|
||||||
|
else:
|
||||||
|
task.status = TaskStatus.SUCCESS
|
||||||
|
task.last_error = None
|
||||||
|
task.retry_count = 0
|
||||||
|
|
||||||
|
task.current_run_id = None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in task completion handler: {e}")
|
||||||
|
|
||||||
|
def _update_next_run_time(self, task: Task) -> None:
|
||||||
|
"""更新任务下次运行时间"""
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
if task.task_type == TaskType.INTERVAL:
|
||||||
|
task.next_run_at = now + task.interval
|
||||||
|
|
||||||
|
elif task.task_type == TaskType.CRON:
|
||||||
|
try:
|
||||||
|
parser = CronParser(task.cron_expression)
|
||||||
|
task.next_run_at = parser.get_next_run_time(now)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating cron next run time for '{task.name}': {e}")
|
||||||
|
|
||||||
|
elif task.task_type == TaskType.DELAY:
|
||||||
|
# 延迟任务只运行一次,禁用任务
|
||||||
|
task.enabled = False
|
||||||
|
task.next_run_at = None
|
||||||
|
|
||||||
|
def get_metrics(self) -> Dict[str, Any]:
|
||||||
|
"""获取调度器指标"""
|
||||||
|
return self.storage.get_metrics_summary()
|
||||||
|
|
||||||
|
def reload(self) -> None:
|
||||||
|
"""重新加载调度器配置"""
|
||||||
|
logger.info("Reloading scheduler configuration...")
|
||||||
|
|
||||||
|
# 这里可以实现配置重新加载逻辑
|
||||||
|
# 目前只是重启调度器
|
||||||
|
was_running = self.is_running()
|
||||||
|
|
||||||
|
if was_running:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
# 重新初始化
|
||||||
|
if self.app:
|
||||||
|
self.init_app(self.app)
|
||||||
|
elif was_running:
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
logger.info("Scheduler configuration reloaded")
|
||||||
168
src/flask/scheduler/storage.py
Normal file
168
src/flask/scheduler/storage.py
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
"""
|
||||||
|
Task storage for persisting task state and metrics
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
from .tasks import Task, TaskStatus
|
||||||
|
from .exceptions import StorageError
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStorage:
|
||||||
|
"""Task state and metrics storage"""
|
||||||
|
|
||||||
|
def __init__(self, storage_path: Optional[str] = None):
|
||||||
|
"""Initialize storage"""
|
||||||
|
self.storage_path = Path(storage_path) if storage_path else None
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
self._tasks: Dict[str, Task] = {}
|
||||||
|
self._metrics: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
if self.storage_path:
|
||||||
|
self.storage_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._load_from_disk()
|
||||||
|
|
||||||
|
def add_task(self, task: Task) -> None:
|
||||||
|
"""Add or update task"""
|
||||||
|
with self._lock:
|
||||||
|
self._tasks[task.name] = task
|
||||||
|
if task.name not in self._metrics:
|
||||||
|
self._metrics[task.name] = {}
|
||||||
|
self._save_to_disk()
|
||||||
|
|
||||||
|
def get_task(self, name: str) -> Optional[Task]:
|
||||||
|
"""Get task by name"""
|
||||||
|
with self._lock:
|
||||||
|
return self._tasks.get(name)
|
||||||
|
|
||||||
|
def get_all_tasks(self) -> List[Task]:
|
||||||
|
"""Get all tasks"""
|
||||||
|
with self._lock:
|
||||||
|
return list(self._tasks.values())
|
||||||
|
|
||||||
|
def remove_task(self, name: str) -> bool:
|
||||||
|
"""Remove task by name"""
|
||||||
|
with self._lock:
|
||||||
|
if name in self._tasks:
|
||||||
|
del self._tasks[name]
|
||||||
|
if name in self._metrics:
|
||||||
|
del self._metrics[name]
|
||||||
|
self._save_to_disk()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_task_status(self, name: str, status: TaskStatus,
|
||||||
|
error: Optional[str] = None) -> None:
|
||||||
|
"""Update task status"""
|
||||||
|
with self._lock:
|
||||||
|
task = self._tasks.get(name)
|
||||||
|
if task:
|
||||||
|
task.status = status
|
||||||
|
if error:
|
||||||
|
task.last_error = error
|
||||||
|
task.metrics.last_error = error
|
||||||
|
self._save_to_disk()
|
||||||
|
|
||||||
|
def update_task_metrics(self, name: str, duration: float,
|
||||||
|
success: bool, error: Optional[str] = None) -> None:
|
||||||
|
"""Update task execution metrics"""
|
||||||
|
with self._lock:
|
||||||
|
task = self._tasks.get(name)
|
||||||
|
if task:
|
||||||
|
metrics = task.metrics
|
||||||
|
metrics.total_runs += 1
|
||||||
|
metrics.last_run_at = datetime.now()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
metrics.successful_runs += 1
|
||||||
|
metrics.last_success_at = datetime.now()
|
||||||
|
else:
|
||||||
|
metrics.failed_runs += 1
|
||||||
|
metrics.last_failure_at = datetime.now()
|
||||||
|
if error:
|
||||||
|
metrics.last_error = error
|
||||||
|
|
||||||
|
# 更新平均执行时间
|
||||||
|
if metrics.average_duration == 0:
|
||||||
|
metrics.average_duration = duration
|
||||||
|
else:
|
||||||
|
metrics.average_duration = (metrics.average_duration + duration) / 2
|
||||||
|
|
||||||
|
self._save_to_disk()
|
||||||
|
|
||||||
|
def get_metrics_summary(self) -> Dict[str, Any]:
|
||||||
|
"""Get metrics summary for all tasks"""
|
||||||
|
with self._lock:
|
||||||
|
total_tasks = len(self._tasks)
|
||||||
|
enabled_tasks = sum(1 for task in self._tasks.values() if task.enabled)
|
||||||
|
|
||||||
|
total_runs = 0
|
||||||
|
successful_runs = 0
|
||||||
|
failed_runs = 0
|
||||||
|
|
||||||
|
for task in self._tasks.values():
|
||||||
|
total_runs += task.metrics.total_runs
|
||||||
|
successful_runs += task.metrics.successful_runs
|
||||||
|
failed_runs += task.metrics.failed_runs
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_tasks': total_tasks,
|
||||||
|
'enabled_tasks': enabled_tasks,
|
||||||
|
'disabled_tasks': total_tasks - enabled_tasks,
|
||||||
|
'total_executions': total_runs,
|
||||||
|
'successful_executions': successful_runs,
|
||||||
|
'failed_executions': failed_runs,
|
||||||
|
'success_rate': (successful_runs / total_runs * 100) if total_runs > 0 else 0,
|
||||||
|
'tasks': {
|
||||||
|
task.name: task.to_dict() for task in self._tasks.values()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _save_to_disk(self) -> None:
|
||||||
|
"""Save state to disk"""
|
||||||
|
if not self.storage_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = {
|
||||||
|
'tasks': {
|
||||||
|
name: task.to_dict() for name, task in self._tasks.items()
|
||||||
|
},
|
||||||
|
'last_updated': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 使用临时文件确保原子性写入
|
||||||
|
temp_path = self.storage_path.with_suffix('.tmp')
|
||||||
|
with open(temp_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 原子性替换
|
||||||
|
temp_path.replace(self.storage_path)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise StorageError(f"Failed to save task state: {e}")
|
||||||
|
|
||||||
|
def _load_from_disk(self) -> None:
|
||||||
|
"""Load state from disk"""
|
||||||
|
if not self.storage_path or not self.storage_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.storage_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# 这里简化处理,实际应该重建Task对象
|
||||||
|
# 由于Task包含函数引用,序列化会比较复杂
|
||||||
|
# 这里只恢复基本状态信息
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 加载失败不影响正常运行
|
||||||
|
print(f"Warning: Failed to load task state: {e}")
|
||||||
|
|
||||||
|
def cleanup_old_metrics(self, days: int = 30) -> int:
|
||||||
|
"""Clean up old metrics data"""
|
||||||
|
# 这里可以实现更复杂的清理逻辑
|
||||||
|
# 目前简化处理
|
||||||
|
return 0
|
||||||
101
src/flask/scheduler/tasks.py
Normal file
101
src/flask/scheduler/tasks.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
"""
|
||||||
|
Task models and enums
|
||||||
|
"""
|
||||||
|
import enum
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Any, Dict, Optional, Callable
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
class TaskType(enum.Enum):
|
||||||
|
"""Task execution types"""
|
||||||
|
INTERVAL = "interval"
|
||||||
|
DELAY = "delay"
|
||||||
|
CRON = "cron"
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStatus(enum.Enum):
|
||||||
|
"""Task execution status"""
|
||||||
|
PENDING = "pending"
|
||||||
|
RUNNING = "running"
|
||||||
|
SUCCESS = "success"
|
||||||
|
FAILED = "failed"
|
||||||
|
CANCELLED = "cancelled"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskMetrics:
|
||||||
|
"""Task execution metrics"""
|
||||||
|
total_runs: int = 0
|
||||||
|
successful_runs: int = 0
|
||||||
|
failed_runs: int = 0
|
||||||
|
last_run_at: Optional[datetime] = None
|
||||||
|
last_success_at: Optional[datetime] = None
|
||||||
|
last_failure_at: Optional[datetime] = None
|
||||||
|
average_duration: float = 0.0
|
||||||
|
last_error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
"""Task definition and state"""
|
||||||
|
name: str
|
||||||
|
func: Callable
|
||||||
|
task_type: TaskType
|
||||||
|
|
||||||
|
# Timing configuration
|
||||||
|
interval: Optional[timedelta] = None
|
||||||
|
delay: Optional[timedelta] = None
|
||||||
|
cron_expression: Optional[str] = None
|
||||||
|
|
||||||
|
# Task metadata
|
||||||
|
description: Optional[str] = None
|
||||||
|
enabled: bool = True
|
||||||
|
max_retries: int = 0
|
||||||
|
timeout: Optional[timedelta] = None
|
||||||
|
|
||||||
|
# Runtime state
|
||||||
|
status: TaskStatus = TaskStatus.PENDING
|
||||||
|
metrics: TaskMetrics = field(default_factory=TaskMetrics)
|
||||||
|
next_run_at: Optional[datetime] = None
|
||||||
|
last_run_at: Optional[datetime] = None
|
||||||
|
current_run_id: Optional[str] = None
|
||||||
|
|
||||||
|
# Error tracking
|
||||||
|
last_error: Optional[str] = None
|
||||||
|
retry_count: int = 0
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
"""Post-initialization setup"""
|
||||||
|
if self.task_type == TaskType.INTERVAL and not self.interval:
|
||||||
|
raise ValueError("Interval tasks must specify interval")
|
||||||
|
if self.task_type == TaskType.DELAY and not self.delay:
|
||||||
|
raise ValueError("Delay tasks must specify delay")
|
||||||
|
if self.task_type == TaskType.CRON and not self.cron_expression:
|
||||||
|
raise ValueError("Cron tasks must specify cron_expression")
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Convert task to dictionary for serialization"""
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'task_type': self.task_type.value,
|
||||||
|
'description': self.description,
|
||||||
|
'enabled': self.enabled,
|
||||||
|
'max_retries': self.max_retries,
|
||||||
|
'status': self.status.value,
|
||||||
|
'next_run_at': self.next_run_at.isoformat() if self.next_run_at else None,
|
||||||
|
'last_run_at': self.last_run_at.isoformat() if self.last_run_at else None,
|
||||||
|
'current_run_id': self.current_run_id,
|
||||||
|
'last_error': self.last_error,
|
||||||
|
'retry_count': self.retry_count,
|
||||||
|
'metrics': {
|
||||||
|
'total_runs': self.metrics.total_runs,
|
||||||
|
'successful_runs': self.metrics.successful_runs,
|
||||||
|
'failed_runs': self.metrics.failed_runs,
|
||||||
|
'last_run_at': self.metrics.last_run_at.isoformat() if self.metrics.last_run_at else None,
|
||||||
|
'last_success_at': self.metrics.last_success_at.isoformat() if self.metrics.last_success_at else None,
|
||||||
|
'last_failure_at': self.metrics.last_failure_at.isoformat() if self.metrics.last_failure_at else None,
|
||||||
|
'average_duration': self.metrics.average_duration,
|
||||||
|
'last_error': self.metrics.last_error
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue