订单跟踪bug16
订单跟踪系统 Bug 分析报告
分析日期:2026-02-21
涉及文件:
src/trading/websocket_order_manager.pysrc/utils/websocket/enhanced_ws_manager.pysrc/services/realtime_kline_service_base.py
已在 debug 8(最新提交)中修复的严重 Bug
🔴 Bug 1(已修复):_resolve_via_http 中 tracking 变量被覆盖 → finally 块崩溃 + HTTP 锁永久泄漏
位置:src/trading/websocket_order_manager.py:433
debug 7 中的问题代码:
acquired = tracking._http_lock.acquire(blocking=False) # 获取 T1 的锁
try:
...
with self._lock:
tracking = self._tracking.get(oid) # ← 覆盖了 tracking!
if not tracking or ...:
return "resolved" # tracking 可能变为 None
...
finally:
tracking._http_lock.release() # AttributeError: NoneType!
触发场景:
- 外层锁获取 T1,释放锁
- 获取 T1 的
_http_lock成功 - WS 路径调用
_resolve(oid)→ T1 被 pop 出 map - 内层锁中
self._tracking.get(oid)返回None return "resolved"触发finallytracking._http_lock.release()→AttributeError崩溃,T1 的_http_lock永久泄漏
后果:后续任何 _resolve_via_http(oid) 调用都会因 _http_lock 不可获取而永久返回 "busy",订单永久卡在 PENDING,wait_for_order 永久阻塞。
debug 8 修复方案:增加 lock_owner = tracking(第 452 行),finally 始终释放原始对象的锁;内层锁改用 current 变量避免覆盖原始引用。
lock_owner = tracking # 固定持锁对象引用
try:
...
with self._lock:
current = self._tracking.get(oid) # 用 current,不覆盖 tracking
if not current or current.status != OrderStatus.PENDING:
return "resolved"
current._terminal_status = status
...
finally:
lock_owner._http_lock.release() # 始终释放原始对象的锁
🔴 Bug 2(已修复):available_balance 取错父对象字段
位置:src/utils/websocket/enhanced_ws_manager.py:1433
# 修复前(debug 7)— margin 对象没有 withdrawable 字段,永远返回 0
available_balance=float(margin.get("withdrawable", 0)),
# 修复后(debug 8)— 正确取 data 对象的 withdrawable 字段
available_balance=float(data.get("withdrawable", 0)),
后果:账户余额事件中 available_balance 字段永远为 0,任何依赖该字段的风控逻辑或仓位管理将得到错误数据,可能导致误判可用余额充足而超额下单。
🟠 Bug 3(已修复):订单消息缓冲区无上限 → 内存无限增长
位置:src/services/realtime_kline_service_base.py:190
# 修复前 — 无 maxlen,订单管理器长期未就绪时缓冲区无限膨胀
self._order_msg_buffer: deque = deque()
# 修复后 — 有界,自动丢弃最早消息,防止 OOM
self._order_msg_buffer: deque = deque(maxlen=500)
后果(修复前):若交易模块初始化延迟,orderUpdates/userFills 消息会持续入队,缓冲区无限增长,最终导致内存溢出或进程 OOM。
🟡 功能改进(已合入 debug 8)
| 项 | 修复前 | 修复后 |
|---|---|---|
verify_pending_orders 重连补查 |
HTTP 失败直接忽略,完全依赖超时兜底 | 收集失败 oid,3 秒后统一重试一次;二次失败才交超时机制兜底 |
| 订阅重连锁持有时间 | 持锁贯穿整个 batch sleep(阻塞 add_subscriptions 数秒至数分钟) |
分 3 阶段:① 持锁清空(极快)→ ② 锁外发送+sleep → ③ 持锁写入成功订阅(极快) |
文档记录的 Bug 2 在当前代码中不存在
订单跟踪bug1.md 描述了 _resolve() 中 pop(oid) 可能移除"错误的" tracking 的问题。在当前代码中该 bug 不存在——_resolve() 的 get(oid) 和 pop(oid) 均在同一个 with self._lock: 块内原子执行,持锁期间不可能有其他线程插入 track_order(oid) 替换 tracking 对象。
当前代码中仍存在的潜在风险
⚠️ 潜在竞态:_resolve_via_http 两段锁之间发生 tracking 替换
场景:
_resolve_via_http内层锁:找到 T1(current = T1),设T1._terminal_status = FILLED,释放锁track_order(oid)创建 T2,T1 被标记 CANCELED +T1.result_event.set()self._resolve(oid)被调用 → 找到 T2,T2._terminal_status is None→ 直接返回,不解析 T2
结果:
- T1 被
track_order标记为 CANCELED(T1 的等待者收到 CANCELED 结果) - T2 的终态需等到自己的超时线程(
_timeout_then_verify)触发才能通过 HTTP 兜底解析
在实际场景中 T1 和 T2 代表同一 exchange oid,该竞态出现概率极低,但一旦触发会导致 T2 等待时间被拉长至超时阈值(默认 600 秒)才能结算。
建议修复方向:在 _resolve_via_http 内层锁中,若检测到 current is not tracking(即 tracking 已被替换),放弃设置 current._terminal_status,直接返回 "resolved"(旧 tracking 已由 track_order 处理完毕)。
修复状态汇总
| 优先级 | Bug | 状态 |
|---|---|---|
| P0 | _resolve_via_http finally 块崩溃 + _http_lock 永久泄漏 |
✅ debug 8 已修复 |
| P0 | available_balance 取值错误(margin vs data) |
✅ debug 8 已修复 |
| P1 | 订单消息缓冲区无上限内存泄漏 | ✅ debug 8 已修复 |
| P1 | verify_pending_orders 缺少重试逻辑 |
✅ debug 8 已修复 |
| P1 | 订阅重连持锁时间过长阻塞并发操作 | ✅ debug 8 已修复 |
| P2 | 两段锁之间 track_order 替换导致 T2 等到超时才解析 |
⚠️ 残留风险,概率极低 |