springbootAI 1.8.0__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.
- spring/__init__.py +66 -0
- spring/ai/__init__.py +78 -0
- spring/ai/advisors.py +139 -0
- spring/ai/annotations.py +74 -0
- spring/ai/autoconfig.py +481 -0
- spring/ai/core.py +391 -0
- spring/ai/etl.py +188 -0
- spring/ai/memory.py +109 -0
- spring/ai/observability.py +129 -0
- spring/ai/providers.py +789 -0
- spring/ai/resilience.py +258 -0
- spring/ai/tools.py +106 -0
- spring/ai/vectorstore.py +303 -0
- spring/annotations/__init__.py +188 -0
- spring/annotations/cache.py +126 -0
- spring/annotations/cloud.py +207 -0
- spring/annotations/conditional.py +272 -0
- spring/annotations/core.py +864 -0
- spring/annotations/messaging.py +107 -0
- spring/aop/__init__.py +4 -0
- spring/aop/cloud_aop.py +404 -0
- spring/aop/comprehensive_aop.py +1015 -0
- spring/aop/method_interceptor.py +19 -0
- spring/aop/proxy_factory.py +55 -0
- spring/cloud/__init__.py +76 -0
- spring/cloud/discovery.py +364 -0
- spring/cloud/feign.py +469 -0
- spring/cloud/gateway.py +452 -0
- spring/cloud/load_balancer.py +149 -0
- spring/cloud/seata.py +557 -0
- spring/cloud/sentinel.py +525 -0
- spring/cloud/tracer.py +337 -0
- spring/config/__init__.py +21 -0
- spring/config/binding.py +206 -0
- spring/config/config_loader.py +405 -0
- spring/context/__init__.py +13 -0
- spring/context/application_context.py +589 -0
- spring/context/bean_definition.py +70 -0
- spring/context/bean_factory.py +1052 -0
- spring/context/registry.py +58 -0
- spring/context/scanner.py +106 -0
- spring/core/__init__.py +3 -0
- spring/core/graceful_shutdown.py +196 -0
- spring/core/typing_utils.py +50 -0
- spring/csv/__init__.py +52 -0
- spring/csv/annotations.py +402 -0
- spring/csv/converters.py +69 -0
- spring/csv/easy_csv.py +95 -0
- spring/csv/exceptions.py +27 -0
- spring/csv/reader.py +195 -0
- spring/csv/writer.py +155 -0
- spring/data/__init__.py +54 -0
- spring/data/page.py +181 -0
- spring/data/repository.py +274 -0
- spring/data/specification.py +228 -0
- spring/datasource/__init__.py +66 -0
- spring/datasource/annotations.py +133 -0
- spring/datasource/context.py +69 -0
- spring/datasource/dynamic.py +148 -0
- spring/event/__init__.py +7 -0
- spring/event/publisher.py +69 -0
- spring/excel/__init__.py +51 -0
- spring/excel/annotations.py +405 -0
- spring/excel/converters.py +231 -0
- spring/excel/easy_excel.py +94 -0
- spring/excel/exceptions.py +31 -0
- spring/excel/reader.py +254 -0
- spring/excel/style.py +95 -0
- spring/excel/writer.py +197 -0
- spring/i18n/__init__.py +97 -0
- spring/i18n/accessor.py +94 -0
- spring/i18n/auto_config.py +177 -0
- spring/i18n/holder.py +106 -0
- spring/i18n/locale.py +152 -0
- spring/i18n/locale_resolver.py +367 -0
- spring/i18n/message_source.py +250 -0
- spring/i18n/middleware.py +79 -0
- spring/i18n/properties.py +168 -0
- spring/i18n/sources.py +255 -0
- spring/logging/__init__.py +1 -0
- spring/logging/loguru_logger.py +228 -0
- spring/main.py +378 -0
- spring/messaging/__init__.py +1 -0
- spring/messaging/rabbitmq.py +302 -0
- spring/monitoring/__init__.py +1 -0
- spring/monitoring/prometheus.py +199 -0
- spring/orm/__init__.py +258 -0
- spring/orm/database.py +222 -0
- spring/orm/ddl_auto.py +1217 -0
- spring/orm/migration.py +419 -0
- spring/orm/mybatis_integration.py +400 -0
- spring/orm/pymybatis/__init__.py +86 -0
- spring/orm/pymybatis/annotations/__init__.py +30 -0
- spring/orm/pymybatis/annotations/annotations.py +332 -0
- spring/orm/pymybatis/cache/__init__.py +47 -0
- spring/orm/pymybatis/cache/cache.py +371 -0
- spring/orm/pymybatis/cache/redis_cache.py +434 -0
- spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
- spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
- spring/orm/pymybatis/configuration.py +525 -0
- spring/orm/pymybatis/core/__init__.py +10 -0
- spring/orm/pymybatis/core/sql_session.py +1382 -0
- spring/orm/pymybatis/core/sql_session_factory.py +76 -0
- spring/orm/pymybatis/dialect/__init__.py +9 -0
- spring/orm/pymybatis/dialect/dialect.py +445 -0
- spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
- spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
- spring/orm/pymybatis/interceptor/__init__.py +31 -0
- spring/orm/pymybatis/interceptor/interceptor.py +427 -0
- spring/orm/pymybatis/mapper/__init__.py +9 -0
- spring/orm/pymybatis/mapper/mapper.py +540 -0
- spring/orm/pymybatis/metrics/__init__.py +41 -0
- spring/orm/pymybatis/metrics/metrics.py +595 -0
- spring/orm/pymybatis/pool/__init__.py +9 -0
- spring/orm/pymybatis/pool/connection_pool.py +711 -0
- spring/orm/pymybatis/security/__init__.py +19 -0
- spring/orm/pymybatis/security/access_control.py +415 -0
- spring/orm/pymybatis/security/password_encoder.py +293 -0
- spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
- spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
- spring/orm/pymybatis/transaction/__init__.py +9 -0
- spring/orm/pymybatis/transaction/transaction.py +288 -0
- spring/orm/pymybatis/type_handler/__init__.py +37 -0
- spring/orm/pymybatis/type_handler/type_handler.py +473 -0
- spring/orm/pymybatis/version.py +9 -0
- spring/orm/pymybatis/xml_parser/__init__.py +9 -0
- spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
- spring/retry/__init__.py +12 -0
- spring/retry/retry_annotations.py +71 -0
- spring/retry/retry_decorator.py +155 -0
- spring/scheduling/__init__.py +3 -0
- spring/scheduling/scheduler.py +389 -0
- spring/security/__init__.py +39 -0
- spring/security/jwt_utils.py +281 -0
- spring/security/replay_protection.py +206 -0
- spring/security/secret_manager.py +226 -0
- spring/security/security_aop.py +248 -0
- spring/security/security_context.py +172 -0
- spring/test/__init__.py +45 -0
- spring/test/slicing.py +341 -0
- spring/tracing/__init__.py +11 -0
- spring/tracing/skywalking.py +229 -0
- spring/tx/__init__.py +52 -0
- spring/tx/events.py +172 -0
- spring/tx/synchronization.py +143 -0
- spring/utils/__init__.py +5 -0
- spring/utils/banner.py +32 -0
- spring/utils/logger.py +73 -0
- spring/utils/redis_client.py +526 -0
- spring/validation/__init__.py +55 -0
- spring/validation/aop.py +141 -0
- spring/validation/constraints.py +357 -0
- spring/validation/exceptions.py +55 -0
- spring/validation/validator.py +139 -0
- spring/web/__init__.py +12 -0
- spring/web/actuator.py +319 -0
- spring/web/exception_handler.py +61 -0
- spring/web/health.py +399 -0
- spring/web/interceptor.py +91 -0
- spring/web/result.py +44 -0
- spring/web/swagger.py +601 -0
- spring/web/web_context.py +755 -0
- spring/websocket/__init__.py +86 -0
- spring/websocket/annotations.py +169 -0
- spring/websocket/broker.py +238 -0
- spring/websocket/exceptions.py +26 -0
- spring/websocket/handler.py +243 -0
- spring/websocket/router.py +526 -0
- spring/websocket/session.py +216 -0
- springbootai-1.8.0.dist-info/METADATA +2796 -0
- springbootai-1.8.0.dist-info/RECORD +175 -0
- springbootai-1.8.0.dist-info/WHEEL +5 -0
- springbootai-1.8.0.dist-info/entry_points.txt +2 -0
- springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
- springbootai-1.8.0.dist-info/top_level.txt +1 -0
spring/cloud/sentinel.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sentinel 内嵌限流降级引擎 (Embedded Sentinel Engine)
|
|
3
|
+
|
|
4
|
+
实现 Alibaba Sentinel 的核心功能,无需外部 Dashboard:
|
|
5
|
+
- 滑动窗口限流(QPS)
|
|
6
|
+
- 异常比例/异常数熔断
|
|
7
|
+
- 慢调用比例熔断
|
|
8
|
+
- 热点参数限流
|
|
9
|
+
- 系统自适应保护
|
|
10
|
+
- 支持内存和Redis两种模式
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
import threading
|
|
15
|
+
import logging
|
|
16
|
+
import math
|
|
17
|
+
from collections import defaultdict, deque
|
|
18
|
+
from enum import Enum
|
|
19
|
+
from typing import Dict, List, Optional, Callable, Any, Tuple
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("Spring.Cloud.Sentinel")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FlowRule:
|
|
25
|
+
"""限流规则"""
|
|
26
|
+
def __init__(self, resource: str, count: float = 100.0, grade: str = "QPS",
|
|
27
|
+
strategy: str = "DIRECT", control_behavior: str = "REJECT",
|
|
28
|
+
warm_up_period_sec: int = 0, max_queueing_timeout_ms: int = 0):
|
|
29
|
+
self.resource = resource
|
|
30
|
+
self.count = count # QPS阈值
|
|
31
|
+
self.grade = grade # QPS or THREAD
|
|
32
|
+
self.strategy = strategy
|
|
33
|
+
self.control_behavior = control_behavior # REJECT, WARM_UP, RATE_LIMITER
|
|
34
|
+
self.warm_up_period_sec = warm_up_period_sec
|
|
35
|
+
self.max_queueing_timeout_ms = max_queueing_timeout_ms
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DegradeRule:
|
|
39
|
+
"""熔断降级规则"""
|
|
40
|
+
def __init__(self, resource: str, grade: str = "EXCEPTION_RATIO",
|
|
41
|
+
count: float = 0.5, time_window_sec: int = 10,
|
|
42
|
+
min_request_amount: int = 5, slow_ratio_rt_threshold_ms: float = 1000.0,
|
|
43
|
+
slow_ratio: float = 1.0):
|
|
44
|
+
self.resource = resource
|
|
45
|
+
self.grade = grade # EXCEPTION_RATIO, EXCEPTION_COUNT, SLOW_RATIO
|
|
46
|
+
self.count = count # 阈值
|
|
47
|
+
self.time_window_sec = time_window_sec # 熔断时长(秒)
|
|
48
|
+
self.min_request_amount = min_request_amount # 最小请求数
|
|
49
|
+
self.slow_ratio_rt_threshold_ms = slow_ratio_rt_threshold_ms
|
|
50
|
+
self.slow_ratio = slow_ratio
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SystemRule:
|
|
54
|
+
"""系统保护规则"""
|
|
55
|
+
def __init__(self, highest_system_load: float = -1.0,
|
|
56
|
+
avg_rt: float = -1.0, max_thread: int = -1,
|
|
57
|
+
qps: float = -1.0):
|
|
58
|
+
self.highest_system_load = highest_system_load
|
|
59
|
+
self.avg_rt = avg_rt
|
|
60
|
+
self.max_thread = max_thread
|
|
61
|
+
self.qps = qps
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class HotParamRule:
|
|
65
|
+
"""热点参数限流规则"""
|
|
66
|
+
def __init__(self, resource: str, param_idx: int = 0, count: float = 100.0,
|
|
67
|
+
duration_sec: int = 1, param_flow_items: Optional[Dict[str, float]] = None):
|
|
68
|
+
self.resource = resource
|
|
69
|
+
self.param_idx = param_idx
|
|
70
|
+
self.count = count
|
|
71
|
+
self.duration_sec = duration_sec
|
|
72
|
+
self.param_flow_items = param_flow_items or {}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CircuitState(Enum):
|
|
76
|
+
CLOSED = "CLOSED" # 正常
|
|
77
|
+
OPEN = "OPEN" # 熔断打开
|
|
78
|
+
HALF_OPEN = "HALF_OPEN" # 半开(尝试恢复)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class BlockException(Exception):
|
|
82
|
+
"""Sentinel阻断异常"""
|
|
83
|
+
def __init__(self, resource: str, rule_type: str, message: str = ""):
|
|
84
|
+
self.resource = resource
|
|
85
|
+
self.rule_type = rule_type
|
|
86
|
+
super().__init__(f"Sentinel blocked [{rule_type}] resource={resource}: {message}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SlidingWindow:
|
|
90
|
+
"""滑动窗口计数器"""
|
|
91
|
+
def __init__(self, window_duration_ms: int = 1000, sample_count: int = 10):
|
|
92
|
+
self.window_duration_ms = window_duration_ms
|
|
93
|
+
self.sample_count = sample_count
|
|
94
|
+
self.bucket_duration_ms = window_duration_ms // sample_count
|
|
95
|
+
self.buckets: deque = deque(maxlen=sample_count)
|
|
96
|
+
self._lock = threading.Lock()
|
|
97
|
+
|
|
98
|
+
def _current_bucket(self, now_ms: int) -> Dict:
|
|
99
|
+
bucket_start = (now_ms // self.bucket_duration_ms) * self.bucket_duration_ms
|
|
100
|
+
with self._lock:
|
|
101
|
+
if self.buckets and self.buckets[-1]['start'] == bucket_start:
|
|
102
|
+
return self.buckets[-1]
|
|
103
|
+
# 过期桶清理
|
|
104
|
+
while self.buckets and (now_ms - self.buckets[0]['start']) >= self.window_duration_ms:
|
|
105
|
+
self.buckets.popleft()
|
|
106
|
+
# 新桶
|
|
107
|
+
bucket = {'start': bucket_start, 'pass': 0, 'block': 0,
|
|
108
|
+
'exception': 0, 'success': 0, 'rt_total': 0.0, 'slow': 0}
|
|
109
|
+
self.buckets.append(bucket)
|
|
110
|
+
return bucket
|
|
111
|
+
|
|
112
|
+
def add_pass(self):
|
|
113
|
+
now_ms = int(time.time() * 1000)
|
|
114
|
+
self._current_bucket(now_ms)['pass'] += 1
|
|
115
|
+
|
|
116
|
+
def add_block(self):
|
|
117
|
+
now_ms = int(time.time() * 1000)
|
|
118
|
+
self._current_bucket(now_ms)['block'] += 1
|
|
119
|
+
|
|
120
|
+
def add_exception(self):
|
|
121
|
+
now_ms = int(time.time() * 1000)
|
|
122
|
+
self._current_bucket(now_ms)['exception'] += 1
|
|
123
|
+
|
|
124
|
+
def add_success(self, rt_ms: float, slow_threshold_ms: float = 1000.0):
|
|
125
|
+
now_ms = int(time.time() * 1000)
|
|
126
|
+
bucket = self._current_bucket(now_ms)
|
|
127
|
+
bucket['success'] += 1
|
|
128
|
+
bucket['rt_total'] += rt_ms
|
|
129
|
+
if rt_ms > slow_threshold_ms:
|
|
130
|
+
bucket['slow'] += 1
|
|
131
|
+
|
|
132
|
+
def get_stats(self) -> Dict[str, float]:
|
|
133
|
+
now_ms = int(time.time() * 1000)
|
|
134
|
+
with self._lock:
|
|
135
|
+
# 清理过期
|
|
136
|
+
while self.buckets and (now_ms - self.buckets[0]['start']) >= self.window_duration_ms:
|
|
137
|
+
self.buckets.popleft()
|
|
138
|
+
total_pass = sum(b['pass'] for b in self.buckets)
|
|
139
|
+
total_block = sum(b['block'] for b in self.buckets)
|
|
140
|
+
total_exception = sum(b['exception'] for b in self.buckets)
|
|
141
|
+
total_success = sum(b['success'] for b in self.buckets)
|
|
142
|
+
total_rt = sum(b['rt_total'] for b in self.buckets)
|
|
143
|
+
total_slow = sum(b['slow'] for b in self.buckets)
|
|
144
|
+
window_sec = self.window_duration_ms / 1000.0
|
|
145
|
+
qps = total_pass / window_sec if window_sec > 0 else 0
|
|
146
|
+
avg_rt = total_rt / total_success if total_success > 0 else 0
|
|
147
|
+
exception_ratio = total_exception / (total_success + total_exception) if (total_success + total_exception) >= 1 else 0
|
|
148
|
+
slow_ratio = total_slow / total_success if total_success >= 1 else 0
|
|
149
|
+
return {
|
|
150
|
+
'qps': qps,
|
|
151
|
+
'pass_qps': total_pass / window_sec if window_sec > 0 else 0,
|
|
152
|
+
'block_qps': total_block / window_sec if window_sec > 0 else 0,
|
|
153
|
+
'exception_ratio': exception_ratio,
|
|
154
|
+
'exception_count': total_exception,
|
|
155
|
+
'success_count': total_success,
|
|
156
|
+
'avg_rt_ms': avg_rt,
|
|
157
|
+
'slow_ratio': slow_ratio,
|
|
158
|
+
'total_requests': total_pass + total_block,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ResourceCircuitBreaker:
|
|
163
|
+
"""单个资源的熔断器"""
|
|
164
|
+
def __init__(self, resource: str):
|
|
165
|
+
self.resource = resource
|
|
166
|
+
self.state = CircuitState.CLOSED
|
|
167
|
+
self._state_lock = threading.Lock()
|
|
168
|
+
self._opened_at: float = 0
|
|
169
|
+
self._recover_after_sec: float = 0
|
|
170
|
+
self._half_open_successes = 0
|
|
171
|
+
self._half_open_required = 3 # 半开状态需要连续成功次数
|
|
172
|
+
self._half_open_permitted = False # 是否已经放行一个探测请求
|
|
173
|
+
|
|
174
|
+
def can_pass(self) -> bool:
|
|
175
|
+
with self._state_lock:
|
|
176
|
+
if self.state == CircuitState.CLOSED:
|
|
177
|
+
return True
|
|
178
|
+
if self.state == CircuitState.OPEN:
|
|
179
|
+
if time.monotonic() - self._opened_at >= self._recover_after_sec:
|
|
180
|
+
self.state = CircuitState.HALF_OPEN
|
|
181
|
+
self._half_open_successes = 0
|
|
182
|
+
self._half_open_permitted = True
|
|
183
|
+
return True
|
|
184
|
+
return False
|
|
185
|
+
# HALF_OPEN: 只允许一个探测请求
|
|
186
|
+
if self._half_open_permitted:
|
|
187
|
+
self._half_open_permitted = False
|
|
188
|
+
return True
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def on_success(self):
|
|
192
|
+
with self._state_lock:
|
|
193
|
+
if self.state == CircuitState.HALF_OPEN:
|
|
194
|
+
self._half_open_successes += 1
|
|
195
|
+
if self._half_open_successes >= self._half_open_required:
|
|
196
|
+
self.state = CircuitState.CLOSED
|
|
197
|
+
self._half_open_permitted = False
|
|
198
|
+
logger.info(f"[Sentinel] Circuit for {self.resource} CLOSED (recovered)")
|
|
199
|
+
|
|
200
|
+
def on_failure(self, time_window_sec: int):
|
|
201
|
+
with self._state_lock:
|
|
202
|
+
self.state = CircuitState.OPEN
|
|
203
|
+
self._opened_at = time.monotonic()
|
|
204
|
+
self._recover_after_sec = float(time_window_sec)
|
|
205
|
+
self._half_open_successes = 0
|
|
206
|
+
self._half_open_permitted = False
|
|
207
|
+
# Use a separate recovery timer to transition to HALF_OPEN after window
|
|
208
|
+
def _recover():
|
|
209
|
+
time.sleep(time_window_sec)
|
|
210
|
+
with self._state_lock:
|
|
211
|
+
if self.state == CircuitState.OPEN:
|
|
212
|
+
self.state = CircuitState.HALF_OPEN
|
|
213
|
+
self._half_open_successes = 0
|
|
214
|
+
self._half_open_permitted = True
|
|
215
|
+
logger.info(f"[Sentinel] Circuit for {self.resource} -> HALF_OPEN")
|
|
216
|
+
t = threading.Thread(target=_recover, daemon=True)
|
|
217
|
+
t.start()
|
|
218
|
+
|
|
219
|
+
def get_state(self) -> str:
|
|
220
|
+
return self.state.value
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class SentinelEngine:
|
|
224
|
+
"""
|
|
225
|
+
Sentinel内嵌引擎
|
|
226
|
+
|
|
227
|
+
实现限流、熔断、热点参数、系统保护
|
|
228
|
+
"""
|
|
229
|
+
_instance = None
|
|
230
|
+
_lock = threading.Lock()
|
|
231
|
+
|
|
232
|
+
def __new__(cls):
|
|
233
|
+
if cls._instance is None:
|
|
234
|
+
with cls._lock:
|
|
235
|
+
if cls._instance is None:
|
|
236
|
+
cls._instance = super().__new__(cls)
|
|
237
|
+
return cls._instance
|
|
238
|
+
|
|
239
|
+
def __init__(self):
|
|
240
|
+
if hasattr(self, '_initialized'):
|
|
241
|
+
return
|
|
242
|
+
self._initialized = True
|
|
243
|
+
self._flow_rules: Dict[str, FlowRule] = {}
|
|
244
|
+
self._degrade_rules: Dict[str, DegradeRule] = {}
|
|
245
|
+
self._system_rules: List[SystemRule] = []
|
|
246
|
+
self._hot_param_rules: Dict[str, HotParamRule] = {}
|
|
247
|
+
self._windows: Dict[str, SlidingWindow] = {}
|
|
248
|
+
self._hot_windows: Dict[str, Dict[str, SlidingWindow]] = {}
|
|
249
|
+
self._circuit_breakers: Dict[str, ResourceCircuitBreaker] = {}
|
|
250
|
+
self._global_lock = threading.Lock()
|
|
251
|
+
self._windows_lock = threading.Lock()
|
|
252
|
+
logger.info("[Sentinel] Engine initialized")
|
|
253
|
+
|
|
254
|
+
def _get_window(self, resource: str) -> SlidingWindow:
|
|
255
|
+
with self._windows_lock:
|
|
256
|
+
if resource not in self._windows:
|
|
257
|
+
self._windows[resource] = SlidingWindow(1000, 10)
|
|
258
|
+
return self._windows[resource]
|
|
259
|
+
|
|
260
|
+
def _get_circuit_breaker(self, resource: str) -> ResourceCircuitBreaker:
|
|
261
|
+
with self._global_lock:
|
|
262
|
+
if resource not in self._circuit_breakers:
|
|
263
|
+
self._circuit_breakers[resource] = ResourceCircuitBreaker(resource)
|
|
264
|
+
return self._circuit_breakers[resource]
|
|
265
|
+
|
|
266
|
+
def load_flow_rules(self, rules: List[FlowRule]):
|
|
267
|
+
with self._global_lock:
|
|
268
|
+
self._flow_rules.clear()
|
|
269
|
+
for r in rules:
|
|
270
|
+
self._flow_rules[r.resource] = r
|
|
271
|
+
logger.info(f"[Sentinel] Loaded {len(rules)} flow rules")
|
|
272
|
+
|
|
273
|
+
def load_degrade_rules(self, rules: List[DegradeRule]):
|
|
274
|
+
with self._global_lock:
|
|
275
|
+
self._degrade_rules.clear()
|
|
276
|
+
for r in rules:
|
|
277
|
+
self._degrade_rules[r.resource] = r
|
|
278
|
+
logger.info(f"[Sentinel] Loaded {len(rules)} degrade rules")
|
|
279
|
+
|
|
280
|
+
def load_system_rules(self, rules: List[SystemRule]):
|
|
281
|
+
with self._global_lock:
|
|
282
|
+
self._system_rules = list(rules)
|
|
283
|
+
|
|
284
|
+
def load_hot_param_rules(self, rules: List[HotParamRule]):
|
|
285
|
+
with self._global_lock:
|
|
286
|
+
self._hot_param_rules.clear()
|
|
287
|
+
for r in rules:
|
|
288
|
+
self._hot_param_rules[r.resource] = r
|
|
289
|
+
|
|
290
|
+
def _check_system_rule(self) -> Optional[str]:
|
|
291
|
+
"""检查系统保护规则"""
|
|
292
|
+
if not self._system_rules:
|
|
293
|
+
return None
|
|
294
|
+
# 简化实现:基于当前进程负载做粗略检查
|
|
295
|
+
try:
|
|
296
|
+
import os
|
|
297
|
+
load_avg = os.getloadavg()[0] if hasattr(os, 'getloadavg') else 0
|
|
298
|
+
for rule in self._system_rules:
|
|
299
|
+
if rule.highest_system_load > 0 and load_avg > rule.highest_system_load:
|
|
300
|
+
return f"System load {load_avg:.2f} > {rule.highest_system_load}"
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
def _check_hot_param(self, resource: str, args: tuple, kwargs: dict) -> Optional[str]:
|
|
306
|
+
"""检查热点参数限流"""
|
|
307
|
+
rule = self._hot_param_rules.get(resource)
|
|
308
|
+
if not rule:
|
|
309
|
+
return None
|
|
310
|
+
# 取参数值
|
|
311
|
+
param_value = None
|
|
312
|
+
if args and rule.param_idx < len(args):
|
|
313
|
+
param_value = args[rule.param_idx]
|
|
314
|
+
if param_value is None:
|
|
315
|
+
try:
|
|
316
|
+
param_names = list(kwargs.keys())
|
|
317
|
+
if rule.param_idx < len(param_names):
|
|
318
|
+
param_value = kwargs[param_names[rule.param_idx]]
|
|
319
|
+
except Exception:
|
|
320
|
+
pass
|
|
321
|
+
if param_value is None:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
param_key = str(param_value)
|
|
325
|
+
threshold = rule.param_flow_items.get(param_key, rule.count)
|
|
326
|
+
# 热点参数窗口
|
|
327
|
+
res_key = f"{resource}__hot"
|
|
328
|
+
if res_key not in self._hot_windows:
|
|
329
|
+
self._hot_windows[res_key] = {}
|
|
330
|
+
if param_key not in self._hot_windows[res_key]:
|
|
331
|
+
self._hot_windows[res_key][param_key] = SlidingWindow(rule.duration_sec * 1000, max(1, rule.duration_sec * 2))
|
|
332
|
+
hw = self._hot_windows[res_key][param_key]
|
|
333
|
+
stats = hw.get_stats()
|
|
334
|
+
if stats['pass_qps'] >= threshold:
|
|
335
|
+
return f"Hot param [{param_key}] QPS {stats['pass_qps']:.1f} >= {threshold}"
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
def entry(self, resource: str, args: tuple = (), kwargs: dict = None) -> 'SentinelEntry':
|
|
339
|
+
"""
|
|
340
|
+
进入资源,执行所有规则检查
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
BlockException: 被限流/熔断时抛出
|
|
344
|
+
"""
|
|
345
|
+
kwargs = kwargs or {}
|
|
346
|
+
# 系统规则检查
|
|
347
|
+
sys_block = self._check_system_rule()
|
|
348
|
+
if sys_block:
|
|
349
|
+
window = self._get_window(resource)
|
|
350
|
+
window.add_block()
|
|
351
|
+
raise BlockException(resource, "SYSTEM", sys_block)
|
|
352
|
+
|
|
353
|
+
# 熔断检查
|
|
354
|
+
cb = self._get_circuit_breaker(resource)
|
|
355
|
+
if not cb.can_pass():
|
|
356
|
+
window = self._get_window(resource)
|
|
357
|
+
window.add_block()
|
|
358
|
+
raise BlockException(resource, "CIRCUIT", f"Circuit is {cb.get_state()}")
|
|
359
|
+
|
|
360
|
+
# 限流检查
|
|
361
|
+
flow_rule = self._flow_rules.get(resource)
|
|
362
|
+
window = self._get_window(resource)
|
|
363
|
+
if flow_rule:
|
|
364
|
+
stats = window.get_stats()
|
|
365
|
+
current_qps = stats['pass_qps']
|
|
366
|
+
if current_qps >= flow_rule.count:
|
|
367
|
+
window.add_block()
|
|
368
|
+
raise BlockException(resource, "FLOW", f"QPS {current_qps:.1f} >= {flow_rule.count}")
|
|
369
|
+
|
|
370
|
+
# 热点参数限流
|
|
371
|
+
hot_block = self._check_hot_param(resource, args, kwargs)
|
|
372
|
+
if hot_block:
|
|
373
|
+
window.add_block()
|
|
374
|
+
raise BlockException(resource, "HOT_PARAM", hot_block)
|
|
375
|
+
|
|
376
|
+
# 通过
|
|
377
|
+
window.add_pass()
|
|
378
|
+
return SentinelEntry(self, resource, window, cb)
|
|
379
|
+
|
|
380
|
+
def record_success(self, resource: str, rt_ms: float = 0.0):
|
|
381
|
+
"""记录成功"""
|
|
382
|
+
window = self._get_window(resource)
|
|
383
|
+
degrade_rule = self._degrade_rules.get(resource)
|
|
384
|
+
slow_threshold = degrade_rule.slow_ratio_rt_threshold_ms if degrade_rule else 1000.0
|
|
385
|
+
window.add_success(rt_ms, slow_threshold)
|
|
386
|
+
cb = self._get_circuit_breaker(resource)
|
|
387
|
+
cb.on_success()
|
|
388
|
+
self._check_degrade_on_success(resource)
|
|
389
|
+
|
|
390
|
+
def record_exception(self, resource: str):
|
|
391
|
+
"""记录异常"""
|
|
392
|
+
window = self._get_window(resource)
|
|
393
|
+
window.add_exception()
|
|
394
|
+
cb = self._get_circuit_breaker(resource)
|
|
395
|
+
degrade_rule = self._degrade_rules.get(resource)
|
|
396
|
+
if degrade_rule:
|
|
397
|
+
stats = window.get_stats()
|
|
398
|
+
total = stats['success_count'] + stats['exception_count']
|
|
399
|
+
if total >= degrade_rule.min_request_amount:
|
|
400
|
+
if degrade_rule.grade == "EXCEPTION_COUNT" and stats['exception_count'] >= degrade_rule.count:
|
|
401
|
+
cb.on_failure(degrade_rule.time_window_sec)
|
|
402
|
+
logger.warning(f"[Sentinel] Circuit OPEN for {resource}: exception count {stats['exception_count']} >= {degrade_rule.count}")
|
|
403
|
+
elif degrade_rule.grade == "EXCEPTION_RATIO" and stats['exception_ratio'] >= degrade_rule.count:
|
|
404
|
+
cb.on_failure(degrade_rule.time_window_sec)
|
|
405
|
+
logger.warning(f"[Sentinel] Circuit OPEN for {resource}: exception ratio {stats['exception_ratio']:.2%} >= {degrade_rule.count}")
|
|
406
|
+
elif degrade_rule.grade == "SLOW_RATIO" and stats['slow_ratio'] >= degrade_rule.slow_ratio:
|
|
407
|
+
cb.on_failure(degrade_rule.time_window_sec)
|
|
408
|
+
logger.warning(f"[Sentinel] Circuit OPEN for {resource}: slow ratio {stats['slow_ratio']:.2%} >= {degrade_rule.slow_ratio}")
|
|
409
|
+
|
|
410
|
+
def _check_degrade_on_success(self, resource: str):
|
|
411
|
+
"""检查是否触发慢调用熔断"""
|
|
412
|
+
degrade_rule = self._degrade_rules.get(resource)
|
|
413
|
+
if degrade_rule and degrade_rule.grade == "SLOW_RATIO":
|
|
414
|
+
window = self._get_window(resource)
|
|
415
|
+
stats = window.get_stats()
|
|
416
|
+
cb = self._get_circuit_breaker(resource)
|
|
417
|
+
total = stats['success_count'] + stats['exception_count']
|
|
418
|
+
if total >= degrade_rule.min_request_amount and stats['slow_ratio'] >= degrade_rule.slow_ratio:
|
|
419
|
+
cb.on_failure(degrade_rule.time_window_sec)
|
|
420
|
+
logger.warning(f"[Sentinel] Circuit OPEN for {resource}: slow ratio {stats['slow_ratio']:.2%}")
|
|
421
|
+
|
|
422
|
+
def get_resource_stats(self, resource: str = None) -> Dict:
|
|
423
|
+
"""获取资源统计"""
|
|
424
|
+
if resource:
|
|
425
|
+
window = self._windows.get(resource)
|
|
426
|
+
cb = self._circuit_breakers.get(resource)
|
|
427
|
+
return {
|
|
428
|
+
resource: {
|
|
429
|
+
'stats': window.get_stats() if window else {},
|
|
430
|
+
'circuit_state': cb.get_state() if cb else 'CLOSED',
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
result = {}
|
|
434
|
+
for res in list(self._windows.keys()):
|
|
435
|
+
window = self._windows[res]
|
|
436
|
+
cb = self._circuit_breakers.get(res)
|
|
437
|
+
result[res] = {
|
|
438
|
+
'stats': window.get_stats(),
|
|
439
|
+
'circuit_state': cb.get_state() if cb else 'CLOSED',
|
|
440
|
+
}
|
|
441
|
+
return result
|
|
442
|
+
|
|
443
|
+
def reset(self):
|
|
444
|
+
"""重置所有状态(测试用)"""
|
|
445
|
+
with self._global_lock:
|
|
446
|
+
self._windows.clear()
|
|
447
|
+
self._hot_windows.clear()
|
|
448
|
+
self._circuit_breakers.clear()
|
|
449
|
+
self._flow_rules.clear()
|
|
450
|
+
self._degrade_rules.clear()
|
|
451
|
+
self._system_rules.clear()
|
|
452
|
+
self._hot_param_rules.clear()
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class SentinelEntry:
|
|
456
|
+
"""Sentinel资源入口上下文管理器"""
|
|
457
|
+
__slots__ = ('_engine', '_resource', '_window', '_cb', '_start_ms', '_done')
|
|
458
|
+
|
|
459
|
+
def __init__(self, engine: SentinelEngine, resource: str, window: SlidingWindow, cb: ResourceCircuitBreaker):
|
|
460
|
+
self._engine = engine
|
|
461
|
+
self._resource = resource
|
|
462
|
+
self._window = window
|
|
463
|
+
self._cb = cb
|
|
464
|
+
self._start_ms = time.monotonic() * 1000
|
|
465
|
+
self._done = False
|
|
466
|
+
|
|
467
|
+
def success(self):
|
|
468
|
+
if not self._done:
|
|
469
|
+
rt = time.monotonic() * 1000 - self._start_ms
|
|
470
|
+
self._engine.record_success(self._resource, rt)
|
|
471
|
+
self._done = True
|
|
472
|
+
|
|
473
|
+
def error(self):
|
|
474
|
+
if not self._done:
|
|
475
|
+
self._engine.record_exception(self._resource)
|
|
476
|
+
self._done = True
|
|
477
|
+
|
|
478
|
+
def __enter__(self):
|
|
479
|
+
return self
|
|
480
|
+
|
|
481
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
482
|
+
if exc_val is None:
|
|
483
|
+
self.success()
|
|
484
|
+
else:
|
|
485
|
+
if not isinstance(exc_val, BlockException):
|
|
486
|
+
self.error()
|
|
487
|
+
return False
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# 全局实例
|
|
491
|
+
sentinel_engine = SentinelEngine()
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def sentinel_protect(resource: str, fallback: Callable = None, block_handler: Callable = None):
|
|
495
|
+
"""
|
|
496
|
+
Sentinel资源保护装饰器
|
|
497
|
+
|
|
498
|
+
Usage:
|
|
499
|
+
@sentinel_protect("my_api", fallback=my_fallback)
|
|
500
|
+
def my_api():
|
|
501
|
+
...
|
|
502
|
+
"""
|
|
503
|
+
import functools
|
|
504
|
+
def decorator(func: Callable) -> Callable:
|
|
505
|
+
@functools.wraps(func)
|
|
506
|
+
def wrapper(*args, **kwargs):
|
|
507
|
+
try:
|
|
508
|
+
entry = sentinel_engine.entry(resource, args=args, kwargs=kwargs)
|
|
509
|
+
try:
|
|
510
|
+
result = func(*args, **kwargs)
|
|
511
|
+
entry.success()
|
|
512
|
+
return result
|
|
513
|
+
except BlockException:
|
|
514
|
+
raise
|
|
515
|
+
except Exception as e:
|
|
516
|
+
entry.error()
|
|
517
|
+
raise
|
|
518
|
+
except BlockException as e:
|
|
519
|
+
if block_handler:
|
|
520
|
+
return block_handler(*args, **kwargs)
|
|
521
|
+
if fallback:
|
|
522
|
+
return fallback(*args, **kwargs)
|
|
523
|
+
raise
|
|
524
|
+
return wrapper
|
|
525
|
+
return decorator
|