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.
- jettask/constants.py +213 -0
- jettask/core/app.py +525 -205
- jettask/core/cli.py +193 -185
- jettask/core/consumer_manager.py +126 -34
- jettask/core/context.py +3 -0
- jettask/core/enums.py +137 -0
- jettask/core/event_pool.py +501 -168
- jettask/core/message.py +147 -0
- jettask/core/offline_worker_recovery.py +181 -114
- jettask/core/task.py +10 -174
- jettask/core/task_batch.py +153 -0
- jettask/core/unified_manager_base.py +243 -0
- jettask/core/worker_scanner.py +54 -54
- jettask/executors/asyncio.py +184 -64
- jettask/webui/backend/config.py +51 -0
- jettask/webui/backend/data_access.py +2083 -92
- jettask/webui/backend/data_api.py +3294 -0
- jettask/webui/backend/dependencies.py +261 -0
- jettask/webui/backend/init_meta_db.py +158 -0
- jettask/webui/backend/main.py +1358 -69
- jettask/webui/backend/main_unified.py +78 -0
- jettask/webui/backend/main_v2.py +394 -0
- jettask/webui/backend/namespace_api.py +295 -0
- jettask/webui/backend/namespace_api_old.py +294 -0
- jettask/webui/backend/namespace_data_access.py +611 -0
- jettask/webui/backend/queue_backlog_api.py +727 -0
- jettask/webui/backend/queue_stats_v2.py +521 -0
- jettask/webui/backend/redis_monitor_api.py +476 -0
- jettask/webui/backend/unified_api_router.py +1601 -0
- jettask/webui/db_init.py +204 -32
- jettask/webui/frontend/package-lock.json +492 -1
- jettask/webui/frontend/package.json +4 -1
- jettask/webui/frontend/src/App.css +105 -7
- jettask/webui/frontend/src/App.jsx +49 -20
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
- jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
- jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
- jettask/webui/frontend/src/components/layout/Header.css +34 -10
- jettask/webui/frontend/src/components/layout/Header.jsx +31 -23
- jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
- jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
- jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
- jettask/webui/frontend/src/main.jsx +1 -0
- jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
- jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
- jettask/webui/frontend/src/pages/QueueDetail.jsx +1109 -10
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +236 -115
- jettask/webui/frontend/src/pages/Queues.jsx +5 -1
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
- jettask/webui/frontend/src/pages/Settings.jsx +800 -0
- jettask/webui/frontend/src/services/api.js +7 -5
- jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
- jettask/webui/frontend/src/utils/userPreferences.js +154 -0
- jettask/webui/multi_namespace_consumer.py +543 -0
- jettask/webui/pg_consumer.py +983 -246
- jettask/webui/static/dist/assets/index-7129cfe1.css +1 -0
- jettask/webui/static/dist/assets/index-8d1935cc.js +774 -0
- jettask/webui/static/dist/index.html +2 -2
- jettask/webui/task_center.py +216 -0
- jettask/webui/task_center_client.py +150 -0
- jettask/webui/unified_consumer_manager.py +193 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/METADATA +1 -1
- jettask-0.2.4.dist-info/RECORD +134 -0
- jettask/webui/pg_consumer_slow.py +0 -1099
- jettask/webui/pg_consumer_test.py +0 -678
- jettask/webui/static/dist/assets/index-823408e8.css +0 -1
- jettask/webui/static/dist/assets/index-9968b0b8.js +0 -543
- jettask/webui/test_pg_consumer_recovery.py +0 -547
- jettask/webui/test_recovery_simple.py +0 -492
- jettask/webui/test_self_recovery.py +0 -467
- jettask-0.2.1.dist-info/RECORD +0 -91
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/WHEEL +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {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())
|