diff --git a/app/ws/runner.py b/app/ws/runner.py index 7796fc2..2757362 100644 --- a/app/ws/runner.py +++ b/app/ws/runner.py @@ -471,14 +471,33 @@ class WsRunner: self._stat["last_reject"] = ( f"{code or '(无 code)'} · {str(pl.get('reason') or '(无 reason)')[:80]}" f" · 针对 {iid or '(消息未带 instruction_id)'}") - await _db(qmt_repo.update_order, iid, status=qmt_repo.OS_REJECTED, - reject_code=code[:32], reject_reason=str(pl.get("reason") or "")[:300], - final_at=datetime.now()) - # 设计 §13: 指令下发失败不自动重发。可重试码也只是记下来, - # 由 executor 下一跳按最新行情重新决定 —— 换价重发的判断权在择时模块。 - lvl = logger.error if code == "INTERNAL" else logger.warning - lvl("[reject] %s %s: %s (retryable=%s, 不自动重发)", iid, code, - pl.get("reason"), pl.get("retryable")) + # reject 按协议是**委托级**响应, 必然带 instruction_id。不带的话它拒的就不是 + # 委托, 而是我们发的某条协议消息 (hello/ping/ack_seq) —— 这是通道故障, 不是 + # 交易结果, 不能拿去改委托状态 (iid 为空时 update_order 是空转, 更糟的是它把 + # 故障伪装成了"没事发生")。这里分开计数并原样打出整条 payload: 对端的 reject + # 可能带我们没读的字段, 猜不如看。 + if not iid: + self._stat["proto_rejects"] = self._stat.get("proto_rejects", 0) + 1 + n = self._stat["proto_rejects"] + if n <= 3 or n % 50 == 0: + logger.error("[协议级 reject #%s] 对端拒绝了我们发的一条协议消息 " + "(非委托, 无 instruction_id)。整条 payload: %s", + n, json.dumps(pl, ensure_ascii=False)[:500]) + if n == 10: + logger.error("协议级 reject 已达 10 条且仍在增长: 很可能是" + "「我们 ack → 对端拒 → 拒绝本身带 seq → 我们又要 ack」" + "的自激循环, 通道看着 ONLINE 其实什么也没在做。" + "请拿上面的 payload 与 QMT 侧对齐消息格式") + else: + await _db(qmt_repo.update_order, iid, status=qmt_repo.OS_REJECTED, + reject_code=code[:32], + reject_reason=str(pl.get("reason") or "")[:300], + final_at=datetime.now()) + # 设计 §13: 指令下发失败不自动重发。可重试码也只是记下来, + # 由 executor 下一跳按最新行情重新决定 —— 换价重发的判断权在择时模块。 + lvl = logger.error if code == "INTERNAL" else logger.warning + lvl("[reject] %s %s: %s (retryable=%s, 不自动重发)", iid, code, + pl.get("reason"), pl.get("retryable")) elif type_ == wsc.T_ORDER_UPDATE: await self._on_order_update(iid, pl) elif type_ == wsc.T_TRADE: diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..8823174 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# tradingSystem (PMS) 一键部署 +# --------------------------------------------------------------------------- +# 为什么需要它: 源码是**打进镜像**的 (docker-compose.yml 只挂了 ./logs), 所以 +# `docker compose restart` 只是把老容器停了再起, 跑的还是旧镜像里的旧代码 —— +# 改完代码 restart 一下看不到变化, 而且**一点报错都没有**, 排查时极其误导。 +# 代码改动要生效必须 build + force-recreate, 这个脚本就是干这个的。 +# +# 用法: +# ./scripts/deploy.sh 拉代码 → 重建镜像 → 重建服务 → 报状态 +# ./scripts/deploy.sh --no-pull 跳过 git pull (本地已改好, 不想拉远端) +# PROFILES="--profile ws" ./scripts/deploy.sh 只跑页面 + ws (默认 sched+ws) +# --------------------------------------------------------------------------- +set -euo pipefail +cd "$(dirname "$0")/.." + +PROFILES="${PROFILES:---profile sched --profile ws}" +DC=(docker compose $PROFILES) + +if [[ "${1:-}" != "--no-pull" ]]; then + echo "==> git pull" + git pull --ff-only +fi + +echo "==> 重建镜像" +"${DC[@]}" build + +echo "==> 重建容器 (force-recreate: 让新镜像真正生效)" +"${DC[@]}" up -d --force-recreate + +echo +"${DC[@]}" ps +echo +echo "==> 建表/补表 (幂等)" +docker compose run --rm --no-deps pms-web python scripts/init_db.py --yes || { + echo "建表失败 —— 上面有原因, 服务已经起来了, 修完重跑本脚本即可"; exit 1; } + +echo +echo "完成。ws 通道状态:" +echo " docker compose run --rm --no-deps pms-web python scripts/ws_smoke.py status" diff --git a/scripts/ws_smoke.py b/scripts/ws_smoke.py index 5292598..0e764eb 100644 --- a/scripts/ws_smoke.py +++ b/scripts/ws_smoke.py @@ -96,7 +96,13 @@ def cmd_status(args): f"持续增长则说明序号语义对不上") if int(s.get("reconnects") or 0) >= 5: bad.append(f"重连 {s['reconnects']} 次: 连接不稳, 见上面「最后断开原因」") - if int(s.get("rejects") or 0) and not qmt_repo.list_orders(limit=1): + if int(s.get("proto_rejects") or 0): + bad.append(f"协议级 reject {s['proto_rejects']} 条 (不带 instruction_id, 拒的不是委托" + f"而是我们发的协议消息)。若 rx≈tx 且两者同步增长, 基本可以断定是" + f"「我们 ack → 对端拒 → 拒绝本身带 seq → 我们又要 ack」的自激循环: " + f"通道显示 ONLINE, 实际什么也没在做。整条 payload 见 " + f"docker compose logs pms-ws | grep 协议级") + elif int(s.get("rejects") or 0) and not qmt_repo.list_orders(limit=1): bad.append(f"出口表一张委托都没有, 却收到 {s['rejects']} 条 reject —— 对端在拒绝我们" f"的非委托消息 (hello/ping/ack?), 见上面「最后一条拒绝」") if ch.get("resync_required"):