jettask 0.2.18__py3-none-any.whl → 0.2.20__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/__init__.py +60 -2
- jettask/cli.py +314 -228
- jettask/config/__init__.py +9 -1
- jettask/config/config.py +245 -0
- jettask/config/env_loader.py +381 -0
- jettask/config/lua_scripts.py +158 -0
- jettask/config/nacos_config.py +132 -5
- jettask/core/__init__.py +1 -1
- jettask/core/app.py +1573 -666
- jettask/core/app_importer.py +33 -16
- jettask/core/container.py +532 -0
- jettask/core/task.py +1 -4
- jettask/core/unified_manager_base.py +2 -2
- jettask/executor/__init__.py +38 -0
- jettask/executor/core.py +625 -0
- jettask/executor/executor.py +338 -0
- jettask/executor/orchestrator.py +290 -0
- jettask/executor/process_entry.py +638 -0
- jettask/executor/task_executor.py +317 -0
- jettask/messaging/__init__.py +68 -0
- jettask/messaging/event_pool.py +2188 -0
- jettask/messaging/reader.py +519 -0
- jettask/messaging/registry.py +266 -0
- jettask/messaging/scanner.py +369 -0
- jettask/messaging/sender.py +312 -0
- jettask/persistence/__init__.py +118 -0
- jettask/persistence/backlog_monitor.py +567 -0
- jettask/{backend/data_access.py → persistence/base.py} +58 -57
- jettask/persistence/consumer.py +315 -0
- jettask/{core → persistence}/db_manager.py +23 -22
- jettask/persistence/maintenance.py +81 -0
- jettask/persistence/message_consumer.py +259 -0
- jettask/{backend/namespace_data_access.py → persistence/namespace.py} +66 -98
- jettask/persistence/offline_recovery.py +196 -0
- jettask/persistence/queue_discovery.py +215 -0
- jettask/persistence/task_persistence.py +218 -0
- jettask/persistence/task_updater.py +583 -0
- jettask/scheduler/__init__.py +2 -2
- jettask/scheduler/loader.py +6 -5
- jettask/scheduler/run_scheduler.py +1 -1
- jettask/scheduler/scheduler.py +7 -7
- jettask/scheduler/{unified_scheduler_manager.py → scheduler_coordinator.py} +18 -13
- jettask/task/__init__.py +16 -0
- jettask/{router.py → task/router.py} +26 -8
- jettask/task/task_center/__init__.py +9 -0
- jettask/task/task_executor.py +318 -0
- jettask/task/task_registry.py +291 -0
- jettask/test_connection_monitor.py +73 -0
- jettask/utils/__init__.py +31 -1
- jettask/{monitor/run_backlog_collector.py → utils/backlog_collector.py} +1 -1
- jettask/utils/db_connector.py +1629 -0
- jettask/{db_init.py → utils/db_init.py} +1 -1
- jettask/utils/rate_limit/__init__.py +30 -0
- jettask/utils/rate_limit/concurrency_limiter.py +665 -0
- jettask/utils/rate_limit/config.py +145 -0
- jettask/utils/rate_limit/limiter.py +41 -0
- jettask/utils/rate_limit/manager.py +269 -0
- jettask/utils/rate_limit/qps_limiter.py +154 -0
- jettask/utils/rate_limit/task_limiter.py +384 -0
- jettask/utils/serializer.py +3 -0
- jettask/{monitor/stream_backlog_monitor.py → utils/stream_backlog.py} +14 -6
- jettask/utils/time_sync.py +173 -0
- jettask/webui/__init__.py +27 -0
- jettask/{api/v1 → webui/api}/alerts.py +1 -1
- jettask/{api/v1 → webui/api}/analytics.py +2 -2
- jettask/{api/v1 → webui/api}/namespaces.py +1 -1
- jettask/{api/v1 → webui/api}/overview.py +1 -1
- jettask/{api/v1 → webui/api}/queues.py +3 -3
- jettask/{api/v1 → webui/api}/scheduled.py +1 -1
- jettask/{api/v1 → webui/api}/settings.py +1 -1
- jettask/{api.py → webui/app.py} +253 -145
- jettask/webui/namespace_manager/__init__.py +10 -0
- jettask/{multi_namespace_consumer.py → webui/namespace_manager/multi.py} +69 -22
- jettask/{unified_consumer_manager.py → webui/namespace_manager/unified.py} +1 -1
- jettask/{run.py → webui/run.py} +2 -2
- jettask/{services → webui/services}/__init__.py +1 -3
- jettask/{services → webui/services}/overview_service.py +34 -16
- jettask/{services → webui/services}/queue_service.py +1 -1
- jettask/{backend → webui/services}/queue_stats_v2.py +1 -1
- jettask/{services → webui/services}/settings_service.py +1 -1
- jettask/worker/__init__.py +53 -0
- jettask/worker/lifecycle.py +1507 -0
- jettask/worker/manager.py +583 -0
- jettask/{core/offline_worker_recovery.py → worker/recovery.py} +268 -175
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/METADATA +2 -71
- jettask-0.2.20.dist-info/RECORD +145 -0
- jettask/__main__.py +0 -140
- jettask/api/__init__.py +0 -103
- jettask/backend/__init__.py +0 -1
- jettask/backend/api/__init__.py +0 -3
- jettask/backend/api/v1/__init__.py +0 -17
- jettask/backend/api/v1/monitoring.py +0 -431
- jettask/backend/api/v1/namespaces.py +0 -504
- jettask/backend/api/v1/queues.py +0 -342
- jettask/backend/api/v1/tasks.py +0 -367
- jettask/backend/core/__init__.py +0 -3
- jettask/backend/core/cache.py +0 -221
- jettask/backend/core/database.py +0 -200
- jettask/backend/core/exceptions.py +0 -102
- jettask/backend/dependencies.py +0 -261
- jettask/backend/init_meta_db.py +0 -158
- jettask/backend/main.py +0 -1426
- jettask/backend/main_unified.py +0 -78
- jettask/backend/main_v2.py +0 -394
- jettask/backend/models/__init__.py +0 -3
- jettask/backend/models/requests.py +0 -236
- jettask/backend/models/responses.py +0 -230
- jettask/backend/namespace_api_old.py +0 -267
- jettask/backend/services/__init__.py +0 -3
- jettask/backend/start.py +0 -42
- jettask/backend/unified_api_router.py +0 -1541
- jettask/cleanup_deprecated_tables.sql +0 -16
- jettask/core/consumer_manager.py +0 -1695
- jettask/core/delay_scanner.py +0 -256
- jettask/core/event_pool.py +0 -1700
- jettask/core/heartbeat_process.py +0 -222
- jettask/core/task_batch.py +0 -153
- jettask/core/worker_scanner.py +0 -271
- jettask/executors/__init__.py +0 -5
- jettask/executors/asyncio.py +0 -876
- jettask/executors/base.py +0 -30
- jettask/executors/common.py +0 -148
- jettask/executors/multi_asyncio.py +0 -309
- jettask/gradio_app.py +0 -570
- jettask/integrated_gradio_app.py +0 -1088
- jettask/main.py +0 -0
- jettask/monitoring/__init__.py +0 -3
- jettask/pg_consumer.py +0 -1896
- jettask/run_monitor.py +0 -22
- jettask/run_webui.py +0 -148
- jettask/scheduler/multi_namespace_scheduler.py +0 -294
- jettask/scheduler/unified_manager.py +0 -450
- jettask/task_center_client.py +0 -150
- jettask/utils/serializer_optimized.py +0 -33
- jettask/webui_exceptions.py +0 -67
- jettask-0.2.18.dist-info/RECORD +0 -150
- /jettask/{constants.py → config/constants.py} +0 -0
- /jettask/{backend/config.py → config/task_center.py} +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/pg_consumer_v2.py +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/add_execution_time_field.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_new_tables.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_tables_v3.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/migrate_to_new_structure.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/modify_time_fields.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql_utils.py +0 -0
- /jettask/{models.py → persistence/models.py} +0 -0
- /jettask/scheduler/{manager.py → task_crud.py} +0 -0
- /jettask/{schema.sql → schemas/schema.sql} +0 -0
- /jettask/{task_center.py → task/task_center/client.py} +0 -0
- /jettask/{monitoring → utils}/file_watcher.py +0 -0
- /jettask/{services/redis_monitor_service.py → utils/redis_monitor.py} +0 -0
- /jettask/{api/v1 → webui/api}/__init__.py +0 -0
- /jettask/{webui_config.py → webui/config.py} +0 -0
- /jettask/{webui_models → webui/models}/__init__.py +0 -0
- /jettask/{webui_models → webui/models}/namespace.py +0 -0
- /jettask/{services → webui/services}/alert_service.py +0 -0
- /jettask/{services → webui/services}/analytics_service.py +0 -0
- /jettask/{services → webui/services}/scheduled_task_service.py +0 -0
- /jettask/{services → webui/services}/task_service.py +0 -0
- /jettask/{webui_sql → webui/sql}/batch_upsert_functions.sql +0 -0
- /jettask/{webui_sql → webui/sql}/verify_database.sql +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/WHEEL +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/top_level.txt +0 -0
@@ -1,450 +0,0 @@
|
|
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.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}/v1/namespaces"
|
137
|
-
else:
|
138
|
-
url = f"{self.task_center_url}/api/v1/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}/v1/namespaces/{namespace}"
|
227
|
-
else:
|
228
|
-
namespace_url = f"{self.task_center_url}/api/v1/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()
|
jettask/task_center_client.py
DELETED
@@ -1,150 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
任务中心客户端 - JetTask App使用的客户端库
|
3
|
-
"""
|
4
|
-
import os
|
5
|
-
import aiohttp
|
6
|
-
import asyncio
|
7
|
-
from typing import Optional, Dict, Any
|
8
|
-
from .models.namespace import TaskCenterConfig
|
9
|
-
import logging
|
10
|
-
|
11
|
-
logger = logging.getLogger(__name__)
|
12
|
-
|
13
|
-
|
14
|
-
class TaskCenterClient:
|
15
|
-
"""任务中心客户端"""
|
16
|
-
|
17
|
-
def __init__(self, task_center_url: Optional[str] = None):
|
18
|
-
"""
|
19
|
-
初始化任务中心客户端
|
20
|
-
|
21
|
-
Args:
|
22
|
-
task_center_url: 任务中心配置URL,支持两种格式:
|
23
|
-
- HTTP格式: http://localhost:8001/api/namespaces/{namespace_id}
|
24
|
-
- 旧格式: taskcenter://namespace/{namespace_id} (向后兼容)
|
25
|
-
"""
|
26
|
-
self.task_center_url = task_center_url
|
27
|
-
self._session: Optional[aiohttp.ClientSession] = None
|
28
|
-
self._cached_config: Optional[Dict[str, Any]] = None
|
29
|
-
self._namespace_id: Optional[str] = None
|
30
|
-
self._namespace_name: Optional[str] = None # 添加命名空间名称
|
31
|
-
|
32
|
-
# 解析URL
|
33
|
-
self._config_url = task_center_url # 用于获取配置的URL
|
34
|
-
if task_center_url:
|
35
|
-
if task_center_url.startswith("http://") or task_center_url.startswith("https://"):
|
36
|
-
# HTTP格式,支持新格式: http://localhost:8001/api/namespaces/{name}
|
37
|
-
import re
|
38
|
-
# 匹配新格式:使用名称而非ID
|
39
|
-
match = re.search(r'/namespaces/([^/]+)$', task_center_url)
|
40
|
-
if match:
|
41
|
-
self._namespace_name = match.group(1)
|
42
|
-
# 新格式直接使用URL,不需要添加/config
|
43
|
-
elif task_center_url.startswith("taskcenter://"):
|
44
|
-
# 旧格式,转换为HTTP格式
|
45
|
-
parts = task_center_url.replace("taskcenter://", "").split("/")
|
46
|
-
if len(parts) >= 2 and parts[0] == "namespace":
|
47
|
-
# 旧格式使用的是ID,转换为按名称查找
|
48
|
-
self._namespace_name = parts[1] # 假设传入的也是名称
|
49
|
-
base_url = os.getenv("TASK_CENTER_BASE_URL", "http://localhost:8001")
|
50
|
-
self._config_url = f"{base_url}/api/v1/namespaces/{self._namespace_name}"
|
51
|
-
|
52
|
-
@property
|
53
|
-
def is_enabled(self) -> bool:
|
54
|
-
"""是否启用任务中心"""
|
55
|
-
return self.task_center_url is not None
|
56
|
-
|
57
|
-
@property
|
58
|
-
def namespace_id(self) -> Optional[str]:
|
59
|
-
"""获取命名空间ID"""
|
60
|
-
return self._namespace_id
|
61
|
-
|
62
|
-
@property
|
63
|
-
def namespace_prefix(self) -> str:
|
64
|
-
"""获取Redis key前缀"""
|
65
|
-
# 优先使用namespace_name,不再添加tc:前缀
|
66
|
-
if self._namespace_name:
|
67
|
-
return self._namespace_name
|
68
|
-
# 如果配置已加载,使用其中的名称
|
69
|
-
elif self._cached_config and self._cached_config.get('namespace_name'):
|
70
|
-
return self._cached_config['namespace_name']
|
71
|
-
# 默认前缀
|
72
|
-
return "jettask"
|
73
|
-
|
74
|
-
async def _get_session(self) -> aiohttp.ClientSession:
|
75
|
-
"""获取HTTP会话"""
|
76
|
-
if self._session is None or self._session.closed:
|
77
|
-
self._session = aiohttp.ClientSession()
|
78
|
-
return self._session
|
79
|
-
|
80
|
-
async def get_config(self) -> Optional[Dict[str, Any]]:
|
81
|
-
"""
|
82
|
-
从任务中心获取配置
|
83
|
-
|
84
|
-
Returns:
|
85
|
-
包含redis_config和pg_config的字典
|
86
|
-
"""
|
87
|
-
if not self.is_enabled:
|
88
|
-
return None
|
89
|
-
|
90
|
-
# 如果已缓存,直接返回
|
91
|
-
if self._cached_config:
|
92
|
-
return self._cached_config
|
93
|
-
|
94
|
-
try:
|
95
|
-
session = await self._get_session()
|
96
|
-
# 使用配置URL(可能带有/config后缀)
|
97
|
-
async with session.get(self._config_url) as resp:
|
98
|
-
if resp.status == 200:
|
99
|
-
data = await resp.json()
|
100
|
-
self._namespace_name = data.get('name') # 保存命名空间名称
|
101
|
-
self._cached_config = {
|
102
|
-
'redis_config': data.get('redis_config'),
|
103
|
-
'pg_config': data.get('pg_config'),
|
104
|
-
'namespace_name': data.get('name'),
|
105
|
-
'namespace_id': self._namespace_id,
|
106
|
-
'version': data.get('version', 1) # 添加版本号
|
107
|
-
}
|
108
|
-
return self._cached_config
|
109
|
-
else:
|
110
|
-
logger.error(f"Failed to get config from task center: {resp.status}")
|
111
|
-
return None
|
112
|
-
except Exception as e:
|
113
|
-
logger.error(f"Error getting config from task center: {e}")
|
114
|
-
return None
|
115
|
-
|
116
|
-
async def get_task_result(self, task_id: str) -> Optional[Dict[str, Any]]:
|
117
|
-
"""
|
118
|
-
从任务中心获取任务结果(当Redis中不存在时)
|
119
|
-
|
120
|
-
Args:
|
121
|
-
task_id: 任务ID
|
122
|
-
|
123
|
-
Returns:
|
124
|
-
任务结果字典
|
125
|
-
"""
|
126
|
-
if not self.is_enabled or not self._namespace_id:
|
127
|
-
return None
|
128
|
-
|
129
|
-
try:
|
130
|
-
session = await self._get_session()
|
131
|
-
# 从配置URL推导出任务结果URL
|
132
|
-
base_url = self.task_center_url.replace(f"/namespace/{self._namespace_id}/config", "")
|
133
|
-
url = f"{base_url}/namespace/{self._namespace_id}/task/{task_id}/result"
|
134
|
-
async with session.get(url) as resp:
|
135
|
-
if resp.status == 200:
|
136
|
-
return await resp.json()
|
137
|
-
elif resp.status == 404:
|
138
|
-
return None
|
139
|
-
else:
|
140
|
-
logger.error(f"Failed to get task result from task center: {resp.status}")
|
141
|
-
return None
|
142
|
-
except Exception as e:
|
143
|
-
logger.error(f"Error getting task result from task center: {e}")
|
144
|
-
return None
|
145
|
-
|
146
|
-
|
147
|
-
async def close(self):
|
148
|
-
"""关闭客户端"""
|
149
|
-
if self._session:
|
150
|
-
await self._session.close()
|
@@ -1,33 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
优化的 MessagePack 序列化工具模块
|
3
|
-
直接使用二进制,避免 Base64 开销
|
4
|
-
"""
|
5
|
-
import msgpack
|
6
|
-
|
7
|
-
|
8
|
-
def dumps(obj):
|
9
|
-
"""序列化对象为字节"""
|
10
|
-
return msgpack.packb(obj, use_bin_type=True)
|
11
|
-
|
12
|
-
|
13
|
-
def loads(data):
|
14
|
-
"""反序列化字节为对象"""
|
15
|
-
if isinstance(data, str):
|
16
|
-
# 兼容性:如果收到字符串,尝试作为 latin-1 解码
|
17
|
-
data = data.encode('latin-1')
|
18
|
-
return msgpack.unpackb(data, raw=False, strict_map_key=False)
|
19
|
-
|
20
|
-
|
21
|
-
# 为了兼容性,保留 dumps_str/loads_str 名称,但不再使用 base64
|
22
|
-
def dumps_str(obj):
|
23
|
-
"""序列化对象为字节(不再使用 base64)"""
|
24
|
-
return msgpack.packb(obj, use_bin_type=True)
|
25
|
-
|
26
|
-
|
27
|
-
def loads_str(data):
|
28
|
-
"""反序列化字节为对象"""
|
29
|
-
return msgpack.unpackb(data, raw=False, strict_map_key=False)
|
30
|
-
|
31
|
-
|
32
|
-
# 导出使用的序列化器名称
|
33
|
-
SERIALIZER = "msgpack_binary"
|