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,492 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python
|
2
|
-
"""简化的恢复机制测试脚本"""
|
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
|
-
logging.basicConfig(
|
26
|
-
level=logging.INFO,
|
27
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
28
|
-
)
|
29
|
-
|
30
|
-
class RecoveryTester:
|
31
|
-
"""恢复机制测试器"""
|
32
|
-
|
33
|
-
def __init__(self):
|
34
|
-
self.pg_config = PostgreSQLConfig(
|
35
|
-
host=os.getenv('JETTASK_PG_HOST', 'localhost'),
|
36
|
-
port=int(os.getenv('JETTASK_PG_PORT', '5432')),
|
37
|
-
database=os.getenv('JETTASK_PG_DB', 'jettask'),
|
38
|
-
user=os.getenv('JETTASK_PG_USER', 'jettask'),
|
39
|
-
password=os.getenv('JETTASK_PG_PASSWORD', '123456'),
|
40
|
-
)
|
41
|
-
|
42
|
-
self.redis_config = RedisConfig(
|
43
|
-
host=os.getenv('REDIS_HOST', 'localhost'),
|
44
|
-
port=int(os.getenv('REDIS_PORT', '6379')),
|
45
|
-
db=int(os.getenv('REDIS_DB', '0')),
|
46
|
-
password=os.getenv('REDIS_PASSWORD'),
|
47
|
-
)
|
48
|
-
|
49
|
-
self.prefix = "jettask"
|
50
|
-
self.redis_client: Optional[Redis] = None
|
51
|
-
self.async_engine = None
|
52
|
-
self.AsyncSessionLocal = None
|
53
|
-
|
54
|
-
async def setup(self):
|
55
|
-
"""初始化连接"""
|
56
|
-
self.redis_client = await redis.Redis(
|
57
|
-
host=self.redis_config.host,
|
58
|
-
port=self.redis_config.port,
|
59
|
-
db=self.redis_config.db,
|
60
|
-
password=self.redis_config.password,
|
61
|
-
decode_responses=False
|
62
|
-
)
|
63
|
-
|
64
|
-
if self.pg_config.dsn.startswith('postgresql://'):
|
65
|
-
dsn = self.pg_config.dsn.replace('postgresql://', 'postgresql+psycopg://', 1)
|
66
|
-
else:
|
67
|
-
dsn = self.pg_config.dsn
|
68
|
-
|
69
|
-
self.async_engine = create_async_engine(dsn, pool_size=20, echo=False)
|
70
|
-
|
71
|
-
self.AsyncSessionLocal = sessionmaker(
|
72
|
-
self.async_engine,
|
73
|
-
class_=AsyncSession,
|
74
|
-
expire_on_commit=False
|
75
|
-
)
|
76
|
-
|
77
|
-
async def cleanup(self):
|
78
|
-
"""清理连接"""
|
79
|
-
if self.redis_client:
|
80
|
-
await self.redis_client.aclose()
|
81
|
-
if self.async_engine:
|
82
|
-
await self.async_engine.dispose()
|
83
|
-
|
84
|
-
async def test_queue_recovery(self):
|
85
|
-
"""测试队列消息的恢复机制"""
|
86
|
-
logger.info("=" * 60)
|
87
|
-
logger.info("测试 _consume_queues 的恢复机制")
|
88
|
-
logger.info("=" * 60)
|
89
|
-
|
90
|
-
queue_name = 'RECOVERY_TEST_QUEUE'
|
91
|
-
stream_key = f"{self.prefix}:QUEUE:{queue_name}"
|
92
|
-
consumer_group = f"{self.prefix}_pg_consumer1"
|
93
|
-
|
94
|
-
# 1. 清理旧数据
|
95
|
-
logger.info("\n1. 清理旧数据...")
|
96
|
-
try:
|
97
|
-
await self.redis_client.delete(stream_key)
|
98
|
-
except:
|
99
|
-
pass
|
100
|
-
|
101
|
-
# 2. 发送测试消息
|
102
|
-
logger.info("\n2. 发送测试消息到队列...")
|
103
|
-
task_ids = []
|
104
|
-
for i in range(5):
|
105
|
-
task_data = {
|
106
|
-
'name': f'recovery_test_task_{i}',
|
107
|
-
'queue': queue_name,
|
108
|
-
'priority': 1,
|
109
|
-
'trigger_time': time.time(),
|
110
|
-
'test_id': f'queue_recovery_{i}'
|
111
|
-
}
|
112
|
-
|
113
|
-
msg_id = await self.redis_client.xadd(
|
114
|
-
stream_key,
|
115
|
-
{'data': dumps_str(task_data)}
|
116
|
-
)
|
117
|
-
task_ids.append(msg_id)
|
118
|
-
logger.info(f" - 发送消息 {i}: {msg_id}")
|
119
|
-
|
120
|
-
# 3. 创建消费者组
|
121
|
-
logger.info("\n3. 创建消费者组...")
|
122
|
-
try:
|
123
|
-
await self.redis_client.xgroup_create(
|
124
|
-
stream_key, consumer_group, id='0', mkstream=True
|
125
|
-
)
|
126
|
-
logger.info(f" - 创建消费者组: {consumer_group}")
|
127
|
-
except:
|
128
|
-
pass
|
129
|
-
|
130
|
-
# 4. 模拟consumer_1读取消息但不ACK(突然挂掉)
|
131
|
-
logger.info("\n4. 模拟 consumer_1 读取消息但不ACK...")
|
132
|
-
consumer_1 = "test_consumer_1"
|
133
|
-
|
134
|
-
messages = await self.redis_client.xreadgroup(
|
135
|
-
consumer_group,
|
136
|
-
consumer_1,
|
137
|
-
{stream_key: '>'},
|
138
|
-
count=5,
|
139
|
-
block=1000
|
140
|
-
)
|
141
|
-
|
142
|
-
if messages:
|
143
|
-
logger.info(f" - consumer_1 读取了 {len(messages[0][1])} 条消息")
|
144
|
-
for msg_id, data in messages[0][1]:
|
145
|
-
logger.info(f" - 消息ID: {msg_id}")
|
146
|
-
|
147
|
-
# 5. 检查pending消息
|
148
|
-
logger.info("\n5. 检查pending消息...")
|
149
|
-
pending_info = await self.redis_client.xpending(stream_key, consumer_group)
|
150
|
-
logger.info(f" - 总pending消息数: {pending_info['pending']}")
|
151
|
-
|
152
|
-
if pending_info['pending'] > 0:
|
153
|
-
detailed = await self.redis_client.xpending_range(
|
154
|
-
stream_key, consumer_group,
|
155
|
-
min='-', max='+', count=10
|
156
|
-
)
|
157
|
-
for msg in detailed:
|
158
|
-
logger.info(f" - 消息 {msg['message_id']}: consumer={msg['consumer']}, times_delivered={msg['times_delivered']}")
|
159
|
-
|
160
|
-
# 6. 模拟consumer_2接管pending消息
|
161
|
-
logger.info("\n6. 模拟 consumer_2 尝试接管pending消息...")
|
162
|
-
consumer_2 = "test_consumer_2"
|
163
|
-
|
164
|
-
# 使用XCLAIM接管消息
|
165
|
-
if pending_info['pending'] > 0:
|
166
|
-
# 获取所有pending消息的ID
|
167
|
-
pending_msg_ids = [msg['message_id'] for msg in detailed]
|
168
|
-
|
169
|
-
# XCLAIM接管消息(idle时间设为0表示立即接管)
|
170
|
-
claimed = await self.redis_client.xclaim(
|
171
|
-
stream_key,
|
172
|
-
consumer_group,
|
173
|
-
consumer_2,
|
174
|
-
min_idle_time=0, # 立即接管
|
175
|
-
message_ids=pending_msg_ids
|
176
|
-
)
|
177
|
-
|
178
|
-
logger.info(f" - consumer_2 接管了 {len(claimed)} 条消息")
|
179
|
-
|
180
|
-
# ACK消息
|
181
|
-
if claimed:
|
182
|
-
msg_ids_to_ack = [msg[0] for msg in claimed]
|
183
|
-
await self.redis_client.xack(stream_key, consumer_group, *msg_ids_to_ack)
|
184
|
-
logger.info(f" - consumer_2 ACK了 {len(msg_ids_to_ack)} 条消息")
|
185
|
-
|
186
|
-
# 7. 再次检查pending消息
|
187
|
-
logger.info("\n7. 再次检查pending消息...")
|
188
|
-
pending_info_after = await self.redis_client.xpending(stream_key, consumer_group)
|
189
|
-
logger.info(f" - 总pending消息数: {pending_info_after['pending']}")
|
190
|
-
|
191
|
-
# 8. 验证结果
|
192
|
-
logger.info("\n" + "=" * 60)
|
193
|
-
if pending_info['pending'] > 0 and pending_info_after['pending'] == 0:
|
194
|
-
logger.info("✓ 队列消息恢复测试通过!pending消息成功被接管和处理")
|
195
|
-
else:
|
196
|
-
logger.warning(f"✗ 队列消息恢复测试失败。恢复前: {pending_info['pending']},恢复后: {pending_info_after['pending']}")
|
197
|
-
|
198
|
-
return pending_info['pending'] > 0 and pending_info_after['pending'] == 0
|
199
|
-
|
200
|
-
async def test_task_changes_recovery(self):
|
201
|
-
"""测试TASK_CHANGES的恢复机制"""
|
202
|
-
logger.info("\n" + "=" * 60)
|
203
|
-
logger.info("测试 _consume_task_changes 的恢复机制")
|
204
|
-
logger.info("=" * 60)
|
205
|
-
|
206
|
-
change_stream_key = f"{self.prefix}:TASK_CHANGES"
|
207
|
-
consumer_group = f"{self.prefix}_changes_consumer"
|
208
|
-
|
209
|
-
# 1. 清理旧数据
|
210
|
-
logger.info("\n1. 清理旧数据...")
|
211
|
-
try:
|
212
|
-
await self.redis_client.delete(change_stream_key)
|
213
|
-
except:
|
214
|
-
pass
|
215
|
-
|
216
|
-
# 2. 发送任务变更事件
|
217
|
-
logger.info("\n2. 发送任务变更事件...")
|
218
|
-
event_ids = []
|
219
|
-
for i in range(5):
|
220
|
-
event_data = {
|
221
|
-
'event_id': f'task_change_test_{i}',
|
222
|
-
'event_type': 'task_updated',
|
223
|
-
'timestamp': str(time.time())
|
224
|
-
}
|
225
|
-
|
226
|
-
msg_id = await self.redis_client.xadd(
|
227
|
-
change_stream_key,
|
228
|
-
event_data
|
229
|
-
)
|
230
|
-
event_ids.append(msg_id)
|
231
|
-
logger.info(f" - 发送事件 {i}: {msg_id}")
|
232
|
-
|
233
|
-
# 同时创建对应的任务数据
|
234
|
-
task_key = f"{self.prefix}:TASK:task_change_test_{i}"
|
235
|
-
task_data = {
|
236
|
-
'status': 'completed',
|
237
|
-
'completed_at': str(time.time()),
|
238
|
-
'result': json.dumps({'success': True})
|
239
|
-
}
|
240
|
-
await self.redis_client.hset(task_key, mapping={
|
241
|
-
k: dumps_str(v) if not isinstance(v, (str, bytes)) else v
|
242
|
-
for k, v in task_data.items()
|
243
|
-
})
|
244
|
-
|
245
|
-
# 3. 创建消费者组
|
246
|
-
logger.info("\n3. 创建消费者组...")
|
247
|
-
try:
|
248
|
-
await self.redis_client.xgroup_create(
|
249
|
-
change_stream_key, consumer_group, id='0', mkstream=True
|
250
|
-
)
|
251
|
-
logger.info(f" - 创建消费者组: {consumer_group}")
|
252
|
-
except:
|
253
|
-
pass
|
254
|
-
|
255
|
-
# 4. 模拟consumer_1读取消息但不ACK
|
256
|
-
logger.info("\n4. 模拟 consumer_1 读取消息但不ACK...")
|
257
|
-
consumer_1 = "changes_consumer_1"
|
258
|
-
|
259
|
-
messages = await self.redis_client.xreadgroup(
|
260
|
-
consumer_group,
|
261
|
-
consumer_1,
|
262
|
-
{change_stream_key: '>'},
|
263
|
-
count=5,
|
264
|
-
block=1000
|
265
|
-
)
|
266
|
-
|
267
|
-
if messages:
|
268
|
-
logger.info(f" - consumer_1 读取了 {len(messages[0][1])} 条事件")
|
269
|
-
for msg_id, data in messages[0][1]:
|
270
|
-
logger.info(f" - 事件ID: {msg_id}")
|
271
|
-
|
272
|
-
# 5. 检查pending消息
|
273
|
-
logger.info("\n5. 检查pending事件...")
|
274
|
-
pending_info = await self.redis_client.xpending(change_stream_key, consumer_group)
|
275
|
-
logger.info(f" - 总pending事件数: {pending_info['pending']}")
|
276
|
-
|
277
|
-
if pending_info['pending'] > 0:
|
278
|
-
detailed = await self.redis_client.xpending_range(
|
279
|
-
change_stream_key, consumer_group,
|
280
|
-
min='-', max='+', count=10
|
281
|
-
)
|
282
|
-
for msg in detailed:
|
283
|
-
logger.info(f" - 事件 {msg['message_id']}: consumer={msg['consumer']}, times_delivered={msg['times_delivered']}")
|
284
|
-
|
285
|
-
# 6. 模拟consumer_2接管pending消息
|
286
|
-
logger.info("\n6. 模拟 consumer_2 接管pending事件...")
|
287
|
-
consumer_2 = "changes_consumer_2"
|
288
|
-
|
289
|
-
if pending_info['pending'] > 0:
|
290
|
-
pending_msg_ids = [msg['message_id'] for msg in detailed]
|
291
|
-
|
292
|
-
claimed = await self.redis_client.xclaim(
|
293
|
-
change_stream_key,
|
294
|
-
consumer_group,
|
295
|
-
consumer_2,
|
296
|
-
min_idle_time=0,
|
297
|
-
message_ids=pending_msg_ids
|
298
|
-
)
|
299
|
-
|
300
|
-
logger.info(f" - consumer_2 接管了 {len(claimed)} 条事件")
|
301
|
-
|
302
|
-
# ACK消息
|
303
|
-
if claimed:
|
304
|
-
msg_ids_to_ack = [msg[0] for msg in claimed]
|
305
|
-
await self.redis_client.xack(change_stream_key, consumer_group, *msg_ids_to_ack)
|
306
|
-
logger.info(f" - consumer_2 ACK了 {len(msg_ids_to_ack)} 条事件")
|
307
|
-
|
308
|
-
# 7. 再次检查pending消息
|
309
|
-
logger.info("\n7. 再次检查pending事件...")
|
310
|
-
pending_info_after = await self.redis_client.xpending(change_stream_key, consumer_group)
|
311
|
-
logger.info(f" - 总pending事件数: {pending_info_after['pending']}")
|
312
|
-
|
313
|
-
# 8. 验证结果
|
314
|
-
logger.info("\n" + "=" * 60)
|
315
|
-
if pending_info['pending'] > 0 and pending_info_after['pending'] == 0:
|
316
|
-
logger.info("✓ TASK_CHANGES恢复测试通过!pending事件成功被接管和处理")
|
317
|
-
else:
|
318
|
-
logger.warning(f"✗ TASK_CHANGES恢复测试失败。恢复前: {pending_info['pending']},恢复后: {pending_info_after['pending']}")
|
319
|
-
|
320
|
-
return pending_info['pending'] > 0 and pending_info_after['pending'] == 0
|
321
|
-
|
322
|
-
async def test_offline_worker_recovery(self):
|
323
|
-
"""测试OfflineWorkerRecovery的功能"""
|
324
|
-
logger.info("\n" + "=" * 60)
|
325
|
-
logger.info("测试 OfflineWorkerRecovery 的功能")
|
326
|
-
logger.info("=" * 60)
|
327
|
-
|
328
|
-
# 导入OfflineWorkerRecovery
|
329
|
-
from jettask.core.offline_worker_recovery import OfflineWorkerRecovery
|
330
|
-
|
331
|
-
# 创建同步Redis客户端(ConsumerManager需要)
|
332
|
-
import redis as sync_redis
|
333
|
-
sync_redis_client = sync_redis.StrictRedis(
|
334
|
-
host=self.redis_config.host,
|
335
|
-
port=self.redis_config.port,
|
336
|
-
db=self.redis_config.db,
|
337
|
-
password=self.redis_config.password,
|
338
|
-
decode_responses=True
|
339
|
-
)
|
340
|
-
|
341
|
-
# 创建ConsumerManager
|
342
|
-
consumer_manager = ConsumerManager(
|
343
|
-
redis_client=sync_redis_client,
|
344
|
-
strategy=ConsumerStrategy.HEARTBEAT,
|
345
|
-
config={
|
346
|
-
'redis_prefix': self.prefix,
|
347
|
-
'queues': ['OFFLINE_TEST_QUEUE'],
|
348
|
-
'worker_prefix': 'TEST_WORKER'
|
349
|
-
}
|
350
|
-
)
|
351
|
-
|
352
|
-
# 创建OfflineWorkerRecovery
|
353
|
-
recovery = OfflineWorkerRecovery(
|
354
|
-
async_redis_client=self.redis_client,
|
355
|
-
redis_prefix=self.prefix,
|
356
|
-
worker_prefix='TEST_WORKER',
|
357
|
-
consumer_manager=consumer_manager
|
358
|
-
)
|
359
|
-
|
360
|
-
# 准备测试数据
|
361
|
-
queue_name = 'OFFLINE_TEST_QUEUE'
|
362
|
-
stream_key = f"{self.prefix}:QUEUE:{queue_name}"
|
363
|
-
consumer_group = f"{self.prefix}_pg_consumer1"
|
364
|
-
|
365
|
-
# 清理旧数据
|
366
|
-
logger.info("\n1. 清理旧数据...")
|
367
|
-
try:
|
368
|
-
await self.redis_client.delete(stream_key)
|
369
|
-
except:
|
370
|
-
pass
|
371
|
-
|
372
|
-
# 发送测试消息
|
373
|
-
logger.info("\n2. 发送测试消息...")
|
374
|
-
for i in range(3):
|
375
|
-
task_data = {
|
376
|
-
'name': f'offline_test_{i}',
|
377
|
-
'queue': queue_name,
|
378
|
-
'test_id': f'offline_{i}'
|
379
|
-
}
|
380
|
-
await self.redis_client.xadd(
|
381
|
-
stream_key,
|
382
|
-
{'data': dumps_str(task_data)}
|
383
|
-
)
|
384
|
-
|
385
|
-
# 创建消费者组
|
386
|
-
try:
|
387
|
-
await self.redis_client.xgroup_create(
|
388
|
-
stream_key, consumer_group, id='0', mkstream=True
|
389
|
-
)
|
390
|
-
except:
|
391
|
-
pass
|
392
|
-
|
393
|
-
# 模拟离线worker读取消息
|
394
|
-
logger.info("\n3. 模拟离线worker读取消息...")
|
395
|
-
offline_worker = "offline_worker_1"
|
396
|
-
messages = await self.redis_client.xreadgroup(
|
397
|
-
consumer_group,
|
398
|
-
offline_worker,
|
399
|
-
{stream_key: '>'},
|
400
|
-
count=3,
|
401
|
-
block=1000
|
402
|
-
)
|
403
|
-
|
404
|
-
if messages:
|
405
|
-
logger.info(f" - 离线worker读取了 {len(messages[0][1])} 条消息")
|
406
|
-
|
407
|
-
# 模拟worker离线(删除其注册信息)
|
408
|
-
worker_key = f"{self.prefix}:TEST_WORKER:{offline_worker}"
|
409
|
-
await self.redis_client.delete(worker_key)
|
410
|
-
logger.info(f" - 模拟worker离线: 删除 {worker_key}")
|
411
|
-
|
412
|
-
# 定义处理回调
|
413
|
-
processed_messages = []
|
414
|
-
async def process_callback(msg_id, msg_data, queue, consumer_id):
|
415
|
-
processed_messages.append({
|
416
|
-
'msg_id': msg_id,
|
417
|
-
'queue': queue,
|
418
|
-
'consumer_id': consumer_id
|
419
|
-
})
|
420
|
-
logger.info(f" - 处理恢复的消息: {msg_id} from {consumer_id}")
|
421
|
-
|
422
|
-
# 执行恢复
|
423
|
-
logger.info("\n4. 执行离线worker恢复...")
|
424
|
-
current_consumer = "active_worker_1"
|
425
|
-
recovered_count = await recovery.recover_offline_workers(
|
426
|
-
queue=queue_name,
|
427
|
-
current_consumer_name=current_consumer,
|
428
|
-
process_message_callback=process_callback
|
429
|
-
)
|
430
|
-
|
431
|
-
logger.info(f" - 恢复了 {recovered_count} 条消息")
|
432
|
-
|
433
|
-
# 检查pending消息
|
434
|
-
logger.info("\n5. 检查恢复后的pending消息...")
|
435
|
-
pending_info = await self.redis_client.xpending(stream_key, consumer_group)
|
436
|
-
logger.info(f" - 剩余pending消息数: {pending_info['pending']}")
|
437
|
-
|
438
|
-
# 验证结果
|
439
|
-
logger.info("\n" + "=" * 60)
|
440
|
-
if recovered_count > 0 and len(processed_messages) == recovered_count:
|
441
|
-
logger.info("✓ OfflineWorkerRecovery测试通过!成功恢复离线worker的消息")
|
442
|
-
else:
|
443
|
-
logger.warning(f"✗ OfflineWorkerRecovery测试失败。恢复数量: {recovered_count},处理数量: {len(processed_messages)}")
|
444
|
-
|
445
|
-
# 清理
|
446
|
-
consumer_manager.cleanup()
|
447
|
-
sync_redis_client.close()
|
448
|
-
|
449
|
-
return recovered_count > 0
|
450
|
-
|
451
|
-
|
452
|
-
async def main():
|
453
|
-
"""主测试函数"""
|
454
|
-
from dotenv import load_dotenv
|
455
|
-
load_dotenv()
|
456
|
-
|
457
|
-
tester = RecoveryTester()
|
458
|
-
await tester.setup()
|
459
|
-
|
460
|
-
try:
|
461
|
-
# 测试1:队列消息恢复
|
462
|
-
queue_test = await tester.test_queue_recovery()
|
463
|
-
await asyncio.sleep(1)
|
464
|
-
|
465
|
-
# 测试2:TASK_CHANGES恢复
|
466
|
-
changes_test = await tester.test_task_changes_recovery()
|
467
|
-
await asyncio.sleep(1)
|
468
|
-
|
469
|
-
# 测试3:OfflineWorkerRecovery
|
470
|
-
offline_test = await tester.test_offline_worker_recovery()
|
471
|
-
|
472
|
-
# 总结
|
473
|
-
logger.info("\n" + "=" * 60)
|
474
|
-
logger.info("测试总结")
|
475
|
-
logger.info("=" * 60)
|
476
|
-
logger.info(f"队列消息恢复测试: {'✓ 通过' if queue_test else '✗ 失败'}")
|
477
|
-
logger.info(f"TASK_CHANGES恢复测试: {'✓ 通过' if changes_test else '✗ 失败'}")
|
478
|
-
logger.info(f"OfflineWorkerRecovery测试: {'✓ 通过' if offline_test else '✗ 失败'}")
|
479
|
-
|
480
|
-
if queue_test and changes_test and offline_test:
|
481
|
-
logger.info("\n🎉 所有测试通过!恢复机制工作正常")
|
482
|
-
else:
|
483
|
-
logger.warning("\n⚠️ 部分测试失败,需要检查恢复机制")
|
484
|
-
|
485
|
-
except Exception as e:
|
486
|
-
logger.error(f"测试出错: {e}", exc_info=True)
|
487
|
-
finally:
|
488
|
-
await tester.cleanup()
|
489
|
-
|
490
|
-
|
491
|
-
if __name__ == '__main__':
|
492
|
-
asyncio.run(main())
|