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.
Files changed (165) hide show
  1. jettask/__init__.py +60 -2
  2. jettask/cli.py +314 -228
  3. jettask/config/__init__.py +9 -1
  4. jettask/config/config.py +245 -0
  5. jettask/config/env_loader.py +381 -0
  6. jettask/config/lua_scripts.py +158 -0
  7. jettask/config/nacos_config.py +132 -5
  8. jettask/core/__init__.py +1 -1
  9. jettask/core/app.py +1573 -666
  10. jettask/core/app_importer.py +33 -16
  11. jettask/core/container.py +532 -0
  12. jettask/core/task.py +1 -4
  13. jettask/core/unified_manager_base.py +2 -2
  14. jettask/executor/__init__.py +38 -0
  15. jettask/executor/core.py +625 -0
  16. jettask/executor/executor.py +338 -0
  17. jettask/executor/orchestrator.py +290 -0
  18. jettask/executor/process_entry.py +638 -0
  19. jettask/executor/task_executor.py +317 -0
  20. jettask/messaging/__init__.py +68 -0
  21. jettask/messaging/event_pool.py +2188 -0
  22. jettask/messaging/reader.py +519 -0
  23. jettask/messaging/registry.py +266 -0
  24. jettask/messaging/scanner.py +369 -0
  25. jettask/messaging/sender.py +312 -0
  26. jettask/persistence/__init__.py +118 -0
  27. jettask/persistence/backlog_monitor.py +567 -0
  28. jettask/{backend/data_access.py → persistence/base.py} +58 -57
  29. jettask/persistence/consumer.py +315 -0
  30. jettask/{core → persistence}/db_manager.py +23 -22
  31. jettask/persistence/maintenance.py +81 -0
  32. jettask/persistence/message_consumer.py +259 -0
  33. jettask/{backend/namespace_data_access.py → persistence/namespace.py} +66 -98
  34. jettask/persistence/offline_recovery.py +196 -0
  35. jettask/persistence/queue_discovery.py +215 -0
  36. jettask/persistence/task_persistence.py +218 -0
  37. jettask/persistence/task_updater.py +583 -0
  38. jettask/scheduler/__init__.py +2 -2
  39. jettask/scheduler/loader.py +6 -5
  40. jettask/scheduler/run_scheduler.py +1 -1
  41. jettask/scheduler/scheduler.py +7 -7
  42. jettask/scheduler/{unified_scheduler_manager.py → scheduler_coordinator.py} +18 -13
  43. jettask/task/__init__.py +16 -0
  44. jettask/{router.py → task/router.py} +26 -8
  45. jettask/task/task_center/__init__.py +9 -0
  46. jettask/task/task_executor.py +318 -0
  47. jettask/task/task_registry.py +291 -0
  48. jettask/test_connection_monitor.py +73 -0
  49. jettask/utils/__init__.py +31 -1
  50. jettask/{monitor/run_backlog_collector.py → utils/backlog_collector.py} +1 -1
  51. jettask/utils/db_connector.py +1629 -0
  52. jettask/{db_init.py → utils/db_init.py} +1 -1
  53. jettask/utils/rate_limit/__init__.py +30 -0
  54. jettask/utils/rate_limit/concurrency_limiter.py +665 -0
  55. jettask/utils/rate_limit/config.py +145 -0
  56. jettask/utils/rate_limit/limiter.py +41 -0
  57. jettask/utils/rate_limit/manager.py +269 -0
  58. jettask/utils/rate_limit/qps_limiter.py +154 -0
  59. jettask/utils/rate_limit/task_limiter.py +384 -0
  60. jettask/utils/serializer.py +3 -0
  61. jettask/{monitor/stream_backlog_monitor.py → utils/stream_backlog.py} +14 -6
  62. jettask/utils/time_sync.py +173 -0
  63. jettask/webui/__init__.py +27 -0
  64. jettask/{api/v1 → webui/api}/alerts.py +1 -1
  65. jettask/{api/v1 → webui/api}/analytics.py +2 -2
  66. jettask/{api/v1 → webui/api}/namespaces.py +1 -1
  67. jettask/{api/v1 → webui/api}/overview.py +1 -1
  68. jettask/{api/v1 → webui/api}/queues.py +3 -3
  69. jettask/{api/v1 → webui/api}/scheduled.py +1 -1
  70. jettask/{api/v1 → webui/api}/settings.py +1 -1
  71. jettask/{api.py → webui/app.py} +253 -145
  72. jettask/webui/namespace_manager/__init__.py +10 -0
  73. jettask/{multi_namespace_consumer.py → webui/namespace_manager/multi.py} +69 -22
  74. jettask/{unified_consumer_manager.py → webui/namespace_manager/unified.py} +1 -1
  75. jettask/{run.py → webui/run.py} +2 -2
  76. jettask/{services → webui/services}/__init__.py +1 -3
  77. jettask/{services → webui/services}/overview_service.py +34 -16
  78. jettask/{services → webui/services}/queue_service.py +1 -1
  79. jettask/{backend → webui/services}/queue_stats_v2.py +1 -1
  80. jettask/{services → webui/services}/settings_service.py +1 -1
  81. jettask/worker/__init__.py +53 -0
  82. jettask/worker/lifecycle.py +1507 -0
  83. jettask/worker/manager.py +583 -0
  84. jettask/{core/offline_worker_recovery.py → worker/recovery.py} +268 -175
  85. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/METADATA +2 -71
  86. jettask-0.2.20.dist-info/RECORD +145 -0
  87. jettask/__main__.py +0 -140
  88. jettask/api/__init__.py +0 -103
  89. jettask/backend/__init__.py +0 -1
  90. jettask/backend/api/__init__.py +0 -3
  91. jettask/backend/api/v1/__init__.py +0 -17
  92. jettask/backend/api/v1/monitoring.py +0 -431
  93. jettask/backend/api/v1/namespaces.py +0 -504
  94. jettask/backend/api/v1/queues.py +0 -342
  95. jettask/backend/api/v1/tasks.py +0 -367
  96. jettask/backend/core/__init__.py +0 -3
  97. jettask/backend/core/cache.py +0 -221
  98. jettask/backend/core/database.py +0 -200
  99. jettask/backend/core/exceptions.py +0 -102
  100. jettask/backend/dependencies.py +0 -261
  101. jettask/backend/init_meta_db.py +0 -158
  102. jettask/backend/main.py +0 -1426
  103. jettask/backend/main_unified.py +0 -78
  104. jettask/backend/main_v2.py +0 -394
  105. jettask/backend/models/__init__.py +0 -3
  106. jettask/backend/models/requests.py +0 -236
  107. jettask/backend/models/responses.py +0 -230
  108. jettask/backend/namespace_api_old.py +0 -267
  109. jettask/backend/services/__init__.py +0 -3
  110. jettask/backend/start.py +0 -42
  111. jettask/backend/unified_api_router.py +0 -1541
  112. jettask/cleanup_deprecated_tables.sql +0 -16
  113. jettask/core/consumer_manager.py +0 -1695
  114. jettask/core/delay_scanner.py +0 -256
  115. jettask/core/event_pool.py +0 -1700
  116. jettask/core/heartbeat_process.py +0 -222
  117. jettask/core/task_batch.py +0 -153
  118. jettask/core/worker_scanner.py +0 -271
  119. jettask/executors/__init__.py +0 -5
  120. jettask/executors/asyncio.py +0 -876
  121. jettask/executors/base.py +0 -30
  122. jettask/executors/common.py +0 -148
  123. jettask/executors/multi_asyncio.py +0 -309
  124. jettask/gradio_app.py +0 -570
  125. jettask/integrated_gradio_app.py +0 -1088
  126. jettask/main.py +0 -0
  127. jettask/monitoring/__init__.py +0 -3
  128. jettask/pg_consumer.py +0 -1896
  129. jettask/run_monitor.py +0 -22
  130. jettask/run_webui.py +0 -148
  131. jettask/scheduler/multi_namespace_scheduler.py +0 -294
  132. jettask/scheduler/unified_manager.py +0 -450
  133. jettask/task_center_client.py +0 -150
  134. jettask/utils/serializer_optimized.py +0 -33
  135. jettask/webui_exceptions.py +0 -67
  136. jettask-0.2.18.dist-info/RECORD +0 -150
  137. /jettask/{constants.py → config/constants.py} +0 -0
  138. /jettask/{backend/config.py → config/task_center.py} +0 -0
  139. /jettask/{pg_consumer → messaging/pg_consumer}/pg_consumer_v2.py +0 -0
  140. /jettask/{pg_consumer → messaging/pg_consumer}/sql/add_execution_time_field.sql +0 -0
  141. /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_new_tables.sql +0 -0
  142. /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_tables_v3.sql +0 -0
  143. /jettask/{pg_consumer → messaging/pg_consumer}/sql/migrate_to_new_structure.sql +0 -0
  144. /jettask/{pg_consumer → messaging/pg_consumer}/sql/modify_time_fields.sql +0 -0
  145. /jettask/{pg_consumer → messaging/pg_consumer}/sql_utils.py +0 -0
  146. /jettask/{models.py → persistence/models.py} +0 -0
  147. /jettask/scheduler/{manager.py → task_crud.py} +0 -0
  148. /jettask/{schema.sql → schemas/schema.sql} +0 -0
  149. /jettask/{task_center.py → task/task_center/client.py} +0 -0
  150. /jettask/{monitoring → utils}/file_watcher.py +0 -0
  151. /jettask/{services/redis_monitor_service.py → utils/redis_monitor.py} +0 -0
  152. /jettask/{api/v1 → webui/api}/__init__.py +0 -0
  153. /jettask/{webui_config.py → webui/config.py} +0 -0
  154. /jettask/{webui_models → webui/models}/__init__.py +0 -0
  155. /jettask/{webui_models → webui/models}/namespace.py +0 -0
  156. /jettask/{services → webui/services}/alert_service.py +0 -0
  157. /jettask/{services → webui/services}/analytics_service.py +0 -0
  158. /jettask/{services → webui/services}/scheduled_task_service.py +0 -0
  159. /jettask/{services → webui/services}/task_service.py +0 -0
  160. /jettask/{webui_sql → webui/sql}/batch_upsert_functions.sql +0 -0
  161. /jettask/{webui_sql → webui/sql}/verify_database.sql +0 -0
  162. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/WHEEL +0 -0
  163. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/entry_points.txt +0 -0
  164. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/licenses/LICENSE +0 -0
  165. {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,384 @@
1
+ """
2
+ 任务级限流器
3
+
4
+ 组合QPS和并发限流,提供统一的任务级限流接口
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import time
10
+ from redis.asyncio import Redis
11
+ from typing import Dict, Optional, List, Tuple, Any
12
+
13
+ from .qps_limiter import QPSRateLimiter
14
+ from .concurrency_limiter import ConcurrencyRateLimiter
15
+ from .config import RateLimitConfig, QPSLimit, ConcurrencyLimit, parse_rate_limit_config
16
+
17
+ logger = logging.getLogger('app')
18
+
19
+
20
+ class TaskRateLimiter:
21
+ """任务级别的限流器
22
+
23
+ 根据配置类型自动选择合适的限流器实现:
24
+ - QPSLimit: 使用本地滑动窗口 + Redis 协调配额
25
+ - ConcurrencyLimit: 使用 Redis 分布式信号量
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ redis_client: Redis,
31
+ task_name: str,
32
+ worker_id: str,
33
+ config: RateLimitConfig,
34
+ redis_prefix: str = "jettask",
35
+ sync_interval: float = 5.0,
36
+ registry_manager = None,
37
+ worker_state_manager = None
38
+ ):
39
+ """初始化任务限流器
40
+
41
+ Args:
42
+ redis_client: 异步 Redis 客户端
43
+ task_name: 任务名称
44
+ worker_id: 当前 worker ID
45
+ config: 限流配置(QPSLimit 或 ConcurrencyLimit)
46
+ redis_prefix: Redis key 前缀
47
+ sync_interval: 配额同步间隔(秒),仅用于 QPS 限流
48
+ registry_manager: RegistryManager 实例
49
+ worker_state_manager: WorkerStateManager 实例
50
+ """
51
+ self.redis = redis_client
52
+ self.task_name = task_name
53
+ self.worker_id = worker_id
54
+ self.redis_prefix = redis_prefix
55
+ self.global_config = config
56
+ self.sync_interval = sync_interval
57
+ self.registry_manager = registry_manager
58
+ self.worker_state_manager = worker_state_manager
59
+
60
+ # Redis key 定义
61
+ self.rate_limit_key = f"{redis_prefix}:RATE_LIMIT:CONFIG:{task_name}"
62
+
63
+ # 根据配置类型创建具体的限流器
64
+ if isinstance(config, QPSLimit):
65
+ self.limiter = QPSRateLimiter(
66
+ qps=config.qps,
67
+ window_size=config.window_size,
68
+ worker_id=worker_id
69
+ )
70
+ self.mode = 'qps'
71
+ # QPS 模式需要配额同步
72
+ self._sync_task: Optional[asyncio.Task] = None
73
+ self._running = False
74
+ elif isinstance(config, ConcurrencyLimit):
75
+ self.limiter = ConcurrencyRateLimiter(
76
+ redis_client=redis_client,
77
+ task_name=task_name,
78
+ worker_id=worker_id,
79
+ max_concurrency=config.max_concurrency,
80
+ redis_prefix=redis_prefix
81
+ )
82
+ self.mode = 'concurrency'
83
+ # 并发模式不需要配额同步(分布式信号量自动协调)
84
+ self._sync_task = None
85
+ self._running = False
86
+ else:
87
+ raise ValueError(f"Unsupported config type: {type(config)}")
88
+
89
+ # 统计信息
90
+ self.stats = {
91
+ 'syncs': 0,
92
+ 'last_sync_time': 0,
93
+ 'current_worker_count': 0,
94
+ }
95
+
96
+ async def start(self):
97
+ """启动限流器"""
98
+ if self._running:
99
+ logger.warning(f"TaskRateLimiter for {self.task_name} already running")
100
+ return
101
+
102
+ logger.debug(
103
+ f"Starting TaskRateLimiter for task={self.task_name}, "
104
+ f"config={self.global_config}, mode={self.mode}"
105
+ )
106
+
107
+ # 初始化 Redis 配置
108
+ await self._initialize_redis()
109
+
110
+ # 只有 QPS 模式需要启动配额同步
111
+ if self.mode == 'qps':
112
+ self._running = True
113
+ self._sync_task = asyncio.create_task(self._sync_loop())
114
+
115
+ # 注册 worker 状态变更回调
116
+ if self.worker_state_manager:
117
+ self.worker_state_manager.register_callback(self._on_worker_state_change)
118
+ logger.debug(f"Registered worker state change listener for {self.task_name}")
119
+
120
+ logger.debug(f"TaskRateLimiter started for {self.task_name}")
121
+ def __delattr__(self, name):
122
+ self.stop()
123
+
124
+ async def stop(self):
125
+ """停止限流器"""
126
+
127
+ print(f'准备退出限流器 {self.task_name} {self.mode}')
128
+ if not self._running and self.mode == 'qps':
129
+ return
130
+
131
+ logger.debug(f"Stopping TaskRateLimiter for {self.task_name}")
132
+
133
+ if self.mode == 'qps':
134
+ self._running = False
135
+
136
+ # 注销状态变更回调
137
+ if self.worker_state_manager:
138
+ self.worker_state_manager.unregister_callback(self._on_worker_state_change)
139
+
140
+ # 取消后台任务
141
+ if self._sync_task:
142
+ self._sync_task.cancel()
143
+ try:
144
+ await self._sync_task
145
+ except asyncio.CancelledError:
146
+ pass
147
+ elif self.mode == 'concurrency':
148
+ # 清理并发限流器(释放所有持有的锁)
149
+ if hasattr(self.limiter, 'stop'):
150
+ try:
151
+ await self.limiter.stop()
152
+ logger.debug(f"Concurrency limiter stopped and locks released for {self.task_name}")
153
+ except Exception as e:
154
+ logger.error(f"Error stopping concurrency limiter for {self.task_name}: {e}")
155
+
156
+ logger.debug(f"TaskRateLimiter stopped for {self.task_name}")
157
+
158
+ async def _initialize_redis(self):
159
+ """初始化 Redis 配置"""
160
+ try:
161
+ exists = await self.redis.exists(self.rate_limit_key)
162
+ if not exists:
163
+ config_dict = self.global_config.to_dict()
164
+ await self.redis.hset(self.rate_limit_key, mapping=config_dict)
165
+ logger.debug(
166
+ f"Initialized rate limit config for {self.task_name}: {self.global_config}"
167
+ )
168
+
169
+ # 将任务名添加到索引集合中(用于避免 scan 操作)
170
+ index_key = f"{self.redis_prefix}:RATE_LIMIT:INDEX"
171
+ await self.redis.sadd(index_key, self.task_name)
172
+ logger.debug(f"Added {self.task_name} to rate limit index")
173
+
174
+ except Exception as e:
175
+ logger.error(f"Failed to initialize Redis: {e}")
176
+
177
+ async def _on_worker_state_change(self, state_data: dict):
178
+ """Worker 状态变更回调(仅用于 QPS 模式)"""
179
+ if self.mode == 'qps':
180
+ logger.debug(f"[{self.task_name}] Worker state changed: {state_data}")
181
+ await self._sync_quota()
182
+
183
+ async def _sync_quota(self):
184
+ """同步配额(仅用于 QPS 模式)"""
185
+ if self.mode != 'qps':
186
+ return
187
+
188
+ try:
189
+ # 1. 从 Redis 读取全局配置
190
+ config_dict = await self.redis.hgetall(self.rate_limit_key)
191
+ if not config_dict:
192
+ logger.warning(f"No config found in Redis for {self.task_name}")
193
+ return
194
+
195
+ # 转换 bytes 为 str
196
+ config_dict = {
197
+ k.decode() if isinstance(k, bytes) else k:
198
+ v.decode() if isinstance(v, bytes) else v
199
+ for k, v in config_dict.items()
200
+ }
201
+
202
+ # 解析配置
203
+ global_config = parse_rate_limit_config(config_dict)
204
+ if not isinstance(global_config, QPSLimit):
205
+ logger.error(f"Invalid QPS config for {self.task_name}: {config_dict}")
206
+ return
207
+
208
+ # 2. 获取当前活跃的 workers
209
+ worker_ids = [self.worker_id]
210
+ if self.registry_manager and hasattr(self.registry_manager, 'get_workers_for_task'):
211
+ try:
212
+ workers = await self.registry_manager.get_workers_for_task(self.task_name, only_alive=True)
213
+ if workers:
214
+ worker_ids = sorted(list(workers))
215
+ except Exception as e:
216
+ logger.debug(f"Failed to get workers from registry: {e}")
217
+ # 降级到只使用当前 worker
218
+
219
+ worker_count = len(worker_ids)
220
+
221
+ # 3. 协商配额分配
222
+ await self._allocate_quotas(global_config.qps, worker_ids)
223
+
224
+ # 4. 读取当前 worker 的配额
225
+ quota_key = f"quota:{self.worker_id}"
226
+ quota_str = await self.redis.hget(self.rate_limit_key, quota_key)
227
+
228
+ if quota_str:
229
+ quota_value = int(quota_str)
230
+ else:
231
+ # 降级方案:平均分配
232
+ quota_value = global_config.qps // worker_count if worker_count > 0 else global_config.qps
233
+
234
+ # 5. 更新本地限流器的配额
235
+ await self.limiter.update_limit(quota_value)
236
+
237
+ # 6. 更新统计信息
238
+ self.stats['syncs'] += 1
239
+ self.stats['last_sync_time'] = time.time()
240
+ self.stats['current_worker_count'] = worker_count
241
+
242
+ except Exception as e:
243
+ logger.error(f"Quota sync error: {e}")
244
+
245
+ async def _sync_loop(self):
246
+ """后台同步循环(仅用于 QPS 模式)"""
247
+ while self._running:
248
+ try:
249
+ await self._sync_quota()
250
+ await asyncio.sleep(self.sync_interval)
251
+ except asyncio.CancelledError:
252
+ break
253
+ except Exception as e:
254
+ logger.error(f"Sync loop error: {e}")
255
+ await asyncio.sleep(1)
256
+
257
+ async def _allocate_quotas(self, total_qps: int, worker_ids: list):
258
+ """协商配额分配(仅用于 QPS 模式)"""
259
+ lock_key = f"{self.rate_limit_key}:lock"
260
+
261
+ try:
262
+ async with self.redis.lock(lock_key, timeout=3, blocking_timeout=2):
263
+ worker_count = len(worker_ids)
264
+ base_quota = total_qps // worker_count
265
+ remainder = total_qps % worker_count
266
+
267
+ # 分配配额
268
+ allocations = {}
269
+ for i, worker_id in enumerate(worker_ids):
270
+ quota = base_quota + (1 if i < remainder else 0)
271
+ allocations[f"quota:{worker_id}"] = quota
272
+
273
+ # 批量写入 Redis
274
+ if allocations:
275
+ await self.redis.hset(self.rate_limit_key, mapping=allocations)
276
+
277
+ # 清理不活跃 workers 的配额
278
+ await self._cleanup_stale_quotas(worker_ids)
279
+
280
+ logger.debug(f"[ALLOCATE] Allocated quotas for {worker_count} workers: {allocations}")
281
+
282
+ except asyncio.TimeoutError:
283
+ logger.debug(f"[ALLOCATE] Failed to acquire lock, skipping allocation")
284
+ except Exception as e:
285
+ logger.error(f"[ALLOCATE] Error allocating quotas: {e}")
286
+
287
+ async def _cleanup_stale_quotas(self, active_worker_ids: list):
288
+ """清理不活跃 workers 的配额"""
289
+ try:
290
+ all_fields = await self.redis.hkeys(self.rate_limit_key)
291
+ active_quota_keys = {f"quota:{wid}" for wid in active_worker_ids}
292
+
293
+ to_delete = []
294
+ for field in all_fields:
295
+ if isinstance(field, bytes):
296
+ field = field.decode('utf-8')
297
+
298
+ if field.startswith("quota:") and field not in active_quota_keys:
299
+ to_delete.append(field)
300
+
301
+ if to_delete:
302
+ await self.redis.hdel(self.rate_limit_key, *to_delete)
303
+ logger.debug(f"[CLEANUP] Removed {len(to_delete)} stale quotas")
304
+
305
+ except Exception as e:
306
+ logger.error(f"[CLEANUP] Error cleaning up quotas: {e}")
307
+
308
+ async def acquire(self, timeout: float = 10.0) -> Optional[str]:
309
+ """获取执行许可
310
+
311
+ Returns:
312
+ 成功返回 task_id (ConcurrencyLimit) 或 True (QPSLimit),失败返回 None
313
+ """
314
+ result = await self.limiter.acquire(timeout=timeout)
315
+ # QPSLimit 返回 bool,ConcurrencyLimit 返回 task_id 或 None
316
+ if self.mode == 'qps':
317
+ return True if result else None
318
+ else:
319
+ return result # ConcurrencyLimit 直接返回 task_id 或 None
320
+
321
+ async def release(self, task_id: Optional[str] = None):
322
+ """释放执行许可
323
+
324
+ Args:
325
+ task_id: 任务ID (仅 ConcurrencyLimit 需要)
326
+ """
327
+ if self.mode == 'concurrency' and task_id is None:
328
+ raise ValueError("ConcurrencyLimit requires task_id for release")
329
+
330
+ if self.mode == 'concurrency':
331
+ await self.limiter.release(task_id)
332
+ else:
333
+ # QPSLimit 不需要 release(本地计数)
334
+ pass
335
+
336
+ async def try_acquire(self) -> bool:
337
+ """尝试获取执行许可(非阻塞)"""
338
+ return await self.limiter.try_acquire()
339
+
340
+ def get_stats(self) -> dict:
341
+ """获取统计信息"""
342
+ return {
343
+ **self.stats,
344
+ 'task_name': self.task_name,
345
+ 'worker_id': self.worker_id,
346
+ 'config': str(self.global_config),
347
+ }
348
+
349
+ async def __aenter__(self):
350
+ """异步上下文管理器入口 - 获取执行许可
351
+
352
+ 使用示例:
353
+ async with rate_limiter:
354
+ # 执行任务
355
+ await do_something()
356
+
357
+ Returns:
358
+ self: 限流器实例
359
+ """
360
+ task_id = await self.acquire()
361
+ if task_id is None:
362
+ raise TimeoutError("Failed to acquire rate limit slot")
363
+ # 保存task_id供__aexit__使用
364
+ self._current_task_id = task_id
365
+ return self
366
+
367
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
368
+ """异步上下文管理器退出 - 自动释放许可"""
369
+ if hasattr(self, '_current_task_id'):
370
+ task_id = self._current_task_id
371
+ delattr(self, '_current_task_id')
372
+ if self.mode == 'concurrency':
373
+ await self.release(task_id)
374
+ # QPSLimit 不需要显式 release
375
+ return False # 不抑制异常
376
+
377
+
378
+ # ============================================================
379
+ # 限流器管理器
380
+ # ============================================================
381
+
382
+
383
+
384
+ __all__ = ['TaskRateLimiter']
@@ -12,6 +12,9 @@ def dumps(obj):
12
12
 
13
13
  def loads(data):
14
14
  """反序列化字节为对象"""
15
+ if isinstance(data, str):
16
+ # 兼容性:如果收到字符串,尝试作为 latin-1 解码
17
+ data = data.encode('latin-1')
15
18
  return msgpack.unpackb(data, raw=False, strict_map_key=False)
16
19
 
17
20
 
@@ -37,10 +37,15 @@ class StreamBacklogMonitor:
37
37
  self.AsyncSessionLocal = None
38
38
 
39
39
  async def initialize(self):
40
- """初始化连接"""
40
+ """初始化连接(使用统一的连接池管理)"""
41
41
  # 初始化Redis连接
42
- self.redis_client = await redis.from_url(self.redis_url, decode_responses=True)
43
-
42
+ from jettask.utils.db_connector import get_async_redis_client
43
+
44
+ self.redis_client = get_async_redis_client(
45
+ redis_url=self.redis_url,
46
+ decode_responses=True
47
+ )
48
+
44
49
  # 初始化PostgreSQL连接
45
50
  self.engine = create_async_engine(self.pg_url, echo=False)
46
51
  self.AsyncSessionLocal = sessionmaker(self.engine, class_=AsyncSession, expire_on_commit=False)
@@ -190,10 +195,13 @@ class StreamBacklogMonitor:
190
195
  for group in groups:
191
196
  group_name = group['name']
192
197
  pending_count = group['pending'] # Redis Stream中的pending数量(已投递未ACK)
193
-
198
+
194
199
  # 从TASK_OFFSETS获取该组的消费offset
195
- # key格式: f"{queue}:{group_name}"
196
- task_offset_key = f"{stream_name}:{group_name}"
200
+ # group_name 中提取 task_name(最后一段)
201
+ task_name = group_name.split(':')[-1]
202
+ # 构建 field:队列名(含优先级)+ 任务名
203
+ # 例如:robust_bench2:8:benchmark_task
204
+ task_offset_key = f"{stream_name}:{task_name}"
197
205
  last_acked_offset = int(task_offsets.get(task_offset_key, 0))
198
206
  print(f'{task_offset_key=} {last_acked_offset=}')
199
207
  # 计算各种积压指标
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python
2
+ """时间同步模块
3
+
4
+ 提供与 Redis 服务器时间同步的功能,避免多个 worker 因系统时间不一致导致的限流问题。
5
+ 在启动时一次性校准与 Redis 服务器的时间差,后续使用本地时间加上时间差来获得标准时间。
6
+ """
7
+
8
+ import time
9
+ import logging
10
+ from typing import Optional
11
+ import redis.asyncio as aioredis
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class TimeSync:
17
+ """时间同步类
18
+
19
+ 在初始化时与 Redis 服务器时间进行一次校准,计算时间差。
20
+ 后续所有时间获取都使用 本地时间 + 时间差 的方式,避免频繁访问 Redis。
21
+
22
+ 使用方式:
23
+ # 初始化(异步)
24
+ time_sync = TimeSync()
25
+ await time_sync.sync(redis_client)
26
+
27
+ # 获取标准时间
28
+ now = time_sync.time()
29
+ """
30
+
31
+ def __init__(self):
32
+ """初始化时间同步器"""
33
+ self._offset = 0.0 # Redis 服务器时间与本地时间的差值(秒)
34
+ self._synced = False # 是否已同步
35
+ self._sync_timestamp = 0.0 # 同步时的本地时间戳
36
+
37
+ async def sync(self, redis_client: aioredis.Redis, retry: int = 3) -> bool:
38
+ """与 Redis 服务器时间同步
39
+
40
+ Args:
41
+ redis_client: 异步 Redis 客户端
42
+ retry: 重试次数
43
+
44
+ Returns:
45
+ 同步是否成功
46
+ """
47
+ for attempt in range(retry):
48
+ try:
49
+ # 记录请求前的本地时间
50
+ local_before = time.time()
51
+
52
+ # 获取 Redis 服务器时间(微秒级)
53
+ # TIME 命令返回 [seconds, microseconds]
54
+ redis_time = await redis_client.time()
55
+
56
+ # 记录请求后的本地时间
57
+ local_after = time.time()
58
+
59
+ # 计算网络往返时间的一半(估算单程延迟)
60
+ network_delay = (local_after - local_before) / 2.0
61
+
62
+ # Redis 时间转换为秒(包含微秒)
63
+ redis_timestamp = float(redis_time[0]) + float(redis_time[1]) / 1_000_000.0
64
+
65
+ # 估算 Redis 服务器在请求时刻的时间
66
+ # 使用请求前时间 + 单程延迟来估算
67
+ estimated_local_time = local_before + network_delay
68
+
69
+ # 计算时间差:Redis 服务器时间 - 本地时间
70
+ self._offset = redis_timestamp - estimated_local_time
71
+ self._sync_timestamp = time.time()
72
+ self._synced = True
73
+
74
+ logger.debug(
75
+ f"时间同步成功: offset={self._offset:.6f}s, "
76
+ f"network_delay={network_delay*1000:.2f}ms, "
77
+ f"redis_time={redis_timestamp:.6f}, "
78
+ f"local_time={estimated_local_time:.6f}"
79
+ )
80
+
81
+ # 如果时间差超过 1 秒,给出警告
82
+ if abs(self._offset) > 1.0:
83
+ logger.warning(
84
+ f"本地时间与 Redis 服务器时间差异较大: {self._offset:.2f}s,"
85
+ f"建议检查系统时间配置"
86
+ )
87
+
88
+ return True
89
+
90
+ except Exception as e:
91
+ logger.error(f"时间同步失败 (attempt {attempt + 1}/{retry}): {e}")
92
+ if attempt < retry - 1:
93
+ await asyncio.sleep(0.1)
94
+
95
+ logger.error("时间同步失败,使用本地时间")
96
+ self._synced = False
97
+ return False
98
+
99
+ def time(self) -> float:
100
+ """获取标准时间
101
+
102
+ Returns:
103
+ 标准时间戳(秒)
104
+ """
105
+ if not self._synced:
106
+ # 未同步时使用本地时间
107
+ return time.time()
108
+
109
+ # 使用本地时间 + 时间差
110
+ return time.time() + self._offset
111
+
112
+ def is_synced(self) -> bool:
113
+ """检查是否已同步
114
+
115
+ Returns:
116
+ 是否已同步
117
+ """
118
+ return self._synced
119
+
120
+ def get_offset(self) -> float:
121
+ """获取时间偏移量
122
+
123
+ Returns:
124
+ 时间偏移量(秒)
125
+ """
126
+ return self._offset
127
+
128
+ def get_sync_info(self) -> dict:
129
+ """获取同步信息
130
+
131
+ Returns:
132
+ 同步信息字典
133
+ """
134
+ return {
135
+ 'synced': self._synced,
136
+ 'offset': self._offset,
137
+ 'sync_timestamp': self._sync_timestamp,
138
+ 'time_since_sync': time.time() - self._sync_timestamp if self._synced else None,
139
+ }
140
+
141
+
142
+ # 全局时间同步实例(单例模式)
143
+ _global_time_sync: Optional[TimeSync] = None
144
+
145
+
146
+ def get_time_sync() -> TimeSync:
147
+ """获取全局时间同步实例
148
+
149
+ Returns:
150
+ TimeSync 实例
151
+ """
152
+ global _global_time_sync
153
+ if _global_time_sync is None:
154
+ _global_time_sync = TimeSync()
155
+ return _global_time_sync
156
+
157
+
158
+ async def init_time_sync(redis_client: aioredis.Redis) -> TimeSync:
159
+ """初始化全局时间同步
160
+
161
+ Args:
162
+ redis_client: 异步 Redis 客户端
163
+
164
+ Returns:
165
+ TimeSync 实例
166
+ """
167
+ time_sync = get_time_sync()
168
+ await time_sync.sync(redis_client)
169
+ return time_sync
170
+
171
+
172
+ # 添加缺失的 asyncio 导入
173
+ import asyncio
@@ -0,0 +1,27 @@
1
+ """
2
+ Web UI 模块
3
+
4
+ 提供 Web 界面和 API 接口。
5
+ """
6
+
7
+ # 不在 __init__ 中导入 app,避免循环导入
8
+ # 使用时直接: from jettask.webui.app import app
9
+ from .config import *
10
+
11
+ # 异常类直接从主模块导入(webui/exceptions.py已废弃并删除)
12
+ from jettask.exceptions import (
13
+ JetTaskException,
14
+ TaskTimeoutError,
15
+ TaskExecutionError,
16
+ TaskNotFoundError,
17
+ RetryableError
18
+ )
19
+
20
+ __all__ = [
21
+ # 'app', # 移除,避免循环导入
22
+ 'JetTaskException',
23
+ 'TaskTimeoutError',
24
+ 'TaskExecutionError',
25
+ 'TaskNotFoundError',
26
+ 'RetryableError',
27
+ ]
@@ -7,7 +7,7 @@ from typing import Optional
7
7
  import logging
8
8
 
9
9
  from jettask.schemas import AlertRuleRequest
10
- from jettask.services.alert_service import AlertService
10
+ from jettask.webui.services.alert_service import AlertService
11
11
 
12
12
  router = APIRouter(prefix="/alerts", tags=["alerts"])
13
13
  logger = logging.getLogger(__name__)
@@ -8,8 +8,8 @@ from datetime import datetime
8
8
  import logging
9
9
 
10
10
  from jettask.schemas import TimeRangeQuery
11
- from jettask.services.analytics_service import AnalyticsService
12
- from jettask.backend.namespace_data_access import get_namespace_data_access
11
+ from jettask.webui.services.analytics_service import AnalyticsService
12
+ from jettask.persistence.namespace import get_namespace_data_access
13
13
 
14
14
  router = APIRouter(prefix="/analytics", tags=["analytics"])
15
15
  logger = logging.getLogger(__name__)
@@ -8,7 +8,7 @@ import logging
8
8
  import traceback
9
9
 
10
10
  from jettask.schemas import NamespaceCreate, NamespaceUpdate, NamespaceResponse
11
- from jettask.services.settings_service import SettingsService
11
+ from jettask.webui.services.settings_service import SettingsService
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
@@ -8,7 +8,7 @@ import logging
8
8
  import traceback
9
9
 
10
10
  from jettask.schemas import TimeRangeQuery
11
- from jettask.services.overview_service import OverviewService
11
+ from jettask.webui.services.overview_service import OverviewService
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
@@ -14,9 +14,9 @@ from jettask.schemas import (
14
14
  TaskActionRequest,
15
15
  BacklogTrendRequest
16
16
  )
17
- from jettask.services.queue_service import QueueService
18
- from jettask.services.task_service import TaskService
19
- from jettask.services.redis_monitor_service import RedisMonitorService
17
+ from jettask.webui.services.queue_service import QueueService
18
+ from jettask.webui.services.task_service import TaskService
19
+ from jettask.utils.redis_monitor import RedisMonitorService
20
20
 
21
21
  router = APIRouter(prefix="/queues", tags=["queues"])
22
22
  logger = logging.getLogger(__name__)