3.8 KiB
给 QMT 侧:补 ack_seq 处理(协议 §4.5)
现象
PMS 每 2 秒发一次 ack_seq,QMT 侧回:
{"instruction_id": null, "code": "BAD_PARAM", "reason": "unknown type ack_seq", "retryable": false}
定位到 handlers.py 的 handle():类型分发链里有 hello / ping / place_order /
cancel_order / query_positions / query_funds / query_orders,唯独没有 ack_seq,
所以落到了最后一行的兜底:
self._reject_raw(envelope, 'BAD_PARAM', f'unknown type {msg_type}', False)
对过一遍:我方下行 8 个类型,贵方实现了 7 个,只差这一个。
为什么会漏
多半是协议文档的锅,不是实现的锅。QMT_WS_PROTOCOL.md 的版本历史表里,
V1.0 定稿那行排在 V0.9.2 上面,而 ack_seq 恰恰是 V0.9.2 才补进来的
(「新增下行消息 ack_seq 累积确认(§4.5)」)。从上往下读到「V1.0 定稿」就停的话,
底下引入 ack_seq 的那行会被跳过去。PMS 侧会把这两行顺序修正,条款不动。
后果(已定位,非猜测)
不处理会形成自激循环:
PMS 发 ack_seq → QMT 回 reject → reject 自身带 seq → PMS 水位推进
→ 2 秒后 PMS 又要 ack → 再被 reject → ...
实测 2 秒一条,十几分钟烧掉 800 多个 seq。通道显示 ONLINE,实际只在刷 reject。
更实质的影响在贵方:§6.1 规定「QMT 收到 ack_seq{seq: N} 后可清理 seq ≤ N 的消息」。
收不到 ack,Redis 里的上行消息只涨不清。联调期 Redis 不设过期(双方已确认),
这部分会一直堆着。
补法
handlers.py 的 handle() 里,在 if msg_type == 'ping': 附近加一个分支:
if msg_type == 'ack_seq':
seq = int(payload.get('seq') or 0)
if seq > 0:
self.store.ack(seq) # 清理 seq <= N;方法名按贵方 store 的实际接口来
return # 不需要回任何消息
三点提醒:
ack_seq不需要回消息。 它是单向通知,回ack或pong都会让 PMS 侧多一条 无主上行。- 累积语义。 收到
{seq: 10450}表示seq ≤ 10450全部已落库,可一次性清理, 不是只清这一条。 - §6.1 的 7 天下限仍要守。 「已确认的消息也至少保留 7 天」——防 PMS 侧库回滚后 无从追溯。所以是「可清理」不是「立即删」,按贵方存储策略取舍。
PMS 侧已做的临时处理
PMS 已加自动降级:收到 reason 里含 ack_seq 的协议级 reject 就停发 ack_seq,
打断循环。水位照常落库,重连补发靠 hello.last_seq(§4.5 原话:「断线重连时以
hello.last_seq 为准,ack_seq 只用于让 QMT 及时释放存储」),不丢成交。
标志位每次重连重置——贵方补上之后,PMS 重连即自动恢复发送,不需要通知我们改配置。
顺带确认的几件事
- 签名完全互通。
crypto1.py的compact_payload_json/build_canonical与 PMS 侧 逐字一致,双向验签实测通过,这一层可以划掉了。 new_nonce()已修。 上一版是token_hex(8)(8 字节),现在是token_hex(16), 符合 §2.2。- 时间窗。
handlers.py对我方每条消息做 ±30 秒检查(超了回 TS_SKEW)。这是对的, 但两机时钟漂开会让place_order直接被拒,现象是「下单没反应」,很难往时钟上想。 PMS 侧已把实测偏差显示出来,建议两边都确认 NTP 在跑。 pong带 seq 是符合 §5「所有上行消息带 seq」的,实现没问题。只是按 §6.1 它也会 进 Redis 并在重连时补发——5 秒一个,一天 1.7 万条纯{}。要不要给pong免掉 seq, 是个可以聊的口径问题,不急。