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.
Files changed (165) hide show
  1. jettask/__init__.py +60 -2
  2. jettask/cli.py +314 -228
  3. jettask/config/__init__.py +9 -1
  4. jettask/config/config.py +245 -0
  5. jettask/config/env_loader.py +381 -0
  6. jettask/config/lua_scripts.py +158 -0
  7. jettask/config/nacos_config.py +132 -5
  8. jettask/core/__init__.py +1 -1
  9. jettask/core/app.py +1573 -666
  10. jettask/core/app_importer.py +33 -16
  11. jettask/core/container.py +532 -0
  12. jettask/core/task.py +1 -4
  13. jettask/core/unified_manager_base.py +2 -2
  14. jettask/executor/__init__.py +38 -0
  15. jettask/executor/core.py +625 -0
  16. jettask/executor/executor.py +338 -0
  17. jettask/executor/orchestrator.py +290 -0
  18. jettask/executor/process_entry.py +638 -0
  19. jettask/executor/task_executor.py +317 -0
  20. jettask/messaging/__init__.py +68 -0
  21. jettask/messaging/event_pool.py +2188 -0
  22. jettask/messaging/reader.py +519 -0
  23. jettask/messaging/registry.py +266 -0
  24. jettask/messaging/scanner.py +369 -0
  25. jettask/messaging/sender.py +312 -0
  26. jettask/persistence/__init__.py +118 -0
  27. jettask/persistence/backlog_monitor.py +567 -0
  28. jettask/{backend/data_access.py → persistence/base.py} +58 -57
  29. jettask/persistence/consumer.py +315 -0
  30. jettask/{core → persistence}/db_manager.py +23 -22
  31. jettask/persistence/maintenance.py +81 -0
  32. jettask/persistence/message_consumer.py +259 -0
  33. jettask/{backend/namespace_data_access.py → persistence/namespace.py} +66 -98
  34. jettask/persistence/offline_recovery.py +196 -0
  35. jettask/persistence/queue_discovery.py +215 -0
  36. jettask/persistence/task_persistence.py +218 -0
  37. jettask/persistence/task_updater.py +583 -0
  38. jettask/scheduler/__init__.py +2 -2
  39. jettask/scheduler/loader.py +6 -5
  40. jettask/scheduler/run_scheduler.py +1 -1
  41. jettask/scheduler/scheduler.py +7 -7
  42. jettask/scheduler/{unified_scheduler_manager.py → scheduler_coordinator.py} +18 -13
  43. jettask/task/__init__.py +16 -0
  44. jettask/{router.py → task/router.py} +26 -8
  45. jettask/task/task_center/__init__.py +9 -0
  46. jettask/task/task_executor.py +318 -0
  47. jettask/task/task_registry.py +291 -0
  48. jettask/test_connection_monitor.py +73 -0
  49. jettask/utils/__init__.py +31 -1
  50. jettask/{monitor/run_backlog_collector.py → utils/backlog_collector.py} +1 -1
  51. jettask/utils/db_connector.py +1629 -0
  52. jettask/{db_init.py → utils/db_init.py} +1 -1
  53. jettask/utils/rate_limit/__init__.py +30 -0
  54. jettask/utils/rate_limit/concurrency_limiter.py +665 -0
  55. jettask/utils/rate_limit/config.py +145 -0
  56. jettask/utils/rate_limit/limiter.py +41 -0
  57. jettask/utils/rate_limit/manager.py +269 -0
  58. jettask/utils/rate_limit/qps_limiter.py +154 -0
  59. jettask/utils/rate_limit/task_limiter.py +384 -0
  60. jettask/utils/serializer.py +3 -0
  61. jettask/{monitor/stream_backlog_monitor.py → utils/stream_backlog.py} +14 -6
  62. jettask/utils/time_sync.py +173 -0
  63. jettask/webui/__init__.py +27 -0
  64. jettask/{api/v1 → webui/api}/alerts.py +1 -1
  65. jettask/{api/v1 → webui/api}/analytics.py +2 -2
  66. jettask/{api/v1 → webui/api}/namespaces.py +1 -1
  67. jettask/{api/v1 → webui/api}/overview.py +1 -1
  68. jettask/{api/v1 → webui/api}/queues.py +3 -3
  69. jettask/{api/v1 → webui/api}/scheduled.py +1 -1
  70. jettask/{api/v1 → webui/api}/settings.py +1 -1
  71. jettask/{api.py → webui/app.py} +253 -145
  72. jettask/webui/namespace_manager/__init__.py +10 -0
  73. jettask/{multi_namespace_consumer.py → webui/namespace_manager/multi.py} +69 -22
  74. jettask/{unified_consumer_manager.py → webui/namespace_manager/unified.py} +1 -1
  75. jettask/{run.py → webui/run.py} +2 -2
  76. jettask/{services → webui/services}/__init__.py +1 -3
  77. jettask/{services → webui/services}/overview_service.py +34 -16
  78. jettask/{services → webui/services}/queue_service.py +1 -1
  79. jettask/{backend → webui/services}/queue_stats_v2.py +1 -1
  80. jettask/{services → webui/services}/settings_service.py +1 -1
  81. jettask/worker/__init__.py +53 -0
  82. jettask/worker/lifecycle.py +1507 -0
  83. jettask/worker/manager.py +583 -0
  84. jettask/{core/offline_worker_recovery.py → worker/recovery.py} +268 -175
  85. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/METADATA +2 -71
  86. jettask-0.2.20.dist-info/RECORD +145 -0
  87. jettask/__main__.py +0 -140
  88. jettask/api/__init__.py +0 -103
  89. jettask/backend/__init__.py +0 -1
  90. jettask/backend/api/__init__.py +0 -3
  91. jettask/backend/api/v1/__init__.py +0 -17
  92. jettask/backend/api/v1/monitoring.py +0 -431
  93. jettask/backend/api/v1/namespaces.py +0 -504
  94. jettask/backend/api/v1/queues.py +0 -342
  95. jettask/backend/api/v1/tasks.py +0 -367
  96. jettask/backend/core/__init__.py +0 -3
  97. jettask/backend/core/cache.py +0 -221
  98. jettask/backend/core/database.py +0 -200
  99. jettask/backend/core/exceptions.py +0 -102
  100. jettask/backend/dependencies.py +0 -261
  101. jettask/backend/init_meta_db.py +0 -158
  102. jettask/backend/main.py +0 -1426
  103. jettask/backend/main_unified.py +0 -78
  104. jettask/backend/main_v2.py +0 -394
  105. jettask/backend/models/__init__.py +0 -3
  106. jettask/backend/models/requests.py +0 -236
  107. jettask/backend/models/responses.py +0 -230
  108. jettask/backend/namespace_api_old.py +0 -267
  109. jettask/backend/services/__init__.py +0 -3
  110. jettask/backend/start.py +0 -42
  111. jettask/backend/unified_api_router.py +0 -1541
  112. jettask/cleanup_deprecated_tables.sql +0 -16
  113. jettask/core/consumer_manager.py +0 -1695
  114. jettask/core/delay_scanner.py +0 -256
  115. jettask/core/event_pool.py +0 -1700
  116. jettask/core/heartbeat_process.py +0 -222
  117. jettask/core/task_batch.py +0 -153
  118. jettask/core/worker_scanner.py +0 -271
  119. jettask/executors/__init__.py +0 -5
  120. jettask/executors/asyncio.py +0 -876
  121. jettask/executors/base.py +0 -30
  122. jettask/executors/common.py +0 -148
  123. jettask/executors/multi_asyncio.py +0 -309
  124. jettask/gradio_app.py +0 -570
  125. jettask/integrated_gradio_app.py +0 -1088
  126. jettask/main.py +0 -0
  127. jettask/monitoring/__init__.py +0 -3
  128. jettask/pg_consumer.py +0 -1896
  129. jettask/run_monitor.py +0 -22
  130. jettask/run_webui.py +0 -148
  131. jettask/scheduler/multi_namespace_scheduler.py +0 -294
  132. jettask/scheduler/unified_manager.py +0 -450
  133. jettask/task_center_client.py +0 -150
  134. jettask/utils/serializer_optimized.py +0 -33
  135. jettask/webui_exceptions.py +0 -67
  136. jettask-0.2.18.dist-info/RECORD +0 -150
  137. /jettask/{constants.py → config/constants.py} +0 -0
  138. /jettask/{backend/config.py → config/task_center.py} +0 -0
  139. /jettask/{pg_consumer → messaging/pg_consumer}/pg_consumer_v2.py +0 -0
  140. /jettask/{pg_consumer → messaging/pg_consumer}/sql/add_execution_time_field.sql +0 -0
  141. /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_new_tables.sql +0 -0
  142. /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_tables_v3.sql +0 -0
  143. /jettask/{pg_consumer → messaging/pg_consumer}/sql/migrate_to_new_structure.sql +0 -0
  144. /jettask/{pg_consumer → messaging/pg_consumer}/sql/modify_time_fields.sql +0 -0
  145. /jettask/{pg_consumer → messaging/pg_consumer}/sql_utils.py +0 -0
  146. /jettask/{models.py → persistence/models.py} +0 -0
  147. /jettask/scheduler/{manager.py → task_crud.py} +0 -0
  148. /jettask/{schema.sql → schemas/schema.sql} +0 -0
  149. /jettask/{task_center.py → task/task_center/client.py} +0 -0
  150. /jettask/{monitoring → utils}/file_watcher.py +0 -0
  151. /jettask/{services/redis_monitor_service.py → utils/redis_monitor.py} +0 -0
  152. /jettask/{api/v1 → webui/api}/__init__.py +0 -0
  153. /jettask/{webui_config.py → webui/config.py} +0 -0
  154. /jettask/{webui_models → webui/models}/__init__.py +0 -0
  155. /jettask/{webui_models → webui/models}/namespace.py +0 -0
  156. /jettask/{services → webui/services}/alert_service.py +0 -0
  157. /jettask/{services → webui/services}/analytics_service.py +0 -0
  158. /jettask/{services → webui/services}/scheduled_task_service.py +0 -0
  159. /jettask/{services → webui/services}/task_service.py +0 -0
  160. /jettask/{webui_sql → webui/sql}/batch_upsert_functions.sql +0 -0
  161. /jettask/{webui_sql → webui/sql}/verify_database.sql +0 -0
  162. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/WHEEL +0 -0
  163. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/entry_points.txt +0 -0
  164. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/licenses/LICENSE +0 -0
  165. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/top_level.txt +0 -0
@@ -1,504 +0,0 @@
1
- """
2
- Namespace management API v1
3
- """
4
- from typing import List, Optional
5
- from fastapi import APIRouter, Depends, HTTPException, Query
6
- from sqlalchemy.ext.asyncio import AsyncSession
7
-
8
- from dependencies import (
9
- get_database_manager, get_request_metrics, RequestMetrics,
10
- validate_page_params
11
- )
12
- from models.requests import NamespaceCreateRequest, NamespaceUpdateRequest, NamespaceListRequest
13
- from models.responses import NamespaceListResponse, NamespaceResponse, BaseResponse
14
- from core.cache import cache_result, invalidate_cache, CACHE_CONFIGS
15
- from core.exceptions import NamespaceNotFoundError, ValidationError
16
- from namespace_data_access import get_namespace_data_access
17
- import logging
18
-
19
- logger = logging.getLogger(__name__)
20
- router = APIRouter()
21
-
22
-
23
- @router.get("", response_model=NamespaceListResponse)
24
- @cache_result(**CACHE_CONFIGS['namespace_config'])
25
- async def list_namespaces(
26
- page: int = Query(1, ge=1, description="页码"),
27
- page_size: int = Query(20, ge=1, le=100, description="每页大小"),
28
- is_active: Optional[bool] = Query(None, description="是否启用"),
29
- search: Optional[str] = Query(None, description="搜索关键词"),
30
- metrics: RequestMetrics = Depends(get_request_metrics)
31
- ):
32
- """获取命名空间列表"""
33
- metrics.start("system", "GET /namespaces")
34
-
35
- try:
36
- namespace_data_access = get_namespace_data_access()
37
-
38
- # 获取所有命名空间配置
39
- all_namespaces = await namespace_data_access.list_namespaces()
40
-
41
- # 转换为响应格式
42
- namespace_list = []
43
- for ns_config in all_namespaces:
44
- namespace_info = {
45
- 'id': ns_config.get('id', ns_config['name']),
46
- 'name': ns_config['name'],
47
- 'display_name': ns_config.get('display_name', ns_config['name']),
48
- 'description': ns_config.get('description'),
49
- 'redis_url': ns_config['redis_url'],
50
- 'pg_url': ns_config.get('pg_url'),
51
- 'created_at': ns_config.get('created_at'),
52
- 'is_active': ns_config.get('is_active', True),
53
- 'queue_count': 0, # 可以后续查询实际数量
54
- 'task_count': 0 # 可以后续查询实际数量
55
- }
56
-
57
- # 应用搜索筛选
58
- if search:
59
- if (search.lower() not in namespace_info['name'].lower() and
60
- search.lower() not in (namespace_info['display_name'] or '').lower()):
61
- continue
62
-
63
- # 应用状态筛选
64
- if is_active is not None and namespace_info['is_active'] != is_active:
65
- continue
66
-
67
- namespace_list.append(namespace_info)
68
-
69
- # 分页
70
- total = len(namespace_list)
71
- start = (page - 1) * page_size
72
- end = start + page_size
73
- paginated_namespaces = namespace_list[start:end]
74
-
75
- return NamespaceListResponse.create(
76
- data=paginated_namespaces,
77
- total=total,
78
- page=page,
79
- page_size=page_size
80
- )
81
-
82
- except Exception as e:
83
- logger.error(f"获取命名空间列表失败: {e}")
84
- raise HTTPException(status_code=500, detail=str(e))
85
- finally:
86
- metrics.finish()
87
-
88
-
89
- @router.get("/{namespace_name}", response_model=NamespaceResponse)
90
- @cache_result(**CACHE_CONFIGS['namespace_config'])
91
- async def get_namespace_detail(
92
- namespace_name: str,
93
- metrics: RequestMetrics = Depends(get_request_metrics)
94
- ):
95
- """获取命名空间详情"""
96
- metrics.start("system", f"GET /namespaces/{namespace_name}")
97
-
98
- try:
99
- namespace_data_access = get_namespace_data_access()
100
-
101
- # 获取命名空间配置
102
- ns_config = await namespace_data_access.get_namespace_config(namespace_name)
103
- if not ns_config:
104
- raise NamespaceNotFoundError(namespace_name)
105
-
106
- # 获取连接以检查状态和统计信息
107
- try:
108
- conn = await namespace_data_access.manager.get_connection(namespace_name)
109
-
110
- # 获取队列统计
111
- queue_count = 0
112
- task_count = 0
113
-
114
- try:
115
- # 从Redis获取队列数量
116
- redis_client = await conn.get_redis_client(decode=False)
117
- try:
118
- # 查询所有队列键
119
- pattern = f"{conn.redis_prefix}:QUEUE:*"
120
- keys = await redis_client.keys(pattern)
121
-
122
- # 去重基础队列名
123
- base_queues = set()
124
- for key in keys:
125
- key_str = key.decode('utf-8') if isinstance(key, bytes) else key
126
- parts = key_str.split(':')
127
- if len(parts) >= 3:
128
- queue_part = ':'.join(parts[2:])
129
- # 去除优先级后缀
130
- if ':' in queue_part:
131
- base_part = queue_part.rsplit(':', 1)
132
- if base_part[1].isdigit():
133
- base_queues.add(base_part[0])
134
- else:
135
- base_queues.add(queue_part)
136
- else:
137
- base_queues.add(queue_part)
138
-
139
- queue_count = len(base_queues)
140
- finally:
141
- await redis_client.aclose()
142
-
143
- # 从PostgreSQL获取任务数量
144
- if conn.AsyncSessionLocal:
145
- async with conn.AsyncSessionLocal() as session:
146
- result = await session.execute("SELECT COUNT(*) FROM tasks")
147
- row = result.fetchone()
148
- if row:
149
- task_count = row[0]
150
-
151
- except Exception as e:
152
- logger.warning(f"获取命名空间 {namespace_name} 统计信息失败: {e}")
153
-
154
- except Exception as e:
155
- logger.warning(f"连接命名空间 {namespace_name} 失败: {e}")
156
-
157
- # 构造响应
158
- namespace_detail = {
159
- 'id': ns_config.get('id', namespace_name),
160
- 'name': namespace_name,
161
- 'display_name': ns_config.get('display_name', namespace_name),
162
- 'description': ns_config.get('description'),
163
- 'redis_url': ns_config['redis_url'],
164
- 'pg_url': ns_config.get('pg_url'),
165
- 'created_at': ns_config.get('created_at'),
166
- 'is_active': ns_config.get('is_active', True),
167
- 'queue_count': queue_count,
168
- 'task_count': task_count
169
- }
170
-
171
- return NamespaceResponse(data=namespace_detail)
172
-
173
- except NamespaceNotFoundError:
174
- raise
175
- except Exception as e:
176
- logger.error(f"获取命名空间详情失败: {e}")
177
- raise HTTPException(status_code=500, detail=str(e))
178
- finally:
179
- metrics.finish()
180
-
181
-
182
- @router.post("", response_model=NamespaceResponse)
183
- @invalidate_cache("namespace_config")
184
- async def create_namespace(
185
- request: NamespaceCreateRequest,
186
- metrics: RequestMetrics = Depends(get_request_metrics)
187
- ):
188
- """创建命名空间"""
189
- metrics.start("system", "POST /namespaces")
190
-
191
- try:
192
- namespace_data_access = get_namespace_data_access()
193
-
194
- # 检查命名空间是否已存在
195
- existing_config = await namespace_data_access.get_namespace_config(request.name)
196
- if existing_config:
197
- raise ValidationError(f"Namespace '{request.name}' already exists")
198
-
199
- # 验证连接配置
200
- await _validate_connection_config(request.redis_url, request.pg_url)
201
-
202
- # 创建命名空间配置
203
- ns_config = {
204
- 'name': request.name,
205
- 'display_name': request.display_name,
206
- 'description': request.description,
207
- 'redis_url': request.redis_url,
208
- 'pg_url': request.pg_url,
209
- 'redis_prefix': request.redis_prefix,
210
- 'is_active': True
211
- }
212
-
213
- created_config = await namespace_data_access.create_namespace(ns_config)
214
-
215
- # 构造响应
216
- namespace_detail = {
217
- 'id': created_config.get('id', request.name),
218
- 'name': request.name,
219
- 'display_name': request.display_name,
220
- 'description': request.description,
221
- 'redis_url': request.redis_url,
222
- 'pg_url': request.pg_url,
223
- 'created_at': created_config.get('created_at'),
224
- 'is_active': True,
225
- 'queue_count': 0,
226
- 'task_count': 0
227
- }
228
-
229
- return NamespaceResponse(
230
- data=namespace_detail,
231
- message="Namespace created successfully"
232
- )
233
-
234
- except ValidationError:
235
- raise
236
- except Exception as e:
237
- logger.error(f"创建命名空间失败: {e}")
238
- raise HTTPException(status_code=500, detail=str(e))
239
- finally:
240
- metrics.finish()
241
-
242
-
243
- @router.put("/{namespace_name}", response_model=NamespaceResponse)
244
- @invalidate_cache("namespace_config")
245
- async def update_namespace(
246
- namespace_name: str,
247
- request: NamespaceUpdateRequest,
248
- metrics: RequestMetrics = Depends(get_request_metrics)
249
- ):
250
- """更新命名空间"""
251
- metrics.start("system", f"PUT /namespaces/{namespace_name}")
252
-
253
- try:
254
- namespace_data_access = get_namespace_data_access()
255
-
256
- # 检查命名空间是否存在
257
- existing_config = await namespace_data_access.get_namespace_config(namespace_name)
258
- if not existing_config:
259
- raise NamespaceNotFoundError(namespace_name)
260
-
261
- # 构造更新数据
262
- update_data = {}
263
- if request.display_name is not None:
264
- update_data['display_name'] = request.display_name
265
- if request.description is not None:
266
- update_data['description'] = request.description
267
- if request.is_active is not None:
268
- update_data['is_active'] = request.is_active
269
-
270
- # 验证连接配置更新
271
- if request.redis_url or request.pg_url:
272
- redis_url = request.redis_url or existing_config['redis_url']
273
- pg_url = request.pg_url or existing_config.get('pg_url')
274
- await _validate_connection_config(redis_url, pg_url)
275
-
276
- if request.redis_url:
277
- update_data['redis_url'] = request.redis_url
278
- if request.pg_url:
279
- update_data['pg_url'] = request.pg_url
280
-
281
- # 执行更新
282
- updated_config = await namespace_data_access.update_namespace(namespace_name, update_data)
283
-
284
- # 构造响应
285
- namespace_detail = {
286
- 'id': updated_config.get('id', namespace_name),
287
- 'name': namespace_name,
288
- 'display_name': updated_config.get('display_name', namespace_name),
289
- 'description': updated_config.get('description'),
290
- 'redis_url': updated_config['redis_url'],
291
- 'pg_url': updated_config.get('pg_url'),
292
- 'created_at': updated_config.get('created_at'),
293
- 'is_active': updated_config.get('is_active', True),
294
- 'queue_count': 0, # 可以后续查询实际数量
295
- 'task_count': 0 # 可以后续查询实际数量
296
- }
297
-
298
- return NamespaceResponse(
299
- data=namespace_detail,
300
- message="Namespace updated successfully"
301
- )
302
-
303
- except NamespaceNotFoundError:
304
- raise
305
- except ValidationError:
306
- raise
307
- except Exception as e:
308
- logger.error(f"更新命名空间失败: {e}")
309
- raise HTTPException(status_code=500, detail=str(e))
310
- finally:
311
- metrics.finish()
312
-
313
-
314
- @router.delete("/{namespace_name}", response_model=BaseResponse)
315
- @invalidate_cache("namespace_config")
316
- async def delete_namespace(
317
- namespace_name: str,
318
- force: bool = Query(False, description="是否强制删除"),
319
- metrics: RequestMetrics = Depends(get_request_metrics)
320
- ):
321
- """删除命名空间"""
322
- metrics.start("system", f"DELETE /namespaces/{namespace_name}")
323
-
324
- try:
325
- if namespace_name == "default":
326
- raise ValidationError("Cannot delete the default namespace")
327
-
328
- namespace_data_access = get_namespace_data_access()
329
-
330
- # 检查命名空间是否存在
331
- existing_config = await namespace_data_access.get_namespace_config(namespace_name)
332
- if not existing_config:
333
- raise NamespaceNotFoundError(namespace_name)
334
-
335
- # 检查是否有活跃的队列或任务(如果不是强制删除)
336
- if not force:
337
- try:
338
- conn = await namespace_data_access.manager.get_connection(namespace_name)
339
-
340
- # 检查Redis中是否有队列
341
- redis_client = await conn.get_redis_client(decode=False)
342
- try:
343
- pattern = f"{conn.redis_prefix}:QUEUE:*"
344
- keys = await redis_client.keys(pattern)
345
- if keys:
346
- raise ValidationError(
347
- f"Namespace has {len(keys)} active queues. Use force=true to delete anyway."
348
- )
349
- finally:
350
- await redis_client.aclose()
351
-
352
- # 检查PostgreSQL中是否有任务
353
- if conn.AsyncSessionLocal:
354
- async with conn.AsyncSessionLocal() as session:
355
- result = await session.execute("SELECT COUNT(*) FROM tasks")
356
- row = result.fetchone()
357
- if row and row[0] > 0:
358
- raise ValidationError(
359
- f"Namespace has {row[0]} tasks. Use force=true to delete anyway."
360
- )
361
-
362
- except ValidationError:
363
- raise
364
- except Exception as e:
365
- logger.warning(f"检查命名空间 {namespace_name} 资源失败: {e}")
366
-
367
- # 执行删除
368
- success = await namespace_data_access.delete_namespace(namespace_name)
369
- if not success:
370
- raise HTTPException(status_code=500, detail="Failed to delete namespace")
371
-
372
- return BaseResponse(
373
- message=f"Namespace '{namespace_name}' deleted successfully"
374
- )
375
-
376
- except NamespaceNotFoundError:
377
- raise
378
- except ValidationError:
379
- raise
380
- except Exception as e:
381
- logger.error(f"删除命名空间失败: {e}")
382
- raise HTTPException(status_code=500, detail=str(e))
383
- finally:
384
- metrics.finish()
385
-
386
-
387
- @router.post("/{namespace_name}/test-connection", response_model=BaseResponse)
388
- async def test_namespace_connection(
389
- namespace_name: str,
390
- metrics: RequestMetrics = Depends(get_request_metrics)
391
- ):
392
- """测试命名空间连接"""
393
- metrics.start("system", f"POST /namespaces/{namespace_name}/test-connection")
394
-
395
- try:
396
- namespace_data_access = get_namespace_data_access()
397
-
398
- # 获取命名空间配置
399
- ns_config = await namespace_data_access.get_namespace_config(namespace_name)
400
- if not ns_config:
401
- raise NamespaceNotFoundError(namespace_name)
402
-
403
- # 测试连接
404
- connection_results = await _test_connections(
405
- ns_config['redis_url'],
406
- ns_config.get('pg_url')
407
- )
408
-
409
- overall_status = "healthy" if all(r['status'] == 'healthy' for r in connection_results.values()) else "unhealthy"
410
-
411
- return BaseResponse(
412
- data={
413
- 'namespace': namespace_name,
414
- 'overall_status': overall_status,
415
- 'connections': connection_results
416
- },
417
- message=f"Connection test completed for namespace '{namespace_name}'"
418
- )
419
-
420
- except NamespaceNotFoundError:
421
- raise
422
- except Exception as e:
423
- logger.error(f"测试命名空间连接失败: {e}")
424
- raise HTTPException(status_code=500, detail=str(e))
425
- finally:
426
- metrics.finish()
427
-
428
-
429
- # 辅助函数
430
-
431
- async def _validate_connection_config(redis_url: str, pg_url: Optional[str] = None):
432
- """验证连接配置"""
433
- connection_results = await _test_connections(redis_url, pg_url)
434
-
435
- failed_connections = [
436
- name for name, result in connection_results.items()
437
- if result['status'] != 'healthy'
438
- ]
439
-
440
- if failed_connections:
441
- raise ValidationError(
442
- f"Connection validation failed for: {', '.join(failed_connections)}"
443
- )
444
-
445
-
446
- async def _test_connections(redis_url: str, pg_url: Optional[str] = None) -> dict:
447
- """测试数据库连接"""
448
- results = {}
449
-
450
- # 测试Redis连接
451
- try:
452
- import redis.asyncio as redis
453
- redis_client = redis.from_url(redis_url, decode_responses=False)
454
- await redis_client.ping()
455
- await redis_client.aclose()
456
- results['redis'] = {
457
- 'status': 'healthy',
458
- 'url': redis_url,
459
- 'message': 'Connection successful'
460
- }
461
- except Exception as e:
462
- results['redis'] = {
463
- 'status': 'unhealthy',
464
- 'url': redis_url,
465
- 'message': f'Connection failed: {str(e)}'
466
- }
467
-
468
- # 测试PostgreSQL连接(如果提供)
469
- if pg_url:
470
- try:
471
- import asyncpg
472
-
473
- # 解析连接字符串
474
- if pg_url.startswith('postgresql://'):
475
- connection_string = pg_url.replace('postgresql://', '')
476
- elif pg_url.startswith('postgresql+asyncpg://'):
477
- connection_string = pg_url.replace('postgresql+asyncpg://', '')
478
- else:
479
- connection_string = pg_url
480
-
481
- auth, host_info = connection_string.split('@')
482
- user, password = auth.split(':')
483
- host_port, database = host_info.split('/')
484
- host, port = host_port.split(':')
485
-
486
- conn = await asyncpg.connect(
487
- host=host, port=int(port), user=user, password=password, database=database
488
- )
489
- await conn.execute("SELECT 1")
490
- await conn.close()
491
-
492
- results['postgresql'] = {
493
- 'status': 'healthy',
494
- 'url': pg_url,
495
- 'message': 'Connection successful'
496
- }
497
- except Exception as e:
498
- results['postgresql'] = {
499
- 'status': 'unhealthy',
500
- 'url': pg_url,
501
- 'message': f'Connection failed: {str(e)}'
502
- }
503
-
504
- return results