diff --git a/docs/项目框架设计/开发计划-基金转换交易.md b/docs/项目框架设计/开发计划-基金转换交易.md index 3488f29..3f76ff5 100644 --- a/docs/项目框架设计/开发计划-基金转换交易.md +++ b/docs/项目框架设计/开发计划-基金转换交易.md @@ -1511,6 +1511,27 @@ sqlite 无 gap lock,故该分支由 `tests/test_convert_core.py` 用注入点 > 已恢复全部突变,`grep -rn "MUTATION\|1 = 1" app/ tests/ scripts/` **无残留**。 +### 10.6 T-13 后补:死锁 1213 服务端自动重试(2026-09-10 同日用户拍板 · 已落地 `43cf2a1`) + +§10.2 遗留的「服务端是否应重试死锁」已拍板:**服务端在数据访问层自动重试**(依据:Oracle MySQL 官方博客 +错误码表 1213 = "Retry the entire transaction")。 + +**改动**(`app/gateway/convert_core_repository.py` + `app/config/settings.py`): +- `apply_convert` 改为重试外壳 + `_apply_convert_once` 事务体:死锁方被整体回滚、库内零残留, + 重跑即重新读库(满足「重试前重新取数」); +- 只重试 **1213**(`_is_deadlock` 取底层 DBAPI `orig.args[0]` 判定);**1205 不在范围** + (其重试语义受 `innodb_rollback_on_timeout` 影响,官方口径为「默认重试语句」,与整事务重试不通用); +- 阈值走 settings:`convert_deadlock_retry_max=2` / `convert_deadlock_retry_base_ms=50`, + 退避 = base·2^(n-1) + [0, base) 随机抖动(架构 §11「阈值不得硬编码」); +- 重试安全:单事务回滚零残留 + 幂等锚点 `out_trade_id` 在 service 层不受影响。 + +**验证** +- sqlite 单测 +3:重试后落库成功(调 3 次、退避递增)/ 预算耗尽上抛(零残留)/ 1205 不重试; +- **突变 2 组精准命中后恢复**:① `_is_deadlock` 恒 False → 重试用例红;② 预算判断 `>=`→`>` → 耗尽用例红; +- 全量 `pytest -q` → **739 passed / 10 skipped**(+3); +- `CONVERT_STRESS=1` 并发 **9 passed**;**8 客户并发实测 1213×4 → 服务端重试全部第 1 次收敛、外部兜底 0 次**; +- 真库脚本复跑零回归:apply **24/24** / service **35/35**。 + --- ## 11. 测试增量与基线预测