jettask 0.2.1__py3-none-any.whl → 0.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. jettask/constants.py +213 -0
  2. jettask/core/app.py +525 -205
  3. jettask/core/cli.py +193 -185
  4. jettask/core/consumer_manager.py +126 -34
  5. jettask/core/context.py +3 -0
  6. jettask/core/enums.py +137 -0
  7. jettask/core/event_pool.py +501 -168
  8. jettask/core/message.py +147 -0
  9. jettask/core/offline_worker_recovery.py +181 -114
  10. jettask/core/task.py +10 -174
  11. jettask/core/task_batch.py +153 -0
  12. jettask/core/unified_manager_base.py +243 -0
  13. jettask/core/worker_scanner.py +54 -54
  14. jettask/executors/asyncio.py +184 -64
  15. jettask/webui/backend/config.py +51 -0
  16. jettask/webui/backend/data_access.py +2083 -92
  17. jettask/webui/backend/data_api.py +3294 -0
  18. jettask/webui/backend/dependencies.py +261 -0
  19. jettask/webui/backend/init_meta_db.py +158 -0
  20. jettask/webui/backend/main.py +1358 -69
  21. jettask/webui/backend/main_unified.py +78 -0
  22. jettask/webui/backend/main_v2.py +394 -0
  23. jettask/webui/backend/namespace_api.py +295 -0
  24. jettask/webui/backend/namespace_api_old.py +294 -0
  25. jettask/webui/backend/namespace_data_access.py +611 -0
  26. jettask/webui/backend/queue_backlog_api.py +727 -0
  27. jettask/webui/backend/queue_stats_v2.py +521 -0
  28. jettask/webui/backend/redis_monitor_api.py +476 -0
  29. jettask/webui/backend/unified_api_router.py +1601 -0
  30. jettask/webui/db_init.py +204 -32
  31. jettask/webui/frontend/package-lock.json +492 -1
  32. jettask/webui/frontend/package.json +4 -1
  33. jettask/webui/frontend/src/App.css +105 -7
  34. jettask/webui/frontend/src/App.jsx +49 -20
  35. jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
  36. jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
  37. jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
  38. jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
  39. jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
  40. jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
  41. jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
  42. jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
  43. jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
  44. jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
  45. jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
  46. jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
  47. jettask/webui/frontend/src/components/layout/Header.css +34 -10
  48. jettask/webui/frontend/src/components/layout/Header.jsx +31 -23
  49. jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
  50. jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
  51. jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
  52. jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
  53. jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
  54. jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
  55. jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
  56. jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
  57. jettask/webui/frontend/src/main.jsx +1 -0
  58. jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
  59. jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
  60. jettask/webui/frontend/src/pages/QueueDetail.jsx +1109 -10
  61. jettask/webui/frontend/src/pages/QueueMonitor.jsx +236 -115
  62. jettask/webui/frontend/src/pages/Queues.jsx +5 -1
  63. jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
  64. jettask/webui/frontend/src/pages/Settings.jsx +800 -0
  65. jettask/webui/frontend/src/services/api.js +7 -5
  66. jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
  67. jettask/webui/frontend/src/utils/userPreferences.js +154 -0
  68. jettask/webui/multi_namespace_consumer.py +543 -0
  69. jettask/webui/pg_consumer.py +983 -246
  70. jettask/webui/static/dist/assets/index-7129cfe1.css +1 -0
  71. jettask/webui/static/dist/assets/index-8d1935cc.js +774 -0
  72. jettask/webui/static/dist/index.html +2 -2
  73. jettask/webui/task_center.py +216 -0
  74. jettask/webui/task_center_client.py +150 -0
  75. jettask/webui/unified_consumer_manager.py +193 -0
  76. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/METADATA +1 -1
  77. jettask-0.2.4.dist-info/RECORD +134 -0
  78. jettask/webui/pg_consumer_slow.py +0 -1099
  79. jettask/webui/pg_consumer_test.py +0 -678
  80. jettask/webui/static/dist/assets/index-823408e8.css +0 -1
  81. jettask/webui/static/dist/assets/index-9968b0b8.js +0 -543
  82. jettask/webui/test_pg_consumer_recovery.py +0 -547
  83. jettask/webui/test_recovery_simple.py +0 -492
  84. jettask/webui/test_self_recovery.py +0 -467
  85. jettask-0.2.1.dist-info/RECORD +0 -91
  86. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/WHEEL +0 -0
  87. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/entry_points.txt +0 -0
  88. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/licenses/LICENSE +0 -0
  89. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/top_level.txt +0 -0
@@ -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
- """确保 Sorted Set 已初始化并保持一致"""
41
- current_time = time.time()
39
+ # async def ensure_initialized(self):
40
+ # """确保 Sorted Set 已初始化并保持一致"""
41
+ # current_time = time.time()
42
42
 
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
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
- """完整同步 Hash 和 Sorted Set"""
51
- try:
52
- logger.debug("Starting full synchronization of ACTIVE_WORKERS")
49
+ # async def _full_sync(self):
50
+ # """完整同步 Hash 和 Sorted Set"""
51
+ # try:
52
+ # logger.debug("Starting full synchronization of ACTIVE_WORKERS")
53
53
 
54
- # 1. 收集所有活跃 worker 的 Hash 数据
55
- pattern = f"{self.redis_prefix}:WORKER:*"
56
- hash_workers = {}
54
+ # # 1. 收集所有活跃 worker 的 Hash 数据
55
+ # pattern = f"{self.redis_prefix}:WORKER:*"
56
+ # hash_workers = {}
57
57
 
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)
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
- 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
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
- # 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}
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
- # 3. 计算差异
77
- hash_ids = set(hash_workers.keys())
78
- zset_ids = set(zset_dict.keys())
76
+ # # 3. 计算差异
77
+ # hash_ids = set(hash_workers.keys())
78
+ # zset_ids = set(zset_dict.keys())
79
79
 
80
- to_add = hash_ids - zset_ids # Hash有但ZSet无
81
- to_remove = zset_ids - hash_ids # ZSet有但Hash无
82
- to_update = {} # 时间戳不一致的
80
+ # to_add = hash_ids - zset_ids # Hash有但ZSet无
81
+ # to_remove = zset_ids - hash_ids # ZSet有但Hash无
82
+ # to_update = {} # 时间戳不一致的
83
83
 
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]
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
- # 4. 批量修复
89
- if to_add or to_remove or to_update:
90
- pipeline = self.async_redis.pipeline()
88
+ # # 4. 批量修复
89
+ # if to_add or to_remove or to_update:
90
+ # pipeline = self.async_redis.pipeline()
91
91
 
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)
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
- if to_remove:
97
- pipeline.zrem(self.active_workers_key, *to_remove)
96
+ # if to_remove:
97
+ # pipeline.zrem(self.active_workers_key, *to_remove)
98
98
 
99
- if to_update:
100
- pipeline.zadd(self.active_workers_key, to_update)
99
+ # if to_update:
100
+ # pipeline.zadd(self.active_workers_key, to_update)
101
101
 
102
- await pipeline.execute()
103
- logger.info(f"Full sync: +{len(to_add)}, -{len(to_remove)}, ~{len(to_update)}")
102
+ # await pipeline.execute()
103
+ # logger.info(f"Full sync: +{len(to_add)}, -{len(to_remove)}, ~{len(to_update)}")
104
104
 
105
- except Exception as e:
106
- logger.error(f"Full sync failed: {e}")
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
@@ -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
- if len(item) == 3:
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 event_id, updates in self.task_info_updates.items():
177
- key = f"{self.prefix}:TASK:{event_id}".encode() # 转为 bytes
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
- task_change_events.append(event_id)
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
- if task_change_events:
190
- change_stream_key = f"{self.prefix}:TASK_CHANGES".encode()
191
- for event_id in task_change_events:
192
- # 只发送事件ID
193
- change_data = {
194
- b'event_id': event_id.encode() if isinstance(event_id, str) else event_id
195
- }
196
- pipeline.xadd(change_stream_key, change_data, maxlen=10000) # 保留最近10000条变更
197
- operations_count += 1
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
- # 初始化所有变量,避免在 finally 块中未定义
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
- task_name = event_data.get("_task_name") or event_data.get("name", "unknown")
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=} {routing=} 未绑定任何task"
391
+ exception = f"{task_name=} {queue=} {event_data=} 未绑定任何task"
333
392
  logger.error(exception)
334
- await self._quick_ack(queue, event_id, group_name)
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[event_id] = {
343
- "status": "error",
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.get("_broadcast"):
371
- # 如果消息包含 event_type 和 customer_data,将它们作为参数传递
372
- if "event_type" in event_data and "customer_data" in event_data:
373
- # 将这些字段作为位置参数传递,其他字段作为kwargs
374
- args = (event_data["event_type"], event_data["customer_data"])
375
- # 保留其他字段在kwargs中,但排除已作为args的字段
376
- broadcast_kwargs = {k: v for k, v in event_data.items()
377
- if k not in ["event_type", "customer_data", "_broadcast", "_target_tasks", "_timestamp", "trigger_time", "name", "_task_name"]}
378
- kwargs_inner.update(broadcast_kwargs)
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[event_id] = {
393
- "status": "rejected",
460
+ self.task_info_updates[status_key] = {
461
+ "status": TaskStatus.REJECTED.value,
394
462
  "consumer": consumer,
395
- "started_at": str(execution_start_time),
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
- self.app.consumer_manager.task_started(queue)
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:{event_id}"
480
+ # running_key = f"{self.prefix}:TASK:{status_key}"
413
481
  # 保存开始信息,但不设置status为running,避免竞态条件
414
- self.task_info_updates[event_id] = {
415
- "status": "running", # 不在这里设置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": "running",
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
- task_result = task(event_id, event_data['trigger_time'], *args, **kwargs_inner)
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=kwargs_inner,
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
- await self._quick_ack(queue, event_id, group_name)
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
- await self._quick_ack(queue, event_id, group_name)
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
- await self._quick_ack(queue, event_id, group_name)
519
- break
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
- await self._quick_ack(queue, event_id, group_name)
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": str(execution_time),
553
- "duration": str(total_latency),
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 event_id in self.task_info_updates:
685
+ if status_key in self.task_info_updates:
573
686
  # 合并更新(保留started_at等之前的信息)
574
687
  # 重要:确保最终状态覆盖之前的running状态
575
- self.task_info_updates[event_id].update(task_info)
688
+ self.task_info_updates[status_key].update(task_info)
576
689
  else:
577
- self.task_info_updates[event_id] = task_info
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=kwargs_inner,
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