jettask 0.2.19__py3-none-any.whl → 0.2.23__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 +12 -3
  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.19.dist-info → jettask-0.2.23.dist-info}/METADATA +2 -71
  86. jettask-0.2.23.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.19.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.19.dist-info → jettask-0.2.23.dist-info}/WHEEL +0 -0
  163. {jettask-0.2.19.dist-info → jettask-0.2.23.dist-info}/entry_points.txt +0 -0
  164. {jettask-0.2.19.dist-info → jettask-0.2.23.dist-info}/licenses/LICENSE +0 -0
  165. {jettask-0.2.19.dist-info → jettask-0.2.23.dist-info}/top_level.txt +0 -0
@@ -1,1088 +0,0 @@
1
- import gradio as gr
2
- import pandas as pd
3
- import plotly.graph_objects as go
4
- import plotly.express as px
5
- from datetime import datetime, timedelta, timezone
6
- import asyncio
7
- import json
8
- import logging
9
- import time
10
- from typing import Dict, List
11
- from concurrent.futures import ThreadPoolExecutor
12
- import redis.asyncio as redis
13
- from sqlalchemy import text
14
- from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
15
- from sqlalchemy.orm import sessionmaker
16
-
17
- from jettask.webui_config import RedisConfig, PostgreSQLConfig
18
-
19
- # 设置日志
20
- logging.basicConfig(level=logging.INFO)
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- class IntegratedDataAccess:
25
- """直接访问数据源,不通过API"""
26
-
27
- def __init__(self):
28
- self.redis_config = None
29
- self.pg_config = None
30
- self.redis_prefix = "jettask"
31
- self.async_engine = None
32
- self.AsyncSessionLocal = None
33
-
34
- async def initialize(self):
35
- """初始化数据库配置"""
36
- # 保存配置
37
- self.redis_config = RedisConfig.from_env()
38
- self.pg_config = PostgreSQLConfig.from_env()
39
-
40
- # 初始化PostgreSQL引擎
41
- if self.pg_config.dsn:
42
- dsn = self.pg_config.dsn
43
- if dsn.startswith('postgresql://'):
44
- dsn = dsn.replace('postgresql://', 'postgresql+psycopg://', 1)
45
-
46
- self.async_engine = create_async_engine(
47
- dsn,
48
- pool_size=20,
49
- max_overflow=10,
50
- pool_pre_ping=True,
51
- echo=False
52
- )
53
-
54
- self.AsyncSessionLocal = sessionmaker(
55
- self.async_engine,
56
- class_=AsyncSession,
57
- expire_on_commit=False
58
- )
59
- logger.info("Database configuration initialized")
60
- else:
61
- logger.warning("PostgreSQL connection not configured")
62
-
63
- async def close(self):
64
- """关闭数据库连接"""
65
- if self.async_engine:
66
- await self.async_engine.dispose()
67
-
68
- async def _get_redis_client(self):
69
- """获取Redis客户端"""
70
- return redis.Redis(
71
- host=self.redis_config.host,
72
- port=self.redis_config.port,
73
- db=self.redis_config.db,
74
- password=self.redis_config.password,
75
- decode_responses=False
76
- )
77
-
78
- async def get_global_stats(self) -> Dict:
79
- """获取全局统计信息"""
80
- redis_client = await self._get_redis_client()
81
- try:
82
- # 获取所有队列
83
- pattern = f"{self.redis_prefix}:QUEUE:*"
84
- all_queues = set()
85
- async for key in redis_client.scan_iter(match=pattern, count=100):
86
- queue_name = key.decode('utf-8').split(":")[-1]
87
- all_queues.add(queue_name)
88
-
89
- # 获取worker信息
90
- worker_pattern = f"{self.redis_prefix}:CONSUMER:*"
91
- all_workers = set()
92
- online_workers = 0
93
-
94
- async for key in redis_client.scan_iter(match=worker_pattern, count=100):
95
- consumer_id = key.decode('utf-8').split(":")[-1]
96
- all_workers.add(consumer_id)
97
-
98
- # 检查是否在线
99
- last_heartbeat = await redis_client.hget(key, b'last_heartbeat')
100
- if last_heartbeat:
101
- try:
102
- last_heartbeat_time = float(last_heartbeat)
103
- if time.time() - last_heartbeat_time < 30:
104
- online_workers += 1
105
- except:
106
- pass
107
-
108
- # 从PostgreSQL获取任务统计
109
- task_stats = {
110
- 'pending_tasks': 0,
111
- 'running_tasks': 0,
112
- 'completed_tasks': 0,
113
- 'failed_tasks': 0,
114
- 'total_tasks': 0
115
- }
116
-
117
- if self.AsyncSessionLocal:
118
- async with self.AsyncSessionLocal() as session:
119
- query = text("""
120
- SELECT
121
- status,
122
- COUNT(*) as count
123
- FROM tasks
124
- GROUP BY status
125
- """)
126
- result = await session.execute(query)
127
- rows = result.mappings().all()
128
-
129
- for row in rows:
130
- status = row['status']
131
- count = row['count']
132
- if status == 'pending':
133
- task_stats['pending_tasks'] = count
134
- elif status == 'running':
135
- task_stats['running_tasks'] = count
136
- elif status in ('success', 'completed'):
137
- task_stats['completed_tasks'] = count
138
- elif status in ('failed', 'error'):
139
- task_stats['failed_tasks'] = count
140
- task_stats['total_tasks'] += count
141
-
142
- return {
143
- 'total_queues': len(all_queues),
144
- 'active_queues': len(all_queues), # 简化处理
145
- 'total_workers': len(all_workers),
146
- 'online_workers': online_workers,
147
- **task_stats
148
- }
149
-
150
- except Exception as e:
151
- logger.error(f"Error getting global stats: {e}")
152
- return {
153
- 'total_queues': 0,
154
- 'active_queues': 0,
155
- 'total_workers': 0,
156
- 'online_workers': 0,
157
- 'pending_tasks': 0,
158
- 'running_tasks': 0,
159
- 'completed_tasks': 0,
160
- 'failed_tasks': 0,
161
- 'total_tasks': 0
162
- }
163
- finally:
164
- await redis_client.close()
165
-
166
- async def get_queues(self) -> List[str]:
167
- """获取所有队列"""
168
- redis_client = await self._get_redis_client()
169
- try:
170
- pattern = f"{self.redis_prefix}:QUEUE:*"
171
- queues = []
172
- async for key in redis_client.scan_iter(match=pattern, count=100):
173
- queue_name = key.decode('utf-8').split(":")[-1]
174
- queues.append(queue_name)
175
- return sorted(queues)
176
- finally:
177
- await redis_client.close()
178
-
179
- async def get_queue_stats(self, queue_name: str) -> Dict:
180
- """获取队列统计信息"""
181
- redis_client = await self._get_redis_client()
182
- try:
183
- stream_key = f"{self.redis_prefix}:QUEUE:{queue_name}"
184
-
185
- try:
186
- # 获取stream信息
187
- info = await redis_client.xinfo_stream(stream_key)
188
-
189
- # 获取消费者组信息
190
- groups = await redis_client.xinfo_groups(stream_key)
191
- consumers = 0
192
- for group in groups:
193
- group_info = await redis_client.xinfo_consumers(stream_key, group[b'name'])
194
- consumers += len(group_info)
195
-
196
- return {
197
- 'messages_ready': info[b'length'],
198
- 'messages_unacknowledged': info.get(b'groups', 0),
199
- 'consumers': consumers
200
- }
201
- except:
202
- return {
203
- 'messages_ready': 0,
204
- 'messages_unacknowledged': 0,
205
- 'consumers': 0
206
- }
207
- finally:
208
- await redis_client.close()
209
-
210
- async def get_queue_timeline(self, queue_names: List[str], start_time: datetime, end_time: datetime) -> Dict:
211
- """获取队列时间线数据"""
212
- if not self.AsyncSessionLocal:
213
- return {"queues": []}
214
-
215
- # # 计算时间间隔
216
- duration = (end_time - start_time).total_seconds()
217
- if duration <= 300: # <= 5分钟
218
- interval_seconds = 1
219
- interval_type = 'millisecond'
220
- elif duration <= 900: # <= 15分钟
221
- interval_seconds = 1
222
- interval_type = 'second'
223
- elif duration <= 1800: # <= 30分钟
224
- interval_seconds = 2
225
- interval_type = 'second'
226
- elif duration <= 3600: # <= 1小时
227
- interval_seconds = 30
228
- interval_type = 'second'
229
- elif duration <= 10800: # <= 3小时
230
- interval_seconds = 300
231
- interval_type = 'minute'
232
- else:
233
- interval_seconds = 3600
234
- interval_type = 'hour'
235
- print(f'{interval_seconds=} {interval_type=}')
236
- result = []
237
-
238
- for queue_name in queue_names[:10]: # 最多10个队列
239
- try:
240
- async with self.AsyncSessionLocal() as session:
241
- # 构建SQL查询
242
- if interval_type == 'millisecond':
243
- query = text(f"""
244
- SELECT
245
- DATE_TRUNC('second', created_at) +
246
- INTERVAL '{interval_seconds} seconds' * FLOOR(EXTRACT(EPOCH FROM created_at) * 2) / 2 as time_bucket,
247
- COUNT(*) as count
248
- FROM tasks
249
- WHERE queue_name = :queue_name
250
- AND created_at >= :start_dt
251
- AND created_at < :end_dt
252
- GROUP BY time_bucket
253
- ORDER BY time_bucket
254
- """)
255
- elif interval_type == 'second':
256
- query = text(f"""
257
- SELECT
258
- DATE_TRUNC('minute', created_at) +
259
- INTERVAL '{interval_seconds} seconds' * FLOOR(EXTRACT(SECOND FROM created_at) / {interval_seconds}) as time_bucket,
260
- COUNT(*) as count
261
- FROM tasks
262
- WHERE queue_name = :queue_name
263
- AND created_at >= :start_dt
264
- AND created_at < :end_dt
265
- GROUP BY time_bucket
266
- ORDER BY time_bucket
267
- """)
268
- elif interval_type == 'minute':
269
- interval_minutes = int(interval_seconds / 60)
270
- query = text(f"""
271
- SELECT
272
- DATE_TRUNC('hour', created_at) +
273
- INTERVAL '{interval_minutes} minutes' * FLOOR(EXTRACT(MINUTE FROM created_at) / {interval_minutes}) as time_bucket,
274
- COUNT(*) as count
275
- FROM tasks
276
- WHERE queue_name = :queue_name
277
- AND created_at >= :start_dt
278
- AND created_at < :end_dt
279
- GROUP BY time_bucket
280
- ORDER BY time_bucket
281
- """)
282
- else: # hour
283
- interval_hours = int(interval_seconds / 3600)
284
- query = text(f"""
285
- SELECT
286
- DATE_TRUNC('day', created_at) +
287
- INTERVAL '{interval_hours} hours' * FLOOR(EXTRACT(HOUR FROM created_at) / {interval_hours}) as time_bucket,
288
- COUNT(*) as count
289
- FROM tasks
290
- WHERE queue_name = :queue_name
291
- AND created_at >= :start_dt
292
- AND created_at < :end_dt
293
- GROUP BY time_bucket
294
- ORDER BY time_bucket
295
- """)
296
-
297
- result_obj = await session.execute(query, {
298
- 'queue_name': queue_name,
299
- 'start_dt': start_time,
300
- 'end_dt': end_time
301
- })
302
- rows = result_obj.mappings().all()
303
-
304
- # 构建时间线
305
- timeline = []
306
- for row in rows:
307
- timeline.append({
308
- "time": row['time_bucket'].isoformat(),
309
- "count": row['count']
310
- })
311
-
312
- # 填充缺失的时间点
313
- filled_timeline = self._fill_missing_timepoints(
314
- timeline, start_time, end_time, interval_seconds
315
- )
316
-
317
- result.append({
318
- "queue": queue_name,
319
- "timeline": {"timeline": filled_timeline}
320
- })
321
-
322
- except Exception as e:
323
- logger.error(f"Error getting timeline for queue {queue_name}: {e}")
324
-
325
- return {"queues": result}
326
-
327
- def _fill_missing_timepoints(self, timeline: List[Dict], start_time: datetime,
328
- end_time: datetime, interval_seconds: float) -> List[Dict]:
329
- """填充缺失的时间点"""
330
- # 创建时间映射
331
- time_map = {}
332
- for item in timeline:
333
- dt = datetime.fromisoformat(item['time'])
334
- time_map[dt] = item['count']
335
-
336
- # 生成完整时间序列
337
- filled = []
338
- current = self._align_time(start_time, interval_seconds)
339
-
340
- while current < end_time:
341
- # 查找最近的数据点
342
- count = 0
343
- for dt, cnt in time_map.items():
344
- if abs((dt - current).total_seconds()) < interval_seconds / 2:
345
- count = cnt
346
- break
347
-
348
- filled.append({
349
- "time": current.isoformat(),
350
- "count": count
351
- })
352
-
353
- current += timedelta(seconds=interval_seconds)
354
-
355
- return filled
356
-
357
- def _align_time(self, dt: datetime, interval_seconds: float) -> datetime:
358
- """对齐时间到间隔"""
359
- if interval_seconds >= 3600:
360
- dt = dt.replace(minute=0, second=0, microsecond=0)
361
- hours = int(interval_seconds / 3600)
362
- aligned_hour = (dt.hour // hours) * hours
363
- return dt.replace(hour=aligned_hour)
364
- elif interval_seconds >= 60:
365
- dt = dt.replace(second=0, microsecond=0)
366
- minutes = int(interval_seconds / 60)
367
- total_minutes = dt.hour * 60 + dt.minute
368
- aligned_minutes = (total_minutes // minutes) * minutes
369
- return dt.replace(hour=aligned_minutes // 60, minute=aligned_minutes % 60)
370
- elif interval_seconds >= 1:
371
- dt = dt.replace(microsecond=0)
372
- aligned_second = int(dt.second // interval_seconds) * int(interval_seconds)
373
- return dt.replace(second=aligned_second)
374
- else:
375
- # 毫秒级别
376
- ms = dt.microsecond / 1000
377
- interval_ms = interval_seconds * 1000
378
- aligned_ms = int(ms // interval_ms) * interval_ms
379
- return dt.replace(microsecond=int(aligned_ms * 1000))
380
-
381
-
382
- class IntegratedJetTaskMonitor:
383
- """集成的JetTask监控器"""
384
-
385
- def __init__(self):
386
- self.data_access = IntegratedDataAccess()
387
- self.queue_data = []
388
- self.executor = None
389
- self.loop = None
390
- self._closed = False
391
-
392
- def _run_async(self, coro):
393
- """在专用线程中运行异步代码"""
394
- if self._closed:
395
- raise RuntimeError("Monitor is closed")
396
-
397
- # 延迟创建 executor
398
- if self.executor is None:
399
- self.executor = ThreadPoolExecutor(max_workers=1)
400
-
401
- def run():
402
- loop = asyncio.new_event_loop()
403
- asyncio.set_event_loop(loop)
404
- try:
405
- return loop.run_until_complete(coro)
406
- finally:
407
- loop.close()
408
-
409
- future = self.executor.submit(run)
410
- return future.result()
411
-
412
- def initialize(self):
413
- """初始化数据访问"""
414
- self._run_async(self.data_access.initialize())
415
-
416
- def close(self):
417
- """关闭连接"""
418
- if self._closed:
419
- return
420
-
421
- self._closed = True
422
-
423
- try:
424
- if self.data_access:
425
- # 使用新的事件循环来关闭连接
426
- loop = asyncio.new_event_loop()
427
- asyncio.set_event_loop(loop)
428
- try:
429
- loop.run_until_complete(self.data_access.close())
430
- finally:
431
- loop.close()
432
- except Exception as e:
433
- logger.error(f"Error closing data access: {e}")
434
-
435
- if self.executor:
436
- self.executor.shutdown(wait=True)
437
- self.executor = None
438
-
439
- def fetch_global_stats(self, return_dict: bool = False):
440
- """获取全局统计数据"""
441
- stats = self._run_async(self.data_access.get_global_stats())
442
-
443
- if return_dict:
444
- # 返回原始字典数据
445
- return stats
446
-
447
- # 构建显示文本
448
- stats_text = f"""
449
- ## 系统概览
450
-
451
- ### Workers
452
- - 在线: {stats.get('online_workers', 0)} / {stats.get('total_workers', 0)}
453
- - 活跃队列: {stats.get('active_queues', 0)} / {stats.get('total_queues', 0)}
454
-
455
- ### 任务统计
456
- - 待处理: {stats.get('pending_tasks', 0):,}
457
- - 运行中: {stats.get('running_tasks', 0):,}
458
- - 已完成: {stats.get('completed_tasks', 0):,}
459
- - 失败: {stats.get('failed_tasks', 0):,}
460
-
461
- ### 实时性能
462
- - 总任务数: {stats.get('total_tasks', 0):,}
463
- - 成功率: {self._calculate_success_rate(stats):.1f}%
464
- """
465
- return stats_text
466
-
467
- def _calculate_success_rate(self, stats: Dict) -> float:
468
- """计算成功率"""
469
- completed = stats.get('completed_tasks', 0)
470
- failed = stats.get('failed_tasks', 0)
471
- total = completed + failed
472
- return (completed / total * 100) if total > 0 else 0
473
-
474
- def fetch_queues_data(self):
475
- """获取队列数据"""
476
- queues = self._run_async(self.data_access.get_queues())
477
-
478
- detailed_queues = []
479
- for queue_name in queues:
480
- stats = self._run_async(self.data_access.get_queue_stats(queue_name))
481
-
482
- queue_info = {
483
- '队列名称': queue_name,
484
- '待处理': stats.get('messages_ready', 0),
485
- '处理中': stats.get('messages_unacknowledged', 0),
486
- '消费者': stats.get('consumers', 0)
487
- }
488
- detailed_queues.append(queue_info)
489
-
490
- self.queue_data = detailed_queues
491
- return pd.DataFrame(detailed_queues) if detailed_queues else pd.DataFrame()
492
-
493
- def create_queue_timeline_chart(self, time_range: str = "1h", selected_queues: List[str] = None,
494
- custom_start: datetime = None, custom_end: datetime = None,
495
- return_with_config: bool = False):
496
- """创建队列时间线图表"""
497
- # 计算时间范围
498
- if custom_start and custom_end:
499
- # 使用自定义时间范围
500
- start_time = custom_start
501
- end_time = custom_end
502
- time_range = "custom"
503
- else:
504
- # 使用预设时间范围
505
- now = datetime.now(timezone.utc)
506
- end_time = now
507
-
508
- if time_range == "15m":
509
- start_time = end_time - timedelta(minutes=15)
510
- elif time_range == "30m":
511
- start_time = end_time - timedelta(minutes=30)
512
- elif time_range == "1h":
513
- start_time = end_time - timedelta(hours=1)
514
- elif time_range == "3h":
515
- start_time = end_time - timedelta(hours=3)
516
- elif time_range == "6h":
517
- start_time = end_time - timedelta(hours=6)
518
- elif time_range == "12h":
519
- start_time = end_time - timedelta(hours=12)
520
- elif time_range == "24h":
521
- start_time = end_time - timedelta(days=1)
522
- elif time_range == "today":
523
- # 今天的开始时间
524
- start_time = now.replace(hour=0, minute=0, second=0, microsecond=0)
525
- end_time = now
526
- elif time_range == "this_week":
527
- # 本周的开始时间(周一)
528
- days_since_monday = now.weekday()
529
- start_time = now - timedelta(days=days_since_monday)
530
- start_time = start_time.replace(hour=0, minute=0, second=0, microsecond=0)
531
- end_time = now
532
- elif time_range.endswith("d"):
533
- # 处理天数
534
- try:
535
- days = int(time_range[:-1])
536
- start_time = end_time - timedelta(days=days)
537
- except:
538
- start_time = end_time - timedelta(hours=1)
539
- elif time_range.endswith("h"):
540
- # 处理小时数
541
- try:
542
- hours = int(time_range[:-1])
543
- start_time = end_time - timedelta(hours=hours)
544
- except:
545
- start_time = end_time - timedelta(hours=1)
546
- elif time_range.endswith("m"):
547
- # 处理分钟数
548
- try:
549
- minutes = int(time_range[:-1])
550
- start_time = end_time - timedelta(minutes=minutes)
551
- except:
552
- start_time = end_time - timedelta(hours=1)
553
- elif time_range == "1y":
554
- start_time = end_time - timedelta(days=365)
555
- else:
556
- start_time = end_time - timedelta(hours=1)
557
-
558
- # 获取队列列表
559
- if not selected_queues or len(selected_queues) == 0:
560
- # 构建标题
561
- if time_range == "custom":
562
- time_display = f"{start_time.strftime('%Y-%m-%d %H:%M')} - {end_time.strftime('%Y-%m-%d %H:%M')}"
563
- else:
564
- time_display = time_range
565
-
566
- return go.Figure().update_layout(
567
- title=f"队列处理趋势 - {time_display} (请选择队列)",
568
- xaxis_title="时间(本地时区)",
569
- yaxis_title="任务数量",
570
- template='plotly_dark',
571
- height=500
572
- )
573
-
574
- # 获取时间线数据
575
- timeline_data = self._run_async(
576
- self.data_access.get_queue_timeline(selected_queues, start_time, end_time)
577
- )
578
-
579
- # 创建图表
580
- fig = go.Figure()
581
- colors = px.colors.qualitative.Plotly + px.colors.qualitative.D3
582
-
583
- for i, queue_data in enumerate(timeline_data.get('queues', [])):
584
- queue_name = queue_data['queue']
585
- timeline = queue_data.get('timeline', {}).get('timeline', [])
586
-
587
- if timeline:
588
- # 转换为本地时间
589
- local_times = []
590
- for item in timeline:
591
- utc_time = datetime.fromisoformat(item['time'].replace('Z', '+00:00'))
592
- local_time = utc_time.replace(tzinfo=timezone.utc).astimezone()
593
- local_times.append(local_time)
594
-
595
- counts = [item['count'] for item in timeline]
596
- hover_times = [t.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] for t in local_times]
597
-
598
- fig.add_trace(go.Scatter(
599
- x=local_times,
600
- y=counts,
601
- name=queue_name,
602
- mode='lines+markers',
603
- line=dict(color=colors[i % len(colors)], width=2),
604
- marker=dict(size=5),
605
- customdata=hover_times,
606
- hovertemplate='<b>%{fullData.name}</b><br>' +
607
- '时间: %{customdata}<br>' +
608
- '任务数: %{y}<br>' +
609
- '<extra></extra>'
610
- ))
611
-
612
- # 构建标题
613
- if time_range == "custom":
614
- time_display = f"{start_time.strftime('%Y-%m-%d %H:%M')} - {end_time.strftime('%Y-%m-%d %H:%M')}"
615
- else:
616
- time_display = time_range
617
-
618
- fig.update_layout(
619
- title=f"队列处理趋势 - {time_display}",
620
- xaxis_title="时间(本地时区)",
621
- yaxis_title="任务数量",
622
- hovermode='x unified',
623
- template='plotly_dark',
624
- height=500,
625
- xaxis=dict(
626
- tickformat='%Y-%m-%d<br>%H:%M:%S.%L',
627
- tickangle=-45,
628
- showgrid=True,
629
- gridcolor='rgba(128, 128, 128, 0.2)',
630
- tickformatstops=[
631
- dict(dtickrange=[None, 1000], value="%H:%M:%S.%L"),
632
- dict(dtickrange=[1000, 60000], value="%H:%M:%S"),
633
- dict(dtickrange=[60000, 3600000], value="%H:%M"),
634
- dict(dtickrange=[3600000, None], value="%Y-%m-%d<br>%H:%M")
635
- ]
636
- ),
637
- yaxis=dict(
638
- showgrid=True,
639
- gridcolor='rgba(128, 128, 128, 0.2)',
640
- fixedrange=True # 固定Y轴,不允许缩放
641
- ),
642
- legend=dict(
643
- orientation="v",
644
- yanchor="top",
645
- y=0.99,
646
- xanchor="left",
647
- x=1.01
648
- ),
649
- margin=dict(r=150),
650
- dragmode='zoom', # 使用缩放模式
651
- selectdirection='h' # 只允许水平选择
652
- )
653
-
654
- # 配置轴以支持正确的选择行为
655
- fig.update_xaxes(
656
- fixedrange=False, # 允许X轴交互
657
- showspikes=True, # 显示垂直线
658
- spikemode='across',
659
- spikesnap='cursor',
660
- spikecolor='gray',
661
- spikethickness=1
662
- )
663
-
664
- fig.update_yaxes(
665
- fixedrange=True # 保持Y轴固定
666
- )
667
-
668
- # 添加自定义数据属性以便跟踪时间范围
669
- fig.add_annotation(
670
- x=0, y=0,
671
- text="",
672
- showarrow=False,
673
- visible=False,
674
- # 存储时间信息
675
- name="time_info"
676
- )
677
-
678
- # 配置图表以支持缩放事件
679
- fig.update_layout(
680
- # 允许在x轴上进行框选缩放
681
- dragmode='zoom',
682
- selectdirection='h',
683
- # 显示缩放和重置按钮
684
- showlegend=True,
685
- hovermode='x unified',
686
- # 保存缩放状态
687
- uirevision='constant',
688
- # 添加范围选择器和范围滑块
689
- xaxis=dict(
690
- rangeslider=dict(visible=True),
691
- rangeselector=dict(
692
- buttons=list([
693
- dict(count=15, label="15分钟", step="minute", stepmode="backward"),
694
- dict(count=1, label="1小时", step="hour", stepmode="backward"),
695
- dict(count=6, label="6小时", step="hour", stepmode="backward"),
696
- dict(count=1, label="1天", step="day", stepmode="backward"),
697
- dict(step="all", label="全部")
698
- ])
699
- )
700
- )
701
- )
702
-
703
- if return_with_config:
704
- return fig, start_time, end_time
705
- return fig
706
-
707
-
708
- # 创建全局监控实例
709
- monitor = None
710
-
711
- def get_or_create_monitor():
712
- """获取或创建监控器实例"""
713
- global monitor
714
- if monitor is None or monitor._closed:
715
- monitor = IntegratedJetTaskMonitor()
716
- monitor.initialize()
717
- return monitor
718
-
719
-
720
- def create_integrated_interface():
721
- """创建集成的Gradio界面"""
722
- # 使用全局监控器
723
- global monitor
724
- monitor = get_or_create_monitor()
725
-
726
- # 自定义CSS样式 - 极简版
727
- custom_css = """
728
- /* 让下拉框更紧凑 */
729
- .gr-dropdown {
730
- min-width: 150px;
731
- }
732
- """
733
-
734
- with gr.Blocks(title="JetTask Monitor", theme=gr.themes.Soft(), css=custom_css) as app:
735
- gr.Markdown("# JetTask Monitor - 任务队列监控平台(集成版)")
736
- gr.Markdown("""
737
- **提示**: 为避免打断您的工作,系统不会自动刷新数据。
738
- - 点击 **刷新数据** 手动更新统计信息
739
- - 选择时间范围或队列时,只有图表会更新
740
- - 在图表上拖动缩放后,点击按钮应用为自定义时间
741
- """)
742
-
743
- with gr.Tab("概览"):
744
- # 队列处理趋势放在最上方
745
- with gr.Row():
746
- with gr.Column(scale=2):
747
- queue_selector_for_timeline = gr.CheckboxGroup(
748
- choices=[],
749
- value=[],
750
- label="选择队列(最多10个)",
751
- interactive=True
752
- )
753
- with gr.Column(scale=3):
754
- # 紧凑的时间选择器
755
- with gr.Row():
756
- with gr.Column(scale=1):
757
- time_range_dropdown = gr.Dropdown(
758
- choices=[
759
- ("最近15分钟", "15m"),
760
- ("最近30分钟", "30m"),
761
- ("最近1小时", "1h"),
762
- ("最近3小时", "3h"),
763
- ("最近6小时", "6h"),
764
- ("最近12小时", "12h"),
765
- ("最近24小时", "24h"),
766
- ("最近7天", "7d"),
767
- ("最近30天", "30d"),
768
- ("今天", "today"),
769
- ("本周", "this_week")
770
- ],
771
- value="15m",
772
- label="时间范围",
773
- interactive=True
774
- )
775
- with gr.Column(scale=1):
776
- refresh_chart_btn = gr.Button(
777
- "🔄 刷新图表",
778
- variant="primary",
779
- size="sm"
780
- )
781
-
782
- # 隐藏的状态存储
783
- time_range = gr.State("15m")
784
- actual_start_time = gr.State("")
785
- actual_end_time = gr.State("")
786
- custom_start_time = gr.State("")
787
- custom_end_time = gr.State("")
788
-
789
- # 队列趋势图表 - 启用交互模式
790
- with gr.Row():
791
- with gr.Column():
792
- queue_timeline_chart = gr.Plot(label="队列处理趋势")
793
-
794
- # 添加自定义HTML和JavaScript来监听Plotly事件
795
- gr.HTML("""
796
- <script>
797
- // 监听Plotly图表的缩放事件
798
- function setupPlotlyZoomListener() {
799
- const plots = document.querySelectorAll('.js-plotly-plot');
800
- plots.forEach(plot => {
801
- if (plot && plot._fullLayout && !plot._zoomListenerAdded) {
802
- plot._zoomListenerAdded = true;
803
-
804
- // 存储原始范围
805
- let originalRange = null;
806
- if (plot._fullLayout.xaxis && plot._fullLayout.xaxis.range) {
807
- originalRange = [...plot._fullLayout.xaxis.range];
808
- }
809
-
810
- plot.on('plotly_relayout', (eventData) => {
811
- // 检查是否有x轴范围变化
812
- if (eventData['xaxis.range[0]'] && eventData['xaxis.range[1]']) {
813
- const start = eventData['xaxis.range[0]'];
814
- const end = eventData['xaxis.range[1]'];
815
-
816
- // 将数据存储到window对象
817
- window.plotlyZoomRange = {
818
- start: start,
819
- end: end,
820
- timestamp: Date.now()
821
- };
822
-
823
- console.log('缩放事件:', start, '到', end);
824
- } else if (eventData['xaxis.autorange']) {
825
- // 双击重置
826
- window.plotlyZoomRange = null;
827
- console.log('重置缩放');
828
- }
829
- });
830
- }
831
- });
832
- }
833
-
834
- // 定期尝试设置监听器
835
- const setupInterval = setInterval(() => {
836
- setupPlotlyZoomListener();
837
- // 如果找到图表就停止
838
- if (document.querySelector('.js-plotly-plot')) {
839
- setTimeout(() => clearInterval(setupInterval), 5000);
840
- }
841
- }, 500);
842
- </script>
843
- """)
844
-
845
- # 用于触发Python回调的隐藏组件
846
- zoom_trigger = gr.Number(visible=False, value=0)
847
- zoom_data = gr.Textbox(visible=False, value="", elem_id="zoom_data")
848
-
849
- # 添加定时器检查缩放状态
850
- gr.HTML("""
851
- <script>
852
- // 定期检查缩放状态并触发更新
853
- let lastProcessedTimestamp = 0;
854
-
855
- setInterval(() => {
856
- if (window.plotlyZoomRange &&
857
- window.plotlyZoomRange.timestamp > lastProcessedTimestamp) {
858
-
859
- lastProcessedTimestamp = window.plotlyZoomRange.timestamp;
860
-
861
- // 更新zoom_data组件的值
862
- const zoomDataInput = document.querySelector('#zoom_data textarea');
863
- if (zoomDataInput) {
864
- const zoomInfo = JSON.stringify(window.plotlyZoomRange);
865
- zoomDataInput.value = zoomInfo;
866
- zoomDataInput.dispatchEvent(new Event('input', { bubbles: true }));
867
-
868
- console.log('触发数据更新:', zoomInfo);
869
- }
870
- }
871
- }, 1000); // 每秒检查一次
872
- </script>
873
- """, elem_id="zoom_checker")
874
-
875
- gr.Markdown("""
876
- **💡 提示**:
877
- - 使用鼠标框选时间范围,系统会自动获取该时段的详细数据
878
- - 双击图表可以重置到原始视图
879
- """)
880
-
881
- # 统计信息和刷新按钮
882
- with gr.Row():
883
- with gr.Column(scale=4):
884
- stats_display = gr.Markdown(monitor.fetch_global_stats())
885
- with gr.Column(scale=1):
886
- refresh_btn = gr.Button("刷新数据", variant="secondary")
887
-
888
- # 队列表格
889
- with gr.Row():
890
- queue_table = gr.DataFrame(
891
- monitor.fetch_queues_data(),
892
- label="队列状态",
893
- interactive=False
894
- )
895
-
896
- # 定义更新函数
897
- def update_stats_only():
898
- """仅更新统计信息和队列表格"""
899
- current_monitor = get_or_create_monitor()
900
- stats = current_monitor.fetch_global_stats()
901
- queues_df = current_monitor.fetch_queues_data()
902
- return stats, queues_df
903
-
904
- def update_overview():
905
- """更新概览页面(包括队列选择器)"""
906
- current_monitor = get_or_create_monitor()
907
- stats = current_monitor.fetch_global_stats()
908
- queues_df = current_monitor.fetch_queues_data()
909
-
910
- # 更新队列选择器
911
- timeline_queue_choices = [q['队列名称'] for q in current_monitor.queue_data]
912
-
913
- return (
914
- stats,
915
- queues_df,
916
- gr.update(choices=timeline_queue_choices, value=timeline_queue_choices[:3] if timeline_queue_choices else [])
917
- )
918
-
919
- def update_timeline_chart(time_range, selected_queues, custom_start=None, custom_end=None):
920
- """更新时间线图表"""
921
- current_monitor = get_or_create_monitor()
922
-
923
- # 如果提供了自定义时间,使用它
924
- if custom_start and custom_end:
925
- try:
926
- start_dt = datetime.fromisoformat(custom_start.replace('Z', '+00:00'))
927
- end_dt = datetime.fromisoformat(custom_end.replace('Z', '+00:00'))
928
- fig, actual_start, actual_end = current_monitor.create_queue_timeline_chart(
929
- "custom", selected_queues, start_dt, end_dt, return_with_config=True
930
- )
931
- except:
932
- # 如果解析失败,使用默认时间范围
933
- fig, actual_start, actual_end = current_monitor.create_queue_timeline_chart(
934
- time_range, selected_queues, return_with_config=True
935
- )
936
- else:
937
- fig, actual_start, actual_end = current_monitor.create_queue_timeline_chart(
938
- time_range, selected_queues, return_with_config=True
939
- )
940
-
941
- return fig, actual_start.isoformat(), actual_end.isoformat()
942
-
943
- def handle_time_range_change(time_value):
944
- """处理时间范围变化"""
945
- return time_value
946
-
947
- def init_timeline_chart():
948
- """初始化时间线图表"""
949
- current_monitor = get_or_create_monitor()
950
- current_monitor.fetch_queues_data() # 获取队列数据
951
- initial_queues = [q['队列名称'] for q in current_monitor.queue_data][:3]
952
-
953
- if initial_queues:
954
- fig, start_time, end_time = current_monitor.create_queue_timeline_chart("15m", initial_queues, return_with_config=True)
955
- else:
956
- fig, start_time, end_time = current_monitor.create_queue_timeline_chart("15m", [], return_with_config=True)
957
-
958
- return fig, start_time.isoformat(), end_time.isoformat()
959
-
960
- def handle_zoom_change(zoom_data_json, selected_queues):
961
- """处理缩放变化,自动获取新数据"""
962
- if not zoom_data_json:
963
- return gr.update(), gr.update(), gr.update(), gr.update()
964
-
965
- try:
966
- import json
967
- zoom_data = json.loads(zoom_data_json)
968
-
969
- # 解析时间
970
- start_str = zoom_data['start']
971
- end_str = zoom_data['end']
972
-
973
- # Plotly返回的时间格式可能是 "2024-01-01 12:00:00" 或 ISO格式
974
- try:
975
- start_dt = datetime.fromisoformat(start_str.replace(' ', 'T').replace('Z', '+00:00'))
976
- except:
977
- start_dt = datetime.strptime(start_str.split('.')[0], "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
978
-
979
- try:
980
- end_dt = datetime.fromisoformat(end_str.replace(' ', 'T').replace('Z', '+00:00'))
981
- except:
982
- end_dt = datetime.strptime(end_str.split('.')[0], "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
983
-
984
- print(f"检测到缩放: {start_dt} 到 {end_dt}")
985
-
986
- # 调用后端接口获取新数据
987
- current_monitor = get_or_create_monitor()
988
- fig, actual_start, actual_end = current_monitor.create_queue_timeline_chart(
989
- "custom", selected_queues, start_dt, end_dt, return_with_config=True
990
- )
991
-
992
- # 计算时间间隔以显示数据粒度
993
- duration = (end_dt - start_dt).total_seconds()
994
- if duration <= 900: # <= 15分钟
995
- granularity = "毫秒级"
996
- elif duration <= 3600: # <= 1小时
997
- granularity = "秒级"
998
- elif duration <= 10800: # <= 3小时
999
- granularity = "30秒间隔"
1000
- elif duration <= 86400: # <= 1天
1001
- granularity = "5分钟间隔"
1002
- else:
1003
- granularity = "1小时间隔"
1004
-
1005
- print(f"自动重新获取数据,粒度: {granularity}")
1006
-
1007
- return fig, actual_start.isoformat(), actual_end.isoformat(), "custom"
1008
- except Exception as e:
1009
- print(f"处理缩放事件出错: {e}")
1010
- return gr.update(), gr.update(), gr.update(), gr.update()
1011
-
1012
- # 事件绑定
1013
- # 手动刷新按钮 - 只更新统计和表格,不改变用户的选择
1014
- refresh_btn.click(
1015
- update_stats_only,
1016
- outputs=[stats_display, queue_table]
1017
- )
1018
-
1019
- # 刷新图表按钮
1020
- refresh_chart_btn.click(
1021
- update_timeline_chart,
1022
- inputs=[time_range, queue_selector_for_timeline],
1023
- outputs=[queue_timeline_chart, actual_start_time, actual_end_time]
1024
- )
1025
-
1026
- # 同时刷新图表(使用当前选择的参数)
1027
- refresh_btn.click(
1028
- update_timeline_chart,
1029
- inputs=[time_range, queue_selector_for_timeline],
1030
- outputs=[queue_timeline_chart, actual_start_time, actual_end_time]
1031
- )
1032
-
1033
- # 时间范围下拉框变化
1034
- time_range_dropdown.change(
1035
- handle_time_range_change,
1036
- inputs=[time_range_dropdown],
1037
- outputs=[time_range]
1038
- ).then(
1039
- update_timeline_chart,
1040
- inputs=[time_range, queue_selector_for_timeline],
1041
- outputs=[queue_timeline_chart, actual_start_time, actual_end_time]
1042
- )
1043
-
1044
- # 队列选择变化时更新图表
1045
- queue_selector_for_timeline.change(
1046
- update_timeline_chart,
1047
- inputs=[time_range, queue_selector_for_timeline],
1048
- outputs=[queue_timeline_chart, actual_start_time, actual_end_time]
1049
- )
1050
-
1051
- # 监听缩放数据变化,自动更新图表
1052
- zoom_data.change(
1053
- handle_zoom_change,
1054
- inputs=[zoom_data, queue_selector_for_timeline],
1055
- outputs=[queue_timeline_chart, actual_start_time, actual_end_time, time_range]
1056
- )
1057
-
1058
- # 页面加载时初始化
1059
- app.load(
1060
- update_overview,
1061
- outputs=[stats_display, queue_table, queue_selector_for_timeline]
1062
- )
1063
-
1064
- app.load(
1065
- init_timeline_chart,
1066
- outputs=[queue_timeline_chart, actual_start_time, actual_end_time]
1067
- )
1068
-
1069
- # 应用关闭时清理资源
1070
- def cleanup():
1071
- global monitor
1072
- if monitor and not monitor._closed:
1073
- monitor.close()
1074
- monitor = None
1075
-
1076
- app.unload(cleanup)
1077
-
1078
- return app
1079
-
1080
-
1081
- if __name__ == "__main__":
1082
- app = create_integrated_interface()
1083
- app.launch(
1084
- server_name="0.0.0.0",
1085
- server_port=7862,
1086
- share=False,
1087
- inbrowser=False
1088
- )