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
jettask/core/worker_scanner.py
CHANGED
@@ -36,81 +36,81 @@ class WorkerScanner:
|
|
36
36
|
self._scan_counter = 0
|
37
37
|
self._partial_check_interval = 10 # 每10次扫描做部分检查
|
38
38
|
|
39
|
-
async def ensure_initialized(self):
|
40
|
-
|
41
|
-
|
39
|
+
# async def ensure_initialized(self):
|
40
|
+
# """确保 Sorted Set 已初始化并保持一致"""
|
41
|
+
# current_time = time.time()
|
42
42
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
43
|
+
# # 首次初始化或定期完整同步
|
44
|
+
# if not self._initialized or (current_time - self._last_full_sync > self._full_sync_interval):
|
45
|
+
# await self._full_sync()
|
46
|
+
# self._initialized = True
|
47
|
+
# self._last_full_sync = current_time
|
48
48
|
|
49
|
-
async def _full_sync(self):
|
50
|
-
|
51
|
-
|
52
|
-
|
49
|
+
# async def _full_sync(self):
|
50
|
+
# """完整同步 Hash 和 Sorted Set"""
|
51
|
+
# try:
|
52
|
+
# logger.debug("Starting full synchronization of ACTIVE_WORKERS")
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
|
54
|
+
# # 1. 收集所有活跃 worker 的 Hash 数据
|
55
|
+
# pattern = f"{self.redis_prefix}:WORKER:*"
|
56
|
+
# hash_workers = {}
|
57
57
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
58
|
+
# async for key in self.async_redis.scan_iter(match=pattern, count=100):
|
59
|
+
# if ':HISTORY:' not in key and ':REUSE:' not in key:
|
60
|
+
# worker_id = key.split(':')[-1]
|
61
|
+
# worker_data = await self.async_redis.hgetall(key)
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
63
|
+
# if worker_data and worker_data.get('is_alive', 'true').lower() == 'true':
|
64
|
+
# try:
|
65
|
+
# heartbeat = float(worker_data.get('last_heartbeat', 0))
|
66
|
+
# hash_workers[worker_id] = heartbeat
|
67
|
+
# except (ValueError, TypeError):
|
68
|
+
# continue
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
70
|
+
# # 2. 获取 Sorted Set 中的数据
|
71
|
+
# zset_workers = await self.async_redis.zrange(
|
72
|
+
# self.active_workers_key, 0, -1, withscores=True
|
73
|
+
# )
|
74
|
+
# zset_dict = {worker_id: score for worker_id, score in zset_workers}
|
75
75
|
|
76
|
-
|
77
|
-
|
78
|
-
|
76
|
+
# # 3. 计算差异
|
77
|
+
# hash_ids = set(hash_workers.keys())
|
78
|
+
# zset_ids = set(zset_dict.keys())
|
79
79
|
|
80
|
-
|
81
|
-
|
82
|
-
|
80
|
+
# to_add = hash_ids - zset_ids # Hash有但ZSet无
|
81
|
+
# to_remove = zset_ids - hash_ids # ZSet有但Hash无
|
82
|
+
# to_update = {} # 时间戳不一致的
|
83
83
|
|
84
|
-
|
85
|
-
|
86
|
-
|
84
|
+
# for worker_id in hash_ids & zset_ids:
|
85
|
+
# if abs(hash_workers[worker_id] - zset_dict[worker_id]) > 0.1:
|
86
|
+
# to_update[worker_id] = hash_workers[worker_id]
|
87
87
|
|
88
|
-
|
89
|
-
|
90
|
-
|
88
|
+
# # 4. 批量修复
|
89
|
+
# if to_add or to_remove or to_update:
|
90
|
+
# pipeline = self.async_redis.pipeline()
|
91
91
|
|
92
|
-
|
93
|
-
|
94
|
-
|
92
|
+
# if to_add:
|
93
|
+
# members = {worker_id: hash_workers[worker_id] for worker_id in to_add}
|
94
|
+
# pipeline.zadd(self.active_workers_key, members)
|
95
95
|
|
96
|
-
|
97
|
-
|
96
|
+
# if to_remove:
|
97
|
+
# pipeline.zrem(self.active_workers_key, *to_remove)
|
98
98
|
|
99
|
-
|
100
|
-
|
99
|
+
# if to_update:
|
100
|
+
# pipeline.zadd(self.active_workers_key, to_update)
|
101
101
|
|
102
|
-
|
103
|
-
|
102
|
+
# await pipeline.execute()
|
103
|
+
# logger.info(f"Full sync: +{len(to_add)}, -{len(to_remove)}, ~{len(to_update)}")
|
104
104
|
|
105
|
-
|
106
|
-
|
105
|
+
# except Exception as e:
|
106
|
+
# logger.error(f"Full sync failed: {e}")
|
107
107
|
|
108
108
|
async def scan_timeout_workers(self) -> List[Dict]:
|
109
109
|
"""
|
110
110
|
快速扫描超时的 worker - O(log N) 复杂度
|
111
111
|
注意:需要考虑每个worker自己的heartbeat_timeout
|
112
112
|
"""
|
113
|
-
await self.ensure_initialized()
|
113
|
+
# await self.ensure_initialized()
|
114
114
|
|
115
115
|
# 定期部分检查
|
116
116
|
self._scan_counter += 1
|
jettask/executors/asyncio.py
CHANGED
@@ -13,9 +13,25 @@ from collections import defaultdict
|
|
13
13
|
from .base import BaseExecutor
|
14
14
|
import random
|
15
15
|
from ..exceptions import RetryableError
|
16
|
+
from ..core.enums import TaskStatus
|
16
17
|
|
17
18
|
logger = logging.getLogger('app')
|
18
19
|
|
20
|
+
# Lua脚本:原子地更新Redis hash中的最大值
|
21
|
+
UPDATE_MAX_OFFSET_LUA = """
|
22
|
+
local hash_key = KEYS[1]
|
23
|
+
local field = KEYS[2]
|
24
|
+
local new_value = tonumber(ARGV[1])
|
25
|
+
|
26
|
+
local current = redis.call('HGET', hash_key, field)
|
27
|
+
if current == false or tonumber(current) < new_value then
|
28
|
+
redis.call('HSET', hash_key, field, new_value)
|
29
|
+
return 1
|
30
|
+
else
|
31
|
+
return 0
|
32
|
+
end
|
33
|
+
"""
|
34
|
+
|
19
35
|
# Try to use uvloop for better performance
|
20
36
|
try:
|
21
37
|
import uvloop
|
@@ -116,11 +132,11 @@ class AsyncioExecutor(BaseExecutor):
|
|
116
132
|
|
117
133
|
return self.pending_cache.get(queue, 0)
|
118
134
|
|
119
|
-
async def _quick_ack(self, queue: str, event_id: str, group_name: str = None):
|
120
|
-
"""Quick ACK with unified pipeline management"""
|
135
|
+
async def _quick_ack(self, queue: str, event_id: str, group_name: str = None, offset: int = None):
|
136
|
+
"""Quick ACK with unified pipeline management and offset tracking"""
|
121
137
|
# 如果没有提供group_name,使用queue作为默认值(兼容旧代码)
|
122
138
|
group_name = group_name or queue
|
123
|
-
self.pending_acks.append((queue, event_id, group_name))
|
139
|
+
self.pending_acks.append((queue, event_id, group_name, offset))
|
124
140
|
current_time = time.time()
|
125
141
|
|
126
142
|
# 检查是否需要刷新统一 Pipeline
|
@@ -147,16 +163,44 @@ class AsyncioExecutor(BaseExecutor):
|
|
147
163
|
# 1. 处理 ACK 操作(使用二进制客户端)
|
148
164
|
if self.pending_acks:
|
149
165
|
acks_by_queue_group = defaultdict(lambda: defaultdict(list))
|
166
|
+
offset_updates = [] # 收集需要更新的offset
|
167
|
+
|
168
|
+
# 按照 queue+group_name 分组,记录每个组的最大offset
|
169
|
+
max_offsets = {} # {(queue, group_name): max_offset}
|
170
|
+
|
150
171
|
for item in self.pending_acks:
|
151
|
-
|
172
|
+
print(f'{item=}')
|
173
|
+
if len(item) == 4:
|
174
|
+
queue, event_id, group_name, offset = item
|
175
|
+
elif len(item) == 3:
|
152
176
|
queue, event_id, group_name = item
|
177
|
+
offset = None
|
153
178
|
else:
|
154
179
|
queue, event_id = item
|
155
180
|
group_name = queue
|
181
|
+
offset = None
|
156
182
|
|
157
183
|
prefixed_queue = self._get_prefixed_queue_cached(queue)
|
158
184
|
acks_by_queue_group[prefixed_queue][group_name].append(event_id)
|
185
|
+
|
186
|
+
# 收集offset更新信息(只记录最大值)
|
187
|
+
if group_name and offset is not None:
|
188
|
+
key = (queue, group_name)
|
189
|
+
if key not in max_offsets or offset > max_offsets[key]:
|
190
|
+
max_offsets[key] = offset
|
191
|
+
|
192
|
+
logger.info(f'{max_offsets=}')
|
193
|
+
# 处理offset更新(使用Lua脚本确保原子性和最大值约束)
|
194
|
+
if max_offsets:
|
195
|
+
task_offset_key = f"{self.prefix}:TASK_OFFSETS"
|
196
|
+
for (queue, group_name), offset in max_offsets.items():
|
197
|
+
task_field = f"{queue}:{group_name}"
|
198
|
+
|
199
|
+
# 使用Lua脚本原子地更新最大offset
|
200
|
+
pipeline.eval(UPDATE_MAX_OFFSET_LUA, 2, task_offset_key, task_field, offset)
|
201
|
+
operations_count += 1
|
159
202
|
|
203
|
+
# 执行stream ACK
|
160
204
|
for prefixed_queue, groups in acks_by_queue_group.items():
|
161
205
|
for group_name, event_ids in groups.items():
|
162
206
|
stream_key = prefixed_queue.encode() if isinstance(prefixed_queue, str) else prefixed_queue
|
@@ -164,7 +208,7 @@ class AsyncioExecutor(BaseExecutor):
|
|
164
208
|
batch_bytes = [b.encode() if isinstance(b, str) else b for b in event_ids]
|
165
209
|
|
166
210
|
# 添加到统一 pipeline
|
167
|
-
logger.info(f'准备ack {batch_bytes=}')
|
211
|
+
# logger.info(f'准备ack {batch_bytes=} {stream_key=} {group_key}')
|
168
212
|
pipeline.xack(stream_key, group_key, *batch_bytes)
|
169
213
|
operations_count += 1
|
170
214
|
|
@@ -173,8 +217,10 @@ class AsyncioExecutor(BaseExecutor):
|
|
173
217
|
# 2. 处理任务信息更新(Hash)
|
174
218
|
task_change_events = [] # 收集变更的任务ID
|
175
219
|
if self.task_info_updates:
|
176
|
-
for
|
177
|
-
|
220
|
+
for event_key, updates in self.task_info_updates.items():
|
221
|
+
# event_key 可能是 "event_id" 或 "event_id:task_name"(广播模式)
|
222
|
+
# key格式: jettask:TASK:event_id:group_name
|
223
|
+
key = f"{self.prefix}:TASK:{event_key}".encode() # 转为 bytes
|
178
224
|
if updates:
|
179
225
|
# 将更新的值编码为 bytes
|
180
226
|
encoded_updates = {k.encode(): v.encode() if isinstance(v, str) else v for k, v in updates.items()}
|
@@ -182,20 +228,22 @@ class AsyncioExecutor(BaseExecutor):
|
|
182
228
|
pipeline.expire(key, 3600)
|
183
229
|
operations_count += 2
|
184
230
|
|
185
|
-
# 收集变更的任务ID
|
186
|
-
|
231
|
+
# 收集变更的任务ID(包含完整的key路径)
|
232
|
+
# event_key 可能是 "event_id" 或 "event_id:task_name"(广播模式)
|
233
|
+
# 发送完整的task_id,例如 "jettask:TASK:1756956517980-0:jettask:QUEUE:queue_name:task_name"
|
234
|
+
full_task_id = f"{self.prefix}:TASK:{event_key}"
|
235
|
+
task_change_events.append(full_task_id)
|
187
236
|
|
188
237
|
# 发送变更事件到专门的 Stream 队列
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
238
|
+
change_stream_key = f"{self.prefix}:TASK_CHANGES".encode()
|
239
|
+
for task_id in task_change_events:
|
240
|
+
# 发送完整的task_id(包含前缀)
|
241
|
+
change_data = {
|
242
|
+
b'id': task_id.encode() if isinstance(task_id, str) else task_id
|
243
|
+
}
|
244
|
+
pipeline.xadd(change_stream_key, change_data, maxlen=1000000) # 保留最近100000条变更
|
245
|
+
operations_count += 1
|
246
|
+
|
199
247
|
self.task_info_updates.clear()
|
200
248
|
|
201
249
|
# 3. 处理统计信息(如果有)
|
@@ -279,19 +327,27 @@ class AsyncioExecutor(BaseExecutor):
|
|
279
327
|
|
280
328
|
async def logic(self, semaphore: asyncio.Semaphore, event_id: str, event_data: dict, queue: str, routing: dict = None, consumer: str = None, group_name: str = None, **kwargs):
|
281
329
|
"""Process a single task"""
|
282
|
-
|
283
|
-
execution_start_time = time.time()
|
284
|
-
status = "error" # 默认状态
|
330
|
+
status = "success" # 默认状态
|
285
331
|
exception = None
|
286
332
|
error_msg = None
|
287
333
|
ret = None
|
288
334
|
task = None # 初始化 task 变量
|
289
335
|
args = () # 初始化参数
|
290
336
|
kwargs_inner = {} # 初始化关键字参数(避免与函数参数 kwargs 冲突)
|
337
|
+
# print(f'{group_name=}')
|
338
|
+
# 尽早初始化status_key,避免在finally块中未定义
|
339
|
+
# 使用传入的group_name参数,如果没有则使用queue作为默认值
|
340
|
+
status_key = f"{event_id}:{group_name}" # 组合key
|
291
341
|
|
292
342
|
# 获取任务名称(尽早获取,以便设置日志上下文)
|
293
|
-
|
294
|
-
|
343
|
+
# 使用_task_name字段(由listen_event_by_task设置)
|
344
|
+
task_name = event_data.get("_task_name") or event_data.get("name")
|
345
|
+
# print(f'{event_data=}')
|
346
|
+
# 如果消息中没有task_name,记录错误并返回
|
347
|
+
if not task_name:
|
348
|
+
logger.error(f"No _task_name in event_data for event {event_id}")
|
349
|
+
# 返回,不处理没有task_name的消息
|
350
|
+
return
|
295
351
|
# 设置任务日志上下文 - 包含整个任务处理流程
|
296
352
|
async with TaskContextManager(
|
297
353
|
event_id=event_id,
|
@@ -328,10 +384,23 @@ class AsyncioExecutor(BaseExecutor):
|
|
328
384
|
logger.error(f"No task name found! event_data keys: {list(event_data.keys())}, event_id: {event_id}")
|
329
385
|
|
330
386
|
task = self.app.get_task_by_name(task_name)
|
387
|
+
|
388
|
+
# status_key已经在方法开头初始化过了
|
389
|
+
|
331
390
|
if not task:
|
332
|
-
exception = f"{task_name=} {queue=} {
|
391
|
+
exception = f"{task_name=} {queue=} {event_data=} 未绑定任何task"
|
333
392
|
logger.error(exception)
|
334
|
-
|
393
|
+
# 从 event_data 中获取 offset
|
394
|
+
offset = None
|
395
|
+
if isinstance(event_data, dict):
|
396
|
+
offset = event_data.get('offset')
|
397
|
+
if offset is not None:
|
398
|
+
try:
|
399
|
+
offset = int(offset)
|
400
|
+
except (ValueError, TypeError):
|
401
|
+
offset = None
|
402
|
+
|
403
|
+
await self._quick_ack(queue, event_id, group_name, offset)
|
335
404
|
|
336
405
|
# 任务不存在时也记录started_at(使用当前时间)
|
337
406
|
current_time = time.time()
|
@@ -339,14 +408,13 @@ class AsyncioExecutor(BaseExecutor):
|
|
339
408
|
trigger_time_float = float(event_data.get('trigger_time', current_time))
|
340
409
|
duration = current_time - trigger_time_float
|
341
410
|
# 使用Hash更新
|
342
|
-
self.task_info_updates[
|
343
|
-
"status":
|
411
|
+
self.task_info_updates[status_key] = {
|
412
|
+
"status": TaskStatus.ERROR.value,
|
344
413
|
"exception": exception,
|
345
414
|
"started_at": str(current_time),
|
346
415
|
"completed_at": str(current_time),
|
347
416
|
"duration": str(duration),
|
348
417
|
"consumer": consumer,
|
349
|
-
"result": "null" # 任务不存在时没有结果
|
350
418
|
}
|
351
419
|
# 使用统一的 pipeline 刷新
|
352
420
|
await self._flush_all_buffers()
|
@@ -357,25 +425,25 @@ class AsyncioExecutor(BaseExecutor):
|
|
357
425
|
# 重置状态为 success(默认是 error)
|
358
426
|
status = "success"
|
359
427
|
|
360
|
-
# 更新执行开始时间(之前已经初始化过)
|
361
|
-
execution_start_time = time.time()
|
362
|
-
|
363
428
|
# 获取参数(现在直接是对象,不需要反序列化)
|
364
429
|
args = event_data.get("args", ()) or ()
|
365
430
|
|
366
431
|
# 统一处理kwargs(现在直接是对象,不需要反序列化)
|
367
432
|
kwargs_inner = event_data.get("kwargs", {}) or {}
|
368
433
|
|
369
|
-
#
|
370
|
-
if event_data
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
434
|
+
# 如果event_data中有scheduled_task_id,添加到kwargs中供TaskContext使用
|
435
|
+
if 'scheduled_task_id' in event_data:
|
436
|
+
kwargs_inner['__scheduled_task_id'] = event_data['scheduled_task_id']
|
437
|
+
|
438
|
+
# 检查是否需要提取特定字段作为参数
|
439
|
+
# 如果消息包含 event_type 和 customer_data,将它们作为参数传递
|
440
|
+
if "event_type" in event_data and "customer_data" in event_data:
|
441
|
+
# 将这些字段作为位置参数传递,其他字段作为kwargs
|
442
|
+
args = (event_data["event_type"], event_data["customer_data"])
|
443
|
+
# 保留其他字段在kwargs中,但排除已作为args的字段
|
444
|
+
extra_kwargs = {k: v for k, v in event_data.items()
|
445
|
+
if k not in ["event_type", "customer_data", "_broadcast", "_target_tasks", "_timestamp", "trigger_time", "name", "_task_name"]}
|
446
|
+
kwargs_inner.update(extra_kwargs)
|
379
447
|
|
380
448
|
# Execute lifecycle methods
|
381
449
|
result = task.on_before(
|
@@ -389,10 +457,10 @@ class AsyncioExecutor(BaseExecutor):
|
|
389
457
|
|
390
458
|
if result and result.reject:
|
391
459
|
# 任务被reject,使用Hash更新
|
392
|
-
self.task_info_updates[
|
393
|
-
|
460
|
+
self.task_info_updates[status_key] = {
|
461
|
+
"status": TaskStatus.REJECTED.value,
|
394
462
|
"consumer": consumer,
|
395
|
-
"started_at": str(
|
463
|
+
"started_at": str(time.time()),
|
396
464
|
"completed_at": str(time.time()),
|
397
465
|
"error_msg": "Task rejected by on_before"
|
398
466
|
}
|
@@ -401,23 +469,23 @@ class AsyncioExecutor(BaseExecutor):
|
|
401
469
|
return
|
402
470
|
|
403
471
|
# 标记任务开始执行
|
404
|
-
if hasattr(self.app, 'consumer_manager') and self.app.consumer_manager:
|
405
|
-
|
472
|
+
# if hasattr(self.app, 'consumer_manager') and self.app.consumer_manager:
|
473
|
+
# self.app.consumer_manager.task_started(queue)
|
406
474
|
|
407
475
|
# 更新任务真正开始执行的时间(在on_before之后)
|
408
476
|
execution_start_time = time.time()
|
409
477
|
|
410
478
|
# 使用Hash更新running状态
|
411
479
|
# 为了让用户能看到任务正在运行,立即写入running状态
|
412
|
-
# running_key = f"{self.prefix}:TASK:{
|
480
|
+
# running_key = f"{self.prefix}:TASK:{status_key}"
|
413
481
|
# 保存开始信息,但不设置status为running,避免竞态条件
|
414
|
-
self.task_info_updates[
|
415
|
-
"status":
|
482
|
+
self.task_info_updates[status_key] = {
|
483
|
+
"status": TaskStatus.RUNNING.value,
|
416
484
|
"consumer": consumer,
|
417
485
|
"started_at": str(execution_start_time)
|
418
486
|
}
|
419
487
|
# await self.app.ep.async_redis_client.hset(running_key, mapping={
|
420
|
-
# "status":
|
488
|
+
# "status": TaskStatus.RUNNING.value,
|
421
489
|
# "consumer": consumer,
|
422
490
|
# "started_at": str(execution_start_time)
|
423
491
|
# })
|
@@ -432,7 +500,12 @@ class AsyncioExecutor(BaseExecutor):
|
|
432
500
|
if current_retry > 0:
|
433
501
|
logger.info(f"Retry attempt {current_retry}/{max_retries} for task {event_id}")
|
434
502
|
|
435
|
-
|
503
|
+
# 从kwargs中移除内部参数,避免传递给用户的任务函数
|
504
|
+
clean_kwargs = {k: v for k, v in kwargs_inner.items()
|
505
|
+
if not k.startswith('_') and not k.startswith('__')}
|
506
|
+
|
507
|
+
logger.debug(f"Calling task with clean_kwargs: {clean_kwargs}")
|
508
|
+
task_result = task(event_id, event_data['trigger_time'], *args, **clean_kwargs)
|
436
509
|
if asyncio.iscoroutine(task_result):
|
437
510
|
ret = await task_result
|
438
511
|
else:
|
@@ -440,14 +513,24 @@ class AsyncioExecutor(BaseExecutor):
|
|
440
513
|
result = task.on_success(
|
441
514
|
event_id=event_id,
|
442
515
|
args=args,
|
443
|
-
kwargs=
|
516
|
+
kwargs=clean_kwargs,
|
444
517
|
result=ret,
|
445
518
|
)
|
446
519
|
if asyncio.iscoroutine(result):
|
447
520
|
await result
|
448
521
|
|
449
522
|
# 任务成功执行,现在可以ACK消息了
|
450
|
-
|
523
|
+
# 从 event_data 中获取 offset
|
524
|
+
offset = None
|
525
|
+
if isinstance(event_data, dict):
|
526
|
+
offset = event_data.get('offset')
|
527
|
+
if offset is not None:
|
528
|
+
try:
|
529
|
+
offset = int(offset)
|
530
|
+
except (ValueError, TypeError):
|
531
|
+
offset = None
|
532
|
+
|
533
|
+
await self._quick_ack(queue, event_id, group_name, offset)
|
451
534
|
|
452
535
|
# 任务成功,跳出重试循环
|
453
536
|
break
|
@@ -459,7 +542,17 @@ class AsyncioExecutor(BaseExecutor):
|
|
459
542
|
exception = "System exit"
|
460
543
|
error_msg = "Task interrupted by shutdown"
|
461
544
|
# 系统退出时也需要ACK消息
|
462
|
-
|
545
|
+
# 从 event_data 中获取 offset
|
546
|
+
offset = None
|
547
|
+
if isinstance(event_data, dict):
|
548
|
+
offset = event_data.get('offset')
|
549
|
+
if offset is not None:
|
550
|
+
try:
|
551
|
+
offset = int(offset)
|
552
|
+
except (ValueError, TypeError):
|
553
|
+
offset = None
|
554
|
+
|
555
|
+
await self._quick_ack(queue, event_id, group_name, offset)
|
463
556
|
break
|
464
557
|
|
465
558
|
except Exception as e:
|
@@ -515,8 +608,18 @@ class AsyncioExecutor(BaseExecutor):
|
|
515
608
|
error_msg = str(e)
|
516
609
|
logger.error(exception)
|
517
610
|
# 任务失败且不重试,需要ACK消息
|
518
|
-
|
519
|
-
|
611
|
+
# 从 event_data 中获取 offset
|
612
|
+
offset = None
|
613
|
+
if isinstance(event_data, dict):
|
614
|
+
offset = event_data.get('offset')
|
615
|
+
if offset is not None:
|
616
|
+
try:
|
617
|
+
offset = int(offset)
|
618
|
+
except (ValueError, TypeError):
|
619
|
+
offset = None
|
620
|
+
|
621
|
+
await self._quick_ack(queue, event_id, group_name, offset)
|
622
|
+
break
|
520
623
|
|
521
624
|
# 如果所有重试都失败了
|
522
625
|
if current_retry > max_retries and last_exception:
|
@@ -525,7 +628,17 @@ class AsyncioExecutor(BaseExecutor):
|
|
525
628
|
exception = filter_framework_traceback()
|
526
629
|
error_msg = str(last_exception)
|
527
630
|
# 任务最终失败,也需要ACK消息
|
528
|
-
|
631
|
+
# 从 event_data 中获取 offset
|
632
|
+
offset = None
|
633
|
+
if isinstance(event_data, dict):
|
634
|
+
offset = event_data.get('offset')
|
635
|
+
if offset is not None:
|
636
|
+
try:
|
637
|
+
offset = int(offset)
|
638
|
+
except (ValueError, TypeError):
|
639
|
+
offset = None
|
640
|
+
|
641
|
+
await self._quick_ack(queue, event_id, group_name, offset)
|
529
642
|
|
530
643
|
# except块已经移到while循环内部,这里不需要了
|
531
644
|
finally:
|
@@ -549,8 +662,8 @@ class AsyncioExecutor(BaseExecutor):
|
|
549
662
|
# 重要:先设置result,再设置status,确保不会出现status=success但result还没写入的情况
|
550
663
|
task_info = {
|
551
664
|
"completed_at": str(completed_at),
|
552
|
-
"execution_time":
|
553
|
-
"duration":
|
665
|
+
"execution_time": execution_time,
|
666
|
+
"duration": total_latency,
|
554
667
|
"consumer": consumer,
|
555
668
|
'status': status
|
556
669
|
}
|
@@ -569,19 +682,24 @@ class AsyncioExecutor(BaseExecutor):
|
|
569
682
|
|
570
683
|
|
571
684
|
# 更新到缓冲区
|
572
|
-
if
|
685
|
+
if status_key in self.task_info_updates:
|
573
686
|
# 合并更新(保留started_at等之前的信息)
|
574
687
|
# 重要:确保最终状态覆盖之前的running状态
|
575
|
-
self.task_info_updates[
|
688
|
+
self.task_info_updates[status_key].update(task_info)
|
576
689
|
else:
|
577
|
-
self.task_info_updates[
|
690
|
+
self.task_info_updates[status_key] = task_info
|
578
691
|
|
579
692
|
# 只有在 task 存在时才调用 on_end
|
580
693
|
if task:
|
694
|
+
# 为on_end使用clean_kwargs(如果clean_kwargs未定义,则创建它)
|
695
|
+
if 'clean_kwargs' not in locals():
|
696
|
+
clean_kwargs = {k: v for k, v in kwargs_inner.items()
|
697
|
+
if not k.startswith('_') and not k.startswith('__')}
|
698
|
+
|
581
699
|
result = task.on_end(
|
582
700
|
event_id=event_id,
|
583
701
|
args=args,
|
584
|
-
kwargs=
|
702
|
+
kwargs=clean_kwargs,
|
585
703
|
result=ret,
|
586
704
|
pedding_count=self.pedding_count,
|
587
705
|
)
|
@@ -664,10 +782,12 @@ class AsyncioExecutor(BaseExecutor):
|
|
664
782
|
if event:
|
665
783
|
event.pop("execute_time", None)
|
666
784
|
tasks_batch.append(event)
|
785
|
+
logger.debug(f"Got event from queue: {event.get('event_id', 'unknown')}")
|
667
786
|
# 批量创建协程任务
|
668
787
|
if tasks_batch:
|
669
788
|
for event in tasks_batch:
|
670
789
|
self.batch_counter += 1
|
790
|
+
logger.debug(f"Creating task for event: {event.get('event_id', 'unknown')}")
|
671
791
|
asyncio.create_task(self.logic(None, **event)) # semaphore 参数暂时传 None
|
672
792
|
|
673
793
|
tasks_batch.clear()
|
@@ -0,0 +1,51 @@
|
|
1
|
+
"""
|
2
|
+
任务中心配置模块
|
3
|
+
明确区分:
|
4
|
+
1. 任务中心元数据库 - 存储命名空间、配置等管理数据
|
5
|
+
2. JetTask应用数据库 - 每个命名空间配置的Redis和PostgreSQL
|
6
|
+
"""
|
7
|
+
import os
|
8
|
+
from typing import Optional
|
9
|
+
|
10
|
+
|
11
|
+
class TaskCenterDatabaseConfig:
|
12
|
+
"""任务中心元数据库配置(用于存储命名空间等配置)"""
|
13
|
+
|
14
|
+
def __init__(self):
|
15
|
+
# 元数据库配置 - 从环境变量读取
|
16
|
+
self.meta_db_host = os.getenv("TASK_CENTER_DB_HOST", "localhost")
|
17
|
+
self.meta_db_port = int(os.getenv("TASK_CENTER_DB_PORT", "5432"))
|
18
|
+
self.meta_db_user = os.getenv("TASK_CENTER_DB_USER", "jettask") # 使用jettask作为默认用户
|
19
|
+
self.meta_db_password = os.getenv("TASK_CENTER_DB_PASSWORD", "123456") # 使用现有密码
|
20
|
+
self.meta_db_name = os.getenv("TASK_CENTER_DB_NAME", "jettask") # 使用现有数据库
|
21
|
+
|
22
|
+
# API服务配置
|
23
|
+
self.api_host = os.getenv("TASK_CENTER_API_HOST", "0.0.0.0")
|
24
|
+
self.api_port = int(os.getenv("TASK_CENTER_API_PORT", "8001"))
|
25
|
+
|
26
|
+
# 基础URL配置(用于生成connection_url)
|
27
|
+
self.base_url = os.getenv("TASK_CENTER_BASE_URL", "http://localhost:8001")
|
28
|
+
|
29
|
+
@property
|
30
|
+
def meta_database_url(self) -> str:
|
31
|
+
"""获取元数据库连接URL"""
|
32
|
+
return f"postgresql+asyncpg://{self.meta_db_user}:{self.meta_db_password}@{self.meta_db_host}:{self.meta_db_port}/{self.meta_db_name}"
|
33
|
+
|
34
|
+
@property
|
35
|
+
def sync_meta_database_url(self) -> str:
|
36
|
+
"""获取同步元数据库连接URL(用于初始化)"""
|
37
|
+
return f"postgresql://{self.meta_db_user}:{self.meta_db_password}@{self.meta_db_host}:{self.meta_db_port}/{self.meta_db_name}"
|
38
|
+
|
39
|
+
@property
|
40
|
+
def pg_url(self) -> str:
|
41
|
+
"""获取PostgreSQL连接URL(兼容旧代码)"""
|
42
|
+
return self.meta_database_url
|
43
|
+
|
44
|
+
|
45
|
+
# 全局配置实例
|
46
|
+
task_center_config = TaskCenterDatabaseConfig()
|
47
|
+
|
48
|
+
|
49
|
+
def get_task_center_config() -> TaskCenterDatabaseConfig:
|
50
|
+
"""获取任务中心配置"""
|
51
|
+
return task_center_config
|