diff --git a/ATestCServer/manage.py b/ATestCServer/manage.py index 7db3d212..6ef1a1d9 100644 --- a/ATestCServer/manage.py +++ b/ATestCServer/manage.py @@ -7,9 +7,16 @@ def main(): sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) os.environ['DJANGO_SETTINGS_MODULE'] = 'ATestCServer.settings' os.environ['DB_NAME'] = 'atestc' - os.environ.setdefault('DJANGO_SECRET_KEY', 'gvsds-django-prod-k8x2m9v4n7p1q6w3j5r0t8y1u4i7o0a2s6d9f3g5h7j2k4l6z8x1c3v5b') - os.environ.setdefault('MSYNC_SERVICE_SECRET', 'atestc-msync-secret') - os.environ.setdefault('DB_PASSWORD', '1Qq@1Qq@190*') + # 安全:禁止硬编码默认密钥/口令,缺失时显式报错(settings.py 亦有 + # ImproperlyConfigured 兜底)。MSYNC_SERVICE_SECRET 与 DB_PASSWORD + # 由部署环境(环境变量)提供。 + required_envs = ['DJANGO_SECRET_KEY', 'MSYNC_SERVICE_SECRET', 'DB_PASSWORD'] + missing = [name for name in required_envs if not os.environ.get(name)] + if missing: + raise SystemExit( + '缺少必需的环境变量: ' + ', '.join(missing) + + '(禁止使用默认值,请通过环境变量注入)' + ) try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/ATestCServer/msync_client/sio_client.py b/ATestCServer/msync_client/sio_client.py index 77fbf94e..5fec7ab5 100644 --- a/ATestCServer/msync_client/sio_client.py +++ b/ATestCServer/msync_client/sio_client.py @@ -103,10 +103,40 @@ class MSyncSocketClient: logger.info(f'[SIO] → connect url={url} ns=/msync') self._sio.connect(url, namespaces=['/msync'], wait=True, wait_timeout=15) except Exception as e: - logger.warning(f'Socket.IO 连接失败: {e},将自动重连') - self._StartHeartbeat() + logger.warning(f'Socket.IO 连接失败: {e},启动兜底重连线程') + self._StartBootstrap() return + self._OnReady() + + def _StartBootstrap(self): + """初次连接失败的兜底:循环重连,成功后走完整注册/认证流程 + + python-socketio 的 reconnection 仅在"已连接后断开"时生效, + 初次 connect 失败不会自动重连,需自行兜底。 + """ + + def bootstrap(): + while self._running and not self._sio.connected: + try: + logger.info(f'[SIO] 兜底重连尝试: {self.MasterURL}') + self._sio.connect( + self.MasterURL, + namespaces=['/msync'], + wait=True, + wait_timeout=10, + ) + break + except Exception as e: + logger.warning(f'[SIO] 兜底重连失败: {e}') + time.sleep(3) + if self._sio.connected: + self._OnReady() + + threading.Thread(target=bootstrap, daemon=True, name='msync-bootstrap').start() + + def _OnReady(self): + """连接建立后的统一就绪流程:注册/认证/心跳/ORM 中转""" logger.info(f'[SIO] Socket 已连接, 开始注册/认证...') for attempt in range(1, 4): reg_ok = self._DoRegister() diff --git a/gvsds/tests/test_msync_saga.py b/gvsds/tests/test_msync_saga.py index aa5514de..d7b1a075 100644 --- a/gvsds/tests/test_msync_saga.py +++ b/gvsds/tests/test_msync_saga.py @@ -53,7 +53,7 @@ def _Run(): from unittest import mock import uuid - from gvsdsdk.msync.transaction import TransactionManager, StepExecutionError + from gvsdsdk.msync.transaction import TransactionManager, StepExecutionError, TransactionError from gvsdsdk.msync.local_tx import local_tx from gvsdsdk.msync.models import ServiceNode @@ -164,6 +164,53 @@ def _Run(): 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) + # ─── 汇总 ─── passed = sum(1 for _, ok in results if ok) print(f'\n结果: {passed}/{len(results)} 通过')