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.
- jettask/monitor/run_backlog_collector.py +96 -0
- jettask/monitor/stream_backlog_monitor.py +362 -0
- jettask/pg_consumer/pg_consumer_v2.py +403 -0
- jettask/pg_consumer/sql_utils.py +182 -0
- jettask/scheduler/__init__.py +17 -0
- jettask/scheduler/add_execution_count.sql +11 -0
- jettask/scheduler/add_priority_field.sql +26 -0
- jettask/scheduler/add_scheduler_id.sql +25 -0
- jettask/scheduler/add_scheduler_id_index.sql +10 -0
- jettask/scheduler/loader.py +249 -0
- jettask/scheduler/make_scheduler_id_required.sql +28 -0
- jettask/scheduler/manager.py +696 -0
- jettask/scheduler/migrate_interval_seconds.sql +9 -0
- jettask/scheduler/models.py +200 -0
- jettask/scheduler/multi_namespace_scheduler.py +294 -0
- jettask/scheduler/performance_optimization.sql +45 -0
- jettask/scheduler/run_scheduler.py +186 -0
- jettask/scheduler/scheduler.py +715 -0
- jettask/scheduler/schema.sql +84 -0
- jettask/scheduler/unified_manager.py +450 -0
- jettask/scheduler/unified_scheduler_manager.py +280 -0
- jettask/webui/backend/api/__init__.py +3 -0
- jettask/webui/backend/api/v1/__init__.py +17 -0
- jettask/webui/backend/api/v1/monitoring.py +431 -0
- jettask/webui/backend/api/v1/namespaces.py +504 -0
- jettask/webui/backend/api/v1/queues.py +342 -0
- jettask/webui/backend/api/v1/tasks.py +367 -0
- jettask/webui/backend/core/__init__.py +3 -0
- jettask/webui/backend/core/cache.py +221 -0
- jettask/webui/backend/core/database.py +200 -0
- jettask/webui/backend/core/exceptions.py +102 -0
- jettask/webui/backend/models/__init__.py +3 -0
- jettask/webui/backend/models/requests.py +236 -0
- jettask/webui/backend/models/responses.py +230 -0
- jettask/webui/backend/services/__init__.py +3 -0
- jettask/webui/frontend/index.html +13 -0
- jettask/webui/models/__init__.py +3 -0
- jettask/webui/models/namespace.py +63 -0
- jettask/webui/sql/batch_upsert_functions.sql +178 -0
- jettask/webui/sql/init_database.sql +640 -0
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/METADATA +80 -10
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/RECORD +46 -53
- jettask/webui/frontend/package-lock.json +0 -4833
- jettask/webui/frontend/package.json +0 -30
- jettask/webui/frontend/src/App.css +0 -109
- jettask/webui/frontend/src/App.jsx +0 -66
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +0 -166
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +0 -298
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +0 -638
- jettask/webui/frontend/src/components/QueueDetailsTable.css +0 -65
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +0 -487
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +0 -465
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +0 -423
- jettask/webui/frontend/src/components/TaskFilter.jsx +0 -425
- jettask/webui/frontend/src/components/TimeRangeSelector.css +0 -21
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +0 -160
- jettask/webui/frontend/src/components/charts/QueueChart.jsx +0 -111
- jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +0 -115
- jettask/webui/frontend/src/components/charts/WorkerChart.jsx +0 -40
- jettask/webui/frontend/src/components/common/StatsCard.jsx +0 -18
- jettask/webui/frontend/src/components/layout/AppLayout.css +0 -95
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +0 -49
- jettask/webui/frontend/src/components/layout/Header.css +0 -106
- jettask/webui/frontend/src/components/layout/Header.jsx +0 -106
- jettask/webui/frontend/src/components/layout/SideMenu.css +0 -137
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +0 -209
- jettask/webui/frontend/src/components/layout/TabsNav.css +0 -244
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +0 -206
- jettask/webui/frontend/src/components/layout/UserInfo.css +0 -197
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +0 -197
- jettask/webui/frontend/src/contexts/LoadingContext.jsx +0 -27
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +0 -72
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +0 -245
- jettask/webui/frontend/src/index.css +0 -114
- jettask/webui/frontend/src/main.jsx +0 -20
- jettask/webui/frontend/src/pages/Alerts.jsx +0 -684
- jettask/webui/frontend/src/pages/Dashboard/index.css +0 -35
- jettask/webui/frontend/src/pages/Dashboard/index.jsx +0 -281
- jettask/webui/frontend/src/pages/Dashboard.jsx +0 -1330
- jettask/webui/frontend/src/pages/QueueDetail.jsx +0 -1117
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +0 -527
- jettask/webui/frontend/src/pages/Queues.jsx +0 -12
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +0 -809
- jettask/webui/frontend/src/pages/Settings.jsx +0 -800
- jettask/webui/frontend/src/pages/Workers.jsx +0 -12
- jettask/webui/frontend/src/services/api.js +0 -114
- jettask/webui/frontend/src/services/queueTrend.js +0 -152
- jettask/webui/frontend/src/utils/suppressWarnings.js +0 -22
- jettask/webui/frontend/src/utils/userPreferences.js +0 -154
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/WHEEL +0 -0
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/licenses/LICENSE +0 -0
- {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;
|