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/seata.py
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""
|
|
2
|
+
分布式事务模块
|
|
3
|
+
集成真实 Seata,并提供仅供开发验证的 HTTP 补偿协调器
|
|
4
|
+
|
|
5
|
+
支持三种模式:
|
|
6
|
+
- local: 本地模式,仅追踪事务状态(默认)
|
|
7
|
+
- http: 实验性 HTTP 补偿模式,不提供 AT 强一致性,禁止用于生产
|
|
8
|
+
- distributed: 真实Seata Server模式(需要seata SDK)
|
|
9
|
+
|
|
10
|
+
实验性 HTTP 补偿模式工作原理:
|
|
11
|
+
1. TM(事务发起方)开启全局事务,生成XID
|
|
12
|
+
2. Feign调用远程服务时,通过 X-TX-XID header 传递XID
|
|
13
|
+
3. RM(分支事务方)注册分支到TC(内嵌协调器)
|
|
14
|
+
4. TM 提交时通知所有分支提交;回滚时通知所有分支回滚
|
|
15
|
+
5. 分支服务暴露 /seata/branch/{branchId}/commit 和 /seata/branch/{branchId}/rollback 端点
|
|
16
|
+
|
|
17
|
+
注意:分布式模式(distributed)要启用完整的分布式事务功能,请:
|
|
18
|
+
1. 安装Seata Server(https://seata.io/zh-cn/docs/overview/what-is-seata.html)
|
|
19
|
+
2. 安装并验证与本适配层 API 兼容的企业 Seata Python SDK
|
|
20
|
+
3. 配置registry.conf和file.conf
|
|
21
|
+
4. 在启动时设置SEATA_ENABLED=true
|
|
22
|
+
"""
|
|
23
|
+
import logging
|
|
24
|
+
import time
|
|
25
|
+
import threading
|
|
26
|
+
import uuid
|
|
27
|
+
import json
|
|
28
|
+
from contextvars import ContextVar
|
|
29
|
+
from typing import Dict, Any, Optional, List, Callable
|
|
30
|
+
from urllib import request as urlrequest
|
|
31
|
+
from urllib.error import URLError, HTTPError
|
|
32
|
+
|
|
33
|
+
# 可选导入Seata
|
|
34
|
+
try:
|
|
35
|
+
import seata
|
|
36
|
+
from seata.rm import DataSourceProxy
|
|
37
|
+
from seata.tm import GlobalTransaction
|
|
38
|
+
from seata.core.context.RootContext import RootContext
|
|
39
|
+
_seata_available = True
|
|
40
|
+
except ImportError:
|
|
41
|
+
seata = None
|
|
42
|
+
DataSourceProxy = None
|
|
43
|
+
GlobalTransaction = None
|
|
44
|
+
RootContext = None
|
|
45
|
+
_seata_available = False
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger("Spring.Cloud.Seata")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BranchStatus:
|
|
51
|
+
"""分支事务状态"""
|
|
52
|
+
REGISTERED = "REGISTERED"
|
|
53
|
+
COMMITTED = "COMMITTED"
|
|
54
|
+
ROLLED_BACK = "ROLLED_BACK"
|
|
55
|
+
FAILED = "FAILED"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SeataTransactionManager:
|
|
59
|
+
"""Seata事务管理器(支持local/http/distributed三种模式)"""
|
|
60
|
+
|
|
61
|
+
_instance = None
|
|
62
|
+
_lock = threading.Lock()
|
|
63
|
+
|
|
64
|
+
def __new__(cls, *args, **kwargs):
|
|
65
|
+
if cls._instance is None:
|
|
66
|
+
with cls._lock:
|
|
67
|
+
if cls._instance is None:
|
|
68
|
+
cls._instance = super().__new__(cls)
|
|
69
|
+
return cls._instance
|
|
70
|
+
|
|
71
|
+
def __init__(self, server_addr: str = "localhost:8091", application_id: str = "",
|
|
72
|
+
transaction_group: str = "my_tx_group", mode: str = "local"):
|
|
73
|
+
if hasattr(self, '_initialized'):
|
|
74
|
+
return
|
|
75
|
+
self.server_addr = server_addr
|
|
76
|
+
self.application_id = application_id
|
|
77
|
+
self.transaction_group = transaction_group
|
|
78
|
+
self.mode = mode # 'local', 'http', or 'distributed'
|
|
79
|
+
self._transaction_context: ContextVar[Optional[Dict[str, Any]]] = ContextVar(
|
|
80
|
+
'springpy_seata_transaction_context', default=None
|
|
81
|
+
)
|
|
82
|
+
self._seata_client_initialized = False
|
|
83
|
+
self._initialized = True
|
|
84
|
+
|
|
85
|
+
# 实验性 HTTP 模式:进程内状态只用于开发验证,不承诺故障恢复。
|
|
86
|
+
self._global_transactions: Dict[str, Dict] = {}
|
|
87
|
+
self._branches: Dict[str, List[Dict]] = {} # xid -> [branch]
|
|
88
|
+
self._gt_lock = threading.Lock()
|
|
89
|
+
|
|
90
|
+
# 分支事务回调注册(本地分支,用于同进程服务调用)
|
|
91
|
+
self._branch_callbacks: Dict[str, Dict[str, Callable]] = {}
|
|
92
|
+
self._cb_lock = threading.Lock()
|
|
93
|
+
|
|
94
|
+
if mode == "distributed":
|
|
95
|
+
self.set_mode(mode)
|
|
96
|
+
|
|
97
|
+
def configure(self, server_addr: str = "localhost:8091", application_id: str = "",
|
|
98
|
+
transaction_group: str = "my_tx_group", mode: str = "local") -> None:
|
|
99
|
+
"""重新配置单例;初始化入口不能依赖第二次构造调用。"""
|
|
100
|
+
self.server_addr = server_addr
|
|
101
|
+
self.application_id = application_id
|
|
102
|
+
self.transaction_group = transaction_group
|
|
103
|
+
self.set_mode(mode)
|
|
104
|
+
|
|
105
|
+
def _get_context(self) -> Dict[str, Any]:
|
|
106
|
+
return self._transaction_context.get() or {}
|
|
107
|
+
|
|
108
|
+
def _set_context(self, **values: Any) -> None:
|
|
109
|
+
current = dict(self._get_context())
|
|
110
|
+
current.update(values)
|
|
111
|
+
self._transaction_context.set(current)
|
|
112
|
+
|
|
113
|
+
def _init_seata_client(self):
|
|
114
|
+
"""初始化 Seata 客户端;分布式模式禁止静默降级。"""
|
|
115
|
+
if not _seata_available:
|
|
116
|
+
raise RuntimeError("seata.mode=distributed requires a compatible Seata Python SDK")
|
|
117
|
+
if not self.application_id:
|
|
118
|
+
raise RuntimeError("seata.application_id is required in distributed mode")
|
|
119
|
+
try:
|
|
120
|
+
# 设置Seata配置环境变量
|
|
121
|
+
import os
|
|
122
|
+
os.environ.setdefault('SEATA_IP', self.server_addr.split(':')[0])
|
|
123
|
+
os.environ.setdefault('SEATA_PORT', self.server_addr.split(':')[1] if ':' in self.server_addr else '8091')
|
|
124
|
+
os.environ.setdefault('SEATA_APPLICATION_ID', self.application_id)
|
|
125
|
+
os.environ.setdefault('SEATA_TX_GROUP', self.transaction_group)
|
|
126
|
+
|
|
127
|
+
logger.info(f"[Seata] Initializing Seata client with server: {self.server_addr}, application_id: {self.application_id}")
|
|
128
|
+
|
|
129
|
+
if hasattr(seata, 'init'):
|
|
130
|
+
seata.init()
|
|
131
|
+
elif hasattr(seata, 'config') and hasattr(seata.config, 'init'):
|
|
132
|
+
seata.config.init()
|
|
133
|
+
else:
|
|
134
|
+
raise RuntimeError("installed Seata SDK does not expose a supported init API")
|
|
135
|
+
|
|
136
|
+
self._seata_client_initialized = True
|
|
137
|
+
logger.info("[Seata] Client initialized successfully in distributed mode")
|
|
138
|
+
except Exception as e:
|
|
139
|
+
self._seata_client_initialized = False
|
|
140
|
+
raise RuntimeError(f"failed to initialize distributed Seata client: {e}") from e
|
|
141
|
+
|
|
142
|
+
def begin_transaction(self, timeout: int = 60000, name: str = "") -> str:
|
|
143
|
+
"""
|
|
144
|
+
开启分布式事务
|
|
145
|
+
|
|
146
|
+
支持三种模式:
|
|
147
|
+
- local: 仅追踪事务上下文
|
|
148
|
+
- http: 实验性 HTTP 补偿模式,生成本地 XID,提交/回滚通过 HTTP 通知分支
|
|
149
|
+
- distributed: 使用真实Seata Server
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
timeout: 事务超时时间(毫秒)
|
|
153
|
+
name: 事务名称
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
事务ID (XID)
|
|
157
|
+
"""
|
|
158
|
+
# 检查是否已经在事务中
|
|
159
|
+
context = self._get_context()
|
|
160
|
+
if context.get('in_transaction', False):
|
|
161
|
+
logger.warning("Nested transaction detected, returning current tx_id")
|
|
162
|
+
return context.get('tx_id', "")
|
|
163
|
+
|
|
164
|
+
# 生成事务ID
|
|
165
|
+
tx_id = uuid.uuid4().hex
|
|
166
|
+
|
|
167
|
+
# 设置事务上下文
|
|
168
|
+
self._transaction_context.set({
|
|
169
|
+
'in_transaction': True,
|
|
170
|
+
'tx_id': tx_id,
|
|
171
|
+
'status': 'BEGIN',
|
|
172
|
+
'timeout': timeout,
|
|
173
|
+
'start_time': time.time(),
|
|
174
|
+
'name': name,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
# 实验性 HTTP 补偿模式:注册进程内全局事务
|
|
178
|
+
if self.mode == "http":
|
|
179
|
+
with self._gt_lock:
|
|
180
|
+
self._global_transactions[tx_id] = {
|
|
181
|
+
'xid': tx_id,
|
|
182
|
+
'name': name,
|
|
183
|
+
'status': 'BEGIN',
|
|
184
|
+
'start_time': time.time(),
|
|
185
|
+
'timeout': timeout,
|
|
186
|
+
}
|
|
187
|
+
self._branches[tx_id] = []
|
|
188
|
+
logger.info(f"[Seata-HTTP] Begin global transaction: {tx_id}")
|
|
189
|
+
|
|
190
|
+
elif self.mode == "distributed":
|
|
191
|
+
if not (_seata_available and self._seata_client_initialized):
|
|
192
|
+
self._cleanup_context()
|
|
193
|
+
raise RuntimeError("distributed Seata client is not initialized")
|
|
194
|
+
try:
|
|
195
|
+
GlobalTransaction.begin(timeout, name)
|
|
196
|
+
seata_tx_id = RootContext.getXID()
|
|
197
|
+
if not seata_tx_id:
|
|
198
|
+
raise RuntimeError("Seata transaction began without an XID")
|
|
199
|
+
tx_id = seata_tx_id
|
|
200
|
+
self._set_context(tx_id=tx_id)
|
|
201
|
+
logger.info(f"[Seata] Begin global transaction (distributed): {tx_id}")
|
|
202
|
+
return tx_id
|
|
203
|
+
except Exception as e:
|
|
204
|
+
self._cleanup_context()
|
|
205
|
+
raise RuntimeError(f"failed to begin distributed Seata transaction: {e}") from e
|
|
206
|
+
|
|
207
|
+
else:
|
|
208
|
+
logger.info(f"[Seata] Begin transaction (local context): {tx_id}")
|
|
209
|
+
|
|
210
|
+
return tx_id
|
|
211
|
+
|
|
212
|
+
def register_branch(self, xid: str, branch_id: str = "", resource_id: str = "",
|
|
213
|
+
callback_url: str = "", commit_cb: Callable = None,
|
|
214
|
+
rollback_cb: Callable = None, service_name: str = "") -> str:
|
|
215
|
+
"""
|
|
216
|
+
注册分支事务(实验性 HTTP 补偿模式)
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
xid: 全局事务ID
|
|
220
|
+
branch_id: 分支ID(自动生成如果为空)
|
|
221
|
+
resource_id: 资源标识(如数据库表名)
|
|
222
|
+
callback_url: 远程回调URL(用于跨服务调用),如 http://order-service/seata/branch
|
|
223
|
+
commit_cb: 本地提交回调函数
|
|
224
|
+
rollback_cb: 本地回滚回调函数
|
|
225
|
+
service_name: 服务名
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
branch_id
|
|
229
|
+
"""
|
|
230
|
+
if not branch_id:
|
|
231
|
+
branch_id = uuid.uuid4().hex[:16]
|
|
232
|
+
|
|
233
|
+
if self.mode == 'http':
|
|
234
|
+
with self._gt_lock:
|
|
235
|
+
if xid not in self._global_transactions:
|
|
236
|
+
raise ValueError(f"Unknown or completed experimental HTTP transaction: {xid}")
|
|
237
|
+
|
|
238
|
+
branch = {
|
|
239
|
+
'branch_id': branch_id,
|
|
240
|
+
'xid': xid,
|
|
241
|
+
'resource_id': resource_id,
|
|
242
|
+
'callback_url': callback_url,
|
|
243
|
+
'service_name': service_name or self.application_id,
|
|
244
|
+
'status': BranchStatus.REGISTERED,
|
|
245
|
+
'registered_at': time.time(),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
with self._gt_lock:
|
|
249
|
+
if xid not in self._branches:
|
|
250
|
+
self._branches[xid] = []
|
|
251
|
+
self._branches[xid].append(branch)
|
|
252
|
+
|
|
253
|
+
# 注册本地回调
|
|
254
|
+
if commit_cb or rollback_cb:
|
|
255
|
+
with self._cb_lock:
|
|
256
|
+
self._branch_callbacks[branch_id] = {
|
|
257
|
+
'commit': commit_cb,
|
|
258
|
+
'rollback': rollback_cb,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
logger.info(f"[Seata-HTTP] Branch registered: xid={xid[:16]}... branch_id={branch_id[:16]}... "
|
|
262
|
+
f"service={service_name} url={callback_url}")
|
|
263
|
+
return branch_id
|
|
264
|
+
|
|
265
|
+
def _notify_branch(self, branch: dict, action: str) -> bool:
|
|
266
|
+
"""通知分支事务提交或回滚"""
|
|
267
|
+
branch_id = branch['branch_id']
|
|
268
|
+
# 本地回调优先
|
|
269
|
+
with self._cb_lock:
|
|
270
|
+
cb = self._branch_callbacks.get(branch_id)
|
|
271
|
+
if cb:
|
|
272
|
+
fn = cb.get('commit') if action == 'commit' else cb.get('rollback')
|
|
273
|
+
if fn:
|
|
274
|
+
try:
|
|
275
|
+
fn(branch['xid'], branch_id)
|
|
276
|
+
return True
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.error(f"[Seata-HTTP] Local branch {action} failed: {e}")
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
# HTTP回调
|
|
282
|
+
url = branch.get('callback_url')
|
|
283
|
+
if url:
|
|
284
|
+
try:
|
|
285
|
+
full_url = f"{url.rstrip('/')}/{branch_id}/{action}"
|
|
286
|
+
req = urlrequest.Request(full_url, method='POST',
|
|
287
|
+
data=json.dumps({'xid': branch['xid'], 'branchId': branch_id}).encode(),
|
|
288
|
+
headers={'Content-Type': 'application/json'})
|
|
289
|
+
resp = urlrequest.urlopen(req, timeout=5)
|
|
290
|
+
return resp.status == 200
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.error(f"[Seata-HTTP] HTTP branch {action} failed for {branch_id[:16]}: {e}")
|
|
293
|
+
return False
|
|
294
|
+
logger.error(
|
|
295
|
+
f"[Seata-HTTP] Branch {branch_id[:16]} has no {action} callback; failing closed"
|
|
296
|
+
)
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
def commit_transaction(self, tx_id: str) -> bool:
|
|
300
|
+
"""
|
|
301
|
+
提交分布式事务
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
tx_id: 事务ID
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
是否成功
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
context = self._get_context()
|
|
311
|
+
current_tx_id = context.get('tx_id', "")
|
|
312
|
+
if current_tx_id and current_tx_id != tx_id:
|
|
313
|
+
logger.error(f"Transaction mismatch: expected {tx_id}, got {current_tx_id}")
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
start_time = context.get('start_time', 0)
|
|
317
|
+
timeout = context.get('timeout', 60000)
|
|
318
|
+
duration = (time.time() - start_time) * 1000
|
|
319
|
+
|
|
320
|
+
if duration > timeout:
|
|
321
|
+
logger.error(f"Transaction timeout: {duration}ms > {timeout}ms")
|
|
322
|
+
self.rollback_transaction(tx_id)
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
# 实验性 HTTP 补偿模式:通知所有分支提交
|
|
326
|
+
if self.mode == "http":
|
|
327
|
+
all_ok = True
|
|
328
|
+
with self._gt_lock:
|
|
329
|
+
branches = list(self._branches.get(tx_id, []))
|
|
330
|
+
transaction = self._global_transactions.get(tx_id)
|
|
331
|
+
if transaction is None:
|
|
332
|
+
logger.error(f"[Seata-HTTP] Unknown transaction: {tx_id}")
|
|
333
|
+
return False
|
|
334
|
+
transaction['status'] = 'COMMITTING'
|
|
335
|
+
for branch in branches:
|
|
336
|
+
ok = self._notify_branch(branch, 'commit')
|
|
337
|
+
branch['status'] = BranchStatus.COMMITTED if ok else BranchStatus.FAILED
|
|
338
|
+
if not ok:
|
|
339
|
+
all_ok = False
|
|
340
|
+
with self._gt_lock:
|
|
341
|
+
self._global_transactions[tx_id]['status'] = 'COMMITTED' if all_ok else 'PARTIAL_COMMIT'
|
|
342
|
+
if all_ok:
|
|
343
|
+
self._cleanup_http_transaction(tx_id)
|
|
344
|
+
logger.info(f"[Seata-HTTP] Commit transaction {tx_id[:16]}... branches={len(branches)} success={all_ok}")
|
|
345
|
+
return all_ok
|
|
346
|
+
|
|
347
|
+
# 分布式模式
|
|
348
|
+
if self.mode == "distributed":
|
|
349
|
+
if not (_seata_available and self._seata_client_initialized):
|
|
350
|
+
logger.error("Distributed Seata client is not initialized; commit rejected")
|
|
351
|
+
return False
|
|
352
|
+
try:
|
|
353
|
+
GlobalTransaction.commit()
|
|
354
|
+
self._set_context(status='COMMITTED')
|
|
355
|
+
logger.info(f"[Seata] Commit global transaction: {tx_id}, duration={duration:.2f}ms")
|
|
356
|
+
return True
|
|
357
|
+
except Exception as e:
|
|
358
|
+
logger.error(f"[Seata] Failed to commit global transaction: {e}. Rolling back...")
|
|
359
|
+
self.rollback_transaction(tx_id)
|
|
360
|
+
return False
|
|
361
|
+
|
|
362
|
+
# 本地模式
|
|
363
|
+
self._set_context(status='COMMITTED')
|
|
364
|
+
logger.info(f"[Seata] Commit transaction (local): {tx_id}, duration={duration:.2f}ms")
|
|
365
|
+
return True
|
|
366
|
+
finally:
|
|
367
|
+
self._cleanup_context()
|
|
368
|
+
|
|
369
|
+
def rollback_transaction(self, tx_id: str) -> bool:
|
|
370
|
+
"""
|
|
371
|
+
回滚分布式事务
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
tx_id: 事务ID
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
是否成功
|
|
378
|
+
"""
|
|
379
|
+
try:
|
|
380
|
+
current_tx_id = self._get_context().get('tx_id', "")
|
|
381
|
+
if current_tx_id and current_tx_id != tx_id:
|
|
382
|
+
logger.error(f"Transaction mismatch: expected {tx_id}, got {current_tx_id}")
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
# 实验性 HTTP 补偿模式:通知所有分支回滚
|
|
386
|
+
if self.mode == "http":
|
|
387
|
+
all_ok = True
|
|
388
|
+
with self._gt_lock:
|
|
389
|
+
branches = list(self._branches.get(tx_id, []))
|
|
390
|
+
if tx_id in self._global_transactions:
|
|
391
|
+
self._global_transactions[tx_id]['status'] = 'ROLLING_BACK'
|
|
392
|
+
for branch in branches:
|
|
393
|
+
ok = self._notify_branch(branch, 'rollback')
|
|
394
|
+
branch['status'] = BranchStatus.ROLLED_BACK if ok else BranchStatus.FAILED
|
|
395
|
+
if not ok:
|
|
396
|
+
all_ok = False
|
|
397
|
+
with self._gt_lock:
|
|
398
|
+
if tx_id in self._global_transactions:
|
|
399
|
+
self._global_transactions[tx_id]['status'] = 'ROLLED_BACK' if all_ok else 'PARTIAL_ROLLBACK'
|
|
400
|
+
if all_ok:
|
|
401
|
+
self._cleanup_http_transaction(tx_id)
|
|
402
|
+
logger.info(f"[Seata-HTTP] Rollback transaction {tx_id[:16]}... branches={len(branches)} success={all_ok}")
|
|
403
|
+
return all_ok
|
|
404
|
+
|
|
405
|
+
# 分布式模式
|
|
406
|
+
if self.mode == "distributed":
|
|
407
|
+
if not (_seata_available and self._seata_client_initialized):
|
|
408
|
+
logger.error("Distributed Seata client is not initialized; rollback failed")
|
|
409
|
+
return False
|
|
410
|
+
try:
|
|
411
|
+
GlobalTransaction.rollback()
|
|
412
|
+
self._set_context(status='ROLLED_BACK')
|
|
413
|
+
logger.info(f"[Seata] Rollback global transaction: {tx_id}")
|
|
414
|
+
return True
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.error(f"[Seata] Failed to rollback global transaction: {e}")
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
# 本地模式
|
|
420
|
+
self._set_context(status='ROLLED_BACK')
|
|
421
|
+
logger.info(f"[Seata] Rollback transaction (local): {tx_id}")
|
|
422
|
+
return True
|
|
423
|
+
finally:
|
|
424
|
+
self._cleanup_context()
|
|
425
|
+
|
|
426
|
+
def _cleanup_context(self):
|
|
427
|
+
"""清理事务上下文"""
|
|
428
|
+
self._transaction_context.set(None)
|
|
429
|
+
|
|
430
|
+
# 清理Seata上下文
|
|
431
|
+
if _seata_available and RootContext:
|
|
432
|
+
try:
|
|
433
|
+
RootContext.unbindXID()
|
|
434
|
+
except Exception:
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
def _cleanup_http_transaction(self, tx_id: str) -> None:
|
|
438
|
+
"""成功完成后移除协调状态和本地回调,避免长期进程内存增长。"""
|
|
439
|
+
with self._gt_lock:
|
|
440
|
+
branches = self._branches.pop(tx_id, [])
|
|
441
|
+
self._global_transactions.pop(tx_id, None)
|
|
442
|
+
with self._cb_lock:
|
|
443
|
+
for branch in branches:
|
|
444
|
+
self._branch_callbacks.pop(branch['branch_id'], None)
|
|
445
|
+
|
|
446
|
+
def is_in_transaction(self) -> bool:
|
|
447
|
+
"""检查是否在事务中"""
|
|
448
|
+
return bool(self._get_context().get('in_transaction', False))
|
|
449
|
+
|
|
450
|
+
def get_current_tx_id(self) -> str:
|
|
451
|
+
"""获取当前事务ID"""
|
|
452
|
+
# 优先从Seata获取
|
|
453
|
+
if _seata_available and RootContext:
|
|
454
|
+
try:
|
|
455
|
+
seata_tx_id = RootContext.getXID()
|
|
456
|
+
if seata_tx_id:
|
|
457
|
+
return seata_tx_id
|
|
458
|
+
except Exception:
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
return self._get_context().get('tx_id', "")
|
|
462
|
+
|
|
463
|
+
def get_transaction_status(self) -> str:
|
|
464
|
+
"""获取当前事务状态"""
|
|
465
|
+
return self._get_context().get('status', "NONE")
|
|
466
|
+
|
|
467
|
+
def get_mode(self) -> str:
|
|
468
|
+
"""获取当前事务模式"""
|
|
469
|
+
return self.mode
|
|
470
|
+
|
|
471
|
+
def set_mode(self, mode: str):
|
|
472
|
+
"""设置事务模式 (local/http/distributed)"""
|
|
473
|
+
if mode not in ["local", "http", "distributed"]:
|
|
474
|
+
raise ValueError("Mode must be 'local', 'http' or 'distributed'")
|
|
475
|
+
|
|
476
|
+
if mode == "distributed":
|
|
477
|
+
# 即使初始化失败也保持 distributed,后续事务必须失败关闭,
|
|
478
|
+
# 不能沿用之前的 local 模式继续执行核心业务。
|
|
479
|
+
self.mode = mode
|
|
480
|
+
if not self._seata_client_initialized:
|
|
481
|
+
self._init_seata_client()
|
|
482
|
+
return
|
|
483
|
+
self.mode = mode
|
|
484
|
+
|
|
485
|
+
def get_transaction_info(self) -> Dict[str, Any]:
|
|
486
|
+
"""获取当前事务信息(用于调试/监控)"""
|
|
487
|
+
with self._gt_lock:
|
|
488
|
+
return {
|
|
489
|
+
'mode': self.mode,
|
|
490
|
+
'active_global_tx': len(self._global_transactions),
|
|
491
|
+
'active_branches': sum(len(b) for b in self._branches.values()),
|
|
492
|
+
'in_transaction': self.is_in_transaction(),
|
|
493
|
+
'current_xid': self.get_current_tx_id(),
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
@staticmethod
|
|
497
|
+
def get_xid_from_headers(headers: Dict[str, str]) -> str:
|
|
498
|
+
"""从HTTP请求头中提取XID"""
|
|
499
|
+
if not headers:
|
|
500
|
+
return ""
|
|
501
|
+
return (headers.get('X-TX-XID') or headers.get('X-Seata-XID') or
|
|
502
|
+
headers.get('x-tx-xid') or headers.get('x-seata-xid') or "")
|
|
503
|
+
|
|
504
|
+
@staticmethod
|
|
505
|
+
def inject_xid_headers(headers: Dict[str, str], xid: str) -> Dict[str, str]:
|
|
506
|
+
"""将XID注入到HTTP请求头(供Feign使用)"""
|
|
507
|
+
if xid:
|
|
508
|
+
headers['X-TX-XID'] = xid
|
|
509
|
+
headers['X-Seata-XID'] = xid
|
|
510
|
+
return headers
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
# 创建全局Seata事务管理器实例
|
|
514
|
+
seata_manager = SeataTransactionManager()
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def init_seata(config: dict) -> None:
|
|
518
|
+
"""
|
|
519
|
+
初始化Seata配置
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
config: 配置字典,包含server_addr, application_id, transaction_group, mode等
|
|
523
|
+
|
|
524
|
+
配置说明:
|
|
525
|
+
server_addr: Seata Server地址,默认 localhost:8091
|
|
526
|
+
application_id: 应用ID,必填(分布式模式)
|
|
527
|
+
transaction_group: 事务分组,默认 my_tx_group
|
|
528
|
+
mode: 事务模式,可选 'local'(默认)或 'distributed'
|
|
529
|
+
|
|
530
|
+
分布式模式要求:
|
|
531
|
+
1. 部署Seata Server
|
|
532
|
+
2. 创建seata_undo_log表(MySQL示例):
|
|
533
|
+
CREATE TABLE IF NOT EXISTS `seata_undo_log` (
|
|
534
|
+
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
|
|
535
|
+
`branch_id` BIGINT(20) NOT NULL COMMENT '分支事务ID',
|
|
536
|
+
`xid` VARCHAR(100) NOT NULL COMMENT '全局事务ID',
|
|
537
|
+
`context` VARCHAR(128) NOT NULL COMMENT '上下文',
|
|
538
|
+
`rollback_info` LONGBLOB NOT NULL COMMENT '回滚信息',
|
|
539
|
+
`log_status` INT(11) NOT NULL COMMENT '状态',
|
|
540
|
+
`log_created` DATETIME NOT NULL COMMENT '创建时间',
|
|
541
|
+
`log_modified` DATETIME NOT NULL COMMENT '修改时间',
|
|
542
|
+
PRIMARY KEY (`id`),
|
|
543
|
+
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
|
|
544
|
+
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='Seata回滚日志表';
|
|
545
|
+
"""
|
|
546
|
+
mode = str(config.get('mode', 'local')).lower()
|
|
547
|
+
if mode == 'http' and not config.get('experimental_http_enabled', False):
|
|
548
|
+
raise ValueError(
|
|
549
|
+
"seata.mode=http is an experimental best-effort compensation mode; "
|
|
550
|
+
"set seata.experimental_http_enabled=true only for development tests"
|
|
551
|
+
)
|
|
552
|
+
seata_manager.configure(
|
|
553
|
+
server_addr=config.get('server_addr', 'localhost:8091'),
|
|
554
|
+
application_id=config.get('application_id', ''),
|
|
555
|
+
transaction_group=config.get('transaction_group', 'my_tx_group'),
|
|
556
|
+
mode=mode,
|
|
557
|
+
)
|