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.
Files changed (89) hide show
  1. jettask/constants.py +213 -0
  2. jettask/core/app.py +525 -205
  3. jettask/core/cli.py +193 -185
  4. jettask/core/consumer_manager.py +126 -34
  5. jettask/core/context.py +3 -0
  6. jettask/core/enums.py +137 -0
  7. jettask/core/event_pool.py +501 -168
  8. jettask/core/message.py +147 -0
  9. jettask/core/offline_worker_recovery.py +181 -114
  10. jettask/core/task.py +10 -174
  11. jettask/core/task_batch.py +153 -0
  12. jettask/core/unified_manager_base.py +243 -0
  13. jettask/core/worker_scanner.py +54 -54
  14. jettask/executors/asyncio.py +184 -64
  15. jettask/webui/backend/config.py +51 -0
  16. jettask/webui/backend/data_access.py +2083 -92
  17. jettask/webui/backend/data_api.py +3294 -0
  18. jettask/webui/backend/dependencies.py +261 -0
  19. jettask/webui/backend/init_meta_db.py +158 -0
  20. jettask/webui/backend/main.py +1358 -69
  21. jettask/webui/backend/main_unified.py +78 -0
  22. jettask/webui/backend/main_v2.py +394 -0
  23. jettask/webui/backend/namespace_api.py +295 -0
  24. jettask/webui/backend/namespace_api_old.py +294 -0
  25. jettask/webui/backend/namespace_data_access.py +611 -0
  26. jettask/webui/backend/queue_backlog_api.py +727 -0
  27. jettask/webui/backend/queue_stats_v2.py +521 -0
  28. jettask/webui/backend/redis_monitor_api.py +476 -0
  29. jettask/webui/backend/unified_api_router.py +1601 -0
  30. jettask/webui/db_init.py +204 -32
  31. jettask/webui/frontend/package-lock.json +492 -1
  32. jettask/webui/frontend/package.json +4 -1
  33. jettask/webui/frontend/src/App.css +105 -7
  34. jettask/webui/frontend/src/App.jsx +49 -20
  35. jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
  36. jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
  37. jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
  38. jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
  39. jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
  40. jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
  41. jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
  42. jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
  43. jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
  44. jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
  45. jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
  46. jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
  47. jettask/webui/frontend/src/components/layout/Header.css +34 -10
  48. jettask/webui/frontend/src/components/layout/Header.jsx +31 -23
  49. jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
  50. jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
  51. jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
  52. jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
  53. jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
  54. jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
  55. jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
  56. jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
  57. jettask/webui/frontend/src/main.jsx +1 -0
  58. jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
  59. jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
  60. jettask/webui/frontend/src/pages/QueueDetail.jsx +1109 -10
  61. jettask/webui/frontend/src/pages/QueueMonitor.jsx +236 -115
  62. jettask/webui/frontend/src/pages/Queues.jsx +5 -1
  63. jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
  64. jettask/webui/frontend/src/pages/Settings.jsx +800 -0
  65. jettask/webui/frontend/src/services/api.js +7 -5
  66. jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
  67. jettask/webui/frontend/src/utils/userPreferences.js +154 -0
  68. jettask/webui/multi_namespace_consumer.py +543 -0
  69. jettask/webui/pg_consumer.py +983 -246
  70. jettask/webui/static/dist/assets/index-7129cfe1.css +1 -0
  71. jettask/webui/static/dist/assets/index-8d1935cc.js +774 -0
  72. jettask/webui/static/dist/index.html +2 -2
  73. jettask/webui/task_center.py +216 -0
  74. jettask/webui/task_center_client.py +150 -0
  75. jettask/webui/unified_consumer_manager.py +193 -0
  76. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/METADATA +1 -1
  77. jettask-0.2.4.dist-info/RECORD +134 -0
  78. jettask/webui/pg_consumer_slow.py +0 -1099
  79. jettask/webui/pg_consumer_test.py +0 -678
  80. jettask/webui/static/dist/assets/index-823408e8.css +0 -1
  81. jettask/webui/static/dist/assets/index-9968b0b8.js +0 -543
  82. jettask/webui/test_pg_consumer_recovery.py +0 -547
  83. jettask/webui/test_recovery_simple.py +0 -492
  84. jettask/webui/test_self_recovery.py +0 -467
  85. jettask-0.2.1.dist-info/RECORD +0 -91
  86. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/WHEEL +0 -0
  87. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/entry_points.txt +0 -0
  88. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/licenses/LICENSE +0 -0
  89. {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