jettask 0.2.1__py3-none-any.whl → 0.2.4__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/constants.py +213 -0
- jettask/core/app.py +525 -205
- jettask/core/cli.py +193 -185
- jettask/core/consumer_manager.py +126 -34
- jettask/core/context.py +3 -0
- jettask/core/enums.py +137 -0
- jettask/core/event_pool.py +501 -168
- jettask/core/message.py +147 -0
- jettask/core/offline_worker_recovery.py +181 -114
- jettask/core/task.py +10 -174
- jettask/core/task_batch.py +153 -0
- jettask/core/unified_manager_base.py +243 -0
- jettask/core/worker_scanner.py +54 -54
- jettask/executors/asyncio.py +184 -64
- jettask/webui/backend/config.py +51 -0
- jettask/webui/backend/data_access.py +2083 -92
- jettask/webui/backend/data_api.py +3294 -0
- jettask/webui/backend/dependencies.py +261 -0
- jettask/webui/backend/init_meta_db.py +158 -0
- jettask/webui/backend/main.py +1358 -69
- jettask/webui/backend/main_unified.py +78 -0
- jettask/webui/backend/main_v2.py +394 -0
- jettask/webui/backend/namespace_api.py +295 -0
- jettask/webui/backend/namespace_api_old.py +294 -0
- jettask/webui/backend/namespace_data_access.py +611 -0
- jettask/webui/backend/queue_backlog_api.py +727 -0
- jettask/webui/backend/queue_stats_v2.py +521 -0
- jettask/webui/backend/redis_monitor_api.py +476 -0
- jettask/webui/backend/unified_api_router.py +1601 -0
- jettask/webui/db_init.py +204 -32
- jettask/webui/frontend/package-lock.json +492 -1
- jettask/webui/frontend/package.json +4 -1
- jettask/webui/frontend/src/App.css +105 -7
- jettask/webui/frontend/src/App.jsx +49 -20
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
- jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
- jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
- jettask/webui/frontend/src/components/layout/Header.css +34 -10
- jettask/webui/frontend/src/components/layout/Header.jsx +31 -23
- jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
- jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
- jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
- jettask/webui/frontend/src/main.jsx +1 -0
- jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
- jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
- jettask/webui/frontend/src/pages/QueueDetail.jsx +1109 -10
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +236 -115
- jettask/webui/frontend/src/pages/Queues.jsx +5 -1
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
- jettask/webui/frontend/src/pages/Settings.jsx +800 -0
- jettask/webui/frontend/src/services/api.js +7 -5
- jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
- jettask/webui/frontend/src/utils/userPreferences.js +154 -0
- jettask/webui/multi_namespace_consumer.py +543 -0
- jettask/webui/pg_consumer.py +983 -246
- jettask/webui/static/dist/assets/index-7129cfe1.css +1 -0
- jettask/webui/static/dist/assets/index-8d1935cc.js +774 -0
- jettask/webui/static/dist/index.html +2 -2
- jettask/webui/task_center.py +216 -0
- jettask/webui/task_center_client.py +150 -0
- jettask/webui/unified_consumer_manager.py +193 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/METADATA +1 -1
- jettask-0.2.4.dist-info/RECORD +134 -0
- jettask/webui/pg_consumer_slow.py +0 -1099
- jettask/webui/pg_consumer_test.py +0 -678
- jettask/webui/static/dist/assets/index-823408e8.css +0 -1
- jettask/webui/static/dist/assets/index-9968b0b8.js +0 -543
- jettask/webui/test_pg_consumer_recovery.py +0 -547
- jettask/webui/test_recovery_simple.py +0 -492
- jettask/webui/test_self_recovery.py +0 -467
- jettask-0.2.1.dist-info/RECORD +0 -91
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/WHEEL +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/top_level.txt +0 -0
jettask/core/task.py
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
from ..utils.serializer import dumps_str, loads_str
|
2
2
|
import time
|
3
3
|
import inspect
|
4
|
+
import asyncio
|
4
5
|
from dataclasses import dataclass
|
5
|
-
from typing import Any, Optional, TYPE_CHECKING, get_type_hints
|
6
|
+
from typing import Any, Optional, TYPE_CHECKING, get_type_hints, Union
|
6
7
|
|
7
8
|
if TYPE_CHECKING:
|
8
9
|
from .app import Jettask
|
9
10
|
|
10
11
|
from .context import TaskContext
|
12
|
+
from .enums import TaskStatus
|
11
13
|
|
12
14
|
|
13
15
|
@dataclass
|
@@ -56,6 +58,10 @@ class Task:
|
|
56
58
|
# 如果获取签名失败,返回原始参数
|
57
59
|
return args, kwargs
|
58
60
|
|
61
|
+
# 从kwargs中提取scheduled_task_id(如果存在)
|
62
|
+
# 这个值由执行器从event_data中提取并传递
|
63
|
+
scheduled_task_id = kwargs.pop('__scheduled_task_id', None)
|
64
|
+
|
59
65
|
# 创建TaskContext实例
|
60
66
|
context = TaskContext(
|
61
67
|
event_id=event_id,
|
@@ -63,6 +69,7 @@ class Task:
|
|
63
69
|
trigger_time=trigger_time,
|
64
70
|
app=self._app,
|
65
71
|
queue=self.queue,
|
72
|
+
scheduled_task_id=scheduled_task_id, # 传递scheduled_task_id
|
66
73
|
# worker_id和retry_count可以从其他地方获取
|
67
74
|
# 暂时使用默认值
|
68
75
|
)
|
@@ -119,70 +126,6 @@ class Task:
|
|
119
126
|
def bind_app(cls, app):
|
120
127
|
cls._app = app
|
121
128
|
|
122
|
-
def apply_async(
|
123
|
-
self,
|
124
|
-
args: tuple = None,
|
125
|
-
kwargs: dict = None,
|
126
|
-
queue: str = None,
|
127
|
-
at_once: bool = True,
|
128
|
-
asyncio: bool = False,
|
129
|
-
routing: dict = None,
|
130
|
-
delay: int = None,
|
131
|
-
timeout: int = None,
|
132
|
-
):
|
133
|
-
"""
|
134
|
-
异步执行任务
|
135
|
-
|
136
|
-
Args:
|
137
|
-
args: 位置参数
|
138
|
-
kwargs: 关键字参数
|
139
|
-
queue: 队列名
|
140
|
-
at_once: 是否立即发送
|
141
|
-
asyncio: 是否使用异步模式
|
142
|
-
routing: 路由信息
|
143
|
-
delay: 延迟执行秒数(最多86400秒/1天)
|
144
|
-
timeout: 任务超时时间
|
145
|
-
|
146
|
-
Returns:
|
147
|
-
任务ID或任务消息
|
148
|
-
"""
|
149
|
-
queue = queue or self.queue
|
150
|
-
message = {
|
151
|
-
"queue": queue,
|
152
|
-
"name": self.name,
|
153
|
-
"args": args or (),
|
154
|
-
"kwargs": kwargs or {},
|
155
|
-
'trigger_time': time.time()
|
156
|
-
}
|
157
|
-
|
158
|
-
if routing:
|
159
|
-
message['routing'] = routing or {}
|
160
|
-
|
161
|
-
# 添加任务选项
|
162
|
-
if timeout:
|
163
|
-
message['timeout'] = timeout
|
164
|
-
|
165
|
-
# 如果有延迟参数,添加到消息中
|
166
|
-
if delay:
|
167
|
-
message['delay'] = delay
|
168
|
-
|
169
|
-
if not at_once:
|
170
|
-
return message
|
171
|
-
|
172
|
-
# 处理延迟任务
|
173
|
-
if delay:
|
174
|
-
if asyncio:
|
175
|
-
import asyncio as aio
|
176
|
-
return aio.create_task(self._send_delayed_task(queue, message, delay))
|
177
|
-
else:
|
178
|
-
return self.send_delayed_task(queue, message, delay)
|
179
|
-
|
180
|
-
# 立即发送任务
|
181
|
-
if asyncio:
|
182
|
-
import asyncio as aio
|
183
|
-
return aio.create_task(self._send_task(queue, message))
|
184
|
-
else:
|
185
|
-
return self.send_task(queue, message)
|
186
129
|
|
187
130
|
def on_before(self, event_id, pedding_count, args, kwargs) -> ExecuteResponse:
|
188
131
|
return ExecuteResponse()
|
@@ -193,114 +136,6 @@ class Task:
|
|
193
136
|
def on_success(self, event_id, args, kwargs, result) -> ExecuteResponse:
|
194
137
|
return ExecuteResponse()
|
195
138
|
|
196
|
-
def send_task(self, queue, message):
|
197
|
-
# 如果queue为None,使用任务名作为队列名
|
198
|
-
actual_queue = queue or self.name
|
199
|
-
message['queue'] = actual_queue
|
200
|
-
|
201
|
-
# 如果任务有默认重试配置且消息中没有重试配置,则添加默认配置
|
202
|
-
if self.retry_config and 'retry_config' not in message:
|
203
|
-
message['retry_config'] = self.retry_config
|
204
|
-
|
205
|
-
event_id = self._app.ep.send_event(actual_queue, message, False)
|
206
|
-
# 只设置status,其他信息从Stream消息获取
|
207
|
-
key = f"{self._app.ep.redis_prefix or 'jettask'}:TASK:{event_id}"
|
208
|
-
self._app.redis.hset(key, "status", "pending")
|
209
|
-
self._app.redis.expire(key, 3600)
|
210
|
-
return event_id
|
211
|
-
|
212
|
-
async def _send_task(self, queue, message):
|
213
|
-
# 如果queue为None,使用任务名作为队列名
|
214
|
-
actual_queue = queue or self.name
|
215
|
-
message['queue'] = actual_queue
|
216
|
-
|
217
|
-
# 如果任务有默认重试配置且消息中没有重试配置,则添加默认配置
|
218
|
-
if self.retry_config and 'retry_config' not in message:
|
219
|
-
message['retry_config'] = self.retry_config
|
220
|
-
|
221
|
-
event_id = await self._app.ep.send_event(actual_queue, message, True)
|
222
|
-
# 只设置status,其他信息从Stream消息获取
|
223
|
-
key = f"{self._app.ep.redis_prefix or 'jettask'}:TASK:{event_id}"
|
224
|
-
await self._app.async_redis.hset(key, "status", "pending")
|
225
|
-
await self._app.async_redis.expire(key, 3600)
|
226
|
-
return event_id
|
227
|
-
|
228
|
-
def send_delayed_task(self, queue, message, delay_seconds):
|
229
|
-
"""发送延迟任务(同步)- 使用Redis zset管理延迟队列"""
|
230
|
-
# 添加执行时间到消息中
|
231
|
-
current_time = time.time()
|
232
|
-
execute_at = current_time + delay_seconds
|
233
|
-
message['execute_at'] = execute_at
|
234
|
-
message['is_delayed'] = 1 # 使用1代替True
|
235
|
-
message['trigger_time'] = current_time # 添加trigger_time字段
|
236
|
-
|
237
|
-
# 如果queue为None,使用任务名作为队列名
|
238
|
-
actual_queue = queue or self.name
|
239
|
-
|
240
|
-
# 更新message中的queue字段
|
241
|
-
message['queue'] = actual_queue
|
242
|
-
|
243
|
-
# 直接发送到正常的队列(Stream),使用stream_id作为event_id
|
244
|
-
stream_id = self._app.ep.send_event(actual_queue, message, asyncio=False)
|
245
|
-
|
246
|
-
# 将stream_id保存到message中,供worker使用
|
247
|
-
message['event_id'] = stream_id
|
248
|
-
|
249
|
-
# 将任务添加到延迟队列zset中
|
250
|
-
# key格式: {prefix}:DELAYED_QUEUE:{queue}
|
251
|
-
delayed_queue_key = f"{self._app.ep.redis_prefix or 'jettask'}:DELAYED_QUEUE:{actual_queue}"
|
252
|
-
# 使用执行时间作为score,stream_id作为member
|
253
|
-
self._app.redis.zadd(delayed_queue_key, {stream_id: execute_at})
|
254
|
-
|
255
|
-
# 只设置status,其他信息从Stream消息获取
|
256
|
-
# 确保stream_id是字符串
|
257
|
-
stream_id_str = stream_id.decode() if isinstance(stream_id, bytes) else stream_id
|
258
|
-
key = f"{self._app.ep.redis_prefix or 'jettask'}:TASK:{stream_id_str}"
|
259
|
-
self._app.redis.hset(key, "status", "delayed")
|
260
|
-
# 确保过期时间是整数(Redis EXPIRE要求整数秒)
|
261
|
-
expire_seconds = max(1, int(delay_seconds + 3600))
|
262
|
-
self._app.redis.expire(key, expire_seconds)
|
263
|
-
|
264
|
-
return stream_id # 返回stream_id作为event_id
|
265
|
-
|
266
|
-
async def _send_delayed_task(self, queue, message, delay_seconds):
|
267
|
-
"""发送延迟任务(异步)- 使用Redis zset管理延迟队列"""
|
268
|
-
# 添加执行时间到消息中
|
269
|
-
current_time = time.time()
|
270
|
-
execute_at = current_time + delay_seconds
|
271
|
-
message['execute_at'] = execute_at
|
272
|
-
message['is_delayed'] = 1 # 使用1代替True
|
273
|
-
message['trigger_time'] = current_time # 添加trigger_time字段
|
274
|
-
|
275
|
-
# 如果queue为None,使用任务名作为队列名
|
276
|
-
actual_queue = queue or self.name
|
277
|
-
|
278
|
-
# 更新message中的queue字段
|
279
|
-
message['queue'] = actual_queue
|
280
|
-
|
281
|
-
# 直接发送到正常的队列(Stream),使用stream_id作为event_id
|
282
|
-
stream_id = await self._app.ep.send_event(actual_queue, message, asyncio=True)
|
283
|
-
|
284
|
-
# 将stream_id保存到message中,供worker使用
|
285
|
-
message['event_id'] = stream_id
|
286
|
-
|
287
|
-
# 将任务添加到延迟队列zset中
|
288
|
-
# key格式: {prefix}:DELAYED_QUEUE:{queue}
|
289
|
-
delayed_queue_key = f"{self._app.ep.redis_prefix or 'jettask'}:DELAYED_QUEUE:{actual_queue}"
|
290
|
-
# 使用执行时间作为score,stream_id作为member
|
291
|
-
await self._app.async_redis.zadd(delayed_queue_key, {stream_id: execute_at})
|
292
|
-
|
293
|
-
# 只设置status,其他信息从Stream消息获取
|
294
|
-
# 确保stream_id是字符串
|
295
|
-
stream_id_str = stream_id.decode() if isinstance(stream_id, bytes) else stream_id
|
296
|
-
key = f"{self._app.ep.redis_prefix or 'jettask'}:TASK:{stream_id_str}"
|
297
|
-
await self._app.async_redis.hset(key, "status", "delayed")
|
298
|
-
# 确保过期时间是整数(Redis EXPIRE要求整数秒)
|
299
|
-
expire_seconds = max(1, int(delay_seconds + 3600))
|
300
|
-
await self._app.async_redis.expire(key, expire_seconds)
|
301
|
-
|
302
|
-
return stream_id # 返回stream_id作为event_id
|
303
|
-
|
304
139
|
def read_pending(
|
305
140
|
self,
|
306
141
|
queue: str = None,
|
@@ -312,4 +147,5 @@ class Task:
|
|
312
147
|
return self._app.ep.read_pending(queue, queue)
|
313
148
|
|
314
149
|
async def _get_pending(self, queue: str):
|
315
|
-
return await self._app.ep.read_pending(queue, queue, asyncio=True)
|
150
|
+
return await self._app.ep.read_pending(queue, queue, asyncio=True)
|
151
|
+
|
@@ -0,0 +1,153 @@
|
|
1
|
+
"""
|
2
|
+
任务批量发送构建器
|
3
|
+
提供类型安全的批量任务发送接口
|
4
|
+
"""
|
5
|
+
from typing import List, Dict, Any, Optional, Union, TYPE_CHECKING
|
6
|
+
import asyncio
|
7
|
+
from dataclasses import dataclass, field
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from .task import Task
|
11
|
+
from .app import JetTaskApp
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class TaskMessage:
|
16
|
+
"""单个任务消息"""
|
17
|
+
task_name: str
|
18
|
+
queue: str
|
19
|
+
args: tuple = field(default_factory=tuple)
|
20
|
+
kwargs: dict = field(default_factory=dict)
|
21
|
+
delay: Optional[int] = None
|
22
|
+
timeout: Optional[int] = None
|
23
|
+
max_retries: Optional[int] = None
|
24
|
+
retry_delay: Optional[int] = None
|
25
|
+
scheduled_task_id: Optional[int] = None
|
26
|
+
routing: Optional[dict] = None
|
27
|
+
|
28
|
+
def to_dict(self) -> dict:
|
29
|
+
"""转换为字典格式"""
|
30
|
+
data = {
|
31
|
+
'task_name': self.task_name,
|
32
|
+
'args': self.args,
|
33
|
+
'kwargs': self.kwargs,
|
34
|
+
}
|
35
|
+
|
36
|
+
# 只添加非None的可选参数
|
37
|
+
optional_fields = ['delay', 'timeout', 'max_retries', 'retry_delay',
|
38
|
+
'scheduled_task_id', 'routing']
|
39
|
+
for field in optional_fields:
|
40
|
+
value = getattr(self, field)
|
41
|
+
if value is not None:
|
42
|
+
data[field] = value
|
43
|
+
|
44
|
+
return data
|
45
|
+
|
46
|
+
|
47
|
+
class TaskBatch:
|
48
|
+
"""
|
49
|
+
任务批量构建器
|
50
|
+
|
51
|
+
使用示例:
|
52
|
+
batch = task.batch()
|
53
|
+
batch.add(args=(1,), kwargs={'user': 'alice'})
|
54
|
+
batch.add(args=(2,), kwargs={'user': 'bob'}, delay=5)
|
55
|
+
results = await batch.send()
|
56
|
+
"""
|
57
|
+
|
58
|
+
def __init__(self, task: 'Task', app: 'JetTaskApp'):
|
59
|
+
self.task = task
|
60
|
+
self.app = app
|
61
|
+
self.messages: List[TaskMessage] = []
|
62
|
+
self._queue = task.queue
|
63
|
+
|
64
|
+
def add(
|
65
|
+
self,
|
66
|
+
args: tuple = None,
|
67
|
+
kwargs: dict = None,
|
68
|
+
queue: str = None,
|
69
|
+
delay: int = None,
|
70
|
+
timeout: int = None,
|
71
|
+
max_retries: int = None,
|
72
|
+
retry_delay: int = None,
|
73
|
+
scheduled_task_id: int = None,
|
74
|
+
routing: dict = None,
|
75
|
+
) -> 'TaskBatch':
|
76
|
+
"""
|
77
|
+
添加一个任务到批量队列
|
78
|
+
|
79
|
+
参数签名与 apply_async 完全一致,保证IDE提示
|
80
|
+
|
81
|
+
Args:
|
82
|
+
args: 位置参数
|
83
|
+
kwargs: 关键字参数
|
84
|
+
queue: 指定队列(默认使用task的队列)
|
85
|
+
delay: 延迟执行时间(秒)
|
86
|
+
timeout: 任务超时时间(秒)
|
87
|
+
max_retries: 最大重试次数
|
88
|
+
retry_delay: 重试间隔(秒)
|
89
|
+
scheduled_task_id: 定时任务ID
|
90
|
+
routing: 路由信息
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
self: 支持链式调用
|
94
|
+
"""
|
95
|
+
message = TaskMessage(
|
96
|
+
task_name=self.task.name,
|
97
|
+
queue=queue or self._queue,
|
98
|
+
args=args or (),
|
99
|
+
kwargs=kwargs or {},
|
100
|
+
delay=delay,
|
101
|
+
timeout=timeout,
|
102
|
+
max_retries=max_retries,
|
103
|
+
retry_delay=retry_delay,
|
104
|
+
scheduled_task_id=scheduled_task_id,
|
105
|
+
routing=routing,
|
106
|
+
)
|
107
|
+
self.messages.append(message)
|
108
|
+
return self
|
109
|
+
|
110
|
+
async def send(self) -> List[str]:
|
111
|
+
"""
|
112
|
+
批量发送所有任务
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
List[str]: 任务ID列表
|
116
|
+
"""
|
117
|
+
if not self.messages:
|
118
|
+
return []
|
119
|
+
|
120
|
+
# 转换为批量写入格式
|
121
|
+
batch_data = []
|
122
|
+
for msg in self.messages:
|
123
|
+
batch_data.append({
|
124
|
+
'queue': msg.queue,
|
125
|
+
'data': msg.to_dict()
|
126
|
+
})
|
127
|
+
|
128
|
+
# 调用app的批量写入方法
|
129
|
+
# 这里假设app有一个内部的批量写入方法
|
130
|
+
result = await self.app._bulk_write_messages(batch_data)
|
131
|
+
|
132
|
+
# 清空消息列表,允许复用
|
133
|
+
self.messages.clear()
|
134
|
+
|
135
|
+
return result
|
136
|
+
|
137
|
+
def send_sync(self) -> List[str]:
|
138
|
+
"""
|
139
|
+
同步版本的批量发送
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
List[str]: 任务ID列表
|
143
|
+
"""
|
144
|
+
loop = asyncio.get_event_loop()
|
145
|
+
return loop.run_until_complete(self.send())
|
146
|
+
|
147
|
+
def __len__(self) -> int:
|
148
|
+
"""返回当前批量任务的数量"""
|
149
|
+
return len(self.messages)
|
150
|
+
|
151
|
+
def clear(self) -> None:
|
152
|
+
"""清空批量任务列表"""
|
153
|
+
self.messages.clear()
|
@@ -0,0 +1,243 @@
|
|
1
|
+
"""
|
2
|
+
统一的多命名空间管理器基类
|
3
|
+
提供单/多命名空间模式的通用功能
|
4
|
+
"""
|
5
|
+
import re
|
6
|
+
import logging
|
7
|
+
import aiohttp
|
8
|
+
from typing import Optional, Set, Dict, Any
|
9
|
+
from abc import ABC, abstractmethod
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
class UnifiedManagerBase(ABC):
|
15
|
+
"""
|
16
|
+
统一管理器基类
|
17
|
+
根据task_center_url自动判断是单命名空间还是多命名空间模式
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self,
|
21
|
+
task_center_url: str,
|
22
|
+
check_interval: int = 30,
|
23
|
+
debug: bool = False):
|
24
|
+
"""
|
25
|
+
初始化基础管理器
|
26
|
+
|
27
|
+
Args:
|
28
|
+
task_center_url: 任务中心URL
|
29
|
+
- 单命名空间: http://localhost:8001/api/namespaces/{name}
|
30
|
+
- 多命名空间: http://localhost:8001 或 http://localhost:8001/api
|
31
|
+
check_interval: 命名空间检测间隔(秒)
|
32
|
+
debug: 是否启用调试模式
|
33
|
+
"""
|
34
|
+
self.task_center_url = task_center_url.rstrip('/')
|
35
|
+
self.check_interval = check_interval
|
36
|
+
self.debug = debug
|
37
|
+
|
38
|
+
# 判断模式
|
39
|
+
self.namespace_name: Optional[str] = None
|
40
|
+
self.is_single_namespace = self._detect_mode()
|
41
|
+
|
42
|
+
# 运行状态
|
43
|
+
self.running = False
|
44
|
+
|
45
|
+
# 设置日志
|
46
|
+
if debug:
|
47
|
+
logging.basicConfig(level=logging.DEBUG)
|
48
|
+
else:
|
49
|
+
logging.basicConfig(level=logging.INFO)
|
50
|
+
|
51
|
+
def _detect_mode(self) -> bool:
|
52
|
+
"""
|
53
|
+
检测是单命名空间还是多命名空间模式
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
True: 单命名空间模式
|
57
|
+
False: 多命名空间模式
|
58
|
+
"""
|
59
|
+
# 检查URL格式
|
60
|
+
# 单命名空间: /api/namespaces/{name}
|
61
|
+
# 多命名空间: 不包含 /api/namespaces/ 或以 /api 结尾
|
62
|
+
|
63
|
+
if '/api/namespaces/' in self.task_center_url:
|
64
|
+
# 提取命名空间名称
|
65
|
+
match = re.search(r'/api/namespaces/([^/]+)/?$', self.task_center_url)
|
66
|
+
if match:
|
67
|
+
self.namespace_name = match.group(1)
|
68
|
+
logger.info(f"检测到单命名空间模式: {self.namespace_name}")
|
69
|
+
return True
|
70
|
+
|
71
|
+
# 多命名空间模式
|
72
|
+
logger.info("检测到多命名空间模式")
|
73
|
+
return False
|
74
|
+
|
75
|
+
def get_base_url(self) -> str:
|
76
|
+
"""
|
77
|
+
获取任务中心的基础URL
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
基础URL(去除命名空间路径)
|
81
|
+
"""
|
82
|
+
if self.is_single_namespace and '/api/namespaces/' in self.task_center_url:
|
83
|
+
# 从 http://localhost:8001/api/namespaces/default 提取 http://localhost:8001
|
84
|
+
return self.task_center_url.split('/api/namespaces/')[0]
|
85
|
+
return self.task_center_url
|
86
|
+
|
87
|
+
def get_target_namespaces(self) -> Optional[Set[str]]:
|
88
|
+
"""
|
89
|
+
获取目标命名空间集合
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
单命名空间模式: 返回包含单个命名空间的集合
|
93
|
+
多命名空间模式: 返回 None(表示所有命名空间)
|
94
|
+
"""
|
95
|
+
if self.is_single_namespace and self.namespace_name:
|
96
|
+
return {self.namespace_name}
|
97
|
+
return None
|
98
|
+
|
99
|
+
async def fetch_namespaces_info(self, namespace_names: Optional[Set[str]] = None) -> list:
|
100
|
+
"""
|
101
|
+
从任务中心API获取命名空间配置
|
102
|
+
|
103
|
+
Args:
|
104
|
+
namespace_names: 要获取的命名空间名称集合,None表示获取所有
|
105
|
+
|
106
|
+
Returns:
|
107
|
+
命名空间配置列表
|
108
|
+
"""
|
109
|
+
namespaces = []
|
110
|
+
|
111
|
+
try:
|
112
|
+
# 获取基础URL
|
113
|
+
base_url = self.get_base_url()
|
114
|
+
|
115
|
+
if namespace_names:
|
116
|
+
# 获取指定的命名空间
|
117
|
+
for name in namespace_names:
|
118
|
+
try:
|
119
|
+
async with aiohttp.ClientSession() as session:
|
120
|
+
url = f"{base_url}/api/namespaces/{name}"
|
121
|
+
logger.debug(f"请求命名空间配置: {url}")
|
122
|
+
|
123
|
+
async with session.get(url) as response:
|
124
|
+
if response.status == 200:
|
125
|
+
data = await response.json()
|
126
|
+
ns_info = {
|
127
|
+
'id': data['id'],
|
128
|
+
'name': data['name'],
|
129
|
+
'redis_config': data.get('redis_config', {}),
|
130
|
+
'pg_config': data.get('pg_config', {}),
|
131
|
+
'redis_prefix': data['name'] # 直接使用命名空间名称作为前缀
|
132
|
+
}
|
133
|
+
namespaces.append(ns_info)
|
134
|
+
logger.info(f"成功获取命名空间 {name} 的配置")
|
135
|
+
else:
|
136
|
+
logger.warning(f"获取命名空间 {name} 失败: HTTP {response.status}")
|
137
|
+
except Exception as e:
|
138
|
+
logger.error(f"获取命名空间 {name} 失败: {e}")
|
139
|
+
else:
|
140
|
+
# 获取所有命名空间
|
141
|
+
async with aiohttp.ClientSession() as session:
|
142
|
+
url = f"{base_url}/api/namespaces"
|
143
|
+
logger.debug(f"请求所有命名空间配置: {url}")
|
144
|
+
|
145
|
+
async with session.get(url) as response:
|
146
|
+
if response.status == 200:
|
147
|
+
data_list = await response.json()
|
148
|
+
for data in data_list:
|
149
|
+
ns_info = {
|
150
|
+
'id': data['id'],
|
151
|
+
'name': data['name'],
|
152
|
+
'redis_config': data.get('redis_config', {}),
|
153
|
+
'pg_config': data.get('pg_config', {}),
|
154
|
+
'redis_prefix': data['name'] # 直接使用命名空间名称作为前缀
|
155
|
+
}
|
156
|
+
namespaces.append(ns_info)
|
157
|
+
logger.info(f"成功获取 {len(namespaces)} 个命名空间的配置")
|
158
|
+
else:
|
159
|
+
logger.error(f"获取命名空间列表失败: HTTP {response.status}")
|
160
|
+
|
161
|
+
except Exception as e:
|
162
|
+
logger.error(f"从任务中心获取命名空间配置失败: {e}")
|
163
|
+
# 如果API调用失败,可以使用默认配置作为回退
|
164
|
+
if not namespaces and (not namespace_names or 'default' in namespace_names):
|
165
|
+
logger.warning("使用默认命名空间配置作为回退")
|
166
|
+
namespaces.append({
|
167
|
+
'id': 1,
|
168
|
+
'name': 'default',
|
169
|
+
'redis_config': {
|
170
|
+
'host': 'localhost',
|
171
|
+
'port': 6379,
|
172
|
+
'db': 0,
|
173
|
+
'password': None
|
174
|
+
},
|
175
|
+
'pg_config': {
|
176
|
+
'host': 'localhost',
|
177
|
+
'port': 5432,
|
178
|
+
'database': 'jettask',
|
179
|
+
'user': 'jettask',
|
180
|
+
'password': '123456'
|
181
|
+
},
|
182
|
+
'redis_prefix': 'default'
|
183
|
+
})
|
184
|
+
|
185
|
+
return namespaces
|
186
|
+
|
187
|
+
async def run(self):
|
188
|
+
"""运行管理器(统一处理单/多命名空间)"""
|
189
|
+
self.running = True
|
190
|
+
|
191
|
+
logger.info(f"启动 {self.__class__.__name__}")
|
192
|
+
logger.info(f"任务中心: {self.task_center_url}")
|
193
|
+
logger.info(f"模式: {'单命名空间' if self.is_single_namespace else '多命名空间'}")
|
194
|
+
|
195
|
+
if not self.is_single_namespace:
|
196
|
+
logger.info(f"检测间隔: {self.check_interval}秒")
|
197
|
+
|
198
|
+
# 获取目标命名空间
|
199
|
+
target_namespaces = self.get_target_namespaces()
|
200
|
+
|
201
|
+
try:
|
202
|
+
if self.is_single_namespace:
|
203
|
+
# 单命名空间模式
|
204
|
+
await self.run_single_namespace(self.namespace_name)
|
205
|
+
else:
|
206
|
+
# 多命名空间模式
|
207
|
+
await self.run_multi_namespace(target_namespaces)
|
208
|
+
except KeyboardInterrupt:
|
209
|
+
logger.info("收到中断信号")
|
210
|
+
except Exception as e:
|
211
|
+
logger.error(f"运行错误: {e}", exc_info=self.debug)
|
212
|
+
finally:
|
213
|
+
self.stop()
|
214
|
+
await self.cleanup()
|
215
|
+
|
216
|
+
@abstractmethod
|
217
|
+
async def run_single_namespace(self, namespace_name: str):
|
218
|
+
"""
|
219
|
+
运行单命名空间模式
|
220
|
+
|
221
|
+
Args:
|
222
|
+
namespace_name: 命名空间名称
|
223
|
+
"""
|
224
|
+
pass
|
225
|
+
|
226
|
+
@abstractmethod
|
227
|
+
async def run_multi_namespace(self, namespace_names: Optional[Set[str]]):
|
228
|
+
"""
|
229
|
+
运行多命名空间模式
|
230
|
+
|
231
|
+
Args:
|
232
|
+
namespace_names: 目标命名空间集合,None表示所有命名空间
|
233
|
+
"""
|
234
|
+
pass
|
235
|
+
|
236
|
+
def stop(self):
|
237
|
+
"""停止管理器"""
|
238
|
+
self.running = False
|
239
|
+
logger.info(f"停止 {self.__class__.__name__}")
|
240
|
+
|
241
|
+
async def cleanup(self):
|
242
|
+
"""清理资源(子类可重写)"""
|
243
|
+
pass
|