jettask 0.2.19__py3-none-any.whl → 0.2.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jettask/__init__.py +10 -3
- jettask/cli.py +314 -228
- jettask/config/__init__.py +9 -1
- jettask/config/config.py +245 -0
- jettask/config/env_loader.py +381 -0
- jettask/config/lua_scripts.py +158 -0
- jettask/config/nacos_config.py +132 -5
- jettask/core/__init__.py +1 -1
- jettask/core/app.py +1573 -666
- jettask/core/app_importer.py +33 -16
- jettask/core/container.py +532 -0
- jettask/core/task.py +1 -4
- jettask/core/unified_manager_base.py +2 -2
- jettask/executor/__init__.py +38 -0
- jettask/executor/core.py +625 -0
- jettask/executor/executor.py +338 -0
- jettask/executor/orchestrator.py +290 -0
- jettask/executor/process_entry.py +638 -0
- jettask/executor/task_executor.py +317 -0
- jettask/messaging/__init__.py +68 -0
- jettask/messaging/event_pool.py +2188 -0
- jettask/messaging/reader.py +519 -0
- jettask/messaging/registry.py +266 -0
- jettask/messaging/scanner.py +369 -0
- jettask/messaging/sender.py +312 -0
- jettask/persistence/__init__.py +118 -0
- jettask/persistence/backlog_monitor.py +567 -0
- jettask/{backend/data_access.py → persistence/base.py} +58 -57
- jettask/persistence/consumer.py +315 -0
- jettask/{core → persistence}/db_manager.py +23 -22
- jettask/persistence/maintenance.py +81 -0
- jettask/persistence/message_consumer.py +259 -0
- jettask/{backend/namespace_data_access.py → persistence/namespace.py} +66 -98
- jettask/persistence/offline_recovery.py +196 -0
- jettask/persistence/queue_discovery.py +215 -0
- jettask/persistence/task_persistence.py +218 -0
- jettask/persistence/task_updater.py +583 -0
- jettask/scheduler/__init__.py +2 -2
- jettask/scheduler/loader.py +6 -5
- jettask/scheduler/run_scheduler.py +1 -1
- jettask/scheduler/scheduler.py +7 -7
- jettask/scheduler/{unified_scheduler_manager.py → scheduler_coordinator.py} +18 -13
- jettask/task/__init__.py +16 -0
- jettask/{router.py → task/router.py} +26 -8
- jettask/task/task_center/__init__.py +9 -0
- jettask/task/task_executor.py +318 -0
- jettask/task/task_registry.py +291 -0
- jettask/test_connection_monitor.py +73 -0
- jettask/utils/__init__.py +31 -1
- jettask/{monitor/run_backlog_collector.py → utils/backlog_collector.py} +1 -1
- jettask/utils/db_connector.py +1629 -0
- jettask/{db_init.py → utils/db_init.py} +1 -1
- jettask/utils/rate_limit/__init__.py +30 -0
- jettask/utils/rate_limit/concurrency_limiter.py +665 -0
- jettask/utils/rate_limit/config.py +145 -0
- jettask/utils/rate_limit/limiter.py +41 -0
- jettask/utils/rate_limit/manager.py +269 -0
- jettask/utils/rate_limit/qps_limiter.py +154 -0
- jettask/utils/rate_limit/task_limiter.py +384 -0
- jettask/utils/serializer.py +3 -0
- jettask/{monitor/stream_backlog_monitor.py → utils/stream_backlog.py} +14 -6
- jettask/utils/time_sync.py +173 -0
- jettask/webui/__init__.py +27 -0
- jettask/{api/v1 → webui/api}/alerts.py +1 -1
- jettask/{api/v1 → webui/api}/analytics.py +2 -2
- jettask/{api/v1 → webui/api}/namespaces.py +1 -1
- jettask/{api/v1 → webui/api}/overview.py +1 -1
- jettask/{api/v1 → webui/api}/queues.py +3 -3
- jettask/{api/v1 → webui/api}/scheduled.py +1 -1
- jettask/{api/v1 → webui/api}/settings.py +1 -1
- jettask/{api.py → webui/app.py} +253 -145
- jettask/webui/namespace_manager/__init__.py +10 -0
- jettask/{multi_namespace_consumer.py → webui/namespace_manager/multi.py} +69 -22
- jettask/{unified_consumer_manager.py → webui/namespace_manager/unified.py} +1 -1
- jettask/{run.py → webui/run.py} +2 -2
- jettask/{services → webui/services}/__init__.py +1 -3
- jettask/{services → webui/services}/overview_service.py +34 -16
- jettask/{services → webui/services}/queue_service.py +1 -1
- jettask/{backend → webui/services}/queue_stats_v2.py +1 -1
- jettask/{services → webui/services}/settings_service.py +1 -1
- jettask/worker/__init__.py +53 -0
- jettask/worker/lifecycle.py +1507 -0
- jettask/worker/manager.py +583 -0
- jettask/{core/offline_worker_recovery.py → worker/recovery.py} +268 -175
- {jettask-0.2.19.dist-info → jettask-0.2.20.dist-info}/METADATA +2 -71
- jettask-0.2.20.dist-info/RECORD +145 -0
- jettask/__main__.py +0 -140
- jettask/api/__init__.py +0 -103
- jettask/backend/__init__.py +0 -1
- jettask/backend/api/__init__.py +0 -3
- jettask/backend/api/v1/__init__.py +0 -17
- jettask/backend/api/v1/monitoring.py +0 -431
- jettask/backend/api/v1/namespaces.py +0 -504
- jettask/backend/api/v1/queues.py +0 -342
- jettask/backend/api/v1/tasks.py +0 -367
- jettask/backend/core/__init__.py +0 -3
- jettask/backend/core/cache.py +0 -221
- jettask/backend/core/database.py +0 -200
- jettask/backend/core/exceptions.py +0 -102
- jettask/backend/dependencies.py +0 -261
- jettask/backend/init_meta_db.py +0 -158
- jettask/backend/main.py +0 -1426
- jettask/backend/main_unified.py +0 -78
- jettask/backend/main_v2.py +0 -394
- jettask/backend/models/__init__.py +0 -3
- jettask/backend/models/requests.py +0 -236
- jettask/backend/models/responses.py +0 -230
- jettask/backend/namespace_api_old.py +0 -267
- jettask/backend/services/__init__.py +0 -3
- jettask/backend/start.py +0 -42
- jettask/backend/unified_api_router.py +0 -1541
- jettask/cleanup_deprecated_tables.sql +0 -16
- jettask/core/consumer_manager.py +0 -1695
- jettask/core/delay_scanner.py +0 -256
- jettask/core/event_pool.py +0 -1700
- jettask/core/heartbeat_process.py +0 -222
- jettask/core/task_batch.py +0 -153
- jettask/core/worker_scanner.py +0 -271
- jettask/executors/__init__.py +0 -5
- jettask/executors/asyncio.py +0 -876
- jettask/executors/base.py +0 -30
- jettask/executors/common.py +0 -148
- jettask/executors/multi_asyncio.py +0 -309
- jettask/gradio_app.py +0 -570
- jettask/integrated_gradio_app.py +0 -1088
- jettask/main.py +0 -0
- jettask/monitoring/__init__.py +0 -3
- jettask/pg_consumer.py +0 -1896
- jettask/run_monitor.py +0 -22
- jettask/run_webui.py +0 -148
- jettask/scheduler/multi_namespace_scheduler.py +0 -294
- jettask/scheduler/unified_manager.py +0 -450
- jettask/task_center_client.py +0 -150
- jettask/utils/serializer_optimized.py +0 -33
- jettask/webui_exceptions.py +0 -67
- jettask-0.2.19.dist-info/RECORD +0 -150
- /jettask/{constants.py → config/constants.py} +0 -0
- /jettask/{backend/config.py → config/task_center.py} +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/pg_consumer_v2.py +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/add_execution_time_field.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_new_tables.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_tables_v3.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/migrate_to_new_structure.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/modify_time_fields.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql_utils.py +0 -0
- /jettask/{models.py → persistence/models.py} +0 -0
- /jettask/scheduler/{manager.py → task_crud.py} +0 -0
- /jettask/{schema.sql → schemas/schema.sql} +0 -0
- /jettask/{task_center.py → task/task_center/client.py} +0 -0
- /jettask/{monitoring → utils}/file_watcher.py +0 -0
- /jettask/{services/redis_monitor_service.py → utils/redis_monitor.py} +0 -0
- /jettask/{api/v1 → webui/api}/__init__.py +0 -0
- /jettask/{webui_config.py → webui/config.py} +0 -0
- /jettask/{webui_models → webui/models}/__init__.py +0 -0
- /jettask/{webui_models → webui/models}/namespace.py +0 -0
- /jettask/{services → webui/services}/alert_service.py +0 -0
- /jettask/{services → webui/services}/analytics_service.py +0 -0
- /jettask/{services → webui/services}/scheduled_task_service.py +0 -0
- /jettask/{services → webui/services}/task_service.py +0 -0
- /jettask/{webui_sql → webui/sql}/batch_upsert_functions.sql +0 -0
- /jettask/{webui_sql → webui/sql}/verify_database.sql +0 -0
- {jettask-0.2.19.dist-info → jettask-0.2.20.dist-info}/WHEEL +0 -0
- {jettask-0.2.19.dist-info → jettask-0.2.20.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.19.dist-info → jettask-0.2.20.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.19.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
|