Files
SMANAGE/gvsds/tests/test_msync_saga.py
TermiNexus 1a73483db5 fix(atest): 中风险项修复——重连锁协议/OGM 键名/RemoteCallDemo
- _DoReconnect 互斥锁移入内部,修复 force_deregister 路径锁协议破坏
- 订单 OGM 键名 GlobalID→ObjectGlobalID(映射不再丢失)
- RemoteCallDemo 对 requests.Response 先解析 JSON,避免 500
- 补充 Compensate 状态守卫回归用例(20/20 通过)
2026-08-10 23:32:10 +08:00

249 lines
9.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""MSYNC Saga 状态机回归测试(独立运行,不依赖主库)
用法:
python gvsds/tests/test_msync_saga.py
覆盖:
- 正向全部成功 → Committed,且写入 TransactionResult 汇总
- 步骤失败 → 逆序补偿已完成步骤 → Compensated
- 自动重试:步骤失败后按 CanRetry 内联重试成功 → Committed
- 补偿失败 → 事务 FAILED,RetryCompensate 成功后 → COMPENSATED
- 本地事务处理器幂等去重(同 Idempotency-Key 返回首次结果)
说明:远程调用通过 mock 注入,不发起真实 HTTP;模型建于 sqlite 内存库。
"""
import os
import sys
# 将项目根加入 sys.path(脚本可能从任意 cwd 运行)
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import django
from django.conf import settings
_TEST_SETTINGS = {
'DEBUG': True,
'SECRET_KEY': 'msync-test-key',
'USE_TZ': False,
'DATABASES': {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
},
},
'INSTALLED_APPS': [
'django.contrib.contenttypes',
'django.contrib.auth',
'gvsdsdk',
'gvsdsdk.msync',
],
}
def _Setup():
settings.configure(**_TEST_SETTINGS)
django.setup()
from django.core.management import call_command
call_command('migrate', run_syncdb=True, verbosity=0, interactive=False)
def _Run():
_Setup()
from unittest import mock
import uuid
from gvsdsdk.msync.transaction import TransactionManager, StepExecutionError, TransactionError
from gvsdsdk.msync.local_tx import local_tx
from gvsdsdk.msync.models import ServiceNode
# 注册参与服务节点(Saga 步骤的 ParticipantService)
ServiceNode.objects.create(
ServiceUUID=uuid.uuid4().bytes,
ServiceName='svc',
ServiceDomain='http://svc.local',
ServiceSecretHash='x',
ServiceStatus=1,
)
results = []
def check(name, cond):
results.append((name, cond))
print(f' {"PASS" if cond else "FAIL"} {name}')
# ─── 1. 正向全部成功 → Committed ───
print('[1] 正向全部成功')
dtx = TransactionManager.Create(
name='t1',
steps_data=[
{'name': 's1', 'ParticipantService': 'svc', 'ActionPayload': {'a': 1}},
{'name': 's2', 'ParticipantService': 'svc', 'ActionPayload': {'b': 2}},
],
)
with mock.patch.object(TransactionManager, '_call_service', side_effect=[
{'user_uuid': 'u1'}, # s1 结果 → 数据流注入 s2
{'ok': True},
]) as call:
TransactionManager.Execute(dtx.TransactionCode)
dtx.refresh_from_db()
check('状态为 Committed', dtx.TransactionStatus == 2)
check('TransactionResult 已写入', bool(dtx.TransactionResult and dtx.TransactionResult.get('Steps')))
# 数据流:s2 的 payload 应包含 s1 结果(user_uuid)
s2_payload = call.call_args_list[1].kwargs['payload']['payload']
check('步骤数据流(s1 结果注入 s2)', s2_payload.get('user_uuid') == 'u1')
# ─── 2. 步骤失败 → 补偿已完成步骤 → Compensated ───
print('[2] 步骤失败触发补偿')
dtx = TransactionManager.Create(
name='t2',
steps_data=[
{'name': 's1', 'ParticipantService': 'svc'},
{'name': 's2', 'ParticipantService': 'svc', 'MaxRetries': 0},
{'name': 's3', 'ParticipantService': 'svc'},
],
)
with mock.patch.object(TransactionManager, '_call_service', side_effect=[
{'ok': 1}, # s1 成功
StepExecutionError('s2 失败'), # s2 失败(自动重试耗尽)
{'ok': 'compensated'}, # 补偿 s1
]):
TransactionManager.Execute(dtx.TransactionCode)
dtx.refresh_from_db()
check('状态为 Compensated', dtx.TransactionStatus == 4)
steps = {s.StepIndex: s for s in dtx.steps.all()}
check('s1 已补偿', steps[0].StepStatus == 4)
check('s2 正向失败', steps[1].StepStatus == 5)
# ─── 3. 自动重试:首次失败后成功 → Committed ───
print('[3] 自动重试')
dtx = TransactionManager.Create(
name='t3',
steps_data=[
{'name': 's1', 'ParticipantService': 'svc', 'MaxRetries': 2},
],
)
with mock.patch.object(TransactionManager, '_call_service', side_effect=[
StepExecutionError('临时失败'),
{'ok': True},
]):
TransactionManager.Execute(dtx.TransactionCode)
dtx.refresh_from_db()
step = dtx.steps.first()
check('状态为 Committed', dtx.TransactionStatus == 2)
check('RetryCount=1', step.RetryCount == 1)
# ─── 4. 补偿失败 → Failed;RetryCompensate 成功后 → Compensated ───
print('[4] 补偿失败可重试')
dtx = TransactionManager.Create(
name='t4',
steps_data=[
{'name': 's1', 'ParticipantService': 'svc'},
{'name': 's2', 'ParticipantService': 'svc', 'MaxRetries': 0},
],
)
with mock.patch.object(TransactionManager, '_call_service', side_effect=[
{'ok': 1},
StepExecutionError('s2 失败'),
StepExecutionError('补偿 s1 失败'),
]):
TransactionManager.Execute(dtx.TransactionCode)
dtx.refresh_from_db()
check('补偿失败后事务 Failed', dtx.TransactionStatus == 5)
# RetryCompensate:先成功补偿 s1
with mock.patch.object(TransactionManager, '_call_service', return_value={'ok': True}):
TransactionManager.RetryCompensate(dtx.TransactionCode, 0)
dtx.refresh_from_db()
check('RetryCompensate 后 Compensated', dtx.TransactionStatus == 4)
# ─── 5. 本地事务幂等去重 ───
print('[5] local_tx 幂等')
r1 = local_tx.ExecuteStep('no_such', {}, 'K:1:0')
r2 = local_tx.ExecuteStep('no_such', {}, 'K:1:0')
check('同键幂等命中', r1 is r2)
r3 = local_tx.ExecuteStep('no_such', {}, 'K:1:1')
check('不同键不命中', r1 is not r3)
# ─── 6. RetryStep 手动重试(自动重试耗尽后仍可重试,计数分离)───
print('[6] RetryStep 手动重试')
dtx = TransactionManager.Create(
name='t6',
steps_data=[
{'name': 's1', 'ParticipantService': 'svc'},
{'name': 's2', 'ParticipantService': 'svc', 'MaxRetries': 0},
],
)
with mock.patch.object(TransactionManager, '_call_service', side_effect=[
{'ok': 1},
StepExecutionError('s2 永久失败'), # s2 自动重试耗尽
StepExecutionError('补偿 s1 也失败'), # 补偿失败 → 事务 FAILED
]):
TransactionManager.Execute(dtx.TransactionCode)
dtx.refresh_from_db()
check('自动重试耗尽后事务 Failed', dtx.TransactionStatus == 5)
# s2 MaxRetries=0 已耗尽,原逻辑(CanRetry)拒绝;现手动重试不受配额限制
with mock.patch.object(TransactionManager, '_call_service', return_value={'ok': True}):
step = TransactionManager.RetryStep(dtx.TransactionCode, 1)
check('手动重试成功 Completed', step.StepStatus == 2)
check('手动重试计数独立', step.RetryCount == 1)
# ─── 7. RetryStep 拒绝已补偿事务(事务状态守卫)───
print('[7] RetryStep 已补偿事务拒绝')
dtx = TransactionManager.Create(
name='t7',
steps_data=[
{'name': 's1', 'ParticipantService': 'svc'},
{'name': 's2', 'ParticipantService': 'svc', 'MaxRetries': 0},
],
)
with mock.patch.object(TransactionManager, '_call_service', side_effect=[
{'ok': 1},
StepExecutionError('s2 失败'),
{'ok': True}, # s1 补偿成功 → 事务 COMPENSATED
]):
TransactionManager.Execute(dtx.TransactionCode)
dtx.refresh_from_db()
check('事务已补偿', dtx.TransactionStatus == 4)
rejected = False
try:
TransactionManager.RetryStep(dtx.TransactionCode, 1) # s2 正向失败步骤
except TransactionError:
rejected = True
check('已补偿事务 RetryStep 被拒绝', rejected)
# ─── 8. Compensate 状态守卫:PENDING/COMMITTED 拒绝补偿 ───
print('[8] Compensate 状态守卫')
dtx8 = TransactionManager.Create(
name='t8',
steps_data=[{'name': 's1', 'ParticipantService': 'svc'}],
)
rejected_pending = False
try:
TransactionManager.Compensate(dtx8.TransactionCode)
except TransactionError:
rejected_pending = True
check('PENDING 事务 Compensate 被拒绝', rejected_pending)
with mock.patch.object(TransactionManager, '_call_service', return_value={'ok': True}):
TransactionManager.Execute(dtx8.TransactionCode)
dtx8.refresh_from_db()
check('事务已提交', dtx8.TransactionStatus == 2)
rejected_committed = False
try:
TransactionManager.Compensate(dtx8.TransactionCode)
except TransactionError:
rejected_committed = True
check('COMMITTED 事务 Compensate 被拒绝', rejected_committed)
# ─── 汇总 ───
passed = sum(1 for _, ok in results if ok)
print(f'\n结果: {passed}/{len(results)} 通过')
if passed != len(results):
for name, ok in results:
if not ok:
print(f' 未通过: {name}')
sys.exit(1)
if __name__ == '__main__':
_Run()