加入了 GVSDSDK 模块,进行了 QModel 兼容层的尝试,生产环境可用
This commit is contained in:
73
gvsdsdk/msync/__init__.py
Normal file
73
gvsdsdk/msync/__init__.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""GVS-DSDK MSYNC 子包
|
||||
|
||||
将 MSYNC 相关模块统一组织在 gvsdsdk.msync 命名空间下。
|
||||
|
||||
自有实现(无 Django 依赖,子服务器可直接使用):
|
||||
- auth: HMAC-SHA256 签名验证、服务令牌编解码
|
||||
- client: 微服务通信客户端(REST)
|
||||
- local_tx: 本地事务步骤处理器
|
||||
|
||||
服务端转发(依赖 Django ORM,仅在主服务器可用):
|
||||
- registry: 服务注册/发现
|
||||
- ogm: 对象全局映射
|
||||
- transaction: Saga 分布式事务
|
||||
"""
|
||||
|
||||
# 自有实现(无 Django 依赖,始终可用)
|
||||
from gvsdsdk.msync.auth import ( # noqa
|
||||
SignPayload,
|
||||
VerifySignature,
|
||||
GenerateChallenge,
|
||||
BuildAuthPayload,
|
||||
SignAuthPayload,
|
||||
VerifyAuthPayload,
|
||||
ServiceTokenEncoder,
|
||||
)
|
||||
from gvsdsdk.msync.client import ServiceClient, ServiceClientError # noqa
|
||||
from gvsdsdk.msync.local_tx import LocalTransactionManager, StepHandler, local_tx # noqa
|
||||
|
||||
# 服务端转发(Django ORM 依赖,延迟导入避免子服务器初始化报错)
|
||||
def __getattr__(name):
|
||||
_server_exports = {
|
||||
'ServiceRegistry': 'gvsdsdk.msync.registry',
|
||||
'RegistryError': 'gvsdsdk.msync.registry',
|
||||
'OGMManager': 'gvsdsdk.msync.ogm',
|
||||
'OGMError': 'gvsdsdk.msync.ogm',
|
||||
'TransactionManager': 'gvsdsdk.msync.transaction',
|
||||
'TransactionError': 'gvsdsdk.msync.transaction',
|
||||
'StepExecutionError': 'gvsdsdk.msync.transaction',
|
||||
}
|
||||
if name in _server_exports:
|
||||
import importlib
|
||||
mod = importlib.import_module(_server_exports[name])
|
||||
return getattr(mod, name)
|
||||
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
||||
|
||||
|
||||
__all__ = [
|
||||
# auth (自有)
|
||||
'SignPayload',
|
||||
'VerifySignature',
|
||||
'GenerateChallenge',
|
||||
'BuildAuthPayload',
|
||||
'SignAuthPayload',
|
||||
'VerifyAuthPayload',
|
||||
'ServiceTokenEncoder',
|
||||
# client (自有)
|
||||
'ServiceClient',
|
||||
'ServiceClientError',
|
||||
# local_tx (自有)
|
||||
'LocalTransactionManager',
|
||||
'StepHandler',
|
||||
'local_tx',
|
||||
# registry (服务端转发)
|
||||
'ServiceRegistry',
|
||||
'RegistryError',
|
||||
# ogm (服务端转发)
|
||||
'OGMManager',
|
||||
'OGMError',
|
||||
# transaction (服务端转发)
|
||||
'TransactionManager',
|
||||
'TransactionError',
|
||||
'StepExecutionError',
|
||||
]
|
||||
130
gvsdsdk/msync/auth.py
Normal file
130
gvsdsdk/msync/auth.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""MSYNC 认证模块
|
||||
|
||||
提供 HMAC-SHA256 签名验证、服务令牌编解码等纯函数。
|
||||
Django ORM 依赖的函数(AuthenticateService, IssueServiceToken 等)
|
||||
仍在 gvsds.apps.msync.auth 中,由服务端调用。
|
||||
"""
|
||||
|
||||
import hmac
|
||||
import hashlib
|
||||
import time
|
||||
import json
|
||||
import base64
|
||||
import uuid
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def SignPayload(Payload, Secret):
|
||||
"""使用 HMAC-SHA256 对载荷签名"""
|
||||
if isinstance(Payload, dict):
|
||||
Payload = json.dumps(Payload, sort_keys=True, separators=(',', ':'))
|
||||
if isinstance(Payload, str):
|
||||
Payload = Payload.encode('utf-8')
|
||||
return hmac.new(Secret.encode('utf-8'), Payload, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def VerifySignature(Payload, Signature, Secret):
|
||||
"""验证 HMAC-SHA256 签名"""
|
||||
Expected = SignPayload(Payload, Secret)
|
||||
return hmac.compare_digest(Expected, Signature)
|
||||
|
||||
|
||||
def GenerateChallenge():
|
||||
"""生成认证挑战字符串(时间戳 + 随机数)"""
|
||||
return f'{time.time():.6f}:{uuid.uuid4().hex}'
|
||||
|
||||
|
||||
def BuildAuthPayload(ServiceName, Challenge, Timestamp=None):
|
||||
"""构建认证请求载荷"""
|
||||
if Timestamp is None:
|
||||
Timestamp = int(time.time())
|
||||
return {
|
||||
'service': ServiceName,
|
||||
'challenge': Challenge,
|
||||
'ts': Timestamp,
|
||||
}
|
||||
|
||||
|
||||
def SignAuthPayload(Payload, ServiceSecret):
|
||||
"""对认证载荷签名"""
|
||||
Canonical = json.dumps(Payload, sort_keys=True, separators=(',', ':'))
|
||||
return SignPayload(Canonical, ServiceSecret)
|
||||
|
||||
|
||||
def VerifyAuthPayload(Payload, Signature, ServiceSecret):
|
||||
"""验证认证载荷签名"""
|
||||
Canonical = json.dumps(Payload, sort_keys=True, separators=(',', ':'))
|
||||
return VerifySignature(Canonical, Signature, ServiceSecret)
|
||||
|
||||
|
||||
class ServiceTokenEncoder:
|
||||
"""服务令牌编解码器——自包含令牌,无需数据库查询即可验证"""
|
||||
|
||||
ALGORITHM = 'HS256'
|
||||
ISSUER = 'MSYNC'
|
||||
|
||||
def __init__(self, SecretKey=None):
|
||||
self.SecretKey = SecretKey or ''
|
||||
|
||||
def Encode(self, service_name=None, scope=None, expires_at=None, master_secret=None, **kwargs):
|
||||
"""编码令牌
|
||||
|
||||
支持两种调用方式:
|
||||
1. 位置参数: Encode(payload_dict) — payload 包含所有字段
|
||||
2. 关键字参数: Encode(service_name=..., scope=..., expires_at=..., master_secret=...)
|
||||
"""
|
||||
# 如果传入的是字典作为第一个位置参数
|
||||
if service_name is not None and isinstance(service_name, dict) and scope is None:
|
||||
Payload = service_name
|
||||
Secret = master_secret or self.SecretKey
|
||||
else:
|
||||
Secret = master_secret or self.SecretKey
|
||||
Payload = {
|
||||
'iss': self.ISSUER,
|
||||
'sub': service_name,
|
||||
'scope': scope or [],
|
||||
}
|
||||
if expires_at is not None:
|
||||
if hasattr(expires_at, 'timestamp'):
|
||||
Payload['exp'] = int(expires_at.timestamp())
|
||||
else:
|
||||
Payload['exp'] = int(expires_at)
|
||||
|
||||
Header = json.dumps({'alg': self.ALGORITHM, 'typ': 'JWT'})
|
||||
PayloadData = json.dumps(Payload, sort_keys=True, separators=(',', ':'), default=str)
|
||||
HeaderB64 = base64.urlsafe_b64encode(Header.encode()).rstrip(b'=').decode()
|
||||
PayloadB64 = base64.urlsafe_b64encode(PayloadData.encode()).rstrip(b'=').decode()
|
||||
SigningInput = f'{HeaderB64}.{PayloadB64}'
|
||||
Signature = hmac.new(
|
||||
Secret.encode('utf-8'),
|
||||
SigningInput.encode('utf-8'),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
return f'{SigningInput}.{Signature}'
|
||||
|
||||
def Decode(self, Token, SecretKey=None):
|
||||
"""解码并验证令牌"""
|
||||
Secret = SecretKey or self.SecretKey
|
||||
try:
|
||||
Parts = Token.split('.')
|
||||
if len(Parts) != 3:
|
||||
return None
|
||||
HeaderB64, PayloadB64, Signature = Parts
|
||||
SigningInput = f'{HeaderB64}.{PayloadB64}'
|
||||
ExpectedSig = hmac.new(
|
||||
Secret.encode('utf-8'),
|
||||
SigningInput.encode('utf-8'),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(ExpectedSig, Signature):
|
||||
return None
|
||||
Padding = 4 - len(PayloadB64) % 4
|
||||
if Padding != 4:
|
||||
PayloadB64 += '=' * Padding
|
||||
PayloadJSON = base64.urlsafe_b64decode(PayloadB64).decode('utf-8')
|
||||
return json.loads(PayloadJSON)
|
||||
except Exception:
|
||||
return None
|
||||
275
gvsdsdk/msync/client.py
Normal file
275
gvsdsdk/msync/client.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""MSYNC 服务客户端模块
|
||||
|
||||
提供 ServiceClient 类,用于微服务间发起认证请求。
|
||||
支持自动令牌获取与刷新、OGM 解析、服务发现。
|
||||
本模块自包含实现(使用 urllib),不依赖 Django。
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
MSYNC_AUTH_PATH = '/api/msync/auth/token/'
|
||||
MSYNC_AUTH_REFRESH_PATH = '/api/msync/auth/token/refresh/'
|
||||
MSYNC_OGM_RESOLVE_PATH = '/api/msync/ogm/{global_id}/'
|
||||
MSYNC_REGISTRY_PATH = '/api/msync/registry/{name}/'
|
||||
|
||||
from gvsdsdk.msync.auth import SignPayload, BuildAuthPayload
|
||||
|
||||
|
||||
def _GenerateChallenge():
|
||||
return f'{int(time.time() * 1000)}-{id(object())}'
|
||||
|
||||
|
||||
class ServiceClientError(Exception):
|
||||
def __init__(self, Message, StatusCode=None, ResponseBody=None):
|
||||
super().__init__(Message)
|
||||
self.StatusCode = StatusCode
|
||||
self.ResponseBody = ResponseBody
|
||||
|
||||
|
||||
class _TokenStore:
|
||||
"""内存中的令牌存储"""
|
||||
|
||||
def __init__(self):
|
||||
self._AccessToken = None
|
||||
self._RefreshToken = None
|
||||
self._ExpiresAt = 0.0
|
||||
self._Scope = []
|
||||
|
||||
@property
|
||||
def AccessToken(self):
|
||||
return self._AccessToken
|
||||
|
||||
@property
|
||||
def RefreshToken(self):
|
||||
return self._RefreshToken
|
||||
|
||||
@property
|
||||
def Scope(self):
|
||||
return self._Scope
|
||||
|
||||
def IsValid(self):
|
||||
return self._AccessToken is not None and time.time() < self._ExpiresAt
|
||||
|
||||
def Store(self, AccessToken, RefreshToken, ExpiresAt, Scope=None):
|
||||
self._AccessToken = AccessToken
|
||||
self._RefreshToken = RefreshToken
|
||||
self._ExpiresAt = ExpiresAt
|
||||
self._Scope = Scope or []
|
||||
|
||||
def Clear(self):
|
||||
self._AccessToken = None
|
||||
self._RefreshToken = None
|
||||
self._ExpiresAt = 0.0
|
||||
self._Scope = []
|
||||
|
||||
|
||||
class ServiceClient:
|
||||
"""微服务通信客户端——用于向其他服务发起认证请求
|
||||
|
||||
用法:
|
||||
client = ServiceClient(
|
||||
ServiceName='gerp',
|
||||
ServiceSecret='your-secret',
|
||||
)
|
||||
client.Authenticate()
|
||||
|
||||
# 获取其他服务信息
|
||||
info = client.DiscoverService('crb')
|
||||
|
||||
# 解析 OGM
|
||||
ogm = client.ResolveOGM('user:abc123')
|
||||
|
||||
# 向其他服务发请求
|
||||
response = client.Request('GET', 'https://crb.gvsds.com/api/wallet', Scope=['ledger:read'])
|
||||
"""
|
||||
|
||||
def __init__(self, ServiceName, ServiceSecret, MasterURL=None):
|
||||
self.ServiceName = ServiceName
|
||||
self.ServiceSecret = ServiceSecret
|
||||
if MasterURL is None:
|
||||
MasterURL = 'https://api.gvsds.com'
|
||||
self.MasterURL = MasterURL.rstrip('/')
|
||||
self._tokens = _TokenStore()
|
||||
|
||||
@property
|
||||
def IsAuthenticated(self):
|
||||
return self._tokens.IsValid()
|
||||
|
||||
@property
|
||||
def AccessToken(self):
|
||||
return self._tokens.AccessToken
|
||||
|
||||
@property
|
||||
def RefreshToken(self):
|
||||
return self._tokens.RefreshToken
|
||||
|
||||
def Authenticate(self):
|
||||
if not self._Authenticate():
|
||||
raise ServiceClientError('认证失败')
|
||||
return self
|
||||
|
||||
def Refresh(self):
|
||||
if not self._RefreshToken():
|
||||
raise ServiceClientError('刷新失败')
|
||||
return self
|
||||
|
||||
def _Authenticate(self):
|
||||
challenge = _GenerateChallenge()
|
||||
payload = BuildAuthPayload(self.ServiceName, challenge)
|
||||
signature = SignPayload(payload, self.ServiceSecret)
|
||||
|
||||
req_body = json.dumps({
|
||||
'ServiceName': self.ServiceName,
|
||||
'Payload': payload,
|
||||
'Signature': signature,
|
||||
}).encode('utf-8')
|
||||
|
||||
req = urllib.request.Request(
|
||||
f'{self.MasterURL}{MSYNC_AUTH_PATH}',
|
||||
data=req_body,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='POST',
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
data = json.loads(resp.read().decode('utf-8'))
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode('utf-8', errors='replace')
|
||||
logger.error(f'认证失败 HTTP {e.code}: {body}')
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f'认证异常: {e}')
|
||||
return False
|
||||
|
||||
if data.get('code') not in (200, 0):
|
||||
logger.error(f'认证业务错误: {data}')
|
||||
return False
|
||||
|
||||
token_data = data.get('data') or {}
|
||||
self._tokens.Store(
|
||||
AccessToken=token_data.get('AccessToken'),
|
||||
RefreshToken=token_data.get('RefreshToken'),
|
||||
ExpiresAt=token_data.get('ExpiresAt', 0),
|
||||
Scope=token_data.get('Scope', []),
|
||||
)
|
||||
return self._tokens.AccessToken is not None
|
||||
|
||||
def _RefreshToken(self):
|
||||
if not self._tokens.RefreshToken:
|
||||
return False
|
||||
|
||||
req_body = json.dumps({
|
||||
'ServiceName': self.ServiceName,
|
||||
'RefreshToken': self._tokens.RefreshToken,
|
||||
}).encode('utf-8')
|
||||
|
||||
req = urllib.request.Request(
|
||||
f'{self.MasterURL}{MSYNC_AUTH_REFRESH_PATH}',
|
||||
data=req_body,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='POST',
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
data = json.loads(resp.read().decode('utf-8'))
|
||||
except Exception as e:
|
||||
logger.error(f'刷新令牌异常: {e}')
|
||||
return False
|
||||
|
||||
if data.get('code') not in (200, 0):
|
||||
return False
|
||||
|
||||
token_data = data.get('data') or {}
|
||||
self._tokens.Store(
|
||||
AccessToken=token_data.get('AccessToken'),
|
||||
RefreshToken=token_data.get('RefreshToken'),
|
||||
ExpiresAt=token_data.get('ExpiresAt', 0),
|
||||
Scope=token_data.get('Scope', []),
|
||||
)
|
||||
return self._tokens.AccessToken is not None
|
||||
|
||||
def _EnsureAuthenticated(self):
|
||||
if self.IsAuthenticated:
|
||||
return
|
||||
if self._tokens.RefreshToken:
|
||||
self._RefreshToken()
|
||||
return
|
||||
self._Authenticate()
|
||||
|
||||
def Request(self, Method, URL, Data=None, Headers=None, Scope=None, Timeout=30):
|
||||
self._EnsureAuthenticated()
|
||||
|
||||
req_headers = {
|
||||
'Authorization': f'Service {self.AccessToken}',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Source-Service': self.ServiceName,
|
||||
}
|
||||
|
||||
if Scope:
|
||||
req_headers['X-Required-Scope'] = ','.join(Scope)
|
||||
|
||||
if Headers:
|
||||
req_headers.update(Headers)
|
||||
|
||||
body = None
|
||||
if Data is not None:
|
||||
if isinstance(Data, dict):
|
||||
body = json.dumps(Data).encode('utf-8')
|
||||
elif isinstance(Data, bytes):
|
||||
body = Data
|
||||
elif isinstance(Data, str):
|
||||
body = Data.encode('utf-8')
|
||||
|
||||
req = urllib.request.Request(URL, data=body, headers=req_headers, method=Method)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=Timeout) as resp:
|
||||
response_data = resp.read().decode('utf-8')
|
||||
content_type = resp.headers.get('Content-Type', '')
|
||||
if 'application/json' in content_type:
|
||||
return json.loads(response_data)
|
||||
return response_data
|
||||
except urllib.error.HTTPError as e:
|
||||
body_text = e.read().decode('utf-8', errors='replace')
|
||||
if e.code == 401:
|
||||
logger.info('令牌可能已失效,尝试刷新')
|
||||
self._tokens.Clear()
|
||||
self._EnsureAuthenticated()
|
||||
req_headers['Authorization'] = f'Service {self.AccessToken}'
|
||||
retry_req = urllib.request.Request(URL, data=body, headers=req_headers, method=Method)
|
||||
try:
|
||||
with urllib.request.urlopen(retry_req, timeout=Timeout) as resp2:
|
||||
response_data = resp2.read().decode('utf-8')
|
||||
content_type = resp2.headers.get('Content-Type', '')
|
||||
if 'application/json' in content_type:
|
||||
return json.loads(response_data)
|
||||
return response_data
|
||||
except urllib.error.HTTPError as e2:
|
||||
body2 = e2.read().decode('utf-8', errors='replace')
|
||||
raise ServiceClientError(
|
||||
f'重试后仍失败: HTTP {e2.code}',
|
||||
StatusCode=e2.code,
|
||||
ResponseBody=body2,
|
||||
)
|
||||
raise ServiceClientError(
|
||||
f'请求失败: HTTP {e.code}',
|
||||
StatusCode=e.code,
|
||||
ResponseBody=body_text,
|
||||
)
|
||||
|
||||
def ResolveOGM(self, GlobalID):
|
||||
url = f'{self.MasterURL}{MSYNC_OGM_RESOLVE_PATH.format(global_id=GlobalID)}'
|
||||
return self.Request('GET', url, Scope=['ogm:read'])
|
||||
|
||||
def DiscoverService(self, ServiceName):
|
||||
url = f'{self.MasterURL}{MSYNC_REGISTRY_PATH.format(name=ServiceName)}'
|
||||
return self.Request('GET', url, Scope=['registry:read'])
|
||||
105
gvsdsdk/msync/local_tx.py
Normal file
105
gvsdsdk/msync/local_tx.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""本地事务管理器
|
||||
|
||||
每个工作服务运行一个 LocalTransactionManager 实例,
|
||||
负责注册本服务的步骤处理器(action + compensate),
|
||||
并在收到主服务器请求时执行本地逻辑。
|
||||
|
||||
用法(在服务启动时注册处理器):
|
||||
from gvsdsdk.msync.local_tx import local_tx
|
||||
|
||||
local_tx.register(
|
||||
'create_user',
|
||||
action=create_user_action,
|
||||
compensate=create_user_compensate,
|
||||
)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StepHandler:
|
||||
"""步骤处理器——封装 action 与 compensate 函数"""
|
||||
|
||||
def __init__(self, name, action, compensate=None):
|
||||
self.Name = name
|
||||
self.Action = action
|
||||
self.Compensate = compensate
|
||||
|
||||
def __repr__(self):
|
||||
return f'<StepHandler:{self.Name}>'
|
||||
|
||||
|
||||
class LocalTransactionManager:
|
||||
"""本地事务管理器——每个工作服务一个实例"""
|
||||
|
||||
def __init__(self):
|
||||
self._handlers = {}
|
||||
|
||||
def register(self, name, action, compensate=None):
|
||||
"""注册步骤处理器"""
|
||||
if name in self._handlers:
|
||||
logger.warning(f'步骤处理器已存在,覆盖: {name}')
|
||||
self._handlers[name] = StepHandler(
|
||||
name=name,
|
||||
action=action,
|
||||
compensate=compensate,
|
||||
)
|
||||
logger.info(f'步骤处理器注册: {name}')
|
||||
|
||||
def unregister(self, name):
|
||||
"""注销步骤处理器"""
|
||||
if name in self._handlers:
|
||||
del self._handlers[name]
|
||||
logger.info(f'步骤处理器注销: {name}')
|
||||
|
||||
def has_handler(self, name):
|
||||
"""检查是否注册了指定步骤处理器"""
|
||||
return name in self._handlers
|
||||
|
||||
def list_handlers(self):
|
||||
"""列出所有已注册的步骤处理器名称"""
|
||||
return list(self._handlers.keys())
|
||||
|
||||
def execute_step(self, name, payload=None):
|
||||
"""执行步骤——调用本地 action 函数"""
|
||||
handler = self._handlers.get(name)
|
||||
if handler is None:
|
||||
msg = f'未注册的步骤处理器: {name}'
|
||||
logger.error(msg)
|
||||
return {'success': False, 'result': None, 'error': msg}
|
||||
|
||||
try:
|
||||
result = handler.Action(payload or {})
|
||||
logger.info(f'步骤执行成功: {name}')
|
||||
return {'success': True, 'result': result, 'error': None}
|
||||
except Exception as e:
|
||||
msg = f'步骤执行失败: {name}: {e}\n{traceback.format_exc()}'
|
||||
logger.error(msg)
|
||||
return {'success': False, 'result': None, 'error': str(e)}
|
||||
|
||||
def compensate_step(self, name, payload=None, action_result=None):
|
||||
"""补偿步骤——调用本地 compensate 函数"""
|
||||
handler = self._handlers.get(name)
|
||||
if handler is None:
|
||||
msg = f'未注册的步骤处理器: {name}'
|
||||
logger.error(msg)
|
||||
return {'success': False, 'result': None, 'error': msg}
|
||||
|
||||
if handler.Compensate is None:
|
||||
logger.warning(f'步骤无补偿函数,跳过: {name}')
|
||||
return {'success': True, 'result': None, 'error': None}
|
||||
|
||||
try:
|
||||
result = handler.Compensate(payload or {}, action_result or {})
|
||||
logger.info(f'步骤补偿成功: {name}')
|
||||
return {'success': True, 'result': result, 'error': None}
|
||||
except Exception as e:
|
||||
msg = f'步骤补偿失败: {name}: {e}\n{traceback.format_exc()}'
|
||||
logger.error(msg)
|
||||
return {'success': False, 'result': None, 'error': str(e)}
|
||||
|
||||
|
||||
local_tx = LocalTransactionManager()
|
||||
11
gvsdsdk/msync/ogm.py
Normal file
11
gvsdsdk/msync/ogm.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""MSYNC OGM(对象全局映射)模块
|
||||
|
||||
薄封装:直接转发到 gvsds.apps.msync.ogm 原生层。
|
||||
"""
|
||||
|
||||
from gvsds.apps.msync.ogm import OGMManager, OGMError
|
||||
|
||||
__all__ = [
|
||||
'OGMManager',
|
||||
'OGMError',
|
||||
]
|
||||
11
gvsdsdk/msync/registry.py
Normal file
11
gvsdsdk/msync/registry.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""MSYNC 注册中心模块
|
||||
|
||||
薄封装:直接转发到 gvsds.apps.msync.registry 原生层。
|
||||
"""
|
||||
|
||||
from gvsds.apps.msync.registry import ServiceRegistry, RegistryError
|
||||
|
||||
__all__ = [
|
||||
'ServiceRegistry',
|
||||
'RegistryError',
|
||||
]
|
||||
Reference in New Issue
Block a user