jettask 0.2.15__py3-none-any.whl → 0.2.17__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 (149) hide show
  1. jettask/__init__.py +14 -35
  2. jettask/{webui/__main__.py → __main__.py} +4 -4
  3. jettask/api/__init__.py +103 -0
  4. jettask/api/v1/__init__.py +29 -0
  5. jettask/api/v1/alerts.py +226 -0
  6. jettask/api/v1/analytics.py +323 -0
  7. jettask/api/v1/namespaces.py +134 -0
  8. jettask/api/v1/overview.py +136 -0
  9. jettask/api/v1/queues.py +530 -0
  10. jettask/api/v1/scheduled.py +420 -0
  11. jettask/api/v1/settings.py +44 -0
  12. jettask/{webui/api.py → api.py} +4 -46
  13. jettask/{webui/backend → backend}/main.py +21 -109
  14. jettask/{webui/backend → backend}/main_unified.py +1 -1
  15. jettask/{webui/backend → backend}/namespace_api_old.py +3 -30
  16. jettask/{webui/backend → backend}/namespace_data_access.py +2 -1
  17. jettask/{webui/backend → backend}/unified_api_router.py +14 -74
  18. jettask/{core/cli.py → cli.py} +106 -26
  19. jettask/config/nacos_config.py +386 -0
  20. jettask/core/app.py +8 -100
  21. jettask/core/db_manager.py +515 -0
  22. jettask/core/event_pool.py +5 -2
  23. jettask/core/unified_manager_base.py +59 -14
  24. jettask/{webui/db_init.py → db_init.py} +1 -1
  25. jettask/executors/asyncio.py +2 -2
  26. jettask/{webui/integrated_gradio_app.py → integrated_gradio_app.py} +1 -1
  27. jettask/{webui/multi_namespace_consumer.py → multi_namespace_consumer.py} +5 -2
  28. jettask/{webui/pg_consumer.py → pg_consumer.py} +137 -69
  29. jettask/{webui/run.py → run.py} +1 -1
  30. jettask/{webui/run_webui.py → run_webui.py} +4 -4
  31. jettask/scheduler/manager.py +6 -0
  32. jettask/scheduler/multi_namespace_scheduler.py +2 -2
  33. jettask/scheduler/unified_manager.py +5 -5
  34. jettask/scheduler/unified_scheduler_manager.py +20 -12
  35. jettask/schemas/__init__.py +166 -0
  36. jettask/schemas/alert.py +99 -0
  37. jettask/schemas/backlog.py +122 -0
  38. jettask/schemas/common.py +139 -0
  39. jettask/schemas/monitoring.py +181 -0
  40. jettask/schemas/namespace.py +168 -0
  41. jettask/schemas/queue.py +83 -0
  42. jettask/schemas/scheduled_task.py +128 -0
  43. jettask/schemas/task.py +70 -0
  44. jettask/services/__init__.py +24 -0
  45. jettask/services/alert_service.py +454 -0
  46. jettask/services/analytics_service.py +46 -0
  47. jettask/services/overview_service.py +978 -0
  48. jettask/services/queue_service.py +711 -0
  49. jettask/services/redis_monitor_service.py +151 -0
  50. jettask/services/scheduled_task_service.py +207 -0
  51. jettask/services/settings_service.py +758 -0
  52. jettask/services/task_service.py +157 -0
  53. jettask/{webui/task_center.py → task_center.py} +30 -8
  54. jettask/{webui/task_center_client.py → task_center_client.py} +1 -1
  55. jettask/{webui/config.py → webui_config.py} +6 -1
  56. jettask/webui_exceptions.py +67 -0
  57. jettask/webui_sql/verify_database.sql +72 -0
  58. {jettask-0.2.15.dist-info → jettask-0.2.17.dist-info}/METADATA +2 -1
  59. jettask-0.2.17.dist-info/RECORD +150 -0
  60. {jettask-0.2.15.dist-info → jettask-0.2.17.dist-info}/entry_points.txt +1 -1
  61. jettask/webui/backend/data_api.py +0 -3294
  62. jettask/webui/backend/namespace_api.py +0 -295
  63. jettask/webui/backend/queue_backlog_api.py +0 -727
  64. jettask/webui/backend/redis_monitor_api.py +0 -476
  65. jettask/webui/frontend/index.html +0 -13
  66. jettask/webui/frontend/package.json +0 -30
  67. jettask/webui/frontend/src/App.css +0 -109
  68. jettask/webui/frontend/src/App.jsx +0 -66
  69. jettask/webui/frontend/src/components/NamespaceSelector.jsx +0 -166
  70. jettask/webui/frontend/src/components/QueueBacklogChart.jsx +0 -298
  71. jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +0 -638
  72. jettask/webui/frontend/src/components/QueueDetailsTable.css +0 -65
  73. jettask/webui/frontend/src/components/QueueDetailsTable.jsx +0 -487
  74. jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +0 -465
  75. jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +0 -423
  76. jettask/webui/frontend/src/components/TaskFilter.jsx +0 -425
  77. jettask/webui/frontend/src/components/TimeRangeSelector.css +0 -21
  78. jettask/webui/frontend/src/components/TimeRangeSelector.jsx +0 -160
  79. jettask/webui/frontend/src/components/charts/QueueChart.jsx +0 -111
  80. jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +0 -115
  81. jettask/webui/frontend/src/components/charts/WorkerChart.jsx +0 -40
  82. jettask/webui/frontend/src/components/common/StatsCard.jsx +0 -18
  83. jettask/webui/frontend/src/components/layout/AppLayout.css +0 -95
  84. jettask/webui/frontend/src/components/layout/AppLayout.jsx +0 -49
  85. jettask/webui/frontend/src/components/layout/Header.css +0 -106
  86. jettask/webui/frontend/src/components/layout/Header.jsx +0 -106
  87. jettask/webui/frontend/src/components/layout/SideMenu.css +0 -137
  88. jettask/webui/frontend/src/components/layout/SideMenu.jsx +0 -209
  89. jettask/webui/frontend/src/components/layout/TabsNav.css +0 -244
  90. jettask/webui/frontend/src/components/layout/TabsNav.jsx +0 -206
  91. jettask/webui/frontend/src/components/layout/UserInfo.css +0 -197
  92. jettask/webui/frontend/src/components/layout/UserInfo.jsx +0 -197
  93. jettask/webui/frontend/src/contexts/LoadingContext.jsx +0 -27
  94. jettask/webui/frontend/src/contexts/NamespaceContext.jsx +0 -72
  95. jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +0 -245
  96. jettask/webui/frontend/src/index.css +0 -114
  97. jettask/webui/frontend/src/main.jsx +0 -22
  98. jettask/webui/frontend/src/pages/Alerts.jsx +0 -684
  99. jettask/webui/frontend/src/pages/Dashboard/index.css +0 -35
  100. jettask/webui/frontend/src/pages/Dashboard/index.jsx +0 -281
  101. jettask/webui/frontend/src/pages/Dashboard.jsx +0 -1330
  102. jettask/webui/frontend/src/pages/QueueDetail.jsx +0 -1117
  103. jettask/webui/frontend/src/pages/QueueMonitor.jsx +0 -527
  104. jettask/webui/frontend/src/pages/Queues.jsx +0 -12
  105. jettask/webui/frontend/src/pages/ScheduledTasks.jsx +0 -810
  106. jettask/webui/frontend/src/pages/Settings.jsx +0 -801
  107. jettask/webui/frontend/src/pages/Workers.jsx +0 -12
  108. jettask/webui/frontend/src/services/api.js +0 -159
  109. jettask/webui/frontend/src/services/queueTrend.js +0 -166
  110. jettask/webui/frontend/src/utils/suppressWarnings.js +0 -22
  111. jettask/webui/frontend/src/utils/userPreferences.js +0 -154
  112. jettask/webui/frontend/vite.config.js +0 -26
  113. jettask/webui/sql/init_database.sql +0 -640
  114. jettask-0.2.15.dist-info/RECORD +0 -172
  115. /jettask/{webui/backend → backend}/__init__.py +0 -0
  116. /jettask/{webui/backend → backend}/api/__init__.py +0 -0
  117. /jettask/{webui/backend → backend}/api/v1/__init__.py +0 -0
  118. /jettask/{webui/backend → backend}/api/v1/monitoring.py +0 -0
  119. /jettask/{webui/backend → backend}/api/v1/namespaces.py +0 -0
  120. /jettask/{webui/backend → backend}/api/v1/queues.py +0 -0
  121. /jettask/{webui/backend → backend}/api/v1/tasks.py +0 -0
  122. /jettask/{webui/backend → backend}/config.py +0 -0
  123. /jettask/{webui/backend → backend}/core/__init__.py +0 -0
  124. /jettask/{webui/backend → backend}/core/cache.py +0 -0
  125. /jettask/{webui/backend → backend}/core/database.py +0 -0
  126. /jettask/{webui/backend → backend}/core/exceptions.py +0 -0
  127. /jettask/{webui/backend → backend}/data_access.py +0 -0
  128. /jettask/{webui/backend → backend}/dependencies.py +0 -0
  129. /jettask/{webui/backend → backend}/init_meta_db.py +0 -0
  130. /jettask/{webui/backend → backend}/main_v2.py +0 -0
  131. /jettask/{webui/backend → backend}/models/__init__.py +0 -0
  132. /jettask/{webui/backend → backend}/models/requests.py +0 -0
  133. /jettask/{webui/backend → backend}/models/responses.py +0 -0
  134. /jettask/{webui/backend → backend}/queue_stats_v2.py +0 -0
  135. /jettask/{webui/backend → backend}/services/__init__.py +0 -0
  136. /jettask/{webui/backend → backend}/start.py +0 -0
  137. /jettask/{webui/cleanup_deprecated_tables.sql → cleanup_deprecated_tables.sql} +0 -0
  138. /jettask/{webui/gradio_app.py → gradio_app.py} +0 -0
  139. /jettask/{webui/__init__.py → main.py} +0 -0
  140. /jettask/{webui/models.py → models.py} +0 -0
  141. /jettask/{webui/run_monitor.py → run_monitor.py} +0 -0
  142. /jettask/{webui/schema.sql → schema.sql} +0 -0
  143. /jettask/{webui/unified_consumer_manager.py → unified_consumer_manager.py} +0 -0
  144. /jettask/{webui/models → webui_models}/__init__.py +0 -0
  145. /jettask/{webui/models → webui_models}/namespace.py +0 -0
  146. /jettask/{webui/sql → webui_sql}/batch_upsert_functions.sql +0 -0
  147. {jettask-0.2.15.dist-info → jettask-0.2.17.dist-info}/WHEEL +0 -0
  148. {jettask-0.2.15.dist-info → jettask-0.2.17.dist-info}/licenses/LICENSE +0 -0
  149. {jettask-0.2.15.dist-info → jettask-0.2.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,515 @@
1
+ """
2
+ 统一的数据库管理模块
3
+ 提供命名空间级别的数据库连接管理
4
+ """
5
+ import os
6
+ import logging
7
+ import asyncio
8
+ from typing import Dict, Optional, Any, AsyncGenerator
9
+ from contextlib import asynccontextmanager
10
+ from urllib.parse import urlparse
11
+
12
+ import redis.asyncio as redis
13
+ import asyncpg
14
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
15
+ from sqlalchemy import text
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class NamespaceConfig:
21
+ """命名空间配置"""
22
+
23
+ def __init__(self, name: str, redis_config: dict, pg_config: dict):
24
+ self.name = name
25
+ self.redis_config = redis_config or {}
26
+ self.pg_config = pg_config or {}
27
+ self._parse_configs()
28
+
29
+ def _parse_configs(self):
30
+ """解析配置,提取URL和配置模式"""
31
+ # Redis配置
32
+ self.redis_mode = self.redis_config.get('config_mode', 'direct')
33
+ self.redis_url = self.redis_config.get('url')
34
+ self.redis_nacos_key = self.redis_config.get('nacos_key')
35
+
36
+ # PostgreSQL配置
37
+ self.pg_mode = self.pg_config.get('config_mode', 'direct')
38
+ self.pg_url = self.pg_config.get('url')
39
+ self.pg_nacos_key = self.pg_config.get('nacos_key')
40
+
41
+ def has_redis(self) -> bool:
42
+ """是否配置了Redis"""
43
+ return bool(self.redis_url)
44
+
45
+ def has_postgres(self) -> bool:
46
+ """是否配置了PostgreSQL"""
47
+ return bool(self.pg_url)
48
+
49
+
50
+ class ConnectionPool:
51
+ """单个命名空间的连接池"""
52
+
53
+ def __init__(self, namespace: str, config: NamespaceConfig):
54
+ self.namespace = namespace
55
+ self.config = config
56
+
57
+ # Redis连接池
58
+ self._redis_pool: Optional[redis.ConnectionPool] = None
59
+ self._binary_redis_pool: Optional[redis.ConnectionPool] = None
60
+
61
+ # PostgreSQL连接池
62
+ self._pg_pool: Optional[asyncpg.Pool] = None
63
+
64
+ # SQLAlchemy引擎和会话工厂
65
+ self._sa_engine: Optional[Any] = None
66
+ self._sa_session_maker: Optional[async_sessionmaker] = None
67
+
68
+ # 初始化锁
69
+ self._init_lock = asyncio.Lock()
70
+ self._initialized = False
71
+
72
+ async def initialize(self):
73
+ """初始化所有连接池"""
74
+ async with self._init_lock:
75
+ if self._initialized:
76
+ return
77
+
78
+ try:
79
+ # 初始化Redis
80
+ if self.config.has_redis():
81
+ await self._init_redis()
82
+
83
+ # 初始化PostgreSQL
84
+ if self.config.has_postgres():
85
+ await self._init_postgres()
86
+
87
+ self._initialized = True
88
+ logger.info(f"命名空间 '{self.namespace}' 连接池初始化成功")
89
+
90
+ except Exception as e:
91
+ logger.error(f"命名空间 '{self.namespace}' 连接池初始化失败: {e}")
92
+ raise
93
+
94
+ async def _init_redis(self):
95
+ """初始化Redis连接池"""
96
+ url = self.config.redis_url
97
+
98
+ # 文本连接池(decode_responses=True)
99
+ self._redis_pool = redis.ConnectionPool.from_url(
100
+ url,
101
+ max_connections=20,
102
+ decode_responses=True,
103
+ encoding='utf-8'
104
+ )
105
+
106
+ # 二进制连接池(decode_responses=False)
107
+ self._binary_redis_pool = redis.ConnectionPool.from_url(
108
+ url,
109
+ max_connections=20,
110
+ decode_responses=False
111
+ )
112
+
113
+ logger.debug(f"Redis连接池已创建: {self.namespace}")
114
+
115
+ async def _init_postgres(self):
116
+ """初始化PostgreSQL连接池"""
117
+ url = self.config.pg_url
118
+
119
+ # 创建asyncpg连接池
120
+ parsed = urlparse(url)
121
+ self._pg_pool = await asyncpg.create_pool(
122
+ host=parsed.hostname,
123
+ port=parsed.port or 5432,
124
+ user=parsed.username,
125
+ password=parsed.password,
126
+ database=parsed.path.lstrip('/'),
127
+ min_size=2,
128
+ max_size=10,
129
+ command_timeout=30
130
+ )
131
+
132
+ # 创建SQLAlchemy引擎
133
+ sa_url = url
134
+ if sa_url.startswith('postgresql://'):
135
+ sa_url = sa_url.replace('postgresql://', 'postgresql+asyncpg://', 1)
136
+
137
+ self._sa_engine = create_async_engine(
138
+ sa_url,
139
+ pool_size=5,
140
+ max_overflow=10,
141
+ pool_timeout=30,
142
+ pool_recycle=3600,
143
+ pool_pre_ping=True,
144
+ echo=False
145
+ )
146
+
147
+ self._sa_session_maker = async_sessionmaker(
148
+ self._sa_engine,
149
+ class_=AsyncSession,
150
+ expire_on_commit=False
151
+ )
152
+
153
+ logger.debug(f"PostgreSQL连接池已创建: {self.namespace}")
154
+
155
+ async def get_redis_client(self, decode: bool = True) -> redis.Redis:
156
+ """获取Redis客户端"""
157
+ if not self._initialized:
158
+ await self.initialize()
159
+
160
+ if not self.config.has_redis():
161
+ raise ValueError(f"命名空间 '{self.namespace}' 未配置Redis")
162
+
163
+ pool = self._redis_pool if decode else self._binary_redis_pool
164
+ return redis.Redis(connection_pool=pool)
165
+
166
+ @asynccontextmanager
167
+ async def get_pg_connection(self) -> AsyncGenerator[asyncpg.Connection, None]:
168
+ """获取PostgreSQL原生连接"""
169
+ if not self._initialized:
170
+ await self.initialize()
171
+
172
+ if not self._pg_pool:
173
+ raise ValueError(f"命名空间 '{self.namespace}' 未配置PostgreSQL")
174
+
175
+ async with self._pg_pool.acquire() as conn:
176
+ yield conn
177
+
178
+ @asynccontextmanager
179
+ async def get_sa_session(self) -> AsyncGenerator[AsyncSession, None]:
180
+ """获取SQLAlchemy会话"""
181
+ if not self._initialized:
182
+ await self.initialize()
183
+
184
+ if not self._sa_session_maker:
185
+ raise ValueError(f"命名空间 '{self.namespace}' 未配置PostgreSQL")
186
+
187
+ async with self._sa_session_maker() as session:
188
+ try:
189
+ yield session
190
+ await session.commit()
191
+ except Exception:
192
+ await session.rollback()
193
+ raise
194
+ finally:
195
+ await session.close()
196
+
197
+ async def close(self):
198
+ """关闭所有连接"""
199
+ try:
200
+ if self._redis_pool:
201
+ await self._redis_pool.aclose()
202
+
203
+ if self._binary_redis_pool:
204
+ await self._binary_redis_pool.aclose()
205
+
206
+ if self._pg_pool:
207
+ await self._pg_pool.close()
208
+
209
+ if self._sa_engine:
210
+ await self._sa_engine.dispose()
211
+
212
+ self._initialized = False
213
+ logger.info(f"命名空间 '{self.namespace}' 连接池已关闭")
214
+
215
+ except Exception as e:
216
+ logger.error(f"关闭命名空间 '{self.namespace}' 连接池失败: {e}")
217
+
218
+
219
+ class UnifiedDatabaseManager:
220
+ """
221
+ 统一的数据库管理器
222
+ 负责管理所有命名空间的数据库连接
223
+ 支持从环境变量或Nacos读取配置
224
+ """
225
+
226
+ def __init__(self, use_nacos: bool = False):
227
+ """
228
+ 初始化数据库管理器
229
+
230
+ Args:
231
+ use_nacos: 是否从Nacos读取配置
232
+ """
233
+ # 连接池缓存
234
+ self._pools: Dict[str, ConnectionPool] = {}
235
+
236
+ # 配置缓存
237
+ self._configs: Dict[str, NamespaceConfig] = {}
238
+
239
+ # 是否使用Nacos配置
240
+ self.use_nacos = use_nacos
241
+
242
+ # 主数据库URL(用于读取命名空间配置)
243
+ if use_nacos:
244
+ # 从Nacos配置读取
245
+ self._load_master_url_from_nacos()
246
+ else:
247
+ # 从环境变量读取
248
+ self.master_pg_url = os.getenv(
249
+ 'JETTASK_PG_URL',
250
+ 'postgresql+asyncpg://jettask:123456@localhost:5432/jettask'
251
+ )
252
+
253
+ # 主数据库连接
254
+ self._master_engine = None
255
+ self._master_session_maker = None
256
+
257
+ # 初始化锁
258
+ self._init_lock = asyncio.Lock()
259
+
260
+ # Nacos配置(如果需要)
261
+ self._nacos_client = None
262
+
263
+ def _load_master_url_from_nacos(self):
264
+ """从Nacos配置加载主数据库URL"""
265
+ try:
266
+ from jettask.config.nacos_config import config
267
+
268
+ # 从Nacos配置获取数据库连接信息
269
+ pg_config = config.config
270
+
271
+ # 构建数据库URL
272
+ jettask_pg_url = pg_config.get('JETTASK_PG_URL')
273
+ self.master_pg_url = jettask_pg_url
274
+ logger.info(f"从Nacos加载数据库配置: {jettask_pg_url=}")
275
+
276
+ except Exception as e:
277
+ logger.error(f"从Nacos加载数据库配置失败: {e}")
278
+ # 失败时使用默认值
279
+ self.master_pg_url = os.getenv(
280
+ 'JETTASK_PG_URL',
281
+ 'postgresql+asyncpg://jettask:123456@localhost:5432/jettask'
282
+ )
283
+ logger.warning("使用默认数据库配置")
284
+
285
+ async def initialize(self):
286
+ """初始化管理器"""
287
+ async with self._init_lock:
288
+ if self._master_engine:
289
+ return
290
+ print(f'{self.master_pg_url=}')
291
+ # 创建主数据库连接
292
+ self._master_engine = create_async_engine(
293
+ self.master_pg_url,
294
+ pool_size=3,
295
+ max_overflow=5,
296
+ pool_pre_ping=True,
297
+ echo=False
298
+ )
299
+
300
+ self._master_session_maker = async_sessionmaker(
301
+ self._master_engine,
302
+ class_=AsyncSession,
303
+ expire_on_commit=False
304
+ )
305
+
306
+ logger.info("数据库管理器初始化完成")
307
+
308
+ async def _fetch_namespace_config(self, namespace: str) -> NamespaceConfig:
309
+ """从数据库获取命名空间配置"""
310
+ if not self._master_session_maker:
311
+ await self.initialize()
312
+
313
+ async with self._master_session_maker() as session:
314
+ query = text("""
315
+ SELECT name, redis_config, pg_config, is_active
316
+ FROM namespaces
317
+ WHERE name = :name
318
+ """)
319
+
320
+ result = await session.execute(query, {'name': namespace})
321
+ row = result.fetchone()
322
+
323
+ if not row:
324
+ raise ValueError(f"命名空间 '{namespace}' 不存在")
325
+
326
+ if not row.is_active:
327
+ raise ValueError(f"命名空间 '{namespace}' 未激活")
328
+
329
+ # 处理Nacos配置
330
+ redis_config = row.redis_config or {}
331
+ pg_config = row.pg_config or {}
332
+
333
+ # 如果是Nacos模式,需要从Nacos获取实际的URL
334
+ if redis_config.get('config_mode') == 'nacos':
335
+ redis_url = await self._get_from_nacos(redis_config.get('nacos_key'))
336
+ redis_config['url'] = redis_url
337
+
338
+ if pg_config.get('config_mode') == 'nacos':
339
+ pg_url = await self._get_from_nacos(pg_config.get('nacos_key'))
340
+ pg_config['url'] = pg_url
341
+
342
+ return NamespaceConfig(row.name, redis_config, pg_config)
343
+
344
+ async def _get_from_nacos(self, key: str) -> str:
345
+ """从Nacos获取配置(需要实现)"""
346
+ try:
347
+ from jettask.config.nacos_config import Config
348
+ if not self._nacos_client:
349
+ self._nacos_client = Config()
350
+ value = self._nacos_client.config.get(key)
351
+ if not value:
352
+ raise ValueError(f"Nacos配置键 '{key}' 不存在")
353
+ return value
354
+ except ImportError:
355
+ logger.warning("Nacos客户端未安装,返回占位URL")
356
+ return f"redis://nacos-placeholder/{key}"
357
+
358
+ async def get_pool(self, namespace: str) -> ConnectionPool:
359
+ """获取命名空间的连接池"""
360
+ # 检查缓存
361
+ if namespace in self._pools:
362
+ return self._pools[namespace]
363
+
364
+ # 从数据库获取配置(而不是通过HTTP)
365
+ if namespace not in self._configs:
366
+ config = await self._fetch_namespace_config(namespace)
367
+ self._configs[namespace] = config
368
+
369
+ # 创建新的连接池
370
+ pool = ConnectionPool(namespace, self._configs[namespace])
371
+ await pool.initialize()
372
+
373
+ # 缓存连接池
374
+ self._pools[namespace] = pool
375
+
376
+ return pool
377
+
378
+ async def get_redis_client(self, namespace: str, decode: bool = True) -> redis.Redis:
379
+ """便捷方法:获取Redis客户端"""
380
+ pool = await self.get_pool(namespace)
381
+ return await pool.get_redis_client(decode)
382
+
383
+ @asynccontextmanager
384
+ async def get_pg_connection(self, namespace: str) -> AsyncGenerator[asyncpg.Connection, None]:
385
+ """便捷方法:获取PostgreSQL连接"""
386
+ pool = await self.get_pool(namespace)
387
+ async with pool.get_pg_connection() as conn:
388
+ yield conn
389
+
390
+ @asynccontextmanager
391
+ async def get_session(self, namespace: str) -> AsyncGenerator[AsyncSession, None]:
392
+ """便捷方法:获取SQLAlchemy会话"""
393
+ pool = await self.get_pool(namespace)
394
+ async with pool.get_sa_session() as session:
395
+ yield session
396
+
397
+ @asynccontextmanager
398
+ async def get_master_session(self) -> AsyncGenerator[AsyncSession, None]:
399
+ """获取主数据库会话(用于管理命名空间)"""
400
+ if not self._master_session_maker:
401
+ await self.initialize()
402
+
403
+ async with self._master_session_maker() as session:
404
+ try:
405
+ yield session
406
+ await session.commit()
407
+ except Exception:
408
+ await session.rollback()
409
+ raise
410
+ finally:
411
+ await session.close()
412
+
413
+ async def refresh_config(self, namespace: str):
414
+ """刷新命名空间配置"""
415
+ # 关闭旧连接
416
+ if namespace in self._pools:
417
+ await self._pools[namespace].close()
418
+ del self._pools[namespace]
419
+
420
+ # 清除配置缓存
421
+ if namespace in self._configs:
422
+ del self._configs[namespace]
423
+
424
+ logger.info(f"已刷新命名空间 '{namespace}' 的配置")
425
+
426
+ async def list_namespaces(self) -> list:
427
+ """列出所有命名空间"""
428
+ async with self.get_master_session() as session:
429
+ query = text("""
430
+ SELECT name, description, is_active, created_at, updated_at
431
+ FROM namespaces
432
+ ORDER BY name
433
+ """)
434
+
435
+ result = await session.execute(query)
436
+ rows = result.fetchall()
437
+
438
+ return [
439
+ {
440
+ 'name': row.name,
441
+ 'description': row.description,
442
+ 'is_active': row.is_active,
443
+ 'created_at': row.created_at.isoformat() if row.created_at else None,
444
+ 'updated_at': row.updated_at.isoformat() if row.updated_at else None
445
+ }
446
+ for row in rows
447
+ ]
448
+
449
+ async def close_all(self):
450
+ """关闭所有连接"""
451
+ # 关闭所有命名空间连接池
452
+ for namespace, pool in list(self._pools.items()):
453
+ await pool.close()
454
+
455
+ self._pools.clear()
456
+ self._configs.clear()
457
+
458
+ # 关闭主数据库连接
459
+ if self._master_engine:
460
+ await self._master_engine.dispose()
461
+
462
+ logger.info("数据库管理器已关闭所有连接")
463
+
464
+ async def __aenter__(self):
465
+ """异步上下文管理器入口"""
466
+ await self.initialize()
467
+ return self
468
+
469
+ async def __aexit__(self, exc_type=None, exc_val=None, exc_tb=None):
470
+ """异步上下文管理器出口"""
471
+ await self.close_all()
472
+
473
+
474
+ # 全局实例
475
+ _db_manager: Optional[UnifiedDatabaseManager] = None
476
+
477
+
478
+ def get_db_manager(use_nacos: bool = None) -> UnifiedDatabaseManager:
479
+ """
480
+ 获取全局数据库管理器实例
481
+
482
+ Args:
483
+ use_nacos: 是否使用Nacos配置,None表示使用已有实例的设置
484
+ """
485
+ global _db_manager
486
+ if _db_manager is None:
487
+ # 如果没有指定,检查环境变量或默认值
488
+ if use_nacos is None:
489
+ use_nacos = os.getenv('USE_NACOS', 'false').lower() == 'true'
490
+ _db_manager = UnifiedDatabaseManager(use_nacos=use_nacos)
491
+ elif use_nacos is not None and _db_manager.use_nacos != use_nacos:
492
+ # 如果配置模式改变了,重新创建实例
493
+ logger.info(f"配置模式改变,重新创建数据库管理器 (use_nacos={use_nacos})")
494
+ _db_manager = UnifiedDatabaseManager(use_nacos=use_nacos)
495
+ return _db_manager
496
+
497
+
498
+ async def init_db_manager(use_nacos: bool = None):
499
+ """
500
+ 初始化全局数据库管理器
501
+
502
+ Args:
503
+ use_nacos: 是否使用Nacos配置
504
+ """
505
+ manager = get_db_manager(use_nacos=use_nacos)
506
+ await manager.initialize()
507
+ return manager
508
+
509
+
510
+ async def close_db_manager():
511
+ """关闭全局数据库管理器"""
512
+ global _db_manager
513
+ if _db_manager:
514
+ await _db_manager.close_all()
515
+ _db_manager = None
@@ -35,7 +35,7 @@ class EventPool(object):
35
35
  ) -> None:
36
36
  self.redis_client = redis_client
37
37
  self.async_redis_client = async_redis_client
38
-
38
+ print(f'{redis_url=}')
39
39
  # 创建用于二进制数据的Redis客户端(用于Stream操作)
40
40
  from ..core.app import get_binary_redis_pool, get_async_binary_redis_pool
41
41
  binary_pool = get_binary_redis_pool(redis_url or 'redis://localhost:6379/0')
@@ -50,10 +50,11 @@ class EventPool(object):
50
50
 
51
51
  # 初始化消费者管理器
52
52
  strategy = ConsumerStrategy(consumer_strategy) if consumer_strategy else ConsumerStrategy.HEARTBEAT
53
- # 确保配置中包含队列信息和redis_prefix
53
+ # 确保配置中包含队列信息、redis_url和redis_prefix
54
54
  manager_config = consumer_config or {}
55
55
  manager_config['queues'] = queues or []
56
56
  manager_config['redis_prefix'] = redis_prefix or 'jettask'
57
+ manager_config['redis_url'] = redis_url or 'redis://localhost:6379/0'
57
58
 
58
59
  # 保存consumer_config供后续使用
59
60
  self.consumer_config = manager_config
@@ -749,6 +750,8 @@ class EventPool(object):
749
750
  sleep_time = max_interval
750
751
 
751
752
  except Exception as e:
753
+ import traceback
754
+ # traceback.print_exc()
752
755
  logger.error(f"Error scanning delayed tasks for queue {queue}: {e}")
753
756
  sleep_time = base_interval
754
757
 
@@ -57,10 +57,20 @@ class UnifiedManagerBase(ABC):
57
57
  False: 多命名空间模式
58
58
  """
59
59
  # 检查URL格式
60
- # 单命名空间: /api/namespaces/{name}
61
- # 多命名空间: 不包含 /api/namespaces/ 或以 /api 结尾
60
+ # 单命名空间: /api/v1/namespaces/{name} 或 /api/namespaces/{name}
61
+ # 多命名空间: 不包含这些路径或以 /api 结尾
62
62
 
63
- if '/api/namespaces/' in self.task_center_url:
63
+ # 检查新格式
64
+ if '/api/v1/namespaces/' in self.task_center_url:
65
+ # 提取命名空间名称
66
+ match = re.search(r'/api/v1/namespaces/([^/]+)/?$', self.task_center_url)
67
+ if match:
68
+ self.namespace_name = match.group(1)
69
+ logger.info(f"检测到单命名空间模式: {self.namespace_name}")
70
+ return True
71
+
72
+ # 兼容旧格式
73
+ elif '/api/namespaces/' in self.task_center_url:
64
74
  # 提取命名空间名称
65
75
  match = re.search(r'/api/namespaces/([^/]+)/?$', self.task_center_url)
66
76
  if match:
@@ -79,9 +89,20 @@ class UnifiedManagerBase(ABC):
79
89
  Returns:
80
90
  基础URL(去除命名空间路径)
81
91
  """
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]
92
+ # 支持新旧两种API路径格式
93
+ if self.is_single_namespace:
94
+ if '/api/v1/namespaces/' in self.task_center_url:
95
+ # 从 http://localhost:8001/api/v1/namespaces/default 提取 http://localhost:8001
96
+ return self.task_center_url.split('/api/v1/namespaces/')[0]
97
+ elif '/api/namespaces/' in self.task_center_url:
98
+ # 兼容旧格式
99
+ return self.task_center_url.split('/api/namespaces/')[0]
100
+
101
+ # 多命名空间模式,如果URL以 /api/v1/ 结尾,去掉 /api/v1/ 部分
102
+ if self.task_center_url.endswith('/api/v1/') or self.task_center_url.endswith('/api/v1'):
103
+ # 从 http://localhost:8001/api/v1/ 提取 http://localhost:8001
104
+ return self.task_center_url.rstrip('/').rsplit('/api/v1', 1)[0]
105
+
85
106
  return self.task_center_url
86
107
 
87
108
  def get_target_namespaces(self) -> Optional[Set[str]]:
@@ -117,17 +138,29 @@ class UnifiedManagerBase(ABC):
117
138
  for name in namespace_names:
118
139
  try:
119
140
  async with aiohttp.ClientSession() as session:
120
- url = f"{base_url}/api/namespaces/{name}"
141
+ url = f"{base_url}/api/v1/namespaces/{name}"
121
142
  logger.debug(f"请求命名空间配置: {url}")
122
143
 
123
144
  async with session.get(url) as response:
124
145
  if response.status == 200:
125
146
  data = await response.json()
147
+ # 构建redis和pg配置
148
+ # 跳过没有有效配置的命名空间
149
+ redis_url = data.get('redis_url', '')
150
+ pg_url = data.get('pg_url', '')
151
+
152
+ if not redis_url or not pg_url:
153
+ logger.warning(f"跳过命名空间 {data['name']}:缺少 Redis 或 PostgreSQL 配置")
154
+ continue
155
+
156
+ redis_config = {'url': redis_url}
157
+ pg_config = {'url': pg_url}
158
+
126
159
  ns_info = {
127
- 'id': data['id'],
160
+ 'id': data.get('id', data['name']), # 如果没有id,使用name作为id
128
161
  'name': data['name'],
129
- 'redis_config': data.get('redis_config', {}),
130
- 'pg_config': data.get('pg_config', {}),
162
+ 'redis_config': redis_config,
163
+ 'pg_config': pg_config,
131
164
  'redis_prefix': data['name'] # 直接使用命名空间名称作为前缀
132
165
  }
133
166
  namespaces.append(ns_info)
@@ -139,18 +172,30 @@ class UnifiedManagerBase(ABC):
139
172
  else:
140
173
  # 获取所有命名空间
141
174
  async with aiohttp.ClientSession() as session:
142
- url = f"{base_url}/api/namespaces"
175
+ url = f"{base_url}/api/v1/namespaces/" # 添加末尾斜杠
143
176
  logger.debug(f"请求所有命名空间配置: {url}")
144
177
 
145
178
  async with session.get(url) as response:
146
179
  if response.status == 200:
147
180
  data_list = await response.json()
148
181
  for data in data_list:
182
+ # 构建redis和pg配置
183
+ # 跳过没有有效配置的命名空间
184
+ redis_url = data.get('redis_url', '')
185
+ pg_url = data.get('pg_url', '')
186
+
187
+ if not redis_url or not pg_url:
188
+ logger.warning(f"跳过命名空间 {data['name']}:缺少 Redis 或 PostgreSQL 配置")
189
+ continue
190
+
191
+ redis_config = {'url': redis_url}
192
+ pg_config = {'url': pg_url}
193
+
149
194
  ns_info = {
150
- 'id': data['id'],
195
+ 'id': data.get('id', data['name']), # 如果没有id,使用name作为id
151
196
  'name': data['name'],
152
- 'redis_config': data.get('redis_config', {}),
153
- 'pg_config': data.get('pg_config', {}),
197
+ 'redis_config': redis_config,
198
+ 'pg_config': pg_config,
154
199
  'redis_prefix': data['name'] # 直接使用命名空间名称作为前缀
155
200
  }
156
201
  namespaces.append(ns_info)
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
 
8
8
  import asyncpg
9
9
 
10
- from jettask.webui.config import PostgreSQLConfig
10
+ from jettask.webui_config import PostgreSQLConfig
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
@@ -169,7 +169,7 @@ class AsyncioExecutor(BaseExecutor):
169
169
  max_offsets = {} # {(queue, group_name): max_offset}
170
170
 
171
171
  for item in self.pending_acks:
172
- print(f'{item=}')
172
+ # print(f'{item=}')
173
173
  if len(item) == 4:
174
174
  queue, event_id, group_name, offset = item
175
175
  elif len(item) == 3:
@@ -189,7 +189,7 @@ class AsyncioExecutor(BaseExecutor):
189
189
  if key not in max_offsets or offset > max_offsets[key]:
190
190
  max_offsets[key] = offset
191
191
 
192
- logger.info(f'{max_offsets=}')
192
+ # logger.info(f'{max_offsets=}')
193
193
  # 处理offset更新(使用Lua脚本确保原子性和最大值约束)
194
194
  if max_offsets:
195
195
  task_offset_key = f"{self.prefix}:TASK_OFFSETS"