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
@@ -0,0 +1,476 @@
1
+ """
2
+ Redis监控API - 提供Redis性能和负载监控数据
3
+ """
4
+ from fastapi import APIRouter, HTTPException
5
+ from typing import Dict, Optional, List, Any
6
+ from datetime import datetime, timedelta
7
+ import logging
8
+ import asyncio
9
+ import time
10
+ import psutil
11
+ from namespace_data_access import get_namespace_data_access
12
+ import traceback
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ router = APIRouter(prefix="/api/redis", tags=["redis-monitor"])
17
+
18
+
19
+ def parse_redis_info(info_str: str) -> Dict[str, Any]:
20
+ """解析Redis INFO命令的输出"""
21
+ sections = {}
22
+ current_section = None
23
+
24
+ for line in info_str.split('\n'):
25
+ line = line.strip()
26
+ if not line or line.startswith('#'):
27
+ # 处理节标题
28
+ if line.startswith('#'):
29
+ section_name = line[1:].strip().lower()
30
+ current_section = section_name
31
+ sections[current_section] = {}
32
+ continue
33
+
34
+ # 解析键值对
35
+ if ':' in line:
36
+ key, value = line.split(':', 1)
37
+ if current_section:
38
+ sections[current_section][key] = value
39
+ else:
40
+ sections[key] = value
41
+
42
+ return sections
43
+
44
+
45
+ def calculate_metrics(info_data: Dict[str, Any]) -> Dict[str, Any]:
46
+ """从Redis INFO数据计算监控指标"""
47
+ metrics = {
48
+ 'timestamp': datetime.now().isoformat(),
49
+ 'status': 'healthy',
50
+ 'cpu': {},
51
+ 'memory': {},
52
+ 'stats': {},
53
+ 'persistence': {},
54
+ 'replication': {},
55
+ 'clients': {},
56
+ 'keyspace': {},
57
+ 'server': {}
58
+ }
59
+
60
+ # redis.asyncio 返回的是扁平的字典,直接从中提取数据
61
+ # CPU相关指标
62
+ metrics['cpu'] = {
63
+ 'used_cpu_sys': float(info_data.get('used_cpu_sys', 0)),
64
+ 'used_cpu_user': float(info_data.get('used_cpu_user', 0)),
65
+ 'used_cpu_sys_children': float(info_data.get('used_cpu_sys_children', 0)),
66
+ 'used_cpu_user_children': float(info_data.get('used_cpu_user_children', 0)),
67
+ 'used_cpu_total': float(info_data.get('used_cpu_sys', 0)) +
68
+ float(info_data.get('used_cpu_user', 0)),
69
+ }
70
+
71
+ # 内存相关指标
72
+ used_memory = int(info_data.get('used_memory', 0))
73
+ max_memory = info_data.get('maxmemory', '0')
74
+ max_memory = int(max_memory) if max_memory != '0' else None
75
+
76
+ metrics['memory'] = {
77
+ 'used_memory': used_memory,
78
+ 'used_memory_human': info_data.get('used_memory_human', '0B'),
79
+ 'used_memory_rss': int(info_data.get('used_memory_rss', 0)),
80
+ 'used_memory_rss_human': info_data.get('used_memory_rss_human', '0B'),
81
+ 'used_memory_peak': int(info_data.get('used_memory_peak', 0)),
82
+ 'used_memory_peak_human': info_data.get('used_memory_peak_human', '0B'),
83
+ 'used_memory_overhead': int(info_data.get('used_memory_overhead', 0)),
84
+ 'used_memory_dataset': int(info_data.get('used_memory_dataset', 0)),
85
+ 'mem_fragmentation_ratio': float(info_data.get('mem_fragmentation_ratio', 1.0)),
86
+ 'maxmemory': max_memory,
87
+ 'maxmemory_human': info_data.get('maxmemory_human', '0B'),
88
+ 'maxmemory_policy': info_data.get('maxmemory_policy', 'noeviction'),
89
+ }
90
+
91
+ # 计算内存使用率
92
+ if max_memory and max_memory > 0:
93
+ # 如果Redis设置了内存限制,使用配置的限制
94
+ metrics['memory']['usage_percentage'] = round((used_memory / max_memory) * 100, 2)
95
+ metrics['memory']['total_memory'] = max_memory
96
+ metrics['memory']['total_memory_human'] = info_data.get('maxmemory_human', '0B')
97
+ else:
98
+ # 如果Redis没有设置内存限制,使用系统总内存进行计算
99
+ try:
100
+ system_memory = psutil.virtual_memory()
101
+ total_memory = system_memory.total
102
+ metrics['memory']['usage_percentage'] = round((used_memory / total_memory) * 100, 2)
103
+ metrics['memory']['total_memory'] = total_memory
104
+ # 转换为人类可读格式
105
+ def format_bytes(bytes_value):
106
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
107
+ if bytes_value < 1024.0:
108
+ return f"{bytes_value:.1f}{unit}"
109
+ bytes_value /= 1024.0
110
+ return f"{bytes_value:.1f}PB"
111
+ metrics['memory']['total_memory_human'] = format_bytes(total_memory)
112
+ except Exception as e:
113
+ logger.warning(f"无法获取系统内存信息: {e}")
114
+ traceback.print_exc()
115
+ metrics['memory']['usage_percentage'] = None
116
+ metrics['memory']['total_memory'] = None
117
+ metrics['memory']['total_memory_human'] = 'Unknown'
118
+
119
+ # 统计信息
120
+ metrics['stats'] = {
121
+ 'total_connections_received': int(info_data.get('total_connections_received', 0)),
122
+ 'total_commands_processed': int(info_data.get('total_commands_processed', 0)),
123
+ 'instantaneous_ops_per_sec': int(info_data.get('instantaneous_ops_per_sec', 0)),
124
+ 'total_net_input_bytes': int(info_data.get('total_net_input_bytes', 0)),
125
+ 'total_net_output_bytes': int(info_data.get('total_net_output_bytes', 0)),
126
+ 'instantaneous_input_kbps': float(info_data.get('instantaneous_input_kbps', 0)),
127
+ 'instantaneous_output_kbps': float(info_data.get('instantaneous_output_kbps', 0)),
128
+ 'rejected_connections': int(info_data.get('rejected_connections', 0)),
129
+ 'expired_keys': int(info_data.get('expired_keys', 0)),
130
+ 'evicted_keys': int(info_data.get('evicted_keys', 0)),
131
+ 'keyspace_hits': int(info_data.get('keyspace_hits', 0)),
132
+ 'keyspace_misses': int(info_data.get('keyspace_misses', 0)),
133
+ }
134
+
135
+ # 计算命中率
136
+ hits = metrics['stats']['keyspace_hits']
137
+ misses = metrics['stats']['keyspace_misses']
138
+ if hits + misses > 0:
139
+ metrics['stats']['hit_rate'] = round((hits / (hits + misses)) * 100, 2)
140
+ else:
141
+ metrics['stats']['hit_rate'] = 0
142
+
143
+ # 持久化信息
144
+ metrics['persistence'] = {
145
+ 'rdb_last_save_time': int(info_data.get('rdb_last_save_time', 0)),
146
+ 'rdb_changes_since_last_save': int(info_data.get('rdb_changes_since_last_save', 0)),
147
+ 'rdb_bgsave_in_progress': int(info_data.get('rdb_bgsave_in_progress', 0)),
148
+ 'rdb_last_bgsave_status': info_data.get('rdb_last_bgsave_status', 'ok'),
149
+ 'aof_enabled': int(info_data.get('aof_enabled', 0)),
150
+ 'aof_rewrite_in_progress': int(info_data.get('aof_rewrite_in_progress', 0)),
151
+ 'aof_last_rewrite_time_sec': int(info_data.get('aof_last_rewrite_time_sec', -1)),
152
+ }
153
+
154
+ # 复制信息
155
+ metrics['replication'] = {
156
+ 'role': info_data.get('role', 'master'),
157
+ 'connected_slaves': int(info_data.get('connected_slaves', 0)),
158
+ 'master_repl_offset': int(info_data.get('master_repl_offset', 0)) if 'master_repl_offset' in info_data else None,
159
+ }
160
+
161
+ # 客户端连接信息
162
+ metrics['clients'] = {
163
+ 'connected_clients': int(info_data.get('connected_clients', 0)),
164
+ 'blocked_clients': int(info_data.get('blocked_clients', 0)),
165
+ 'tracking_clients': int(info_data.get('tracking_clients', 0)),
166
+ }
167
+
168
+ # 键空间信息
169
+ total_keys = 0
170
+ total_expires = 0
171
+ db_stats = {}
172
+
173
+ for key, value in info_data.items():
174
+ if key.startswith('db') and key[2:].isdigit():
175
+ # 解析格式: keys=100,expires=10,avg_ttl=3600
176
+ if isinstance(value, str):
177
+ parts = value.split(',')
178
+ db_info = {}
179
+ for part in parts:
180
+ if '=' in part:
181
+ k, v = part.split('=')
182
+ db_info[k] = int(v) if v.isdigit() else v
183
+ else:
184
+ db_info = value
185
+
186
+ db_stats[key] = db_info
187
+ total_keys += db_info.get('keys', 0)
188
+ total_expires += db_info.get('expires', 0)
189
+
190
+ metrics['keyspace'] = {
191
+ 'databases': db_stats,
192
+ 'total_keys': total_keys,
193
+ 'total_expires': total_expires,
194
+ }
195
+
196
+ # 服务器信息
197
+ metrics['server'] = {
198
+ 'redis_version': info_data.get('redis_version', 'unknown'),
199
+ 'redis_mode': info_data.get('redis_mode', 'standalone'),
200
+ 'uptime_in_seconds': int(info_data.get('uptime_in_seconds', 0)),
201
+ 'uptime_in_days': int(info_data.get('uptime_in_days', 0)),
202
+ }
203
+
204
+ return metrics
205
+
206
+
207
+ @router.get("/monitor/{namespace}")
208
+ async def get_redis_monitor(namespace: str) -> Dict[str, Any]:
209
+ """
210
+ 获取指定命名空间的Redis监控数据
211
+
212
+ 包括:
213
+ - CPU使用情况
214
+ - 内存使用情况
215
+ - 连接数
216
+ - 命令处理速率
217
+ - 键空间统计
218
+ - 持久化状态
219
+ - 复制状态
220
+ """
221
+ try:
222
+ namespace_data = get_namespace_data_access()
223
+ connection = await namespace_data.manager.get_connection(namespace)
224
+
225
+ if not connection:
226
+ raise HTTPException(status_code=404, detail=f"命名空间 {namespace} 不存在")
227
+
228
+ # 获取Redis客户端
229
+ redis_client = await connection.get_redis_client(decode=True)
230
+
231
+ # 执行INFO命令获取Redis状态信息
232
+ info_data = await redis_client.info()
233
+
234
+ # 如果返回的是字符串,需要解析;如果已经是字典,直接使用
235
+ if isinstance(info_data, str):
236
+ info_data = parse_redis_info(info_data)
237
+
238
+ # 计算监控指标
239
+ metrics = calculate_metrics(info_data)
240
+
241
+ # 添加命名空间信息
242
+ metrics['namespace'] = namespace
243
+
244
+ # 获取慢查询日志(最近10条)
245
+ try:
246
+ slowlog = await redis_client.slowlog_get(10)
247
+ metrics['slowlog'] = []
248
+
249
+ for log_entry in slowlog:
250
+ if isinstance(log_entry, dict):
251
+ # 新版Redis返回字典格式
252
+ command = log_entry.get('command', '')
253
+ if isinstance(command, bytes):
254
+ command = command.decode('utf-8', errors='ignore')
255
+
256
+ # 限制命令长度
257
+ if len(command) > 100:
258
+ command = command[:100] + '...'
259
+
260
+ metrics['slowlog'].append({
261
+ 'id': log_entry.get('id', 0),
262
+ 'timestamp': datetime.fromtimestamp(log_entry.get('start_time', 0)).isoformat(),
263
+ 'duration_us': log_entry.get('duration', 0),
264
+ 'command': command,
265
+ })
266
+ elif isinstance(log_entry, (list, tuple)) and len(log_entry) >= 4:
267
+ # 旧版Redis返回列表格式 [id, timestamp, duration, command]
268
+ command_parts = log_entry[3] if len(log_entry) > 3 else []
269
+ command = ' '.join(str(arg) for arg in command_parts[:5])
270
+ if len(command_parts) > 5:
271
+ command += '...'
272
+
273
+ metrics['slowlog'].append({
274
+ 'id': log_entry[0],
275
+ 'timestamp': datetime.fromtimestamp(log_entry[1]).isoformat(),
276
+ 'duration_us': log_entry[2],
277
+ 'command': command,
278
+ })
279
+
280
+ except Exception as e:
281
+ logger.warning(f"获取慢查询日志失败: {e}")
282
+ traceback.print_exc()
283
+ metrics['slowlog'] = []
284
+
285
+ return {
286
+ 'success': True,
287
+ 'data': metrics
288
+ }
289
+
290
+ except HTTPException:
291
+ raise
292
+ except Exception as e:
293
+ logger.error(f"获取Redis监控数据失败: {e}")
294
+ traceback.print_exc()
295
+ raise HTTPException(status_code=500, detail=f"获取Redis监控数据失败: {str(e)}")
296
+
297
+
298
+ @router.get("/config/{namespace}")
299
+ async def get_redis_config(namespace: str) -> Dict[str, Any]:
300
+ """
301
+ 获取Redis配置信息
302
+ """
303
+ try:
304
+ namespace_data = get_namespace_data_access()
305
+ connection = await namespace_data.manager.get_connection(namespace)
306
+
307
+ if not connection:
308
+ raise HTTPException(status_code=404, detail=f"命名空间 {namespace} 不存在")
309
+
310
+ # 获取Redis客户端
311
+ redis_client = await connection.get_redis_client(decode=True)
312
+
313
+ # 获取所有配置
314
+ config = await redis_client.config_get()
315
+
316
+ # 组织配置信息
317
+ important_configs = {
318
+ 'maxmemory': config.get('maxmemory', '0'),
319
+ 'maxmemory-policy': config.get('maxmemory-policy', 'noeviction'),
320
+ 'timeout': config.get('timeout', '0'),
321
+ 'tcp-keepalive': config.get('tcp-keepalive', '0'),
322
+ 'databases': config.get('databases', '16'),
323
+ 'save': config.get('save', ''),
324
+ 'appendonly': config.get('appendonly', 'no'),
325
+ 'appendfsync': config.get('appendfsync', 'everysec'),
326
+ 'slowlog-log-slower-than': config.get('slowlog-log-slower-than', '10000'),
327
+ 'slowlog-max-len': config.get('slowlog-max-len', '128'),
328
+ }
329
+
330
+ return {
331
+ 'success': True,
332
+ 'data': {
333
+ 'namespace': namespace,
334
+ 'important_configs': important_configs,
335
+ 'all_configs': config
336
+ }
337
+ }
338
+
339
+ except HTTPException:
340
+ raise
341
+ except Exception as e:
342
+ logger.error(f"获取Redis配置失败: {e}")
343
+ traceback.print_exc()
344
+ raise HTTPException(status_code=500, detail=f"获取Redis配置失败: {str(e)}")
345
+
346
+
347
+ @router.post("/command/{namespace}")
348
+ async def execute_redis_command(
349
+ namespace: str,
350
+ command: str,
351
+ args: List[str] = None
352
+ ) -> Dict[str, Any]:
353
+ """
354
+ 执行Redis命令(仅限安全的只读命令)
355
+ """
356
+ # 允许的安全命令白名单
357
+ SAFE_COMMANDS = {
358
+ 'ping', 'info', 'dbsize', 'lastsave', 'type', 'ttl', 'pttl',
359
+ 'exists', 'keys', 'scan', 'get', 'mget', 'strlen', 'getrange',
360
+ 'llen', 'lrange', 'lindex', 'scard', 'smembers', 'sismember',
361
+ 'zcard', 'zrange', 'zrevrange', 'zrank', 'zrevrank', 'zscore',
362
+ 'hlen', 'hkeys', 'hvals', 'hget', 'hmget', 'hgetall', 'hexists',
363
+ 'memory', 'client', 'config', 'slowlog'
364
+ }
365
+
366
+ command_lower = command.lower()
367
+
368
+ # 检查命令是否在白名单中
369
+ if command_lower not in SAFE_COMMANDS:
370
+ raise HTTPException(
371
+ status_code=403,
372
+ detail=f"命令 {command} 不被允许。仅支持只读命令。"
373
+ )
374
+
375
+ try:
376
+ namespace_data = get_namespace_data_access()
377
+ connection = await namespace_data.manager.get_connection(namespace)
378
+
379
+ if not connection:
380
+ raise HTTPException(status_code=404, detail=f"命名空间 {namespace} 不存在")
381
+
382
+ # 获取Redis客户端
383
+ redis_client = await connection.get_redis_client(decode=True)
384
+
385
+ # 执行命令
386
+ if args:
387
+ result = await redis_client.execute_command(command, *args)
388
+ else:
389
+ result = await redis_client.execute_command(command)
390
+
391
+ # 处理不同类型的返回值
392
+ if isinstance(result, bytes):
393
+ result = result.decode('utf-8')
394
+ elif isinstance(result, (list, tuple)):
395
+ result = [r.decode('utf-8') if isinstance(r, bytes) else r for r in result]
396
+
397
+ return {
398
+ 'success': True,
399
+ 'data': {
400
+ 'command': f"{command} {' '.join(args or [])}".strip(),
401
+ 'result': result
402
+ }
403
+ }
404
+
405
+ except HTTPException:
406
+ raise
407
+ except Exception as e:
408
+ logger.error(f"执行Redis命令失败: {e}")
409
+ traceback.print_exc()
410
+ raise HTTPException(status_code=500, detail=f"执行Redis命令失败: {str(e)}")
411
+
412
+
413
+ @router.get("/performance/{namespace}")
414
+ async def get_redis_performance(
415
+ namespace: str,
416
+ duration_seconds: int = 5
417
+ ) -> Dict[str, Any]:
418
+ """
419
+ 获取Redis性能测试数据
420
+ 通过执行多次PING命令来测试延迟
421
+ """
422
+ try:
423
+ namespace_data = get_namespace_data_access()
424
+ connection = await namespace_data.manager.get_connection(namespace)
425
+
426
+ if not connection:
427
+ raise HTTPException(status_code=404, detail=f"命名空间 {namespace} 不存在")
428
+
429
+ # 获取Redis客户端
430
+ redis_client = await connection.get_redis_client(decode=True)
431
+
432
+ # 性能测试
433
+ latencies = []
434
+ test_count = 100
435
+
436
+ for _ in range(test_count):
437
+ start_time = time.perf_counter()
438
+ await redis_client.ping()
439
+ end_time = time.perf_counter()
440
+ latency_ms = (end_time - start_time) * 1000
441
+ latencies.append(latency_ms)
442
+ await asyncio.sleep(duration_seconds / test_count)
443
+
444
+ # 计算统计数据
445
+ latencies.sort()
446
+ avg_latency = sum(latencies) / len(latencies)
447
+ min_latency = latencies[0]
448
+ max_latency = latencies[-1]
449
+ p50_latency = latencies[int(len(latencies) * 0.5)]
450
+ p95_latency = latencies[int(len(latencies) * 0.95)]
451
+ p99_latency = latencies[int(len(latencies) * 0.99)]
452
+
453
+ return {
454
+ 'success': True,
455
+ 'data': {
456
+ 'namespace': namespace,
457
+ 'test_count': test_count,
458
+ 'duration_seconds': duration_seconds,
459
+ 'latency_ms': {
460
+ 'avg': round(avg_latency, 3),
461
+ 'min': round(min_latency, 3),
462
+ 'max': round(max_latency, 3),
463
+ 'p50': round(p50_latency, 3),
464
+ 'p95': round(p95_latency, 3),
465
+ 'p99': round(p99_latency, 3),
466
+ },
467
+ 'timestamp': datetime.now().isoformat()
468
+ }
469
+ }
470
+
471
+ except HTTPException:
472
+ raise
473
+ except Exception as e:
474
+ logger.error(f"Redis性能测试失败: {e}")
475
+ traceback.print_exc()
476
+ raise HTTPException(status_code=500, detail=f"Redis性能测试失败: {str(e)}")