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,145 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
"""限流配置类
|
3
|
+
|
4
|
+
定义各种限流策略的配置类,通过不同的类来区分限流模式。
|
5
|
+
"""
|
6
|
+
|
7
|
+
from abc import ABC, abstractmethod
|
8
|
+
from typing import Optional
|
9
|
+
|
10
|
+
|
11
|
+
class RateLimitConfig(ABC):
|
12
|
+
"""限流配置基类"""
|
13
|
+
|
14
|
+
@abstractmethod
|
15
|
+
def to_dict(self) -> dict:
|
16
|
+
"""转换为字典格式,用于存储到 Redis"""
|
17
|
+
pass
|
18
|
+
|
19
|
+
@abstractmethod
|
20
|
+
def get_type(self) -> str:
|
21
|
+
"""获取限流类型标识"""
|
22
|
+
pass
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
@abstractmethod
|
26
|
+
def from_dict(cls, data: dict) -> 'RateLimitConfig':
|
27
|
+
"""从字典创建配置实例"""
|
28
|
+
pass
|
29
|
+
|
30
|
+
|
31
|
+
class QPSLimit(RateLimitConfig):
|
32
|
+
"""QPS 限流配置
|
33
|
+
|
34
|
+
控制每秒最多执行多少个任务(使用滑动窗口算法)
|
35
|
+
|
36
|
+
Args:
|
37
|
+
qps: 每秒允许的最大请求数(所有 workers 总和)
|
38
|
+
window_size: 滑动窗口大小(秒),默认 1.0
|
39
|
+
|
40
|
+
Example:
|
41
|
+
@app.task(rate_limit=QPSLimit(qps=100))
|
42
|
+
async def my_task():
|
43
|
+
pass
|
44
|
+
"""
|
45
|
+
|
46
|
+
def __init__(self, qps: int, window_size: float = 1.0):
|
47
|
+
# 转换为正确的类型(处理从 Redis 读取的字符串)
|
48
|
+
qps = int(qps)
|
49
|
+
window_size = float(window_size)
|
50
|
+
|
51
|
+
if qps <= 0:
|
52
|
+
raise ValueError(f"qps must be positive, got {qps}")
|
53
|
+
if window_size <= 0:
|
54
|
+
raise ValueError(f"window_size must be positive, got {window_size}")
|
55
|
+
|
56
|
+
self.qps = qps
|
57
|
+
self.window_size = window_size
|
58
|
+
|
59
|
+
def to_dict(self) -> dict:
|
60
|
+
"""转换为字典格式"""
|
61
|
+
return {
|
62
|
+
'type': 'qps',
|
63
|
+
'qps': self.qps,
|
64
|
+
'window_size': self.window_size,
|
65
|
+
}
|
66
|
+
|
67
|
+
def get_type(self) -> str:
|
68
|
+
"""获取限流类型标识"""
|
69
|
+
return 'qps'
|
70
|
+
|
71
|
+
@classmethod
|
72
|
+
def from_dict(cls, data: dict) -> 'QPSLimit':
|
73
|
+
"""从字典创建配置实例"""
|
74
|
+
return cls(
|
75
|
+
qps=data['qps'],
|
76
|
+
window_size=data.get('window_size', 1.0)
|
77
|
+
)
|
78
|
+
|
79
|
+
def __repr__(self):
|
80
|
+
return f"QPSLimit(qps={self.qps}, window_size={self.window_size})"
|
81
|
+
|
82
|
+
|
83
|
+
class ConcurrencyLimit(RateLimitConfig):
|
84
|
+
"""并发限流配置
|
85
|
+
|
86
|
+
控制同一时刻最多可以运行多少个任务
|
87
|
+
|
88
|
+
Args:
|
89
|
+
max_concurrency: 最大并发数(所有 workers 总和)
|
90
|
+
|
91
|
+
Example:
|
92
|
+
@app.task(rate_limit=ConcurrencyLimit(max_concurrency=10))
|
93
|
+
async def my_task():
|
94
|
+
pass
|
95
|
+
"""
|
96
|
+
|
97
|
+
def __init__(self, max_concurrency: int):
|
98
|
+
# 转换为正确的类型(处理从 Redis 读取的字符串)
|
99
|
+
max_concurrency = int(max_concurrency)
|
100
|
+
|
101
|
+
if max_concurrency <= 0:
|
102
|
+
raise ValueError(f"max_concurrency must be positive, got {max_concurrency}")
|
103
|
+
|
104
|
+
self.max_concurrency = max_concurrency
|
105
|
+
|
106
|
+
def to_dict(self) -> dict:
|
107
|
+
"""转换为字典格式"""
|
108
|
+
return {
|
109
|
+
'type': 'concurrency',
|
110
|
+
'max_concurrency': self.max_concurrency,
|
111
|
+
}
|
112
|
+
|
113
|
+
def get_type(self) -> str:
|
114
|
+
"""获取限流类型标识"""
|
115
|
+
return 'concurrency'
|
116
|
+
|
117
|
+
@classmethod
|
118
|
+
def from_dict(cls, data: dict) -> 'ConcurrencyLimit':
|
119
|
+
"""从字典创建配置实例"""
|
120
|
+
return cls(max_concurrency=data['max_concurrency'])
|
121
|
+
|
122
|
+
def __repr__(self):
|
123
|
+
return f"ConcurrencyLimit(max_concurrency={self.max_concurrency})"
|
124
|
+
|
125
|
+
|
126
|
+
def parse_rate_limit_config(data: dict) -> Optional[RateLimitConfig]:
|
127
|
+
"""从字典解析限流配置
|
128
|
+
|
129
|
+
Args:
|
130
|
+
data: 限流配置字典
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
RateLimitConfig 实例,如果解析失败则返回 None
|
134
|
+
"""
|
135
|
+
if not data or not isinstance(data, dict):
|
136
|
+
return None
|
137
|
+
|
138
|
+
limit_type = data.get('type')
|
139
|
+
|
140
|
+
if limit_type == 'qps':
|
141
|
+
return QPSLimit.from_dict(data)
|
142
|
+
elif limit_type == 'concurrency':
|
143
|
+
return ConcurrencyLimit.from_dict(data)
|
144
|
+
else:
|
145
|
+
return None
|
@@ -0,0 +1,41 @@
|
|
1
|
+
"""
|
2
|
+
限流器 - 兼容性导入
|
3
|
+
|
4
|
+
⚠️ 此文件保持向后兼容,将导入重定向到新的模块化结构。
|
5
|
+
原代码已被拆分到以下模块:
|
6
|
+
- jettask.rate_limit.qps_limiter: QPSRateLimiter 类
|
7
|
+
- jettask.rate_limit.concurrency_limiter: ConcurrencyRateLimiter 类
|
8
|
+
- jettask.rate_limit.task_limiter: TaskRateLimiter 类
|
9
|
+
- jettask.rate_limit.manager: RateLimiterManager 类
|
10
|
+
|
11
|
+
新的模块结构:
|
12
|
+
- qps_limiter.py: QPS限流器
|
13
|
+
- concurrency_limiter.py: 并发限流器
|
14
|
+
- task_limiter.py: 任务级限流器
|
15
|
+
- manager.py: 限流器管理器
|
16
|
+
|
17
|
+
使用示例:
|
18
|
+
# 旧的导入方式(仍然可用)
|
19
|
+
from jettask.rate_limit.limiter import RateLimiterManager, QPSRateLimiter
|
20
|
+
|
21
|
+
# 新的导入方式(推荐)
|
22
|
+
from jettask.rate_limit import RateLimiterManager, QPSRateLimiter
|
23
|
+
"""
|
24
|
+
|
25
|
+
# 导入新模块的所有公开接口
|
26
|
+
from .config import RateLimitConfig, QPSLimit, ConcurrencyLimit
|
27
|
+
from .qps_limiter import QPSRateLimiter
|
28
|
+
from .concurrency_limiter import ConcurrencyRateLimiter
|
29
|
+
from .task_limiter import TaskRateLimiter
|
30
|
+
from .manager import RateLimiterManager
|
31
|
+
|
32
|
+
# 保持向后兼容
|
33
|
+
__all__ = [
|
34
|
+
'RateLimitConfig',
|
35
|
+
'QPSLimit',
|
36
|
+
'ConcurrencyLimit',
|
37
|
+
'QPSRateLimiter',
|
38
|
+
'ConcurrencyRateLimiter',
|
39
|
+
'TaskRateLimiter',
|
40
|
+
'RateLimiterManager',
|
41
|
+
]
|
@@ -0,0 +1,269 @@
|
|
1
|
+
"""
|
2
|
+
限流器管理器
|
3
|
+
|
4
|
+
统一管理所有任务的限流器实例
|
5
|
+
职责:
|
6
|
+
1. 限流器生命周期管理
|
7
|
+
2. 配置动态更新
|
8
|
+
3. 统一的限流接口
|
9
|
+
"""
|
10
|
+
|
11
|
+
import asyncio
|
12
|
+
import logging
|
13
|
+
import time
|
14
|
+
from redis.asyncio import Redis
|
15
|
+
from typing import Dict, Optional, List, Any
|
16
|
+
|
17
|
+
from .task_limiter import TaskRateLimiter
|
18
|
+
from .config import RateLimitConfig, parse_rate_limit_config
|
19
|
+
|
20
|
+
logger = logging.getLogger('app')
|
21
|
+
|
22
|
+
|
23
|
+
class RateLimiterManager:
|
24
|
+
"""限流器管理器
|
25
|
+
|
26
|
+
管理多个任务的限流器。
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(
|
30
|
+
self,
|
31
|
+
redis_client: Redis,
|
32
|
+
worker_id: str,
|
33
|
+
redis_prefix: str = "jettask",
|
34
|
+
registry_manager = None,
|
35
|
+
worker_state_manager = None
|
36
|
+
):
|
37
|
+
"""初始化限流器管理器
|
38
|
+
|
39
|
+
Args:
|
40
|
+
redis_client: 异步 Redis 客户端
|
41
|
+
worker_id: 当前 worker ID
|
42
|
+
redis_prefix: Redis key 前缀
|
43
|
+
registry_manager: RegistryManager 实例
|
44
|
+
worker_state_manager: WorkerStateManager 实例
|
45
|
+
"""
|
46
|
+
self.redis = redis_client
|
47
|
+
self.worker_id = worker_id
|
48
|
+
self.redis_prefix = redis_prefix
|
49
|
+
self.registry_manager = registry_manager
|
50
|
+
self.worker_state_manager = worker_state_manager
|
51
|
+
|
52
|
+
# 任务名 -> 限流器映射
|
53
|
+
self.limiters: Dict[str, TaskRateLimiter] = {}
|
54
|
+
|
55
|
+
logger.debug(f"RateLimiterManager initialized for worker {worker_id}")
|
56
|
+
|
57
|
+
@staticmethod
|
58
|
+
def register_rate_limit_config(redis_client, task_name: str, config: RateLimitConfig, redis_prefix: str = "jettask"):
|
59
|
+
"""注册任务的限流配置到 Redis(同步方法)
|
60
|
+
|
61
|
+
Args:
|
62
|
+
redis_client: 同步 Redis 客户端
|
63
|
+
task_name: 任务名称
|
64
|
+
config: RateLimitConfig 对象(ConcurrencyLimit 或 QPSLimit)
|
65
|
+
redis_prefix: Redis key 前缀
|
66
|
+
"""
|
67
|
+
try:
|
68
|
+
rate_limit_key = f"{redis_prefix}:RATE_LIMIT:CONFIG:{task_name}"
|
69
|
+
# 将配置对象转换为字典并保存到 Redis
|
70
|
+
config_dict = config.to_dict()
|
71
|
+
redis_client.hset(rate_limit_key, mapping=config_dict)
|
72
|
+
|
73
|
+
# 将任务名添加到索引集合中(用于避免 scan 操作)
|
74
|
+
index_key = f"{redis_prefix}:RATE_LIMIT:INDEX"
|
75
|
+
redis_client.sadd(index_key, task_name)
|
76
|
+
|
77
|
+
logger.debug(f"Registered rate limit config for task '{task_name}': {config}")
|
78
|
+
except Exception as e:
|
79
|
+
logger.error(f"Failed to register rate limit config for task '{task_name}': {e}")
|
80
|
+
|
81
|
+
@staticmethod
|
82
|
+
def unregister_rate_limit_config(redis_client, task_name: str, redis_prefix: str = "jettask"):
|
83
|
+
"""从 Redis 中删除任务的限流配置(同步方法)
|
84
|
+
|
85
|
+
Args:
|
86
|
+
redis_client: 同步 Redis 客户端
|
87
|
+
task_name: 任务名称
|
88
|
+
redis_prefix: Redis key 前缀
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
是否成功删除
|
92
|
+
"""
|
93
|
+
try:
|
94
|
+
rate_limit_key = f"{redis_prefix}:RATE_LIMIT:CONFIG:{task_name}"
|
95
|
+
deleted = redis_client.delete(rate_limit_key)
|
96
|
+
|
97
|
+
if deleted:
|
98
|
+
# 从索引集合中移除
|
99
|
+
index_key = f"{redis_prefix}:RATE_LIMIT:INDEX"
|
100
|
+
redis_client.srem(index_key, task_name)
|
101
|
+
logger.debug(f"Removed rate limit config for task '{task_name}'")
|
102
|
+
return True
|
103
|
+
return False
|
104
|
+
except Exception as e:
|
105
|
+
logger.error(f"Failed to remove rate limit config for task '{task_name}': {e}")
|
106
|
+
return False
|
107
|
+
|
108
|
+
async def add_limiter(self, task_name: str, config: RateLimitConfig):
|
109
|
+
"""添加限流器
|
110
|
+
|
111
|
+
Args:
|
112
|
+
task_name: 任务名称
|
113
|
+
config: 限流配置(QPSLimit 或 ConcurrencyLimit)
|
114
|
+
"""
|
115
|
+
if task_name in self.limiters:
|
116
|
+
logger.warning(f"Limiter for {task_name} already exists")
|
117
|
+
return
|
118
|
+
|
119
|
+
limiter = TaskRateLimiter(
|
120
|
+
redis_client=self.redis,
|
121
|
+
task_name=task_name,
|
122
|
+
worker_id=self.worker_id,
|
123
|
+
config=config,
|
124
|
+
redis_prefix=self.redis_prefix,
|
125
|
+
registry_manager=self.registry_manager,
|
126
|
+
worker_state_manager=self.worker_state_manager
|
127
|
+
)
|
128
|
+
|
129
|
+
await limiter.start()
|
130
|
+
self.limiters[task_name] = limiter
|
131
|
+
|
132
|
+
logger.debug(f"Added rate limiter for {task_name}: {config}")
|
133
|
+
|
134
|
+
async def remove_limiter(self, task_name: str, remove_from_redis: bool = False):
|
135
|
+
"""移除任务限流器
|
136
|
+
|
137
|
+
Args:
|
138
|
+
task_name: 任务名称
|
139
|
+
remove_from_redis: 是否从 Redis 中删除配置(默认 False)
|
140
|
+
"""
|
141
|
+
if task_name not in self.limiters:
|
142
|
+
return
|
143
|
+
|
144
|
+
limiter = self.limiters.pop(task_name)
|
145
|
+
await limiter.stop()
|
146
|
+
|
147
|
+
# 如果需要,从 Redis 中删除配置和索引
|
148
|
+
if remove_from_redis:
|
149
|
+
try:
|
150
|
+
# 删除配置 Hash
|
151
|
+
rate_limit_key = f"{self.redis_prefix}:RATE_LIMIT:CONFIG:{task_name}"
|
152
|
+
await self.redis.delete(rate_limit_key)
|
153
|
+
|
154
|
+
# 从索引集合中移除
|
155
|
+
index_key = f"{self.redis_prefix}:RATE_LIMIT:INDEX"
|
156
|
+
await self.redis.srem(index_key, task_name)
|
157
|
+
|
158
|
+
logger.debug(f"Removed rate limit config and index for {task_name} from Redis")
|
159
|
+
except Exception as e:
|
160
|
+
logger.error(f"Failed to remove rate limit config from Redis: {e}")
|
161
|
+
|
162
|
+
logger.debug(f"Removed rate limiter for {task_name}")
|
163
|
+
|
164
|
+
async def load_config_from_redis(self, task_names: list = None):
|
165
|
+
"""从 Redis 加载限流配置
|
166
|
+
|
167
|
+
Args:
|
168
|
+
task_names: 任务名称列表(如果提供,只加载这些任务的配置;否则从索引集合中加载)
|
169
|
+
"""
|
170
|
+
try:
|
171
|
+
config_count = 0
|
172
|
+
loaded_limiters = []
|
173
|
+
|
174
|
+
# 如果没有提供 task_names,尝试从索引集合中获取
|
175
|
+
if not task_names:
|
176
|
+
index_key = f"{self.redis_prefix}:RATE_LIMIT:INDEX"
|
177
|
+
task_names_bytes = await self.redis.smembers(index_key)
|
178
|
+
task_names = [
|
179
|
+
name.decode('utf-8') if isinstance(name, bytes) else name
|
180
|
+
for name in task_names_bytes
|
181
|
+
]
|
182
|
+
|
183
|
+
if not task_names:
|
184
|
+
logger.debug(f"No rate limit configs found in index {index_key}")
|
185
|
+
return
|
186
|
+
|
187
|
+
# 遍历所有任务名称,加载配置
|
188
|
+
for task_name in task_names:
|
189
|
+
key = f"{self.redis_prefix}:RATE_LIMIT:CONFIG:{task_name}"
|
190
|
+
|
191
|
+
# 检查 key 是否存在
|
192
|
+
exists = await self.redis.exists(key)
|
193
|
+
if not exists:
|
194
|
+
continue
|
195
|
+
|
196
|
+
# 检查 key 类型
|
197
|
+
key_type = await self.redis.type(key)
|
198
|
+
if key_type != "hash":
|
199
|
+
logger.debug(f"Skipping non-hash key: {key} (type: {key_type})")
|
200
|
+
continue
|
201
|
+
|
202
|
+
# 从 Hash 中读取配置
|
203
|
+
config_dict = await self.redis.hgetall(key)
|
204
|
+
if not config_dict:
|
205
|
+
continue
|
206
|
+
|
207
|
+
# 转换 bytes 为 str
|
208
|
+
config_dict = {
|
209
|
+
k.decode() if isinstance(k, bytes) else k:
|
210
|
+
v.decode() if isinstance(v, bytes) else v
|
211
|
+
for k, v in config_dict.items()
|
212
|
+
}
|
213
|
+
|
214
|
+
# 解析配置
|
215
|
+
config = parse_rate_limit_config(config_dict)
|
216
|
+
if config and task_name not in self.limiters:
|
217
|
+
try:
|
218
|
+
await self.add_limiter(task_name, config)
|
219
|
+
config_count += 1
|
220
|
+
loaded_limiters.append(task_name)
|
221
|
+
except Exception as e:
|
222
|
+
logger.error(f"Failed to add limiter for {task_name}: {e}")
|
223
|
+
|
224
|
+
logger.debug(f"Loaded {config_count} rate limit configs from Redis")
|
225
|
+
logger.debug(f"Loaded rate limit config from Redis, limiters: {loaded_limiters}")
|
226
|
+
except Exception as e:
|
227
|
+
logger.error(f"Failed to load config from Redis: {e}")
|
228
|
+
|
229
|
+
async def acquire(self, task_name: str, timeout: float = 10.0) -> Optional[str]:
|
230
|
+
"""获取指定任务的执行许可
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
成功返回 task_id (或 True), 失败返回 None
|
234
|
+
"""
|
235
|
+
limiter = self.limiters.get(task_name)
|
236
|
+
if not limiter:
|
237
|
+
# 没有限流,直接返回 True
|
238
|
+
return True
|
239
|
+
|
240
|
+
return await limiter.acquire(timeout)
|
241
|
+
|
242
|
+
async def release(self, task_name: str, task_id: Optional[str] = None):
|
243
|
+
"""释放指定任务的执行许可
|
244
|
+
|
245
|
+
Args:
|
246
|
+
task_name: 任务名称
|
247
|
+
task_id: 任务ID (ConcurrencyLimit 需要)
|
248
|
+
"""
|
249
|
+
limiter = self.limiters.get(task_name)
|
250
|
+
if limiter:
|
251
|
+
await limiter.release(task_id)
|
252
|
+
|
253
|
+
async def stop_all(self):
|
254
|
+
"""停止所有限流器"""
|
255
|
+
for task_name, limiter in list(self.limiters.items()):
|
256
|
+
await limiter.stop()
|
257
|
+
self.limiters.clear()
|
258
|
+
logger.debug("Stopped all rate limiters")
|
259
|
+
|
260
|
+
def get_all_stats(self) -> Dict[str, dict]:
|
261
|
+
"""获取所有限流器的统计信息"""
|
262
|
+
stats = {}
|
263
|
+
for task_name, limiter in self.limiters.items():
|
264
|
+
if hasattr(limiter, 'get_stats'):
|
265
|
+
stats[task_name] = limiter.get_stats()
|
266
|
+
return stats
|
267
|
+
|
268
|
+
|
269
|
+
__all__ = ['RateLimiterManager']
|
@@ -0,0 +1,154 @@
|
|
1
|
+
"""
|
2
|
+
QPS(每秒查询数)限流器
|
3
|
+
|
4
|
+
基于滑动窗口算法的QPS限流实现
|
5
|
+
"""
|
6
|
+
|
7
|
+
import asyncio
|
8
|
+
import logging
|
9
|
+
import time
|
10
|
+
from collections import deque
|
11
|
+
from redis.asyncio import Redis
|
12
|
+
from typing import Dict, Optional
|
13
|
+
|
14
|
+
from jettask.utils.time_sync import get_time_sync
|
15
|
+
|
16
|
+
logger = logging.getLogger('app')
|
17
|
+
|
18
|
+
|
19
|
+
class QPSRateLimiter:
|
20
|
+
"""QPS 限流器 - 本地滑动窗口算法
|
21
|
+
|
22
|
+
使用滑动窗口算法在本地限流,不访问 Redis。
|
23
|
+
记录每个任务的执行时间戳,检查窗口内的执行次数。
|
24
|
+
使用全局时间同步器保证多个 worker 时间一致性。
|
25
|
+
|
26
|
+
特点:
|
27
|
+
- 纯本地计算,零 Redis 访问
|
28
|
+
- 高性能,低延迟
|
29
|
+
- 通过 Redis 协调配额分配
|
30
|
+
"""
|
31
|
+
|
32
|
+
def __init__(
|
33
|
+
self,
|
34
|
+
qps: int,
|
35
|
+
window_size: float = 1.0,
|
36
|
+
worker_id: str = "unknown"
|
37
|
+
):
|
38
|
+
"""初始化 QPS 限流器
|
39
|
+
|
40
|
+
Args:
|
41
|
+
qps: 当前 worker 的 QPS 配额
|
42
|
+
window_size: 滑动窗口大小(秒)
|
43
|
+
worker_id: Worker ID,用于日志标识
|
44
|
+
"""
|
45
|
+
self.qps_limit = qps
|
46
|
+
self.window_size = window_size
|
47
|
+
self.worker_id = worker_id
|
48
|
+
self.timestamps = deque() # 记录执行时间戳
|
49
|
+
self._lock = asyncio.Lock()
|
50
|
+
self.time_sync = get_time_sync()
|
51
|
+
|
52
|
+
def _get_time(self) -> float:
|
53
|
+
"""获取当前时间"""
|
54
|
+
return self.time_sync.time()
|
55
|
+
|
56
|
+
async def acquire(self, timeout: float = 10.0) -> bool:
|
57
|
+
"""获取一个执行许可
|
58
|
+
|
59
|
+
Args:
|
60
|
+
timeout: 等待超时时间(秒)
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
成功获取返回 True,超时返回 False
|
64
|
+
"""
|
65
|
+
start_time = self._get_time()
|
66
|
+
|
67
|
+
while True:
|
68
|
+
async with self._lock:
|
69
|
+
now = self._get_time()
|
70
|
+
|
71
|
+
# 清理过期的时间戳(窗口外的)
|
72
|
+
cutoff = now - self.window_size
|
73
|
+
while self.timestamps and self.timestamps[0] < cutoff:
|
74
|
+
self.timestamps.popleft()
|
75
|
+
|
76
|
+
# 检查 QPS 限制
|
77
|
+
current_count = len(self.timestamps)
|
78
|
+
if current_count < self.qps_limit:
|
79
|
+
# 未达到限制,允许执行
|
80
|
+
self.timestamps.append(now)
|
81
|
+
logger.debug(
|
82
|
+
f"[QPS] Acquired, count={current_count + 1}/{self.qps_limit}"
|
83
|
+
)
|
84
|
+
return True
|
85
|
+
|
86
|
+
# 达到限制,检查超时
|
87
|
+
if timeout is not None and self._get_time() - start_time > timeout:
|
88
|
+
logger.warning(f"[QPS] Acquire timeout after {timeout}s")
|
89
|
+
return False
|
90
|
+
|
91
|
+
# 短暂休眠后重试
|
92
|
+
await asyncio.sleep(0.01)
|
93
|
+
|
94
|
+
async def try_acquire(self) -> bool:
|
95
|
+
"""尝试获取执行许可(非阻塞)"""
|
96
|
+
async with self._lock:
|
97
|
+
now = self._get_time()
|
98
|
+
|
99
|
+
# 清理过期的时间戳
|
100
|
+
cutoff = now - self.window_size
|
101
|
+
while self.timestamps and self.timestamps[0] < cutoff:
|
102
|
+
self.timestamps.popleft()
|
103
|
+
|
104
|
+
# 检查是否可以执行
|
105
|
+
current_count = len(self.timestamps)
|
106
|
+
if current_count < self.qps_limit:
|
107
|
+
self.timestamps.append(now)
|
108
|
+
logger.debug(f"[QPS] Try acquired, count={current_count + 1}/{self.qps_limit}")
|
109
|
+
return True
|
110
|
+
|
111
|
+
logger.debug(f"[QPS] Try acquire failed, count={current_count}/{self.qps_limit}")
|
112
|
+
return False
|
113
|
+
|
114
|
+
async def release(self):
|
115
|
+
"""释放执行许可(QPS 模式不需要 release)"""
|
116
|
+
pass
|
117
|
+
|
118
|
+
async def update_limit(self, new_limit: int):
|
119
|
+
"""动态更新 QPS 限制
|
120
|
+
|
121
|
+
Args:
|
122
|
+
new_limit: 新的 QPS 限制
|
123
|
+
"""
|
124
|
+
async with self._lock:
|
125
|
+
if self.qps_limit != new_limit:
|
126
|
+
old_limit = self.qps_limit
|
127
|
+
self.qps_limit = new_limit
|
128
|
+
logger.debug(
|
129
|
+
f"[WORKER:{self.worker_id}] [QPS] Limit changed: {old_limit} → {new_limit}"
|
130
|
+
)
|
131
|
+
|
132
|
+
async def get_stats(self) -> dict:
|
133
|
+
"""获取统计信息"""
|
134
|
+
async with self._lock:
|
135
|
+
now = self._get_time()
|
136
|
+
cutoff = now - self.window_size
|
137
|
+
while self.timestamps and self.timestamps[0] < cutoff:
|
138
|
+
self.timestamps.popleft()
|
139
|
+
|
140
|
+
return {
|
141
|
+
'mode': 'qps',
|
142
|
+
'qps_limit': self.qps_limit,
|
143
|
+
'current_qps': len(self.timestamps),
|
144
|
+
'window_size': self.window_size,
|
145
|
+
}
|
146
|
+
|
147
|
+
|
148
|
+
# ============================================================
|
149
|
+
# 并发限流器 - 分布式信号量
|
150
|
+
# ============================================================
|
151
|
+
|
152
|
+
|
153
|
+
|
154
|
+
__all__ = ['QPSRateLimiter']
|