jettask 0.2.18__py3-none-any.whl → 0.2.20__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/__init__.py +60 -2
- jettask/cli.py +314 -228
- jettask/config/__init__.py +9 -1
- jettask/config/config.py +245 -0
- jettask/config/env_loader.py +381 -0
- jettask/config/lua_scripts.py +158 -0
- jettask/config/nacos_config.py +132 -5
- jettask/core/__init__.py +1 -1
- jettask/core/app.py +1573 -666
- jettask/core/app_importer.py +33 -16
- jettask/core/container.py +532 -0
- jettask/core/task.py +1 -4
- jettask/core/unified_manager_base.py +2 -2
- jettask/executor/__init__.py +38 -0
- jettask/executor/core.py +625 -0
- jettask/executor/executor.py +338 -0
- jettask/executor/orchestrator.py +290 -0
- jettask/executor/process_entry.py +638 -0
- jettask/executor/task_executor.py +317 -0
- jettask/messaging/__init__.py +68 -0
- jettask/messaging/event_pool.py +2188 -0
- jettask/messaging/reader.py +519 -0
- jettask/messaging/registry.py +266 -0
- jettask/messaging/scanner.py +369 -0
- jettask/messaging/sender.py +312 -0
- jettask/persistence/__init__.py +118 -0
- jettask/persistence/backlog_monitor.py +567 -0
- jettask/{backend/data_access.py → persistence/base.py} +58 -57
- jettask/persistence/consumer.py +315 -0
- jettask/{core → persistence}/db_manager.py +23 -22
- jettask/persistence/maintenance.py +81 -0
- jettask/persistence/message_consumer.py +259 -0
- jettask/{backend/namespace_data_access.py → persistence/namespace.py} +66 -98
- jettask/persistence/offline_recovery.py +196 -0
- jettask/persistence/queue_discovery.py +215 -0
- jettask/persistence/task_persistence.py +218 -0
- jettask/persistence/task_updater.py +583 -0
- jettask/scheduler/__init__.py +2 -2
- jettask/scheduler/loader.py +6 -5
- jettask/scheduler/run_scheduler.py +1 -1
- jettask/scheduler/scheduler.py +7 -7
- jettask/scheduler/{unified_scheduler_manager.py → scheduler_coordinator.py} +18 -13
- jettask/task/__init__.py +16 -0
- jettask/{router.py → task/router.py} +26 -8
- jettask/task/task_center/__init__.py +9 -0
- jettask/task/task_executor.py +318 -0
- jettask/task/task_registry.py +291 -0
- jettask/test_connection_monitor.py +73 -0
- jettask/utils/__init__.py +31 -1
- jettask/{monitor/run_backlog_collector.py → utils/backlog_collector.py} +1 -1
- jettask/utils/db_connector.py +1629 -0
- jettask/{db_init.py → utils/db_init.py} +1 -1
- jettask/utils/rate_limit/__init__.py +30 -0
- jettask/utils/rate_limit/concurrency_limiter.py +665 -0
- jettask/utils/rate_limit/config.py +145 -0
- jettask/utils/rate_limit/limiter.py +41 -0
- jettask/utils/rate_limit/manager.py +269 -0
- jettask/utils/rate_limit/qps_limiter.py +154 -0
- jettask/utils/rate_limit/task_limiter.py +384 -0
- jettask/utils/serializer.py +3 -0
- jettask/{monitor/stream_backlog_monitor.py → utils/stream_backlog.py} +14 -6
- jettask/utils/time_sync.py +173 -0
- jettask/webui/__init__.py +27 -0
- jettask/{api/v1 → webui/api}/alerts.py +1 -1
- jettask/{api/v1 → webui/api}/analytics.py +2 -2
- jettask/{api/v1 → webui/api}/namespaces.py +1 -1
- jettask/{api/v1 → webui/api}/overview.py +1 -1
- jettask/{api/v1 → webui/api}/queues.py +3 -3
- jettask/{api/v1 → webui/api}/scheduled.py +1 -1
- jettask/{api/v1 → webui/api}/settings.py +1 -1
- jettask/{api.py → webui/app.py} +253 -145
- jettask/webui/namespace_manager/__init__.py +10 -0
- jettask/{multi_namespace_consumer.py → webui/namespace_manager/multi.py} +69 -22
- jettask/{unified_consumer_manager.py → webui/namespace_manager/unified.py} +1 -1
- jettask/{run.py → webui/run.py} +2 -2
- jettask/{services → webui/services}/__init__.py +1 -3
- jettask/{services → webui/services}/overview_service.py +34 -16
- jettask/{services → webui/services}/queue_service.py +1 -1
- jettask/{backend → webui/services}/queue_stats_v2.py +1 -1
- jettask/{services → webui/services}/settings_service.py +1 -1
- jettask/worker/__init__.py +53 -0
- jettask/worker/lifecycle.py +1507 -0
- jettask/worker/manager.py +583 -0
- jettask/{core/offline_worker_recovery.py → worker/recovery.py} +268 -175
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/METADATA +2 -71
- jettask-0.2.20.dist-info/RECORD +145 -0
- jettask/__main__.py +0 -140
- jettask/api/__init__.py +0 -103
- jettask/backend/__init__.py +0 -1
- jettask/backend/api/__init__.py +0 -3
- jettask/backend/api/v1/__init__.py +0 -17
- jettask/backend/api/v1/monitoring.py +0 -431
- jettask/backend/api/v1/namespaces.py +0 -504
- jettask/backend/api/v1/queues.py +0 -342
- jettask/backend/api/v1/tasks.py +0 -367
- jettask/backend/core/__init__.py +0 -3
- jettask/backend/core/cache.py +0 -221
- jettask/backend/core/database.py +0 -200
- jettask/backend/core/exceptions.py +0 -102
- jettask/backend/dependencies.py +0 -261
- jettask/backend/init_meta_db.py +0 -158
- jettask/backend/main.py +0 -1426
- jettask/backend/main_unified.py +0 -78
- jettask/backend/main_v2.py +0 -394
- jettask/backend/models/__init__.py +0 -3
- jettask/backend/models/requests.py +0 -236
- jettask/backend/models/responses.py +0 -230
- jettask/backend/namespace_api_old.py +0 -267
- jettask/backend/services/__init__.py +0 -3
- jettask/backend/start.py +0 -42
- jettask/backend/unified_api_router.py +0 -1541
- jettask/cleanup_deprecated_tables.sql +0 -16
- jettask/core/consumer_manager.py +0 -1695
- jettask/core/delay_scanner.py +0 -256
- jettask/core/event_pool.py +0 -1700
- jettask/core/heartbeat_process.py +0 -222
- jettask/core/task_batch.py +0 -153
- jettask/core/worker_scanner.py +0 -271
- jettask/executors/__init__.py +0 -5
- jettask/executors/asyncio.py +0 -876
- jettask/executors/base.py +0 -30
- jettask/executors/common.py +0 -148
- jettask/executors/multi_asyncio.py +0 -309
- jettask/gradio_app.py +0 -570
- jettask/integrated_gradio_app.py +0 -1088
- jettask/main.py +0 -0
- jettask/monitoring/__init__.py +0 -3
- jettask/pg_consumer.py +0 -1896
- jettask/run_monitor.py +0 -22
- jettask/run_webui.py +0 -148
- jettask/scheduler/multi_namespace_scheduler.py +0 -294
- jettask/scheduler/unified_manager.py +0 -450
- jettask/task_center_client.py +0 -150
- jettask/utils/serializer_optimized.py +0 -33
- jettask/webui_exceptions.py +0 -67
- jettask-0.2.18.dist-info/RECORD +0 -150
- /jettask/{constants.py → config/constants.py} +0 -0
- /jettask/{backend/config.py → config/task_center.py} +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/pg_consumer_v2.py +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/add_execution_time_field.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_new_tables.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_tables_v3.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/migrate_to_new_structure.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/modify_time_fields.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql_utils.py +0 -0
- /jettask/{models.py → persistence/models.py} +0 -0
- /jettask/scheduler/{manager.py → task_crud.py} +0 -0
- /jettask/{schema.sql → schemas/schema.sql} +0 -0
- /jettask/{task_center.py → task/task_center/client.py} +0 -0
- /jettask/{monitoring → utils}/file_watcher.py +0 -0
- /jettask/{services/redis_monitor_service.py → utils/redis_monitor.py} +0 -0
- /jettask/{api/v1 → webui/api}/__init__.py +0 -0
- /jettask/{webui_config.py → webui/config.py} +0 -0
- /jettask/{webui_models → webui/models}/__init__.py +0 -0
- /jettask/{webui_models → webui/models}/namespace.py +0 -0
- /jettask/{services → webui/services}/alert_service.py +0 -0
- /jettask/{services → webui/services}/analytics_service.py +0 -0
- /jettask/{services → webui/services}/scheduled_task_service.py +0 -0
- /jettask/{services → webui/services}/task_service.py +0 -0
- /jettask/{webui_sql → webui/sql}/batch_upsert_functions.sql +0 -0
- /jettask/{webui_sql → webui/sql}/verify_database.sql +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/WHEEL +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,266 @@
|
|
1
|
+
"""
|
2
|
+
队列注册管理模块
|
3
|
+
负责队列、延迟队列、消费者组的注册和查询功能
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
from typing import Set, List
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
class QueueRegistry:
|
13
|
+
"""
|
14
|
+
队列注册管理器
|
15
|
+
维护队列的注册信息,提供队列发现功能
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, redis_client, async_redis_client, redis_prefix: str = 'jettask'):
|
19
|
+
self.redis = redis_client
|
20
|
+
self.async_redis = async_redis_client
|
21
|
+
self.redis_prefix = redis_prefix
|
22
|
+
|
23
|
+
# 注册表键
|
24
|
+
self.queues_registry_key = f"{redis_prefix}:REGISTRY:QUEUES" # 存储所有队列(包括优先级队列)
|
25
|
+
self.delayed_queues_registry_key = f"{redis_prefix}:REGISTRY:DELAYED_QUEUES"
|
26
|
+
self.consumer_groups_registry_key = f"{redis_prefix}:REGISTRY:CONSUMER_GROUPS"
|
27
|
+
|
28
|
+
# ========== 队列管理 ==========
|
29
|
+
|
30
|
+
def register_queue_sync(self, queue_name: str):
|
31
|
+
"""注册队列(同步)"""
|
32
|
+
self.redis.sadd(self.queues_registry_key, queue_name)
|
33
|
+
logger.debug(f"Registered queue: {queue_name}")
|
34
|
+
|
35
|
+
async def register_queue(self, queue_name: str):
|
36
|
+
"""注册队列(异步)"""
|
37
|
+
await self.async_redis.sadd(self.queues_registry_key, queue_name)
|
38
|
+
logger.debug(f"Registered queue: {queue_name}")
|
39
|
+
|
40
|
+
async def unregister_queue(self, queue_name: str):
|
41
|
+
"""注销队列"""
|
42
|
+
await self.async_redis.srem(self.queues_registry_key, queue_name)
|
43
|
+
logger.debug(f"Unregistered queue: {queue_name}")
|
44
|
+
|
45
|
+
async def get_all_queues(self) -> Set[str]:
|
46
|
+
"""获取所有队列(不使用 SCAN)"""
|
47
|
+
return await self.async_redis.smembers(self.queues_registry_key)
|
48
|
+
|
49
|
+
def get_all_queues_sync(self) -> Set[str]:
|
50
|
+
"""同步获取所有队列"""
|
51
|
+
return self.redis.smembers(self.queues_registry_key)
|
52
|
+
|
53
|
+
async def get_queue_count(self) -> int:
|
54
|
+
"""获取队列数量"""
|
55
|
+
return await self.async_redis.scard(self.queues_registry_key)
|
56
|
+
|
57
|
+
# ========== 延迟队列管理 ==========
|
58
|
+
|
59
|
+
async def register_delayed_queue(self, queue_name: str):
|
60
|
+
"""注册延迟队列"""
|
61
|
+
await self.async_redis.sadd(self.delayed_queues_registry_key, queue_name)
|
62
|
+
logger.debug(f"Registered delayed queue: {queue_name}")
|
63
|
+
|
64
|
+
async def unregister_delayed_queue(self, queue_name: str):
|
65
|
+
"""注销延迟队列"""
|
66
|
+
await self.async_redis.srem(self.delayed_queues_registry_key, queue_name)
|
67
|
+
logger.debug(f"Unregistered delayed queue: {queue_name}")
|
68
|
+
|
69
|
+
async def get_all_delayed_queues(self) -> Set[str]:
|
70
|
+
"""获取所有延迟队列"""
|
71
|
+
return await self.async_redis.smembers(self.delayed_queues_registry_key)
|
72
|
+
|
73
|
+
def get_all_delayed_queues_sync(self) -> Set[str]:
|
74
|
+
"""同步获取所有延迟队列"""
|
75
|
+
return self.redis.smembers(self.delayed_queues_registry_key)
|
76
|
+
|
77
|
+
async def get_delayed_queue_count(self) -> int:
|
78
|
+
"""获取延迟队列数量"""
|
79
|
+
return await self.async_redis.scard(self.delayed_queues_registry_key)
|
80
|
+
|
81
|
+
# ========== Consumer Group 管理 ==========
|
82
|
+
|
83
|
+
async def register_consumer_group(self, queue: str, group_name: str):
|
84
|
+
"""注册 Consumer Group"""
|
85
|
+
key = f"{self.consumer_groups_registry_key}:{queue}"
|
86
|
+
await self.async_redis.sadd(key, group_name)
|
87
|
+
logger.debug(f"Registered consumer group: {group_name} for queue: {queue}")
|
88
|
+
|
89
|
+
async def unregister_consumer_group(self, queue: str, group_name: str):
|
90
|
+
"""注销 Consumer Group"""
|
91
|
+
key = f"{self.consumer_groups_registry_key}:{queue}"
|
92
|
+
await self.async_redis.srem(key, group_name)
|
93
|
+
logger.debug(f"Unregistered consumer group: {group_name} for queue: {queue}")
|
94
|
+
|
95
|
+
async def get_consumer_groups_for_queue(self, queue: str) -> Set[str]:
|
96
|
+
"""获取队列的所有 Consumer Group"""
|
97
|
+
key = f"{self.consumer_groups_registry_key}:{queue}"
|
98
|
+
return await self.async_redis.smembers(key)
|
99
|
+
|
100
|
+
# ========== 优先级队列管理 ==========
|
101
|
+
|
102
|
+
def register_priority_queue_sync(self, base_queue: str, priority: int):
|
103
|
+
"""注册优先级队列(同步)
|
104
|
+
|
105
|
+
直接添加到全局队列注册表
|
106
|
+
"""
|
107
|
+
priority_queue = f"{base_queue}:{priority}"
|
108
|
+
self.redis.sadd(self.queues_registry_key, priority_queue)
|
109
|
+
logger.debug(f"Registered priority queue: {priority_queue}")
|
110
|
+
|
111
|
+
async def register_priority_queue(self, base_queue: str, priority: int):
|
112
|
+
"""注册优先级队列(异步)
|
113
|
+
|
114
|
+
直接添加到全局队列注册表
|
115
|
+
"""
|
116
|
+
priority_queue = f"{base_queue}:{priority}"
|
117
|
+
await self.async_redis.sadd(self.queues_registry_key, priority_queue)
|
118
|
+
logger.debug(f"Registered priority queue: {priority_queue}")
|
119
|
+
|
120
|
+
async def unregister_priority_queue(self, base_queue: str, priority: int):
|
121
|
+
"""注销优先级队列"""
|
122
|
+
priority_queue = f"{base_queue}:{priority}"
|
123
|
+
await self.async_redis.srem(self.queues_registry_key, priority_queue)
|
124
|
+
logger.debug(f"Unregistered priority queue: {priority_queue}")
|
125
|
+
|
126
|
+
async def get_priority_queues_for_base(self, base_queue: str) -> List[str]:
|
127
|
+
"""获取基础队列的所有优先级队列
|
128
|
+
|
129
|
+
从全局队列注册表中过滤出该基础队列的所有优先级队列
|
130
|
+
"""
|
131
|
+
# 获取所有队列
|
132
|
+
all_queues = await self.async_redis.smembers(self.queues_registry_key)
|
133
|
+
|
134
|
+
# 过滤出该基础队列的优先级队列
|
135
|
+
result = []
|
136
|
+
for queue in all_queues:
|
137
|
+
if isinstance(queue, bytes):
|
138
|
+
queue = queue.decode('utf-8')
|
139
|
+
|
140
|
+
# 检查是否是该基础队列的优先级队列
|
141
|
+
# 格式:base_queue:priority(priority 是数字)
|
142
|
+
if queue.startswith(f"{base_queue}:"):
|
143
|
+
# 提取最后部分,检查是否是数字
|
144
|
+
parts = queue.split(':')
|
145
|
+
if len(parts) >= 2 and parts[-1].isdigit():
|
146
|
+
result.append(queue)
|
147
|
+
|
148
|
+
# 按优先级排序(数字越小优先级越高)
|
149
|
+
result.sort(key=lambda x: int(x.split(':')[-1]))
|
150
|
+
return result
|
151
|
+
|
152
|
+
async def clear_priority_queues_for_base(self, base_queue: str):
|
153
|
+
"""清理基础队列的所有优先级队列注册信息"""
|
154
|
+
# 获取该基础队列的所有优先级队列
|
155
|
+
priority_queues = await self.get_priority_queues_for_base(base_queue)
|
156
|
+
|
157
|
+
# 从全局队列注册表中删除
|
158
|
+
if priority_queues:
|
159
|
+
await self.async_redis.srem(self.queues_registry_key, *priority_queues)
|
160
|
+
logger.debug(f"Cleared {len(priority_queues)} priority queues for base queue: {base_queue}")
|
161
|
+
|
162
|
+
# ========== 任务名称查询 ==========
|
163
|
+
|
164
|
+
def get_task_names_by_queue_sync(self, base_queue: str) -> Set[str]:
|
165
|
+
"""
|
166
|
+
通过基础队列名获取所有关联的任务名称(同步)
|
167
|
+
|
168
|
+
从 READ_OFFSETS 中提取,key 格式可能是:
|
169
|
+
- robust_bench2:benchmark_task (基础队列)
|
170
|
+
- robust_bench2:8:benchmark_task (优先级队列)
|
171
|
+
|
172
|
+
Args:
|
173
|
+
base_queue: 基础队列名(不含优先级)
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
Set[str]: 任务名称集合(去重后)
|
177
|
+
|
178
|
+
Examples:
|
179
|
+
>>> registry.get_task_names_by_queue_sync("robust_bench2")
|
180
|
+
{'benchmark_task', 'another_task'}
|
181
|
+
"""
|
182
|
+
read_offsets_key = f"{self.redis_prefix}:READ_OFFSETS"
|
183
|
+
|
184
|
+
# 获取所有 keys
|
185
|
+
all_keys = self.redis.hkeys(read_offsets_key)
|
186
|
+
|
187
|
+
task_names = set()
|
188
|
+
for key in all_keys:
|
189
|
+
# 解码 key
|
190
|
+
if isinstance(key, bytes):
|
191
|
+
key = key.decode('utf-8')
|
192
|
+
|
193
|
+
# 检查是否以 base_queue 开头
|
194
|
+
if not key.startswith(f"{base_queue}:"):
|
195
|
+
continue
|
196
|
+
|
197
|
+
# 去掉队列名前缀
|
198
|
+
suffix = key[len(base_queue) + 1:] # +1 for the ':'
|
199
|
+
|
200
|
+
# suffix 可能是 "benchmark_task" 或 "8:benchmark_task"
|
201
|
+
parts = suffix.split(':')
|
202
|
+
|
203
|
+
# 如果第一部分是数字,说明是优先级,task_name 是后面的部分
|
204
|
+
if parts[0].isdigit() and len(parts) > 1:
|
205
|
+
# 支持 task_name 中可能包含 ':'
|
206
|
+
task_name = ':'.join(parts[1:])
|
207
|
+
else:
|
208
|
+
# 没有优先级,整个 suffix 就是 task_name
|
209
|
+
task_name = suffix
|
210
|
+
|
211
|
+
if task_name: # 过滤空字符串
|
212
|
+
task_names.add(task_name)
|
213
|
+
|
214
|
+
return task_names
|
215
|
+
|
216
|
+
async def get_task_names_by_queue(self, base_queue: str) -> Set[str]:
|
217
|
+
"""
|
218
|
+
通过基础队列名获取所有关联的任务名称(异步)
|
219
|
+
|
220
|
+
从 READ_OFFSETS 中提取,key 格式可能是:
|
221
|
+
- robust_bench2:benchmark_task (基础队列)
|
222
|
+
- robust_bench2:8:benchmark_task (优先级队列)
|
223
|
+
|
224
|
+
Args:
|
225
|
+
base_queue: 基础队列名(不含优先级)
|
226
|
+
|
227
|
+
Returns:
|
228
|
+
Set[str]: 任务名称集合(去重后)
|
229
|
+
|
230
|
+
Examples:
|
231
|
+
>>> await registry.get_task_names_by_queue("robust_bench2")
|
232
|
+
{'benchmark_task', 'another_task'}
|
233
|
+
"""
|
234
|
+
read_offsets_key = f"{self.redis_prefix}:READ_OFFSETS"
|
235
|
+
|
236
|
+
# 获取所有 keys
|
237
|
+
all_keys = await self.async_redis.hkeys(read_offsets_key)
|
238
|
+
|
239
|
+
task_names = set()
|
240
|
+
for key in all_keys:
|
241
|
+
# 解码 key
|
242
|
+
if isinstance(key, bytes):
|
243
|
+
key = key.decode('utf-8')
|
244
|
+
|
245
|
+
# 检查是否以 base_queue 开头
|
246
|
+
if not key.startswith(f"{base_queue}:"):
|
247
|
+
continue
|
248
|
+
|
249
|
+
# 去掉队列名前缀
|
250
|
+
suffix = key[len(base_queue) + 1:] # +1 for the ':'
|
251
|
+
|
252
|
+
# suffix 可能是 "benchmark_task" 或 "8:benchmark_task"
|
253
|
+
parts = suffix.split(':')
|
254
|
+
|
255
|
+
# 如果第一部分是数字,说明是优先级,task_name 是后面的部分
|
256
|
+
if parts[0].isdigit() and len(parts) > 1:
|
257
|
+
# 支持 task_name 中可能包含 ':'
|
258
|
+
task_name = ':'.join(parts[1:])
|
259
|
+
else:
|
260
|
+
# 没有优先级,整个 suffix 就是 task_name
|
261
|
+
task_name = suffix
|
262
|
+
|
263
|
+
if task_name: # 过滤空字符串
|
264
|
+
task_names.add(task_name)
|
265
|
+
|
266
|
+
return task_names
|
@@ -0,0 +1,369 @@
|
|
1
|
+
"""
|
2
|
+
延迟消息扫描器 - 扫描延迟队列并将到期任务移到普通队列
|
3
|
+
从 EventPool 中提取的延迟任务扫描逻辑
|
4
|
+
"""
|
5
|
+
|
6
|
+
import time
|
7
|
+
import logging
|
8
|
+
import asyncio
|
9
|
+
from typing import List, Dict, Callable, Optional
|
10
|
+
from redis.asyncio import Redis as AsyncRedis
|
11
|
+
|
12
|
+
logger = logging.getLogger('app')
|
13
|
+
|
14
|
+
|
15
|
+
class DelayedMessageScanner:
|
16
|
+
"""
|
17
|
+
延迟消息扫描器
|
18
|
+
|
19
|
+
职责:
|
20
|
+
1. 扫描 Redis Sorted Set 中到期的延迟任务
|
21
|
+
2. 从 Stream 中读取任务数据
|
22
|
+
3. 将到期任务从延迟队列移除
|
23
|
+
4. 通过回调函数通知调用方处理到期任务
|
24
|
+
|
25
|
+
工作原理:
|
26
|
+
- 延迟任务发送时会:
|
27
|
+
1. 写入 Stream(包含实际数据)
|
28
|
+
2. 在 Sorted Set 中记录 Stream ID 和执行时间
|
29
|
+
- 扫描器定期检查 Sorted Set,找到到期的 Stream ID
|
30
|
+
- 从 Stream 读取完整数据后回调处理
|
31
|
+
- 从 Sorted Set 移除已处理的任务
|
32
|
+
"""
|
33
|
+
|
34
|
+
def __init__(
|
35
|
+
self,
|
36
|
+
async_binary_redis_client: AsyncRedis,
|
37
|
+
redis_prefix: str = 'jettask',
|
38
|
+
scan_interval: float = 0.05, # 扫描间隔(秒)
|
39
|
+
batch_size: int = 100, # 每次扫描最多处理的任务数
|
40
|
+
priority_discovery_callback: Optional[Callable] = None, # 优先级队列发现回调
|
41
|
+
ensure_consumer_group_callback: Optional[Callable] = None # 确保consumer group存在的回调
|
42
|
+
):
|
43
|
+
"""
|
44
|
+
初始化延迟消息扫描器
|
45
|
+
|
46
|
+
Args:
|
47
|
+
async_binary_redis_client: 异步Redis客户端(二进制模式)
|
48
|
+
redis_prefix: Redis键前缀
|
49
|
+
scan_interval: 扫描间隔(秒),默认50ms
|
50
|
+
batch_size: 每次扫描最多处理的任务数
|
51
|
+
priority_discovery_callback: 优先级队列发现回调,签名: async def (base_queue: str) -> List[str]
|
52
|
+
ensure_consumer_group_callback: 确保consumer group存在的回调,签名: async def (queue: str) -> None
|
53
|
+
"""
|
54
|
+
self.redis = async_binary_redis_client
|
55
|
+
self.redis_prefix = redis_prefix
|
56
|
+
self.scan_interval = scan_interval
|
57
|
+
self.batch_size = batch_size
|
58
|
+
self.priority_discovery_callback = priority_discovery_callback
|
59
|
+
self.ensure_consumer_group_callback = ensure_consumer_group_callback
|
60
|
+
|
61
|
+
# 任务回调:{queue_name: callback_function}
|
62
|
+
self._callbacks: Dict[str, Callable] = {}
|
63
|
+
|
64
|
+
# Lua脚本缓存
|
65
|
+
self._scan_script = None
|
66
|
+
|
67
|
+
# 控制标志
|
68
|
+
self._running = False
|
69
|
+
self._scan_tasks: List[asyncio.Task] = []
|
70
|
+
|
71
|
+
# 优先级队列缓存:{base_queue: [priority_queues]}
|
72
|
+
self._priority_queues_cache: Dict[str, List[str]] = {}
|
73
|
+
|
74
|
+
logger.debug(f"DelayedMessageScanner initialized with prefix: {redis_prefix}")
|
75
|
+
|
76
|
+
def _get_delayed_queue_name(self, queue: str) -> str:
|
77
|
+
"""获取延迟队列名(Sorted Set)"""
|
78
|
+
return f"{self.redis_prefix}:DELAYED_QUEUE:{queue}"
|
79
|
+
|
80
|
+
def _get_prefixed_queue_name(self, queue: str) -> str:
|
81
|
+
"""获取带前缀的队列名(Stream)"""
|
82
|
+
return f"{self.redis_prefix}:QUEUE:{queue}"
|
83
|
+
|
84
|
+
def register_callback(self, queue: str, callback: Callable):
|
85
|
+
"""
|
86
|
+
注册队列的到期任务回调函数
|
87
|
+
|
88
|
+
Args:
|
89
|
+
queue: 队列名
|
90
|
+
callback: 回调函数,签名: async def callback(tasks: List[Dict])
|
91
|
+
tasks格式: [{'event_id': '...', 'data': {...}}, ...]
|
92
|
+
|
93
|
+
示例:
|
94
|
+
async def handle_expired_tasks(tasks):
|
95
|
+
for task in tasks:
|
96
|
+
print(f"Task {task['event_id']} expired")
|
97
|
+
await process(task['data'])
|
98
|
+
|
99
|
+
scanner.register_callback("orders", handle_expired_tasks)
|
100
|
+
"""
|
101
|
+
self._callbacks[queue] = callback
|
102
|
+
logger.info(f"Registered callback for queue {queue}")
|
103
|
+
|
104
|
+
async def start(self, queues: List[str]):
|
105
|
+
"""
|
106
|
+
启动扫描器,为每个队列创建独立的扫描任务
|
107
|
+
|
108
|
+
Args:
|
109
|
+
queues: 队列名列表
|
110
|
+
"""
|
111
|
+
if self._running:
|
112
|
+
logger.warning("DelayedMessageScanner is already running")
|
113
|
+
return
|
114
|
+
|
115
|
+
self._running = True
|
116
|
+
logger.info(f"Starting DelayedMessageScanner for queues: {queues}")
|
117
|
+
|
118
|
+
# 为每个队列创建独立的扫描任务
|
119
|
+
for queue in queues:
|
120
|
+
task = asyncio.create_task(self._scan_queue_loop(queue))
|
121
|
+
self._scan_tasks.append(task)
|
122
|
+
|
123
|
+
logger.info(f"DelayedMessageScanner started with {len(self._scan_tasks)} scan tasks")
|
124
|
+
|
125
|
+
async def stop(self):
|
126
|
+
"""停止扫描器"""
|
127
|
+
if not self._running:
|
128
|
+
return
|
129
|
+
|
130
|
+
logger.info("Stopping DelayedMessageScanner...")
|
131
|
+
self._running = False
|
132
|
+
|
133
|
+
# 取消所有扫描任务
|
134
|
+
for task in self._scan_tasks:
|
135
|
+
task.cancel()
|
136
|
+
|
137
|
+
# 等待所有任务完成
|
138
|
+
await asyncio.gather(*self._scan_tasks, return_exceptions=True)
|
139
|
+
self._scan_tasks.clear()
|
140
|
+
|
141
|
+
logger.info("DelayedMessageScanner stopped")
|
142
|
+
|
143
|
+
async def _scan_queue_loop(self, queue: str):
|
144
|
+
"""
|
145
|
+
单个队列的扫描循环(支持动态优先级队列发现)
|
146
|
+
|
147
|
+
Args:
|
148
|
+
queue: 基础队列名(不含优先级后缀)
|
149
|
+
"""
|
150
|
+
base_interval = self.scan_interval
|
151
|
+
max_interval = 0.5 # 最大间隔500ms
|
152
|
+
priority_check_interval = 1.0 # 优先级队列检查间隔(秒)
|
153
|
+
last_priority_check = 0
|
154
|
+
|
155
|
+
logger.info(f"Starting delayed task scanner for queue {queue}, interval={base_interval}")
|
156
|
+
|
157
|
+
# 初始化优先级队列列表
|
158
|
+
priority_queues = []
|
159
|
+
if self.priority_discovery_callback:
|
160
|
+
try:
|
161
|
+
priority_queues = await self.priority_discovery_callback(queue)
|
162
|
+
self._priority_queues_cache[queue] = priority_queues
|
163
|
+
logger.info(f"Discovered priority queues for {queue}: {priority_queues}")
|
164
|
+
except Exception as e:
|
165
|
+
logger.error(f"Error discovering priority queues for {queue}: {e}")
|
166
|
+
|
167
|
+
while self._running:
|
168
|
+
try:
|
169
|
+
# 定期检查优先级队列是否有变化(每1秒)
|
170
|
+
current_time = time.time()
|
171
|
+
if self.priority_discovery_callback and (current_time - last_priority_check >= priority_check_interval):
|
172
|
+
try:
|
173
|
+
new_priority_queues = await self.priority_discovery_callback(queue)
|
174
|
+
if new_priority_queues != priority_queues:
|
175
|
+
logger.info(f"Priority queues updated for {queue}: {priority_queues} -> {new_priority_queues}")
|
176
|
+
|
177
|
+
# 为新增的优先级队列确保consumer group存在
|
178
|
+
if self.ensure_consumer_group_callback:
|
179
|
+
new_queues = set(new_priority_queues) - set(priority_queues)
|
180
|
+
for new_q in new_queues:
|
181
|
+
try:
|
182
|
+
await self.ensure_consumer_group_callback(new_q)
|
183
|
+
logger.info(f"Ensured consumer group for new priority queue: {new_q}")
|
184
|
+
except Exception as e:
|
185
|
+
logger.error(f"Error ensuring consumer group for {new_q}: {e}")
|
186
|
+
|
187
|
+
priority_queues = new_priority_queues
|
188
|
+
self._priority_queues_cache[queue] = priority_queues
|
189
|
+
except Exception as e:
|
190
|
+
logger.error(f"Error checking priority queues for {queue}: {e}")
|
191
|
+
last_priority_check = current_time
|
192
|
+
|
193
|
+
# 扫描基础队列 + 所有优先级队列
|
194
|
+
all_queues_to_scan = [queue] + priority_queues
|
195
|
+
all_expired_tasks = []
|
196
|
+
|
197
|
+
for q in all_queues_to_scan:
|
198
|
+
expired_tasks = await self._scan_and_get_expired_tasks(q)
|
199
|
+
if expired_tasks:
|
200
|
+
all_expired_tasks.extend(expired_tasks)
|
201
|
+
|
202
|
+
# 如果有任务到期,通知回调
|
203
|
+
if all_expired_tasks and queue in self._callbacks:
|
204
|
+
try:
|
205
|
+
await self._callbacks[queue](all_expired_tasks)
|
206
|
+
except Exception as e:
|
207
|
+
logger.error(f"Error in callback for queue {queue}: {e}", exc_info=True)
|
208
|
+
|
209
|
+
# 动态调整扫描间隔
|
210
|
+
if all_expired_tasks:
|
211
|
+
# 有任务到期,使用较短的间隔
|
212
|
+
sleep_time = base_interval
|
213
|
+
else:
|
214
|
+
# 没有任务到期,检查下一个任务的到期时间
|
215
|
+
min_next_time = None
|
216
|
+
for q in all_queues_to_scan:
|
217
|
+
next_time = await self._get_next_task_time(q)
|
218
|
+
if next_time is not None:
|
219
|
+
if min_next_time is None or next_time < min_next_time:
|
220
|
+
min_next_time = next_time
|
221
|
+
|
222
|
+
if min_next_time is not None:
|
223
|
+
# 计算到下一个任务的时间,但不超过max_interval
|
224
|
+
sleep_time = min(max_interval, max(base_interval, min_next_time - time.time() - 0.01))
|
225
|
+
else:
|
226
|
+
# 没有待处理的延迟任务
|
227
|
+
sleep_time = max_interval
|
228
|
+
|
229
|
+
except asyncio.CancelledError:
|
230
|
+
logger.info(f"Delayed task scanner for queue {queue} cancelled")
|
231
|
+
break
|
232
|
+
except Exception as e:
|
233
|
+
logger.error(f"Error scanning delayed tasks for queue {queue}: {e}", exc_info=True)
|
234
|
+
sleep_time = base_interval
|
235
|
+
|
236
|
+
await asyncio.sleep(sleep_time)
|
237
|
+
|
238
|
+
async def _scan_and_get_expired_tasks(self, queue: str) -> List[Dict]:
|
239
|
+
"""
|
240
|
+
扫描并获取到期的延迟任务
|
241
|
+
|
242
|
+
Args:
|
243
|
+
queue: 队列名
|
244
|
+
|
245
|
+
Returns:
|
246
|
+
List[Dict]: 到期任务列表,格式: [{'event_id': '...', 'data': {...}}, ...]
|
247
|
+
"""
|
248
|
+
try:
|
249
|
+
current_time = time.time()
|
250
|
+
delayed_queue_key = self._get_delayed_queue_name(queue)
|
251
|
+
|
252
|
+
# 使用Lua脚本原子地获取到期的任务ID
|
253
|
+
# 注意:我们只获取ID,不读取数据,因为后续需要用XCLAIM转移所有权
|
254
|
+
lua_script = """
|
255
|
+
local delayed_queue_key = KEYS[1]
|
256
|
+
local current_time = ARGV[1]
|
257
|
+
local limit = ARGV[2]
|
258
|
+
|
259
|
+
-- 获取到期的任务ID(这些是Stream消息ID)
|
260
|
+
local expired_task_ids = redis.call('ZRANGEBYSCORE', delayed_queue_key, 0, current_time, 'LIMIT', 0, limit)
|
261
|
+
|
262
|
+
if #expired_task_ids == 0 then
|
263
|
+
return {}
|
264
|
+
end
|
265
|
+
|
266
|
+
-- 从延迟队列中移除这些任务(标记为已到期)
|
267
|
+
for i, task_id in ipairs(expired_task_ids) do
|
268
|
+
redis.call('ZREM', delayed_queue_key, task_id)
|
269
|
+
end
|
270
|
+
|
271
|
+
return expired_task_ids
|
272
|
+
"""
|
273
|
+
|
274
|
+
# 注册Lua脚本
|
275
|
+
if not self._scan_script:
|
276
|
+
self._scan_script = self.redis.register_script(lua_script)
|
277
|
+
|
278
|
+
# 执行脚本(只传入延迟队列key,返回到期的消息ID列表)
|
279
|
+
expired_task_ids = await self._scan_script(
|
280
|
+
keys=[delayed_queue_key],
|
281
|
+
args=[str(current_time), str(self.batch_size)]
|
282
|
+
)
|
283
|
+
|
284
|
+
if not expired_task_ids:
|
285
|
+
return []
|
286
|
+
|
287
|
+
# 返回到期的消息ID列表(不包含数据)
|
288
|
+
# 数据将在 worker 端通过 XCLAIM 获取,这样能正确转移消息所有权
|
289
|
+
tasks_to_return = []
|
290
|
+
for task_id in expired_task_ids:
|
291
|
+
try:
|
292
|
+
# 解码消息ID
|
293
|
+
event_id = task_id if isinstance(task_id, str) else task_id.decode('utf-8')
|
294
|
+
|
295
|
+
# 只返回消息ID和队列信息
|
296
|
+
# worker会使用XCLAIM来获取消息并转移所有权
|
297
|
+
tasks_to_return.append({
|
298
|
+
'event_id': event_id,
|
299
|
+
'queue': queue # 队列名(可能包含优先级后缀)
|
300
|
+
})
|
301
|
+
|
302
|
+
except Exception as e:
|
303
|
+
logger.error(f"Error processing delayed task ID: {e}", exc_info=True)
|
304
|
+
|
305
|
+
if tasks_to_return:
|
306
|
+
logger.info(f"Found {len(tasks_to_return)} expired tasks in queue {queue}")
|
307
|
+
|
308
|
+
return tasks_to_return
|
309
|
+
|
310
|
+
except Exception as e:
|
311
|
+
logger.error(f"Error scanning delayed tasks for queue {queue}: {e}", exc_info=True)
|
312
|
+
return []
|
313
|
+
|
314
|
+
async def _get_next_task_time(self, queue: str) -> Optional[float]:
|
315
|
+
"""
|
316
|
+
获取下一个任务的到期时间
|
317
|
+
|
318
|
+
Args:
|
319
|
+
queue: 队列名
|
320
|
+
|
321
|
+
Returns:
|
322
|
+
Optional[float]: 到期时间(Unix时间戳),如果没有任务返回None
|
323
|
+
"""
|
324
|
+
try:
|
325
|
+
delayed_queue_key = self._get_delayed_queue_name(queue)
|
326
|
+
|
327
|
+
# 获取分数最小的任务(最早到期的)
|
328
|
+
result = await self.redis.zrange(
|
329
|
+
delayed_queue_key, 0, 0, withscores=True
|
330
|
+
)
|
331
|
+
|
332
|
+
if result:
|
333
|
+
# result格式: [(task_id, score)]
|
334
|
+
return result[0][1]
|
335
|
+
|
336
|
+
return None
|
337
|
+
|
338
|
+
except Exception as e:
|
339
|
+
logger.error(f"Error getting next task time for queue {queue}: {e}")
|
340
|
+
return None
|
341
|
+
|
342
|
+
async def get_delayed_count(self, queue: str) -> int:
|
343
|
+
"""
|
344
|
+
获取延迟队列中的任务数量
|
345
|
+
|
346
|
+
Args:
|
347
|
+
queue: 队列名
|
348
|
+
|
349
|
+
Returns:
|
350
|
+
int: 任务数量
|
351
|
+
"""
|
352
|
+
delayed_queue_key = self._get_delayed_queue_name(queue)
|
353
|
+
return await self.redis.zcard(delayed_queue_key)
|
354
|
+
|
355
|
+
async def get_expired_count(self, queue: str) -> int:
|
356
|
+
"""
|
357
|
+
获取已到期但未处理的任务数量
|
358
|
+
|
359
|
+
Args:
|
360
|
+
queue: 队列名
|
361
|
+
|
362
|
+
Returns:
|
363
|
+
int: 已到期任务数量
|
364
|
+
"""
|
365
|
+
delayed_queue_key = self._get_delayed_queue_name(queue)
|
366
|
+
current_time = time.time()
|
367
|
+
|
368
|
+
count = await self.redis.zcount(delayed_queue_key, 0, current_time)
|
369
|
+
return count
|