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,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']
|
jettask/utils/serializer.py
CHANGED
@@ -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
|
-
|
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
|
-
#
|
196
|
-
|
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.
|
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
|
|
@@ -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.
|
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__)
|