jettask 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. jettask/monitor/run_backlog_collector.py +96 -0
  2. jettask/monitor/stream_backlog_monitor.py +362 -0
  3. jettask/pg_consumer/pg_consumer_v2.py +403 -0
  4. jettask/pg_consumer/sql_utils.py +182 -0
  5. jettask/scheduler/__init__.py +17 -0
  6. jettask/scheduler/add_execution_count.sql +11 -0
  7. jettask/scheduler/add_priority_field.sql +26 -0
  8. jettask/scheduler/add_scheduler_id.sql +25 -0
  9. jettask/scheduler/add_scheduler_id_index.sql +10 -0
  10. jettask/scheduler/loader.py +249 -0
  11. jettask/scheduler/make_scheduler_id_required.sql +28 -0
  12. jettask/scheduler/manager.py +696 -0
  13. jettask/scheduler/migrate_interval_seconds.sql +9 -0
  14. jettask/scheduler/models.py +200 -0
  15. jettask/scheduler/multi_namespace_scheduler.py +294 -0
  16. jettask/scheduler/performance_optimization.sql +45 -0
  17. jettask/scheduler/run_scheduler.py +186 -0
  18. jettask/scheduler/scheduler.py +715 -0
  19. jettask/scheduler/schema.sql +84 -0
  20. jettask/scheduler/unified_manager.py +450 -0
  21. jettask/scheduler/unified_scheduler_manager.py +280 -0
  22. jettask/webui/backend/api/__init__.py +3 -0
  23. jettask/webui/backend/api/v1/__init__.py +17 -0
  24. jettask/webui/backend/api/v1/monitoring.py +431 -0
  25. jettask/webui/backend/api/v1/namespaces.py +504 -0
  26. jettask/webui/backend/api/v1/queues.py +342 -0
  27. jettask/webui/backend/api/v1/tasks.py +367 -0
  28. jettask/webui/backend/core/__init__.py +3 -0
  29. jettask/webui/backend/core/cache.py +221 -0
  30. jettask/webui/backend/core/database.py +200 -0
  31. jettask/webui/backend/core/exceptions.py +102 -0
  32. jettask/webui/backend/models/__init__.py +3 -0
  33. jettask/webui/backend/models/requests.py +236 -0
  34. jettask/webui/backend/models/responses.py +230 -0
  35. jettask/webui/backend/services/__init__.py +3 -0
  36. jettask/webui/frontend/index.html +13 -0
  37. jettask/webui/models/__init__.py +3 -0
  38. jettask/webui/models/namespace.py +63 -0
  39. jettask/webui/sql/batch_upsert_functions.sql +178 -0
  40. jettask/webui/sql/init_database.sql +640 -0
  41. {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/METADATA +80 -10
  42. {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/RECORD +46 -53
  43. jettask/webui/frontend/package-lock.json +0 -4833
  44. jettask/webui/frontend/package.json +0 -30
  45. jettask/webui/frontend/src/App.css +0 -109
  46. jettask/webui/frontend/src/App.jsx +0 -66
  47. jettask/webui/frontend/src/components/NamespaceSelector.jsx +0 -166
  48. jettask/webui/frontend/src/components/QueueBacklogChart.jsx +0 -298
  49. jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +0 -638
  50. jettask/webui/frontend/src/components/QueueDetailsTable.css +0 -65
  51. jettask/webui/frontend/src/components/QueueDetailsTable.jsx +0 -487
  52. jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +0 -465
  53. jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +0 -423
  54. jettask/webui/frontend/src/components/TaskFilter.jsx +0 -425
  55. jettask/webui/frontend/src/components/TimeRangeSelector.css +0 -21
  56. jettask/webui/frontend/src/components/TimeRangeSelector.jsx +0 -160
  57. jettask/webui/frontend/src/components/charts/QueueChart.jsx +0 -111
  58. jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +0 -115
  59. jettask/webui/frontend/src/components/charts/WorkerChart.jsx +0 -40
  60. jettask/webui/frontend/src/components/common/StatsCard.jsx +0 -18
  61. jettask/webui/frontend/src/components/layout/AppLayout.css +0 -95
  62. jettask/webui/frontend/src/components/layout/AppLayout.jsx +0 -49
  63. jettask/webui/frontend/src/components/layout/Header.css +0 -106
  64. jettask/webui/frontend/src/components/layout/Header.jsx +0 -106
  65. jettask/webui/frontend/src/components/layout/SideMenu.css +0 -137
  66. jettask/webui/frontend/src/components/layout/SideMenu.jsx +0 -209
  67. jettask/webui/frontend/src/components/layout/TabsNav.css +0 -244
  68. jettask/webui/frontend/src/components/layout/TabsNav.jsx +0 -206
  69. jettask/webui/frontend/src/components/layout/UserInfo.css +0 -197
  70. jettask/webui/frontend/src/components/layout/UserInfo.jsx +0 -197
  71. jettask/webui/frontend/src/contexts/LoadingContext.jsx +0 -27
  72. jettask/webui/frontend/src/contexts/NamespaceContext.jsx +0 -72
  73. jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +0 -245
  74. jettask/webui/frontend/src/index.css +0 -114
  75. jettask/webui/frontend/src/main.jsx +0 -20
  76. jettask/webui/frontend/src/pages/Alerts.jsx +0 -684
  77. jettask/webui/frontend/src/pages/Dashboard/index.css +0 -35
  78. jettask/webui/frontend/src/pages/Dashboard/index.jsx +0 -281
  79. jettask/webui/frontend/src/pages/Dashboard.jsx +0 -1330
  80. jettask/webui/frontend/src/pages/QueueDetail.jsx +0 -1117
  81. jettask/webui/frontend/src/pages/QueueMonitor.jsx +0 -527
  82. jettask/webui/frontend/src/pages/Queues.jsx +0 -12
  83. jettask/webui/frontend/src/pages/ScheduledTasks.jsx +0 -809
  84. jettask/webui/frontend/src/pages/Settings.jsx +0 -800
  85. jettask/webui/frontend/src/pages/Workers.jsx +0 -12
  86. jettask/webui/frontend/src/services/api.js +0 -114
  87. jettask/webui/frontend/src/services/queueTrend.js +0 -152
  88. jettask/webui/frontend/src/utils/suppressWarnings.js +0 -22
  89. jettask/webui/frontend/src/utils/userPreferences.js +0 -154
  90. {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/WHEEL +0 -0
  91. {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/entry_points.txt +0 -0
  92. {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/licenses/LICENSE +0 -0
  93. {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,200 @@
1
+ """
2
+ 定时任务数据模型
3
+ """
4
+ import json
5
+ from datetime import datetime, timedelta
6
+ from typing import Optional, Dict, List, Any
7
+ from dataclasses import dataclass, field, asdict
8
+ from enum import Enum
9
+ from decimal import Decimal
10
+ import croniter
11
+
12
+
13
+ class TaskType(Enum):
14
+ """任务类型"""
15
+ ONCE = "once" # 一次性任务
16
+ INTERVAL = "interval" # 间隔任务
17
+ CRON = "cron" # Cron表达式任务
18
+
19
+
20
+ class TaskStatus(Enum):
21
+ """任务执行状态"""
22
+ PENDING = "pending"
23
+ RUNNING = "running"
24
+ SUCCESS = "success"
25
+ FAILED = "failed"
26
+ TIMEOUT = "timeout"
27
+ CANCELLED = "cancelled"
28
+
29
+
30
+ @dataclass
31
+ class ScheduledTask:
32
+ """定时任务模型"""
33
+ task_name: str # 要执行的函数名(对应@app.task注册的函数名)
34
+ task_type: TaskType # 任务类型
35
+ queue_name: str # 目标队列
36
+
37
+ # 可选字段
38
+ id: Optional[int] = None # 数据库自增ID(唯一标识)
39
+ scheduler_id: Optional[str] = None # 任务的唯一标识符(用于去重)
40
+ namespace: str = 'default' # 命名空间
41
+ task_args: List[Any] = field(default_factory=list)
42
+ task_kwargs: Dict[str, Any] = field(default_factory=dict)
43
+ cron_expression: Optional[str] = None
44
+ interval_seconds: Optional[float] = None
45
+ next_run_time: Optional[datetime] = None
46
+ last_run_time: Optional[datetime] = None
47
+ enabled: bool = True
48
+ max_retries: int = 3
49
+ retry_delay: int = 60
50
+ timeout: int = 300
51
+ priority: Optional[int] = None # 任务优先级 (1=最高, 数字越大优先级越低,None=默认最低)
52
+ description: Optional[str] = None
53
+ tags: List[str] = field(default_factory=list)
54
+ metadata: Dict[str, Any] = field(default_factory=dict)
55
+ created_at: Optional[datetime] = None
56
+ updated_at: Optional[datetime] = None
57
+
58
+ def __post_init__(self):
59
+ """初始化后处理"""
60
+ # 转换枚举类型
61
+ if isinstance(self.task_type, str):
62
+ self.task_type = TaskType(self.task_type)
63
+
64
+ # 验证配置
65
+ self._validate()
66
+
67
+ # 计算下次执行时间
68
+ if self.next_run_time is None:
69
+ self.next_run_time = self.calculate_next_run_time()
70
+
71
+ def _validate(self):
72
+ """验证任务配置"""
73
+ if self.task_type == TaskType.CRON and not self.cron_expression:
74
+ raise ValueError(f"Task {self.task_name} with type CRON must have cron_expression")
75
+
76
+ if self.task_type == TaskType.INTERVAL and not self.interval_seconds:
77
+ raise ValueError(f"Task {self.task_name} with type INTERVAL must have interval_seconds")
78
+
79
+ # ONCE类型任务不应该有interval_seconds参数
80
+ if self.task_type == TaskType.ONCE and self.interval_seconds is not None:
81
+ raise ValueError(f"Task {self.task_name} with type ONCE should not have interval_seconds. Use next_run_time to specify when to run the task")
82
+
83
+ def calculate_next_run_time(self, from_time: Optional[datetime] = None) -> Optional[datetime]:
84
+ """计算下次执行时间"""
85
+ if not self.enabled:
86
+ return None
87
+
88
+ from_time = from_time or datetime.now()
89
+
90
+ if self.task_type == TaskType.ONCE:
91
+ # 一次性任务,如果没有执行过就返回设定的时间
92
+ if self.last_run_time is None:
93
+ return self.next_run_time or from_time
94
+ return None
95
+
96
+ elif self.task_type == TaskType.INTERVAL:
97
+ # 间隔任务
98
+ if self.last_run_time:
99
+ return self.last_run_time + timedelta(seconds=float(self.interval_seconds))
100
+ return from_time
101
+
102
+ elif self.task_type == TaskType.CRON:
103
+ # Cron表达式任务
104
+ cron = croniter.croniter(self.cron_expression, from_time)
105
+ return cron.get_next(datetime)
106
+
107
+ return None
108
+
109
+ def update_next_run_time(self):
110
+ """更新下次执行时间"""
111
+ self.last_run_time = datetime.now()
112
+ self.next_run_time = self.calculate_next_run_time(from_time=self.last_run_time)
113
+
114
+ def to_dict(self) -> dict:
115
+ """转换为字典"""
116
+ data = asdict(self)
117
+ data['task_type'] = self.task_type.value
118
+
119
+ # 转换datetime为字符串
120
+ for key in ['next_run_time', 'last_run_time', 'created_at', 'updated_at']:
121
+ if data.get(key):
122
+ data[key] = data[key].isoformat() if isinstance(data[key], datetime) else data[key]
123
+
124
+ # 转换Decimal为float
125
+ if data.get('interval_seconds') and isinstance(data['interval_seconds'], Decimal):
126
+ data['interval_seconds'] = float(data['interval_seconds'])
127
+
128
+ return data
129
+
130
+ @classmethod
131
+ def from_dict(cls, data: dict) -> 'ScheduledTask':
132
+ """从字典创建实例"""
133
+ # 转换datetime字符串
134
+ for key in ['next_run_time', 'last_run_time', 'created_at', 'updated_at']:
135
+ if data.get(key) and isinstance(data[key], str):
136
+ data[key] = datetime.fromisoformat(data[key])
137
+
138
+ # 转换task_type为枚举
139
+ if 'task_type' in data and isinstance(data['task_type'], str):
140
+ data['task_type'] = TaskType(data['task_type'])
141
+
142
+ # 转换interval_seconds为float(处理Decimal类型)
143
+ if data.get('interval_seconds'):
144
+ if isinstance(data['interval_seconds'], Decimal):
145
+ data['interval_seconds'] = float(data['interval_seconds'])
146
+
147
+ return cls(**data)
148
+
149
+ def to_redis_value(self) -> str:
150
+ """转换为Redis存储的值"""
151
+ return json.dumps(self.to_dict())
152
+
153
+ @classmethod
154
+ def from_redis_value(cls, value: str) -> 'ScheduledTask':
155
+ """从Redis值创建实例"""
156
+ return cls.from_dict(json.loads(value))
157
+
158
+
159
+ @dataclass
160
+ class TaskExecutionHistory:
161
+ """任务执行历史"""
162
+ task_id: int # 对应 ScheduledTask 的 id
163
+ event_id: str
164
+ scheduled_time: datetime
165
+ status: TaskStatus
166
+
167
+ # 可选字段
168
+ started_at: Optional[datetime] = None
169
+ finished_at: Optional[datetime] = None
170
+ result: Optional[Dict[str, Any]] = None
171
+ error_message: Optional[str] = None
172
+ retry_count: int = 0
173
+ duration_ms: Optional[int] = None
174
+ worker_id: Optional[str] = None
175
+ created_at: Optional[datetime] = None
176
+
177
+ def __post_init__(self):
178
+ """初始化后处理"""
179
+ if isinstance(self.status, str):
180
+ self.status = TaskStatus(self.status)
181
+
182
+ if self.created_at is None:
183
+ self.created_at = datetime.now()
184
+
185
+ # 计算执行耗时
186
+ if self.started_at and self.finished_at and self.duration_ms is None:
187
+ delta = self.finished_at - self.started_at
188
+ self.duration_ms = int(delta.total_seconds() * 1000)
189
+
190
+ def to_dict(self) -> dict:
191
+ """转换为字典"""
192
+ data = asdict(self)
193
+ data['status'] = self.status.value
194
+
195
+ # 转换datetime为字符串
196
+ for key in ['scheduled_time', 'started_at', 'finished_at', 'created_at']:
197
+ if data.get(key):
198
+ data[key] = data[key].isoformat() if isinstance(data[key], datetime) else data[key]
199
+
200
+ return data
@@ -0,0 +1,294 @@
1
+ """
2
+ 多命名空间调度器管理器
3
+ 自动检测和管理多个命名空间的调度器实例
4
+ """
5
+ import asyncio
6
+ import logging
7
+ import multiprocessing
8
+ import traceback
9
+ from typing import Dict, Set
10
+
11
+ from jettask import Jettask
12
+ from jettask.webui.task_center import TaskCenter
13
+ from jettask.scheduler.scheduler import TaskScheduler
14
+ from jettask.scheduler.manager import ScheduledTaskManager
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class MultiNamespaceSchedulerManager:
20
+ """多命名空间调度器管理器"""
21
+
22
+ def __init__(self,
23
+ task_center_base_url: str,
24
+ check_interval: int = 30,
25
+ scheduler_interval: float = 0.1,
26
+ batch_size: int = 100,
27
+ debug: bool = False):
28
+ """
29
+ 初始化多命名空间调度器管理器
30
+
31
+ Args:
32
+ task_center_base_url: 任务中心基础URL (如 http://localhost:8001)
33
+ check_interval: 命名空间检测间隔(秒)
34
+ scheduler_interval: 调度器扫描间隔(秒)
35
+ batch_size: 每批处理的最大任务数
36
+ debug: 是否启用调试模式
37
+ """
38
+ self.task_center_base_url = task_center_base_url.rstrip('/')
39
+ self.check_interval = check_interval
40
+ self.scheduler_interval = scheduler_interval
41
+ self.batch_size = batch_size
42
+ self.debug = debug
43
+
44
+ # 存储每个命名空间的进程
45
+ self.scheduler_processes: Dict[str, multiprocessing.Process] = {}
46
+ self.running = False
47
+
48
+ # 设置日志级别
49
+ if debug:
50
+ logging.basicConfig(level=logging.DEBUG)
51
+ else:
52
+ logging.basicConfig(level=logging.INFO)
53
+
54
+ async def get_active_namespaces(self) -> Set[str]:
55
+ """获取所有活跃的命名空间"""
56
+ import aiohttp
57
+
58
+ try:
59
+ # 直接调用API获取命名空间列表
60
+ url = f"{self.task_center_base_url}/api/namespaces"
61
+
62
+ async with aiohttp.ClientSession() as session:
63
+ async with session.get(url) as response:
64
+ if response.status == 200:
65
+ namespaces = await response.json()
66
+ if namespaces:
67
+ active_names = {ns['name'] for ns in namespaces if ns.get('name')}
68
+ logger.info(f"发现 {len(active_names)} 个活跃的命名空间: {active_names}")
69
+ return active_names
70
+ else:
71
+ logger.warning("没有找到任何命名空间")
72
+ return set()
73
+ else:
74
+ logger.error(f"获取命名空间列表失败,状态码: {response.status}")
75
+ return set()
76
+
77
+ except Exception as e:
78
+ logger.error(f"获取命名空间列表失败: {e}")
79
+ return set()
80
+
81
+ def start_scheduler_for_namespace(self, namespace: str):
82
+ """为指定命名空间启动调度器进程"""
83
+ if namespace in self.scheduler_processes:
84
+ # 检查进程是否还在运行
85
+ if self.scheduler_processes[namespace].is_alive():
86
+ return
87
+ else:
88
+ # 清理已停止的进程
89
+ logger.info(f"清理已停止的调度器进程: {namespace}")
90
+ self.scheduler_processes[namespace].terminate()
91
+ self.scheduler_processes[namespace].join(timeout=5)
92
+ del self.scheduler_processes[namespace]
93
+
94
+ # 创建新进程
95
+ process = multiprocessing.Process(
96
+ target=run_scheduler_for_namespace,
97
+ args=(
98
+ namespace,
99
+ self.task_center_base_url,
100
+ self.scheduler_interval,
101
+ self.batch_size,
102
+ self.debug
103
+ ),
104
+ name=f"scheduler_{namespace}"
105
+ )
106
+
107
+ process.start()
108
+ self.scheduler_processes[namespace] = process
109
+ logger.info(f"启动命名空间 {namespace} 的调度器进程, PID: {process.pid}")
110
+
111
+ def stop_scheduler_for_namespace(self, namespace: str):
112
+ """停止指定命名空间的调度器进程"""
113
+ if namespace in self.scheduler_processes:
114
+ process = self.scheduler_processes[namespace]
115
+ if process.is_alive():
116
+ logger.info(f"停止命名空间 {namespace} 的调度器进程")
117
+ process.terminate()
118
+ process.join(timeout=10)
119
+
120
+ if process.is_alive():
121
+ logger.warning(f"强制停止命名空间 {namespace} 的调度器进程")
122
+ process.kill()
123
+ process.join(timeout=5)
124
+
125
+ del self.scheduler_processes[namespace]
126
+
127
+ async def check_and_update_schedulers(self):
128
+ """检查并更新调度器(添加新的,停止已删除的)"""
129
+ # 获取当前活跃的命名空间
130
+ active_namespaces = await self.get_active_namespaces()
131
+ current_namespaces = set(self.scheduler_processes.keys())
132
+
133
+ # 找出需要添加的命名空间
134
+ to_add = active_namespaces - current_namespaces
135
+ # 找出需要删除的命名空间
136
+ to_remove = current_namespaces - active_namespaces
137
+
138
+ # 启动新的调度器
139
+ for namespace in to_add:
140
+ logger.info(f"检测到新命名空间: {namespace}")
141
+ self.start_scheduler_for_namespace(namespace)
142
+
143
+ # 停止已删除的调度器
144
+ for namespace in to_remove:
145
+ logger.info(f"检测到命名空间已删除: {namespace}")
146
+ self.stop_scheduler_for_namespace(namespace)
147
+
148
+ # 检查现有进程的健康状态
149
+ for namespace in active_namespaces & current_namespaces:
150
+ process = self.scheduler_processes.get(namespace)
151
+ if process and not process.is_alive():
152
+ logger.warning(f"调度器进程 {namespace} 已停止,重新启动")
153
+ del self.scheduler_processes[namespace]
154
+ self.start_scheduler_for_namespace(namespace)
155
+
156
+ async def run(self):
157
+ """运行多命名空间调度器管理器"""
158
+ self.running = True
159
+ logger.info(f"启动多命名空间调度器管理器")
160
+ logger.info(f"任务中心: {self.task_center_base_url}")
161
+ logger.info(f"命名空间检测间隔: {self.check_interval} 秒")
162
+ logger.info(f"调度器扫描间隔: {self.scheduler_interval} 秒")
163
+ logger.info(f"批处理大小: {self.batch_size}")
164
+
165
+ # 初始检查和启动
166
+ await self.check_and_update_schedulers()
167
+
168
+ # 定期检查命名空间变化
169
+ while self.running:
170
+ try:
171
+ await asyncio.sleep(self.check_interval)
172
+ await self.check_and_update_schedulers()
173
+ except asyncio.CancelledError:
174
+ break
175
+ except Exception as e:
176
+ logger.error(f"检查命名空间时出错: {e}")
177
+ if self.debug:
178
+ traceback.print_exc()
179
+
180
+ def stop(self):
181
+ """停止管理器和所有调度器"""
182
+ logger.info("停止多命名空间调度器管理器")
183
+ self.running = False
184
+
185
+ # 停止所有调度器进程
186
+ for namespace in list(self.scheduler_processes.keys()):
187
+ self.stop_scheduler_for_namespace(namespace)
188
+
189
+ logger.info("所有调度器已停止")
190
+
191
+
192
+ def run_scheduler_for_namespace(namespace: str,
193
+ task_center_base_url: str,
194
+ interval: float,
195
+ batch_size: int,
196
+ debug: bool):
197
+ """在独立进程中运行指定命名空间的调度器"""
198
+ import asyncio
199
+ import logging
200
+ import signal
201
+ import sys
202
+
203
+ # 设置进程标题(如果可用)
204
+ try:
205
+ import setproctitle # type: ignore
206
+ setproctitle.setproctitle(f"jettask-scheduler-{namespace}")
207
+ except ImportError:
208
+ pass
209
+
210
+ # 配置日志
211
+ logging.basicConfig(
212
+ level=logging.DEBUG if debug else logging.INFO,
213
+ format=f'%(asctime)s - %(levelname)s - [{namespace}] %(message)s'
214
+ )
215
+ logger = logging.getLogger(__name__)
216
+
217
+ async def run_scheduler():
218
+ """运行调度器的异步函数"""
219
+ scheduler_instance = None
220
+ try:
221
+ # 构建命名空间特定的URL
222
+ task_center_url = f"{task_center_base_url}/api/namespaces/{namespace}"
223
+ logger.info(f"连接到任务中心: {task_center_url}")
224
+
225
+ # 连接任务中心
226
+ tc = TaskCenter(task_center_url)
227
+ if not tc._connect_sync():
228
+ logger.error(f"无法连接到任务中心: {namespace}")
229
+ return
230
+
231
+ logger.info(f"成功连接到命名空间: {tc.namespace_name}")
232
+
233
+ # 创建app实例
234
+ app = Jettask(task_center=tc)
235
+
236
+ if not app.redis_url or not app.pg_url:
237
+ logger.error(f"任务中心配置不完整: {namespace}")
238
+ return
239
+
240
+ # 显示配置信息
241
+ logger.info(f"命名空间 {namespace} 的调度器配置:")
242
+ logger.info(f" Redis: {app.redis_url}")
243
+ logger.info(f" PostgreSQL: {app.pg_url}")
244
+ logger.info(f" 间隔: {interval} 秒")
245
+ logger.info(f" 批大小: {batch_size}")
246
+
247
+ # 创建调度器实例
248
+ manager = ScheduledTaskManager(app)
249
+ scheduler_instance = TaskScheduler(
250
+ app=app,
251
+ db_manager=manager,
252
+ scan_interval=interval,
253
+ batch_size=batch_size
254
+ )
255
+
256
+ # 运行调度器(run方法内部会处理连接)
257
+ logger.info(f"启动命名空间 {namespace} 的调度器...")
258
+ await scheduler_instance.run()
259
+
260
+ except asyncio.CancelledError:
261
+ logger.info(f"调度器 {namespace} 收到取消信号")
262
+ except KeyboardInterrupt:
263
+ logger.info(f"调度器 {namespace} 收到中断信号")
264
+ except Exception as e:
265
+ logger.error(f"调度器 {namespace} 运行错误: {e}")
266
+ if debug:
267
+ traceback.print_exc()
268
+ finally:
269
+ # 清理资源
270
+ if scheduler_instance:
271
+ scheduler_instance.stop()
272
+
273
+ logger.info(f"调度器 {namespace} 已停止")
274
+
275
+ # 设置信号处理
276
+ import signal
277
+ import sys
278
+
279
+ def signal_handler(signum, frame):
280
+ logger.info(f"调度器 {namespace} 收到信号 {signum}")
281
+ sys.exit(0)
282
+
283
+ signal.signal(signal.SIGTERM, signal_handler)
284
+ signal.signal(signal.SIGINT, signal_handler)
285
+
286
+ # 运行调度器
287
+ try:
288
+ asyncio.run(run_scheduler())
289
+ except (KeyboardInterrupt, SystemExit):
290
+ logger.info(f"调度器 {namespace} 正常退出")
291
+ except Exception as e:
292
+ logger.error(f"调度器 {namespace} 异常退出: {e}")
293
+ if debug:
294
+ traceback.print_exc()
@@ -0,0 +1,45 @@
1
+ -- Performance optimization for scheduled_tasks table
2
+ -- This migration adds necessary indexes for optimal query performance
3
+
4
+ -- 1. Unique index on scheduler_id (已经存在,但确保创建)
5
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_scheduled_tasks_scheduler_id
6
+ ON scheduled_tasks(scheduler_id);
7
+
8
+ -- 2. Index for ready tasks query (get_ready_tasks)
9
+ -- 这是最频繁的查询之一,需要复合索引
10
+ CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_ready
11
+ ON scheduled_tasks(next_run_time, enabled)
12
+ WHERE enabled = true AND next_run_time IS NOT NULL;
13
+
14
+ -- 3. Index for task listing with filters (list_tasks)
15
+ -- created_at用于排序
16
+ CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_created
17
+ ON scheduled_tasks(created_at DESC);
18
+
19
+ -- 4. Index for task_name lookups (常用于查找特定任务)
20
+ CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_name
21
+ ON scheduled_tasks(task_name);
22
+
23
+ -- 5. Composite index for enabled tasks with type
24
+ CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_enabled_type
25
+ ON scheduled_tasks(enabled, task_type)
26
+ WHERE enabled = true;
27
+
28
+ -- 6. Index for update operations by id (primary key已自动有索引)
29
+ -- 但确保主键约束存在
30
+ -- (主键自动创建索引,无需额外操作)
31
+
32
+ -- 7. Index for task execution history queries
33
+ CREATE INDEX IF NOT EXISTS idx_task_history_task_scheduled
34
+ ON task_execution_history(task_id, scheduled_time DESC);
35
+
36
+ -- 8. Index for history status queries
37
+ CREATE INDEX IF NOT EXISTS idx_task_history_status_created
38
+ ON task_execution_history(status, created_at DESC);
39
+
40
+ -- 添加表注释
41
+ COMMENT ON TABLE scheduled_tasks IS 'Scheduled tasks configuration table with optimized indexes';
42
+
43
+ -- 分析表以更新统计信息
44
+ ANALYZE scheduled_tasks;
45
+ ANALYZE task_execution_history;