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
@@ -1,547 +0,0 @@
1
- #!/usr/bin/env python
2
- """测试PG Consumer的恢复机制"""
3
-
4
- import asyncio
5
- import json
6
- import logging
7
- import os
8
- import signal
9
- import sys
10
- import time
11
- from typing import Dict, List, Optional
12
- from datetime import datetime, timezone
13
-
14
- import redis.asyncio as redis
15
- from redis.asyncio import Redis
16
- from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
17
- from sqlalchemy.orm import sessionmaker
18
- from sqlalchemy import text
19
-
20
- from jettask.webui.config import PostgreSQLConfig, RedisConfig
21
- from jettask.core.consumer_manager import ConsumerManager, ConsumerStrategy
22
- from jettask.utils.serializer import dumps_str
23
-
24
- logger = logging.getLogger(__name__)
25
-
26
- class TestHelper:
27
- """测试辅助类"""
28
-
29
- def __init__(self, pg_config: PostgreSQLConfig, redis_config: RedisConfig, prefix: str = "jettask"):
30
- self.pg_config = pg_config
31
- self.redis_config = redis_config
32
- self.prefix = prefix
33
- self.redis_client: Optional[Redis] = None
34
- self.async_engine = None
35
- self.AsyncSessionLocal = None
36
-
37
- async def setup(self):
38
- """初始化连接"""
39
- # 连接Redis
40
- self.redis_client = await redis.Redis(
41
- host=self.redis_config.host,
42
- port=self.redis_config.port,
43
- db=self.redis_config.db,
44
- password=self.redis_config.password,
45
- decode_responses=False
46
- )
47
-
48
- # 创建SQLAlchemy异步引擎
49
- if self.pg_config.dsn.startswith('postgresql://'):
50
- dsn = self.pg_config.dsn.replace('postgresql://', 'postgresql+psycopg://', 1)
51
- else:
52
- dsn = self.pg_config.dsn
53
-
54
- self.async_engine = create_async_engine(
55
- dsn,
56
- pool_size=20,
57
- echo=False
58
- )
59
-
60
- self.AsyncSessionLocal = sessionmaker(
61
- self.async_engine,
62
- class_=AsyncSession,
63
- expire_on_commit=False
64
- )
65
-
66
- async def cleanup(self):
67
- """清理连接"""
68
- if self.redis_client:
69
- await self.redis_client.close()
70
- if self.async_engine:
71
- await self.async_engine.dispose()
72
-
73
- async def send_test_messages(self, queue_name: str, count: int = 10, delay: float = 0.1):
74
- """发送测试消息到指定队列"""
75
- stream_key = f"{self.prefix}:QUEUE:{queue_name}"
76
-
77
- for i in range(count):
78
- task_data = {
79
- 'name': f'test_task_{i}',
80
- 'queue': queue_name,
81
- 'priority': 1,
82
- 'trigger_time': time.time(),
83
- 'test_id': f'recovery_test_{i}',
84
- 'created_at': datetime.now(timezone.utc).isoformat()
85
- }
86
-
87
- # 使用XADD添加消息到stream
88
- msg_id = await self.redis_client.xadd(
89
- stream_key,
90
- {'data': dumps_str(task_data)}
91
- )
92
-
93
- logger.info(f"Sent test message {i} to {queue_name}: {msg_id}")
94
-
95
- if delay > 0:
96
- await asyncio.sleep(delay)
97
-
98
- async def send_task_change_events(self, count: int = 10, delay: float = 0.1):
99
- """发送任务变更事件"""
100
- change_stream_key = f"{self.prefix}:TASK_CHANGES"
101
-
102
- for i in range(count):
103
- event_data = {
104
- 'event_id': f'task_change_{i}',
105
- 'event_type': 'task_updated',
106
- 'timestamp': time.time()
107
- }
108
-
109
- # 添加到TASK_CHANGES stream
110
- msg_id = await self.redis_client.xadd(
111
- change_stream_key,
112
- event_data
113
- )
114
-
115
- # 同时在Redis中创建对应的任务数据(模拟真实场景)
116
- task_key = f"{self.prefix}:TASK:task_change_{i}"
117
- task_data = {
118
- 'status': 'running',
119
- 'started_at': str(time.time()),
120
- 'worker_id': 'test_worker_1'
121
- }
122
- await self.redis_client.hset(task_key, mapping={
123
- k: dumps_str(v) if not isinstance(v, (str, bytes)) else v
124
- for k, v in task_data.items()
125
- })
126
-
127
- logger.info(f"Sent task change event {i}: {msg_id}")
128
-
129
- if delay > 0:
130
- await asyncio.sleep(delay)
131
-
132
- async def check_pending_messages(self, queue_name: str = None, check_task_changes: bool = False):
133
- """检查pending消息数量"""
134
- results = {}
135
-
136
- if queue_name:
137
- # 检查指定队列的pending消息
138
- stream_key = f"{self.prefix}:QUEUE:{queue_name}"
139
- consumer_group = f"{self.prefix}_pg_consumer1"
140
-
141
- try:
142
- info = await self.redis_client.xpending(stream_key, consumer_group)
143
- results[queue_name] = {
144
- 'total_pending': info['pending'],
145
- 'consumers': {}
146
- }
147
-
148
- # 获取每个consumer的pending消息详情
149
- if info['pending'] > 0:
150
- detailed = await self.redis_client.xpending_range(
151
- stream_key, consumer_group,
152
- min='-', max='+', count=100
153
- )
154
-
155
- for msg in detailed:
156
- consumer = msg['consumer'].decode('utf-8') if isinstance(msg['consumer'], bytes) else msg['consumer']
157
- if consumer not in results[queue_name]['consumers']:
158
- results[queue_name]['consumers'][consumer] = []
159
-
160
- msg_id = msg['message_id'].decode('utf-8') if isinstance(msg['message_id'], bytes) else msg['message_id']
161
- results[queue_name]['consumers'][consumer].append({
162
- 'message_id': msg_id,
163
- 'time_since_delivered': msg['time_since_delivered'],
164
- 'times_delivered': msg['times_delivered']
165
- })
166
-
167
- except Exception as e:
168
- logger.error(f"Error checking pending for queue {queue_name}: {e}")
169
-
170
- if check_task_changes:
171
- # 检查TASK_CHANGES的pending消息
172
- change_stream_key = f"{self.prefix}:TASK_CHANGES"
173
- consumer_group = f"{self.prefix}_changes_consumer"
174
-
175
- try:
176
- info = await self.redis_client.xpending(change_stream_key, consumer_group)
177
- results['TASK_CHANGES'] = {
178
- 'total_pending': info['pending'],
179
- 'consumers': {}
180
- }
181
-
182
- # 获取每个consumer的pending消息详情
183
- if info['pending'] > 0:
184
- detailed = await self.redis_client.xpending_range(
185
- change_stream_key, consumer_group,
186
- min='-', max='+', count=100
187
- )
188
-
189
- for msg in detailed:
190
- consumer = msg['consumer'].decode('utf-8') if isinstance(msg['consumer'], bytes) else msg['consumer']
191
- if consumer not in results['TASK_CHANGES']['consumers']:
192
- results['TASK_CHANGES']['consumers'][consumer] = []
193
-
194
- msg_id = msg['message_id'].decode('utf-8') if isinstance(msg['message_id'], bytes) else msg['message_id']
195
- results['TASK_CHANGES']['consumers'][consumer].append({
196
- 'message_id': msg_id,
197
- 'time_since_delivered': msg['time_since_delivered'],
198
- 'times_delivered': msg['times_delivered']
199
- })
200
-
201
- except Exception as e:
202
- logger.error(f"Error checking pending for TASK_CHANGES: {e}")
203
-
204
- return results
205
-
206
- async def check_database_tasks(self, queue_name: str = None, limit: int = 100):
207
- """检查数据库中的任务"""
208
- async with self.AsyncSessionLocal() as session:
209
- if queue_name:
210
- query = text("""
211
- SELECT id, queue_name, task_name, status, created_at, worker_id
212
- FROM tasks
213
- WHERE queue_name = :queue_name
214
- ORDER BY created_at DESC
215
- LIMIT :limit
216
- """)
217
- result = await session.execute(query, {'queue_name': queue_name, 'limit': limit})
218
- else:
219
- query = text("""
220
- SELECT id, queue_name, task_name, status, created_at, worker_id
221
- FROM tasks
222
- ORDER BY created_at DESC
223
- LIMIT :limit
224
- """)
225
- result = await session.execute(query, {'limit': limit})
226
-
227
- tasks = []
228
- for row in result:
229
- tasks.append({
230
- 'id': row[0],
231
- 'queue_name': row[1],
232
- 'task_name': row[2],
233
- 'status': row[3],
234
- 'created_at': row[4].isoformat() if row[4] else None,
235
- 'worker_id': row[5]
236
- })
237
-
238
- return tasks
239
-
240
- async def get_online_workers(self, worker_prefix: str = 'PG_CONSUMER'):
241
- """获取在线的worker列表"""
242
- pattern = f"{self.prefix}:{worker_prefix}:*"
243
- workers = []
244
-
245
- async for key in self.redis_client.scan_iter(match=pattern, count=100):
246
- key_str = key.decode('utf-8') if isinstance(key, bytes) else key
247
- worker_id = key_str.split(':')[-1]
248
-
249
- # 获取worker信息
250
- worker_info = await self.redis_client.hgetall(key_str)
251
- if worker_info:
252
- info = {}
253
- for k, v in worker_info.items():
254
- k_str = k.decode('utf-8') if isinstance(k, bytes) else k
255
- v_str = v.decode('utf-8') if isinstance(v, bytes) else v
256
- info[k_str] = v_str
257
-
258
- workers.append({
259
- 'worker_id': worker_id,
260
- 'info': info
261
- })
262
-
263
- return workers
264
-
265
- async def kill_worker_by_id(self, worker_id: str, worker_prefix: str = 'PG_CONSUMER'):
266
- """模拟worker突然挂掉(删除其在Redis中的信息)"""
267
- worker_key = f"{self.prefix}:{worker_prefix}:{worker_id}"
268
-
269
- # 删除worker的注册信息
270
- result = await self.redis_client.delete(worker_key)
271
-
272
- if result:
273
- logger.info(f"Killed worker {worker_id} (deleted Redis key: {worker_key})")
274
- else:
275
- logger.warning(f"Worker {worker_id} not found")
276
-
277
- return result > 0
278
-
279
-
280
- async def test_single_consumer_recovery():
281
- """测试单个consumer挂掉后重启的恢复"""
282
- logger.info("=" * 60)
283
- logger.info("Test 1: Single Consumer Recovery")
284
- logger.info("=" * 60)
285
-
286
- # 初始化测试辅助类
287
- helper = TestHelper(
288
- PostgreSQLConfig(
289
- host=os.getenv('JETTASK_PG_HOST', 'localhost'),
290
- port=int(os.getenv('JETTASK_PG_PORT', '5432')),
291
- database=os.getenv('JETTASK_PG_DB', 'jettask'),
292
- user=os.getenv('JETTASK_PG_USER', 'jettask'),
293
- password=os.getenv('JETTASK_PG_PASSWORD', '123456'),
294
- ),
295
- RedisConfig(
296
- host=os.getenv('REDIS_HOST', 'localhost'),
297
- port=int(os.getenv('REDIS_PORT', '6379')),
298
- db=int(os.getenv('REDIS_DB', '0')),
299
- password=os.getenv('REDIS_PASSWORD'),
300
- )
301
- )
302
-
303
- await helper.setup()
304
-
305
- try:
306
- # 1. 发送测试消息
307
- logger.info("\n1. Sending test messages...")
308
- await helper.send_test_messages('TEST_QUEUE', count=5)
309
- await helper.send_task_change_events(count=5)
310
-
311
- # 2. 启动PG Consumer(会在另一个进程中运行)
312
- logger.info("\n2. Starting PG Consumer process...")
313
- import subprocess
314
- env = os.environ.copy()
315
- env['PYTHONPATH'] = '/home/yuyang/easy-task'
316
- consumer_process = subprocess.Popen([
317
- sys.executable, '-m', 'jettask.webui.pg_consumer_slow'
318
- ], env=env)
319
-
320
- # 等待consumer开始处理
321
- await asyncio.sleep(3)
322
-
323
- # 3. 检查pending消息状态
324
- logger.info("\n3. Checking pending messages before kill...")
325
- pending_before = await helper.check_pending_messages('TEST_QUEUE', check_task_changes=True)
326
- logger.info(f"Pending messages: {json.dumps(pending_before, indent=2)}")
327
-
328
- # 4. 强制终止consumer(模拟突然挂掉)
329
- logger.info("\n4. Killing consumer process...")
330
- consumer_process.kill()
331
- consumer_process.wait()
332
-
333
- # 等待一下
334
- await asyncio.sleep(2)
335
-
336
- # 5. 检查pending消息(应该还在)
337
- logger.info("\n5. Checking pending messages after kill...")
338
- pending_after_kill = await helper.check_pending_messages('TEST_QUEUE', check_task_changes=True)
339
- logger.info(f"Pending messages after kill: {json.dumps(pending_after_kill, indent=2)}")
340
-
341
- # 6. 重新启动consumer
342
- logger.info("\n6. Restarting consumer...")
343
- env = os.environ.copy()
344
- env['PYTHONPATH'] = '/home/yuyang/easy-task'
345
- consumer_process2 = subprocess.Popen([
346
- sys.executable, '-m', 'jettask.webui.pg_consumer'
347
- ], env=env)
348
-
349
- # 等待恢复处理
350
- await asyncio.sleep(10)
351
-
352
- # 7. 再次检查pending消息(应该被处理了)
353
- logger.info("\n7. Checking pending messages after restart...")
354
- pending_after_restart = await helper.check_pending_messages('TEST_QUEUE', check_task_changes=True)
355
- logger.info(f"Pending messages after restart: {json.dumps(pending_after_restart, indent=2)}")
356
-
357
- # 8. 检查数据库中的任务
358
- logger.info("\n8. Checking database tasks...")
359
- db_tasks = await helper.check_database_tasks('TEST_QUEUE')
360
- logger.info(f"Found {len(db_tasks)} tasks in database")
361
- for task in db_tasks[:5]:
362
- logger.info(f" - {task['task_name']}: {task['status']}")
363
-
364
- # 清理
365
- consumer_process2.terminate()
366
- consumer_process2.wait()
367
-
368
- # 验证结果
369
- logger.info("\n" + "=" * 60)
370
- logger.info("Test Result:")
371
-
372
- # 检查TEST_QUEUE的恢复
373
- if 'TEST_QUEUE' in pending_before and 'TEST_QUEUE' in pending_after_restart:
374
- before_count = pending_before['TEST_QUEUE']['total_pending']
375
- after_count = pending_after_restart['TEST_QUEUE']['total_pending']
376
- if before_count > 0 and after_count == 0:
377
- logger.info("✓ TEST_QUEUE: Pending messages recovered successfully!")
378
- else:
379
- logger.warning(f"✗ TEST_QUEUE: Recovery may have issues. Before: {before_count}, After: {after_count}")
380
-
381
- # 检查TASK_CHANGES的恢复
382
- if 'TASK_CHANGES' in pending_before and 'TASK_CHANGES' in pending_after_restart:
383
- before_count = pending_before['TASK_CHANGES']['total_pending']
384
- after_count = pending_after_restart['TASK_CHANGES']['total_pending']
385
- if before_count > 0 and after_count == 0:
386
- logger.info("✓ TASK_CHANGES: Pending messages recovered successfully!")
387
- else:
388
- logger.warning(f"✗ TASK_CHANGES: Recovery may have issues. Before: {before_count}, After: {after_count}")
389
-
390
- finally:
391
- await helper.cleanup()
392
-
393
-
394
- async def test_multiple_consumers_takeover():
395
- """测试多个consumer时的接管机制"""
396
- logger.info("=" * 60)
397
- logger.info("Test 2: Multiple Consumers Takeover")
398
- logger.info("=" * 60)
399
-
400
- # 初始化测试辅助类
401
- helper = TestHelper(
402
- PostgreSQLConfig(
403
- host=os.getenv('JETTASK_PG_HOST', 'localhost'),
404
- port=int(os.getenv('JETTASK_PG_PORT', '5432')),
405
- database=os.getenv('JETTASK_PG_DB', 'jettask'),
406
- user=os.getenv('JETTASK_PG_USER', 'jettask'),
407
- password=os.getenv('JETTASK_PG_PASSWORD', '123456'),
408
- ),
409
- RedisConfig(
410
- host=os.getenv('REDIS_HOST', 'localhost'),
411
- port=int(os.getenv('REDIS_PORT', '6379')),
412
- db=int(os.getenv('REDIS_DB', '0')),
413
- password=os.getenv('REDIS_PASSWORD'),
414
- )
415
- )
416
-
417
- await helper.setup()
418
-
419
- try:
420
- # 1. 启动第一个consumer(慢速版本)
421
- logger.info("\n1. Starting first consumer (slow version)...")
422
- env = os.environ.copy()
423
- env['PYTHONPATH'] = '/home/yuyang/easy-task'
424
- env['CONSUMER_ID'] = 'consumer_1'
425
- consumer1 = subprocess.Popen([
426
- sys.executable, '-m', 'jettask.webui.pg_consumer_slow'
427
- ], env=env)
428
-
429
- await asyncio.sleep(2)
430
-
431
- # 2. 发送测试消息
432
- logger.info("\n2. Sending test messages...")
433
- await helper.send_test_messages('MULTI_TEST_QUEUE', count=10)
434
- await helper.send_task_change_events(count=10)
435
-
436
- # 等待第一个consumer开始处理
437
- await asyncio.sleep(3)
438
-
439
- # 3. 检查当前状态
440
- logger.info("\n3. Checking current state...")
441
- workers_before = await helper.get_online_workers()
442
- logger.info(f"Online workers: {len(workers_before)}")
443
- for worker in workers_before:
444
- logger.info(f" - {worker['worker_id']}: {worker['info'].get('queues', 'N/A')}")
445
-
446
- pending_before = await helper.check_pending_messages('MULTI_TEST_QUEUE', check_task_changes=True)
447
- logger.info(f"Pending messages: {json.dumps(pending_before, indent=2)}")
448
-
449
- # 4. 启动第二个consumer(正常速度)
450
- logger.info("\n4. Starting second consumer...")
451
- env = os.environ.copy()
452
- env['PYTHONPATH'] = '/home/yuyang/easy-task'
453
- env['CONSUMER_ID'] = 'consumer_2'
454
- consumer2 = subprocess.Popen([
455
- sys.executable, '-m', 'jettask.webui.pg_consumer'
456
- ], env=env)
457
-
458
- await asyncio.sleep(2)
459
-
460
- # 5. 强制终止第一个consumer
461
- logger.info("\n5. Killing first consumer...")
462
- consumer1.kill()
463
- consumer1.wait()
464
-
465
- # 6. 等待第二个consumer接管
466
- logger.info("\n6. Waiting for takeover...")
467
- await asyncio.sleep(10)
468
-
469
- # 7. 检查接管后的状态
470
- logger.info("\n7. Checking state after takeover...")
471
- workers_after = await helper.get_online_workers()
472
- logger.info(f"Online workers: {len(workers_after)}")
473
- for worker in workers_after:
474
- logger.info(f" - {worker['worker_id']}: {worker['info'].get('queues', 'N/A')}")
475
-
476
- pending_after = await helper.check_pending_messages('MULTI_TEST_QUEUE', check_task_changes=True)
477
- logger.info(f"Pending messages after takeover: {json.dumps(pending_after, indent=2)}")
478
-
479
- # 8. 检查数据库
480
- logger.info("\n8. Checking database...")
481
- db_tasks = await helper.check_database_tasks('MULTI_TEST_QUEUE')
482
- logger.info(f"Found {len(db_tasks)} tasks in database")
483
-
484
- # 清理
485
- consumer2.terminate()
486
- consumer2.wait()
487
-
488
- # 验证结果
489
- logger.info("\n" + "=" * 60)
490
- logger.info("Test Result:")
491
-
492
- # 检查MULTI_TEST_QUEUE的接管
493
- if 'MULTI_TEST_QUEUE' in pending_before and 'MULTI_TEST_QUEUE' in pending_after:
494
- before_count = pending_before['MULTI_TEST_QUEUE']['total_pending']
495
- after_count = pending_after['MULTI_TEST_QUEUE']['total_pending']
496
- if before_count > 0 and after_count == 0:
497
- logger.info("✓ MULTI_TEST_QUEUE: Messages taken over successfully!")
498
- else:
499
- logger.warning(f"✗ MULTI_TEST_QUEUE: Takeover may have issues. Before: {before_count}, After: {after_count}")
500
-
501
- # 检查TASK_CHANGES的接管
502
- if 'TASK_CHANGES' in pending_before and 'TASK_CHANGES' in pending_after:
503
- before_count = pending_before['TASK_CHANGES']['total_pending']
504
- after_count = pending_after['TASK_CHANGES']['total_pending']
505
- if before_count > 0 and after_count == 0:
506
- logger.info("✓ TASK_CHANGES: Messages taken over successfully!")
507
- else:
508
- logger.warning(f"✗ TASK_CHANGES: Takeover may have issues. Before: {before_count}, After: {after_count}")
509
-
510
- finally:
511
- await helper.cleanup()
512
-
513
-
514
- async def main():
515
- """主测试函数"""
516
- logging.basicConfig(
517
- level=logging.INFO,
518
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
519
- )
520
-
521
- from dotenv import load_dotenv
522
- load_dotenv()
523
-
524
- # 运行测试
525
- try:
526
- # 测试1:单个consumer的恢复
527
- await test_single_consumer_recovery()
528
-
529
- # 等待一下再进行下一个测试
530
- await asyncio.sleep(5)
531
-
532
- # 测试2:多个consumer的接管
533
- await test_multiple_consumers_takeover()
534
-
535
- logger.info("\n" + "=" * 60)
536
- logger.info("All tests completed!")
537
- logger.info("=" * 60)
538
-
539
- except KeyboardInterrupt:
540
- logger.info("Tests interrupted by user")
541
- except Exception as e:
542
- logger.error(f"Test failed with error: {e}", exc_info=True)
543
-
544
-
545
- if __name__ == '__main__':
546
- import subprocess
547
- asyncio.run(main())