jettask 0.2.4__py3-none-any.whl → 0.2.6__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/core/cli.py +20 -24
- 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.4.dist-info → jettask-0.2.6.dist-info}/METADATA +11 -9
- {jettask-0.2.4.dist-info → jettask-0.2.6.dist-info}/RECORD +47 -54
- 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.4.dist-info → jettask-0.2.6.dist-info}/WHEEL +0 -0
- {jettask-0.2.4.dist-info → jettask-0.2.6.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.4.dist-info → jettask-0.2.6.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.4.dist-info → jettask-0.2.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
-- 定时任务表
|
2
|
+
CREATE TABLE IF NOT EXISTS scheduled_tasks (
|
3
|
+
id BIGSERIAL PRIMARY KEY, -- 自增主键(任务唯一标识)
|
4
|
+
scheduler_id VARCHAR(255) NOT NULL UNIQUE, -- 任务的唯一标识符(必填,用于去重)
|
5
|
+
task_name VARCHAR(255) NOT NULL, -- 要执行的函数名(对应@app.task注册的任务名)
|
6
|
+
task_type VARCHAR(50) NOT NULL, -- 任务类型: cron, interval, once
|
7
|
+
|
8
|
+
-- 任务执行相关
|
9
|
+
queue_name VARCHAR(100) NOT NULL, -- 目标队列名
|
10
|
+
task_args JSONB DEFAULT '[]', -- 任务参数
|
11
|
+
task_kwargs JSONB DEFAULT '{}', -- 任务关键字参数
|
12
|
+
|
13
|
+
-- 调度相关
|
14
|
+
cron_expression VARCHAR(100), -- cron表达式 (task_type=cron时使用)
|
15
|
+
interval_seconds NUMERIC(10,2), -- 间隔秒数 (task_type=interval时使用,支持小数)
|
16
|
+
next_run_time TIMESTAMP WITH TIME ZONE, -- 下次执行时间
|
17
|
+
last_run_time TIMESTAMP WITH TIME ZONE, -- 上次执行时间
|
18
|
+
|
19
|
+
-- 状态和控制
|
20
|
+
enabled BOOLEAN DEFAULT true, -- 是否启用
|
21
|
+
max_retries INTEGER DEFAULT 3, -- 最大重试次数
|
22
|
+
retry_delay INTEGER DEFAULT 60, -- 重试延迟(秒)
|
23
|
+
timeout INTEGER DEFAULT 300, -- 任务超时时间(秒)
|
24
|
+
priority INTEGER DEFAULT NULL, -- 任务优先级 (1=最高, 数字越大优先级越低,NULL=默认最低)
|
25
|
+
|
26
|
+
-- 元数据
|
27
|
+
description TEXT, -- 任务描述
|
28
|
+
tags JSONB DEFAULT '[]', -- 标签
|
29
|
+
metadata JSONB DEFAULT '{}', -- 额外元数据
|
30
|
+
|
31
|
+
-- 时间戳
|
32
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
33
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
34
|
+
);
|
35
|
+
|
36
|
+
-- 索引
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_next_run ON scheduled_tasks(next_run_time) WHERE enabled = true;
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_task_type ON scheduled_tasks(task_type);
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_queue ON scheduled_tasks(queue_name);
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_enabled ON scheduled_tasks(enabled);
|
41
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_scheduled_tasks_scheduler_id ON scheduled_tasks(scheduler_id);
|
42
|
+
|
43
|
+
-- 任务执行历史表
|
44
|
+
CREATE TABLE IF NOT EXISTS task_execution_history (
|
45
|
+
id BIGSERIAL PRIMARY KEY,
|
46
|
+
task_id BIGINT NOT NULL, -- 关联的任务ID(外键到 scheduled_tasks.id)
|
47
|
+
event_id VARCHAR(255) NOT NULL, -- 执行事件ID
|
48
|
+
|
49
|
+
-- 执行信息
|
50
|
+
scheduled_time TIMESTAMP WITH TIME ZONE NOT NULL, -- 计划执行时间
|
51
|
+
started_at TIMESTAMP WITH TIME ZONE, -- 实际开始时间
|
52
|
+
finished_at TIMESTAMP WITH TIME ZONE, -- 完成时间
|
53
|
+
|
54
|
+
-- 执行结果
|
55
|
+
status VARCHAR(50) NOT NULL, -- pending, running, success, failed, timeout
|
56
|
+
result JSONB, -- 执行结果
|
57
|
+
error_message TEXT, -- 错误信息
|
58
|
+
retry_count INTEGER DEFAULT 0, -- 重试次数
|
59
|
+
|
60
|
+
-- 性能指标
|
61
|
+
duration_ms INTEGER, -- 执行耗时(毫秒)
|
62
|
+
worker_id VARCHAR(100), -- 执行的worker ID
|
63
|
+
|
64
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
65
|
+
);
|
66
|
+
|
67
|
+
-- 索引
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_task_history_task_id ON task_execution_history(task_id);
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_task_history_event_id ON task_execution_history(event_id);
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_task_history_status ON task_execution_history(status);
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_task_history_scheduled ON task_execution_history(scheduled_time);
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_task_history_created ON task_execution_history(created_at);
|
73
|
+
|
74
|
+
-- 更新时间触发器
|
75
|
+
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
76
|
+
RETURNS TRIGGER AS $$
|
77
|
+
BEGIN
|
78
|
+
NEW.updated_at = CURRENT_TIMESTAMP;
|
79
|
+
RETURN NEW;
|
80
|
+
END;
|
81
|
+
$$ language 'plpgsql';
|
82
|
+
|
83
|
+
CREATE TRIGGER update_scheduled_tasks_updated_at BEFORE UPDATE
|
84
|
+
ON scheduled_tasks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
@@ -0,0 +1,450 @@
|
|
1
|
+
"""
|
2
|
+
统一的定时任务调度管理器
|
3
|
+
自动识别单命名空间和多命名空间模式
|
4
|
+
"""
|
5
|
+
import asyncio
|
6
|
+
import logging
|
7
|
+
import multiprocessing
|
8
|
+
import re
|
9
|
+
from typing import Dict, Set, Optional, List
|
10
|
+
import aiohttp
|
11
|
+
import traceback
|
12
|
+
|
13
|
+
from jettask import Jettask
|
14
|
+
from jettask.webui.task_center import TaskCenter
|
15
|
+
from .scheduler import TaskScheduler
|
16
|
+
from .manager import ScheduledTaskManager
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class UnifiedSchedulerManager:
|
22
|
+
"""
|
23
|
+
统一的调度器管理器
|
24
|
+
根据task_center_url自动判断是单命名空间还是多命名空间模式
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(self,
|
28
|
+
task_center_url: str,
|
29
|
+
scan_interval: float = 0.1,
|
30
|
+
batch_size: int = 100,
|
31
|
+
check_interval: int = 30,
|
32
|
+
debug: bool = False):
|
33
|
+
"""
|
34
|
+
初始化统一调度器管理器
|
35
|
+
|
36
|
+
Args:
|
37
|
+
task_center_url: 任务中心URL
|
38
|
+
- 单命名空间: http://localhost:8001/api/namespaces/{name}
|
39
|
+
- 多命名空间: http://localhost:8001 或 http://localhost:8001/api
|
40
|
+
scan_interval: 调度器扫描间隔(秒)
|
41
|
+
batch_size: 每批处理的最大任务数
|
42
|
+
check_interval: 命名空间检测间隔(秒),仅多命名空间模式使用
|
43
|
+
debug: 是否启用调试模式
|
44
|
+
"""
|
45
|
+
self.task_center_url = task_center_url.rstrip('/')
|
46
|
+
self.scan_interval = scan_interval
|
47
|
+
self.batch_size = batch_size
|
48
|
+
self.check_interval = check_interval
|
49
|
+
self.debug = debug
|
50
|
+
|
51
|
+
# 判断模式
|
52
|
+
self.namespace_name: Optional[str] = None
|
53
|
+
self.is_single_namespace = self._detect_mode()
|
54
|
+
|
55
|
+
# 单命名空间模式:直接管理TaskScheduler
|
56
|
+
self.scheduler_instance: Optional[TaskScheduler] = None
|
57
|
+
|
58
|
+
# 多命名空间模式:管理多个进程
|
59
|
+
self.scheduler_processes: Dict[str, multiprocessing.Process] = {}
|
60
|
+
|
61
|
+
self.running = False
|
62
|
+
|
63
|
+
# 设置日志
|
64
|
+
if debug:
|
65
|
+
logging.basicConfig(level=logging.DEBUG)
|
66
|
+
else:
|
67
|
+
logging.basicConfig(level=logging.INFO)
|
68
|
+
|
69
|
+
def _detect_mode(self) -> bool:
|
70
|
+
"""
|
71
|
+
检测是单命名空间还是多命名空间模式
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
True: 单命名空间模式
|
75
|
+
False: 多命名空间模式
|
76
|
+
"""
|
77
|
+
# 检查URL格式
|
78
|
+
# 单命名空间: /api/namespaces/{name}
|
79
|
+
# 多命名空间: 不包含 /api/namespaces/ 或以 /api 结尾
|
80
|
+
|
81
|
+
if '/api/namespaces/' in self.task_center_url:
|
82
|
+
# 提取命名空间名称
|
83
|
+
match = re.search(r'/api/namespaces/([^/]+)/?$', self.task_center_url)
|
84
|
+
if match:
|
85
|
+
self.namespace_name = match.group(1)
|
86
|
+
logger.info(f"检测到单命名空间模式: {self.namespace_name}")
|
87
|
+
return True
|
88
|
+
|
89
|
+
# 多命名空间模式
|
90
|
+
logger.info("检测到多命名空间模式")
|
91
|
+
return False
|
92
|
+
|
93
|
+
async def run(self):
|
94
|
+
"""运行调度器管理器(统一处理单/多命名空间)"""
|
95
|
+
self.running = True
|
96
|
+
|
97
|
+
logger.info(f"启动调度器管理器")
|
98
|
+
logger.info(f"任务中心: {self.task_center_url}")
|
99
|
+
logger.info(f"模式: {'单命名空间' if self.is_single_namespace else '多命名空间'}")
|
100
|
+
if not self.is_single_namespace:
|
101
|
+
logger.info(f"命名空间检测间隔: {self.check_interval} 秒")
|
102
|
+
logger.info(f"调度器扫描间隔: {self.scan_interval} 秒")
|
103
|
+
logger.info(f"批处理大小: {self.batch_size}")
|
104
|
+
|
105
|
+
# 初始检查和启动
|
106
|
+
await self._check_and_update_schedulers()
|
107
|
+
|
108
|
+
# 如果是单命名空间,不需要定期检查
|
109
|
+
if self.is_single_namespace:
|
110
|
+
# 等待直到停止
|
111
|
+
while self.running:
|
112
|
+
await asyncio.sleep(1)
|
113
|
+
else:
|
114
|
+
# 多命名空间模式:定期检查命名空间变化
|
115
|
+
while self.running:
|
116
|
+
try:
|
117
|
+
await asyncio.sleep(self.check_interval)
|
118
|
+
await self._check_and_update_schedulers()
|
119
|
+
except asyncio.CancelledError:
|
120
|
+
break
|
121
|
+
except Exception as e:
|
122
|
+
logger.error(f"检查命名空间时出错: {e}")
|
123
|
+
if self.debug:
|
124
|
+
traceback.print_exc()
|
125
|
+
|
126
|
+
async def _get_active_namespaces(self) -> Set[str]:
|
127
|
+
"""获取所有活跃的命名空间"""
|
128
|
+
# 单命名空间模式:直接返回指定的命名空间
|
129
|
+
if self.is_single_namespace:
|
130
|
+
return {self.namespace_name} if self.namespace_name else set()
|
131
|
+
|
132
|
+
# 多命名空间模式:从API获取
|
133
|
+
try:
|
134
|
+
# 构建API URL
|
135
|
+
if self.task_center_url.endswith('/api'):
|
136
|
+
url = f"{self.task_center_url}/namespaces"
|
137
|
+
else:
|
138
|
+
url = f"{self.task_center_url}/api/namespaces"
|
139
|
+
|
140
|
+
async with aiohttp.ClientSession() as session:
|
141
|
+
async with session.get(url) as response:
|
142
|
+
if response.status == 200:
|
143
|
+
namespaces = await response.json()
|
144
|
+
if namespaces:
|
145
|
+
active_names = {ns['name'] for ns in namespaces if ns.get('name')}
|
146
|
+
logger.info(f"发现 {len(active_names)} 个活跃的命名空间: {active_names}")
|
147
|
+
return active_names
|
148
|
+
else:
|
149
|
+
logger.warning("没有找到任何命名空间")
|
150
|
+
return set()
|
151
|
+
else:
|
152
|
+
logger.error(f"获取命名空间列表失败,状态码: {response.status}")
|
153
|
+
return set()
|
154
|
+
|
155
|
+
except Exception as e:
|
156
|
+
logger.error(f"获取命名空间列表失败: {e}")
|
157
|
+
return set()
|
158
|
+
|
159
|
+
async def _check_and_update_schedulers(self):
|
160
|
+
"""检查并更新调度器(添加新的,停止已删除的)"""
|
161
|
+
# 获取当前活跃的命名空间
|
162
|
+
active_namespaces = await self._get_active_namespaces()
|
163
|
+
current_namespaces = set(self.scheduler_processes.keys())
|
164
|
+
|
165
|
+
# 找出需要添加的命名空间
|
166
|
+
to_add = active_namespaces - current_namespaces
|
167
|
+
# 找出需要删除的命名空间
|
168
|
+
to_remove = current_namespaces - active_namespaces
|
169
|
+
|
170
|
+
# 启动新的调度器
|
171
|
+
for namespace in to_add:
|
172
|
+
logger.info(f"检测到新命名空间: {namespace}")
|
173
|
+
self._start_scheduler_for_namespace(namespace)
|
174
|
+
|
175
|
+
# 停止已删除的调度器
|
176
|
+
for namespace in to_remove:
|
177
|
+
logger.info(f"检测到命名空间已删除: {namespace}")
|
178
|
+
self._stop_scheduler_for_namespace(namespace)
|
179
|
+
|
180
|
+
# 检查现有进程的健康状态
|
181
|
+
for namespace in active_namespaces & current_namespaces:
|
182
|
+
process = self.scheduler_processes.get(namespace)
|
183
|
+
if process and not process.is_alive():
|
184
|
+
logger.warning(f"调度器进程 {namespace} 已停止,重新启动")
|
185
|
+
del self.scheduler_processes[namespace]
|
186
|
+
self._start_scheduler_for_namespace(namespace)
|
187
|
+
|
188
|
+
def _start_scheduler_for_namespace(self, namespace: str):
|
189
|
+
"""为指定命名空间启动调度器"""
|
190
|
+
# 单命名空间模式:直接在当前进程运行
|
191
|
+
if self.is_single_namespace:
|
192
|
+
# 构建命名空间URL(单命名空间已经有完整URL)
|
193
|
+
namespace_url = self.task_center_url
|
194
|
+
|
195
|
+
# 创建异步任务运行调度器
|
196
|
+
async def run_single_scheduler():
|
197
|
+
await _run_scheduler_async(
|
198
|
+
namespace=namespace,
|
199
|
+
task_center_url=namespace_url,
|
200
|
+
scan_interval=self.scan_interval,
|
201
|
+
batch_size=self.batch_size,
|
202
|
+
debug=self.debug
|
203
|
+
)
|
204
|
+
|
205
|
+
# 创建任务
|
206
|
+
task = asyncio.create_task(run_single_scheduler())
|
207
|
+
# 保存任务引用(使用相同的字典结构,方便统一管理)
|
208
|
+
self.scheduler_processes[namespace] = task
|
209
|
+
logger.info(f"启动命名空间 {namespace} 的调度器(同进程)")
|
210
|
+
return
|
211
|
+
|
212
|
+
# 多命名空间模式:创建独立进程
|
213
|
+
if namespace in self.scheduler_processes:
|
214
|
+
process = self.scheduler_processes[namespace]
|
215
|
+
if isinstance(process, multiprocessing.Process) and process.is_alive():
|
216
|
+
return
|
217
|
+
else:
|
218
|
+
logger.info(f"清理已停止的调度器: {namespace}")
|
219
|
+
if isinstance(process, multiprocessing.Process):
|
220
|
+
process.terminate()
|
221
|
+
process.join(timeout=5)
|
222
|
+
del self.scheduler_processes[namespace]
|
223
|
+
|
224
|
+
# 构建命名空间URL
|
225
|
+
if self.task_center_url.endswith('/api'):
|
226
|
+
namespace_url = f"{self.task_center_url}/namespaces/{namespace}"
|
227
|
+
else:
|
228
|
+
namespace_url = f"{self.task_center_url}/api/namespaces/{namespace}"
|
229
|
+
|
230
|
+
# 创建新进程
|
231
|
+
process = multiprocessing.Process(
|
232
|
+
target=_run_scheduler_in_process,
|
233
|
+
args=(
|
234
|
+
namespace,
|
235
|
+
namespace_url,
|
236
|
+
self.scan_interval,
|
237
|
+
self.batch_size,
|
238
|
+
self.debug
|
239
|
+
),
|
240
|
+
name=f"scheduler_{namespace}"
|
241
|
+
)
|
242
|
+
|
243
|
+
process.start()
|
244
|
+
self.scheduler_processes[namespace] = process
|
245
|
+
logger.info(f"启动命名空间 {namespace} 的调度器进程, PID: {process.pid}")
|
246
|
+
|
247
|
+
def _stop_scheduler_for_namespace(self, namespace: str):
|
248
|
+
"""停止指定命名空间的调度器"""
|
249
|
+
if namespace in self.scheduler_processes:
|
250
|
+
scheduler = self.scheduler_processes[namespace]
|
251
|
+
|
252
|
+
# 处理异步任务(单命名空间模式)
|
253
|
+
if isinstance(scheduler, asyncio.Task):
|
254
|
+
if not scheduler.done():
|
255
|
+
logger.info(f"停止命名空间 {namespace} 的调度器任务")
|
256
|
+
scheduler.cancel()
|
257
|
+
|
258
|
+
# 处理进程(多命名空间模式)
|
259
|
+
elif isinstance(scheduler, multiprocessing.Process):
|
260
|
+
if scheduler.is_alive():
|
261
|
+
logger.info(f"停止命名空间 {namespace} 的调度器进程")
|
262
|
+
scheduler.terminate()
|
263
|
+
scheduler.join(timeout=10)
|
264
|
+
|
265
|
+
if scheduler.is_alive():
|
266
|
+
logger.warning(f"强制停止命名空间 {namespace} 的调度器进程")
|
267
|
+
scheduler.kill()
|
268
|
+
scheduler.join(timeout=5)
|
269
|
+
|
270
|
+
del self.scheduler_processes[namespace]
|
271
|
+
|
272
|
+
def stop(self):
|
273
|
+
"""停止管理器"""
|
274
|
+
logger.info("停止调度器管理器")
|
275
|
+
self.running = False
|
276
|
+
|
277
|
+
# 统一处理:停止所有调度器(不管是任务还是进程)
|
278
|
+
for namespace in list(self.scheduler_processes.keys()):
|
279
|
+
self._stop_scheduler_for_namespace(namespace)
|
280
|
+
|
281
|
+
logger.info("调度器管理器已停止")
|
282
|
+
|
283
|
+
def add_scheduler(self, namespace: str, scheduler: TaskScheduler):
|
284
|
+
"""
|
285
|
+
添加TaskScheduler实例(预留接口)
|
286
|
+
|
287
|
+
Args:
|
288
|
+
namespace: 命名空间名称
|
289
|
+
scheduler: TaskScheduler实例
|
290
|
+
"""
|
291
|
+
# 这个方法预留给未来可能的扩展
|
292
|
+
# 比如动态添加调度器而不需要重启
|
293
|
+
pass
|
294
|
+
|
295
|
+
|
296
|
+
async def _run_scheduler_async(namespace: str,
|
297
|
+
task_center_url: str,
|
298
|
+
scan_interval: float,
|
299
|
+
batch_size: int,
|
300
|
+
debug: bool):
|
301
|
+
"""异步运行指定命名空间的调度器(用于单命名空间模式)"""
|
302
|
+
scheduler_instance = None
|
303
|
+
try:
|
304
|
+
logger.info(f"连接到任务中心: {task_center_url}")
|
305
|
+
|
306
|
+
# 连接任务中心
|
307
|
+
tc = TaskCenter(task_center_url)
|
308
|
+
if not tc._connect_sync():
|
309
|
+
logger.error(f"无法连接到任务中心: {namespace}")
|
310
|
+
return
|
311
|
+
|
312
|
+
logger.info(f"成功连接到命名空间: {tc.namespace_name}")
|
313
|
+
|
314
|
+
# 创建app实例
|
315
|
+
app = Jettask(task_center=tc)
|
316
|
+
|
317
|
+
if not app.redis_url or not app.pg_url:
|
318
|
+
logger.error(f"任务中心配置不完整: {namespace}")
|
319
|
+
return
|
320
|
+
|
321
|
+
# 显示配置信息
|
322
|
+
logger.info(f"命名空间 {namespace} 的调度器配置:")
|
323
|
+
logger.info(f" Redis: {app.redis_url}")
|
324
|
+
logger.info(f" PostgreSQL: {app.pg_url}")
|
325
|
+
logger.info(f" 间隔: {scan_interval} 秒")
|
326
|
+
logger.info(f" 批大小: {batch_size}")
|
327
|
+
|
328
|
+
# 创建调度器实例
|
329
|
+
db_manager = ScheduledTaskManager(app)
|
330
|
+
scheduler_instance = TaskScheduler(
|
331
|
+
app=app,
|
332
|
+
db_manager=db_manager,
|
333
|
+
scan_interval=scan_interval,
|
334
|
+
batch_size=batch_size
|
335
|
+
)
|
336
|
+
|
337
|
+
# 运行调度器
|
338
|
+
logger.info(f"启动命名空间 {namespace} 的调度器...")
|
339
|
+
await scheduler_instance.run()
|
340
|
+
|
341
|
+
except asyncio.CancelledError:
|
342
|
+
logger.info(f"调度器 {namespace} 收到取消信号")
|
343
|
+
except KeyboardInterrupt:
|
344
|
+
logger.info(f"调度器 {namespace} 收到中断信号")
|
345
|
+
except Exception as e:
|
346
|
+
logger.error(f"调度器 {namespace} 运行错误: {e}")
|
347
|
+
if debug:
|
348
|
+
traceback.print_exc()
|
349
|
+
finally:
|
350
|
+
if scheduler_instance:
|
351
|
+
scheduler_instance.stop()
|
352
|
+
logger.info(f"调度器 {namespace} 已停止")
|
353
|
+
|
354
|
+
|
355
|
+
def _run_scheduler_in_process(namespace: str,
|
356
|
+
task_center_url: str,
|
357
|
+
scan_interval: float,
|
358
|
+
batch_size: int,
|
359
|
+
debug: bool):
|
360
|
+
"""在独立进程中运行指定命名空间的调度器(用于多命名空间模式)"""
|
361
|
+
import asyncio
|
362
|
+
import logging
|
363
|
+
import signal
|
364
|
+
import sys
|
365
|
+
|
366
|
+
# 设置进程标题(如果可用)
|
367
|
+
try:
|
368
|
+
import setproctitle # type: ignore
|
369
|
+
setproctitle.setproctitle(f"jettask-scheduler-{namespace}")
|
370
|
+
except ImportError:
|
371
|
+
pass
|
372
|
+
|
373
|
+
# 配置日志
|
374
|
+
logging.basicConfig(
|
375
|
+
level=logging.DEBUG if debug else logging.INFO,
|
376
|
+
format=f'%(asctime)s - %(levelname)s - [{namespace}] %(message)s'
|
377
|
+
)
|
378
|
+
logger = logging.getLogger(__name__)
|
379
|
+
|
380
|
+
async def run_scheduler():
|
381
|
+
"""运行调度器的异步函数"""
|
382
|
+
scheduler_instance = None
|
383
|
+
try:
|
384
|
+
logger.info(f"连接到任务中心: {task_center_url}")
|
385
|
+
|
386
|
+
# 连接任务中心
|
387
|
+
tc = TaskCenter(task_center_url)
|
388
|
+
if not tc._connect_sync():
|
389
|
+
logger.error(f"无法连接到任务中心: {namespace}")
|
390
|
+
return
|
391
|
+
|
392
|
+
logger.info(f"成功连接到命名空间: {tc.namespace_name}")
|
393
|
+
|
394
|
+
# 创建app实例
|
395
|
+
app = Jettask(task_center=tc)
|
396
|
+
|
397
|
+
if not app.redis_url or not app.pg_url:
|
398
|
+
logger.error(f"任务中心配置不完整: {namespace}")
|
399
|
+
return
|
400
|
+
|
401
|
+
# 显示配置信息
|
402
|
+
logger.info(f"命名空间 {namespace} 的调度器配置:")
|
403
|
+
logger.info(f" Redis: {app.redis_url}")
|
404
|
+
logger.info(f" PostgreSQL: {app.pg_url}")
|
405
|
+
logger.info(f" 间隔: {scan_interval} 秒")
|
406
|
+
logger.info(f" 批大小: {batch_size}")
|
407
|
+
|
408
|
+
# 创建调度器实例
|
409
|
+
db_manager = ScheduledTaskManager(app)
|
410
|
+
scheduler_instance = TaskScheduler(
|
411
|
+
app=app,
|
412
|
+
db_manager=db_manager,
|
413
|
+
scan_interval=scan_interval,
|
414
|
+
batch_size=batch_size
|
415
|
+
)
|
416
|
+
|
417
|
+
# 运行调度器
|
418
|
+
logger.info(f"启动命名空间 {namespace} 的调度器...")
|
419
|
+
await scheduler_instance.run()
|
420
|
+
|
421
|
+
except asyncio.CancelledError:
|
422
|
+
logger.info(f"调度器 {namespace} 收到取消信号")
|
423
|
+
except KeyboardInterrupt:
|
424
|
+
logger.info(f"调度器 {namespace} 收到中断信号")
|
425
|
+
except Exception as e:
|
426
|
+
logger.error(f"调度器 {namespace} 运行错误: {e}")
|
427
|
+
if debug:
|
428
|
+
traceback.print_exc()
|
429
|
+
finally:
|
430
|
+
if scheduler_instance:
|
431
|
+
scheduler_instance.stop()
|
432
|
+
logger.info(f"调度器 {namespace} 已停止")
|
433
|
+
|
434
|
+
# 设置信号处理
|
435
|
+
def signal_handler(signum, frame):
|
436
|
+
logger.info(f"调度器 {namespace} 收到信号 {signum}")
|
437
|
+
sys.exit(0)
|
438
|
+
|
439
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
440
|
+
signal.signal(signal.SIGINT, signal_handler)
|
441
|
+
|
442
|
+
# 运行调度器
|
443
|
+
try:
|
444
|
+
asyncio.run(run_scheduler())
|
445
|
+
except (KeyboardInterrupt, SystemExit):
|
446
|
+
logger.info(f"调度器 {namespace} 正常退出")
|
447
|
+
except Exception as e:
|
448
|
+
logger.error(f"调度器 {namespace} 异常退出: {e}")
|
449
|
+
if debug:
|
450
|
+
traceback.print_exc()
|