基金转换 T-4+T-5(并行组 A):convert_repository 代理侧 + locks.try_lock 非阻塞抢锁
T-4(convert_repository · agent 库 risk_convert_detail): - 新增 7 方法:insert_placeholder(占位 pending) / complete_convert(回写 completed+折算详情) / mark_failed / get_by_group_id / get_by_client_request_id(None 直接返回 None) / list_expired_candidates(hours) / mark_expired(S2 标记不硬删) - 引擎走 agent 库 rw(D20);status 5 值枚举;不与 risk_repository 混职责 - Decimal 折算字段经 _to_bind 转 float 绑定(sqlite 不支持直接绑 Decimal,同 T-3) T-5(locks.try_lock): - 单次非阻塞抢锁:抢到返回 _Token(with 进入 True),抢不到立即 _NoLock(进入 False,不等不降级) - Redis 不可用退回进程内 Lock.acquire(blocking=False);_NoLock.__enter__ 返回 False 避免 with None 报错 - 不改 run_locked(D4:其等 2s 后降级 fn(False),语义相反;3 处调用点零改动) 测试:test_convert_repository.py 6 用例 + test_locks_redis.py +4 用例(既有 6 例零改动) pytest 全量 634 passed / 3 skipped(基线 624 + 10,零回归) Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
This commit is contained in:
@@ -787,10 +787,21 @@ T-7 幂等窗口 · T-13 的 50 并发压测与性能补录 · PRD §5.3 实算
|
||||
| `list_expired_candidates(hours)` | 供 `cleanup_pending_convert.py` 取超 24h 的 `pending` |
|
||||
| `mark_expired(group_id)` | 置 `expired`(**标记不硬删**,S2) |
|
||||
|
||||
**DoD**
|
||||
- [ ] sqlite 单测覆盖:占位 → completed / 占位 → failed / 超时 → expired
|
||||
- [ ] `client_request_id` 为 `None` 时不受 `uk_idem` 约束(MySQL 允许多个 NULL;sqlite 侧需断言行为一致)
|
||||
- [ ] 不与 `risk_repository` 混职责(后者不动)
|
||||
**DoD(全部达成,见下方执行记录)**
|
||||
- [x] sqlite 单测覆盖:占位 → completed / 占位 → failed / 超时 → expired
|
||||
- [x] `client_request_id` 为 `None` 时不受 `uk_idem` 约束(MySQL 允许多个 NULL;sqlite 侧断言一致)
|
||||
- [x] 不与 `risk_repository` 混职责(后者不动)
|
||||
|
||||
**执行记录(2026-09-10)**
|
||||
|
||||
| 项 | 内容 |
|
||||
| --- | --- |
|
||||
| 新增文件 | `app/repository/convert_repository.py`【agent 库 `risk_convert_detail` 读写】· `tests/test_convert_repository.py`【6 用例】 |
|
||||
| 方法 | `insert_placeholder`(占位 `pending`,`estimated=0` 真实请求)· `complete_convert`(回写 `completed`+折算详情)· `mark_failed` · `get_by_group_id` · `get_by_client_request_id`(None 直接返回 None)· `list_expired_candidates(hours)`(pending 且 `created_at < now-hours`)· `mark_expired`(S2 标记不硬删) |
|
||||
| status 枚举 | 5 值 `pending/completed/failed/cancelled/expired`(架构 §9,建表即全量、零 ALTER) |
|
||||
| 引擎 | `get_engine(settings.mysql_database, "rw")`(D20:`xh_agent_rw` 含 `risk_convert_detail`) |
|
||||
| 踩坑(同 T-3) | `complete_convert` 的 `nav`/`fee_amount` 为 Decimal → 经 `_to_bind` 转 `float` 绑定(sqlite 不支持直接绑 Decimal) |
|
||||
| 验证 | `pytest tests/test_convert_repository.py` 6 passed;全量 634 passed / 3 skipped(基线 624 + 10) |
|
||||
|
||||
**依赖**:T-1
|
||||
|
||||
@@ -806,10 +817,20 @@ def try_lock(key: str, ttl_seconds: int = LOCK_TTL_SECONDS) -> _Token | None:
|
||||
- **不改 `run_locked`**(D4:`run_locked` 会等 2s 并降级 `fn(False)`,与「抢不到立即 202」语义相反;且它已有 3 处调用:`alert_service.py:180` 的 `agg:event:` / `:239` 的 `agg:suitability:` 等)
|
||||
- 返回 `_NoLock` 哨兵对象,`__enter__` 返回 `False`,避免 `with None` 报错
|
||||
|
||||
**DoD**
|
||||
- [ ] 抢到 / 抢不到 / Redis 不可用回退 三条路径单测
|
||||
- [ ] key 形态覆盖:`convert:idem:{cid_req}` / `convert:rerun:{gid}`
|
||||
- [ ] `run_locked` 的既有用例零改动
|
||||
**DoD(全部达成,见下方执行记录)**
|
||||
- [x] 抢到 / 抢不到 / Redis 不可用回退 三条路径单测
|
||||
- [x] key 形态覆盖:`convert:idem:{cid_req}` / `convert:rerun:{gid}`
|
||||
- [x] `run_locked` 的既有用例零改动
|
||||
|
||||
**执行记录(2026-09-10)**
|
||||
|
||||
| 项 | 内容 |
|
||||
| --- | --- |
|
||||
| 改动文件 | `app/service/risk/locks.py`【改·+`_Token`/`_NoLock` + `try_lock`】· `tests/test_locks_redis.py`【+4 用例】 |
|
||||
| 语义 | `try_lock` **单次非阻塞**:抢到返回 `_Token`(`with` 进入 True),抢不到立即 `_NoLock`(进入 False,不等、不降级);Redis 不可用 → 退回进程内 `Lock.acquire(blocking=False)`(语义一致) |
|
||||
| 不改 | `run_locked`(D4:其会等 2s 后降级 `fn(False)`,与「抢不到立即 202」相反;3 处调用点零改动) |
|
||||
| 哨兵 | `_NoLock.__enter__` 返回 False,避免 `with None` 报错;`_Token.__exit__` 按来源释放 Redis 或进程内锁 |
|
||||
| 验证 | `pytest tests/test_locks_redis.py` 10 passed(既有 6 + 新增 4);全量 634 passed / 3 skipped |
|
||||
|
||||
**依赖**:无
|
||||
|
||||
|
||||
Reference in New Issue
Block a user