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.
Files changed (175) hide show
  1. spring/__init__.py +66 -0
  2. spring/ai/__init__.py +78 -0
  3. spring/ai/advisors.py +139 -0
  4. spring/ai/annotations.py +74 -0
  5. spring/ai/autoconfig.py +481 -0
  6. spring/ai/core.py +391 -0
  7. spring/ai/etl.py +188 -0
  8. spring/ai/memory.py +109 -0
  9. spring/ai/observability.py +129 -0
  10. spring/ai/providers.py +789 -0
  11. spring/ai/resilience.py +258 -0
  12. spring/ai/tools.py +106 -0
  13. spring/ai/vectorstore.py +303 -0
  14. spring/annotations/__init__.py +188 -0
  15. spring/annotations/cache.py +126 -0
  16. spring/annotations/cloud.py +207 -0
  17. spring/annotations/conditional.py +272 -0
  18. spring/annotations/core.py +864 -0
  19. spring/annotations/messaging.py +107 -0
  20. spring/aop/__init__.py +4 -0
  21. spring/aop/cloud_aop.py +404 -0
  22. spring/aop/comprehensive_aop.py +1015 -0
  23. spring/aop/method_interceptor.py +19 -0
  24. spring/aop/proxy_factory.py +55 -0
  25. spring/cloud/__init__.py +76 -0
  26. spring/cloud/discovery.py +364 -0
  27. spring/cloud/feign.py +469 -0
  28. spring/cloud/gateway.py +452 -0
  29. spring/cloud/load_balancer.py +149 -0
  30. spring/cloud/seata.py +557 -0
  31. spring/cloud/sentinel.py +525 -0
  32. spring/cloud/tracer.py +337 -0
  33. spring/config/__init__.py +21 -0
  34. spring/config/binding.py +206 -0
  35. spring/config/config_loader.py +405 -0
  36. spring/context/__init__.py +13 -0
  37. spring/context/application_context.py +589 -0
  38. spring/context/bean_definition.py +70 -0
  39. spring/context/bean_factory.py +1052 -0
  40. spring/context/registry.py +58 -0
  41. spring/context/scanner.py +106 -0
  42. spring/core/__init__.py +3 -0
  43. spring/core/graceful_shutdown.py +196 -0
  44. spring/core/typing_utils.py +50 -0
  45. spring/csv/__init__.py +52 -0
  46. spring/csv/annotations.py +402 -0
  47. spring/csv/converters.py +69 -0
  48. spring/csv/easy_csv.py +95 -0
  49. spring/csv/exceptions.py +27 -0
  50. spring/csv/reader.py +195 -0
  51. spring/csv/writer.py +155 -0
  52. spring/data/__init__.py +54 -0
  53. spring/data/page.py +181 -0
  54. spring/data/repository.py +274 -0
  55. spring/data/specification.py +228 -0
  56. spring/datasource/__init__.py +66 -0
  57. spring/datasource/annotations.py +133 -0
  58. spring/datasource/context.py +69 -0
  59. spring/datasource/dynamic.py +148 -0
  60. spring/event/__init__.py +7 -0
  61. spring/event/publisher.py +69 -0
  62. spring/excel/__init__.py +51 -0
  63. spring/excel/annotations.py +405 -0
  64. spring/excel/converters.py +231 -0
  65. spring/excel/easy_excel.py +94 -0
  66. spring/excel/exceptions.py +31 -0
  67. spring/excel/reader.py +254 -0
  68. spring/excel/style.py +95 -0
  69. spring/excel/writer.py +197 -0
  70. spring/i18n/__init__.py +97 -0
  71. spring/i18n/accessor.py +94 -0
  72. spring/i18n/auto_config.py +177 -0
  73. spring/i18n/holder.py +106 -0
  74. spring/i18n/locale.py +152 -0
  75. spring/i18n/locale_resolver.py +367 -0
  76. spring/i18n/message_source.py +250 -0
  77. spring/i18n/middleware.py +79 -0
  78. spring/i18n/properties.py +168 -0
  79. spring/i18n/sources.py +255 -0
  80. spring/logging/__init__.py +1 -0
  81. spring/logging/loguru_logger.py +228 -0
  82. spring/main.py +378 -0
  83. spring/messaging/__init__.py +1 -0
  84. spring/messaging/rabbitmq.py +302 -0
  85. spring/monitoring/__init__.py +1 -0
  86. spring/monitoring/prometheus.py +199 -0
  87. spring/orm/__init__.py +258 -0
  88. spring/orm/database.py +222 -0
  89. spring/orm/ddl_auto.py +1217 -0
  90. spring/orm/migration.py +419 -0
  91. spring/orm/mybatis_integration.py +400 -0
  92. spring/orm/pymybatis/__init__.py +86 -0
  93. spring/orm/pymybatis/annotations/__init__.py +30 -0
  94. spring/orm/pymybatis/annotations/annotations.py +332 -0
  95. spring/orm/pymybatis/cache/__init__.py +47 -0
  96. spring/orm/pymybatis/cache/cache.py +371 -0
  97. spring/orm/pymybatis/cache/redis_cache.py +434 -0
  98. spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
  99. spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
  100. spring/orm/pymybatis/configuration.py +525 -0
  101. spring/orm/pymybatis/core/__init__.py +10 -0
  102. spring/orm/pymybatis/core/sql_session.py +1382 -0
  103. spring/orm/pymybatis/core/sql_session_factory.py +76 -0
  104. spring/orm/pymybatis/dialect/__init__.py +9 -0
  105. spring/orm/pymybatis/dialect/dialect.py +445 -0
  106. spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
  107. spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
  108. spring/orm/pymybatis/interceptor/__init__.py +31 -0
  109. spring/orm/pymybatis/interceptor/interceptor.py +427 -0
  110. spring/orm/pymybatis/mapper/__init__.py +9 -0
  111. spring/orm/pymybatis/mapper/mapper.py +540 -0
  112. spring/orm/pymybatis/metrics/__init__.py +41 -0
  113. spring/orm/pymybatis/metrics/metrics.py +595 -0
  114. spring/orm/pymybatis/pool/__init__.py +9 -0
  115. spring/orm/pymybatis/pool/connection_pool.py +711 -0
  116. spring/orm/pymybatis/security/__init__.py +19 -0
  117. spring/orm/pymybatis/security/access_control.py +415 -0
  118. spring/orm/pymybatis/security/password_encoder.py +293 -0
  119. spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
  120. spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
  121. spring/orm/pymybatis/transaction/__init__.py +9 -0
  122. spring/orm/pymybatis/transaction/transaction.py +288 -0
  123. spring/orm/pymybatis/type_handler/__init__.py +37 -0
  124. spring/orm/pymybatis/type_handler/type_handler.py +473 -0
  125. spring/orm/pymybatis/version.py +9 -0
  126. spring/orm/pymybatis/xml_parser/__init__.py +9 -0
  127. spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
  128. spring/retry/__init__.py +12 -0
  129. spring/retry/retry_annotations.py +71 -0
  130. spring/retry/retry_decorator.py +155 -0
  131. spring/scheduling/__init__.py +3 -0
  132. spring/scheduling/scheduler.py +389 -0
  133. spring/security/__init__.py +39 -0
  134. spring/security/jwt_utils.py +281 -0
  135. spring/security/replay_protection.py +206 -0
  136. spring/security/secret_manager.py +226 -0
  137. spring/security/security_aop.py +248 -0
  138. spring/security/security_context.py +172 -0
  139. spring/test/__init__.py +45 -0
  140. spring/test/slicing.py +341 -0
  141. spring/tracing/__init__.py +11 -0
  142. spring/tracing/skywalking.py +229 -0
  143. spring/tx/__init__.py +52 -0
  144. spring/tx/events.py +172 -0
  145. spring/tx/synchronization.py +143 -0
  146. spring/utils/__init__.py +5 -0
  147. spring/utils/banner.py +32 -0
  148. spring/utils/logger.py +73 -0
  149. spring/utils/redis_client.py +526 -0
  150. spring/validation/__init__.py +55 -0
  151. spring/validation/aop.py +141 -0
  152. spring/validation/constraints.py +357 -0
  153. spring/validation/exceptions.py +55 -0
  154. spring/validation/validator.py +139 -0
  155. spring/web/__init__.py +12 -0
  156. spring/web/actuator.py +319 -0
  157. spring/web/exception_handler.py +61 -0
  158. spring/web/health.py +399 -0
  159. spring/web/interceptor.py +91 -0
  160. spring/web/result.py +44 -0
  161. spring/web/swagger.py +601 -0
  162. spring/web/web_context.py +755 -0
  163. spring/websocket/__init__.py +86 -0
  164. spring/websocket/annotations.py +169 -0
  165. spring/websocket/broker.py +238 -0
  166. spring/websocket/exceptions.py +26 -0
  167. spring/websocket/handler.py +243 -0
  168. spring/websocket/router.py +526 -0
  169. spring/websocket/session.py +216 -0
  170. springbootai-1.8.0.dist-info/METADATA +2796 -0
  171. springbootai-1.8.0.dist-info/RECORD +175 -0
  172. springbootai-1.8.0.dist-info/WHEEL +5 -0
  173. springbootai-1.8.0.dist-info/entry_points.txt +2 -0
  174. springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
  175. springbootai-1.8.0.dist-info/top_level.txt +1 -0
@@ -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