jettask 0.2.19__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 +10 -3
  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.19.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.19.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.19.dist-info → jettask-0.2.20.dist-info}/WHEEL +0 -0
  163. {jettask-0.2.19.dist-info → jettask-0.2.20.dist-info}/entry_points.txt +0 -0
  164. {jettask-0.2.19.dist-info → jettask-0.2.20.dist-info}/licenses/LICENSE +0 -0
  165. {jettask-0.2.19.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']