[K-00][medium] 四套 SDK 对外语义不一致,需要先定一份统一的 SDK 行为约定(连接超时、错误码、返回值、取消、退避、心跳、URL 映射) #57

Closed
opened 2026-09-30 13:57:06 +08:00 by nixevol · 2 comments
Owner

编号:K-00 严重级:medium 工作线:SDK(sdk/) + 测试与文档(test/、docs/*) 来源:审查 S-16、S-24
依赖:无 被依赖:K-01 (#58)、K-02 (#59)、K-03 (#60)、K-04 (#61) 中涉及对外语义的条目

结论与统一方案

四套 SDK 在多项对外语义上各行其是(审查 S-16、S-24,另见 S-11、S-14、S-15、S-18 的约定部分)。先由本 issue 定一份约定,写进 docs/DEVELOPMENT.md 第 9 节附录"SDK 行为约定",四套实现 issue(K-01 至 K-04)照此统一。约定如下(总审查人推荐值,负责人审核时可调整):

项 约定
首次连接超时 默认 30 秒内没完成握手:停止重连,返回 not_connected;Client 可再次调用 connect
认证失败错误码 直接用原因:bad_credentials、session_invalid、disabled、deleted、password_reset、taken_over、rate_limited;事件 auth_failed / kicked 带同一原因
顶号原因名 0x8E 一律报 taken_over,不报 "0x8E"
服务器关闭 DISCONNECT 0x8B 按可重试处理(L-03 停机依赖这一点)
本地队列满 统一 queue_full,不用服务端的 quota_exceeded
请求返回值 只返回 resp 的 data,不返回整个信封
sendAt 与 delay 同时给 本地返回 bad_request(PRD F11)
停止重连后再 send 立即以停止原因失败,不入队
logout 服务端请求失败(含离线)时把错误返回给应用;本地照常停止重连、清空令牌、断开传输,队列里的发送按"停止重连后再 send"一行的停止原因结束,并发 offline 事件(Python、Java 现在吞掉请求失败;Go、JS 不结束队列也不发 offline,见各自的 S-13 条目)(第二轮 SDK 审查)
时长参数 毫秒时长和时间戳一律用 64 位整数(Java 用 long / Long)。Java updateSelf 的 defaultDelayMs 现在是 Integer,最多约 24.8 天,达不到 max_schedule_seconds 默认的 365 天(第二轮 SDK 审查)
max_receive_bytes 小于 1024 本地返回 bad_request
URL 映射 http→ws、https→wss;ws/wss 原样;mqtt/mqtts 只在显式开启裸 TCP 选项时允许;路径为空或 "/" 时用 "/mqtt",否则按原路径
取消与超时 尚未发出的条目取消即出队;已发出的返回"结果未知"错误,并在文档说明应以同一消息号重试(审查 S-24)
rate_limited 重交退避 每条发送单独计数:1 秒起、翻倍、上限 30 秒、±30% 抖动,成功后清零;每次重交(包括断线后的重交)都重新生成 rid 并重新序列化帧,消息 id、正文和 send_at_ms 保持不变(DEVELOPMENT 第 6 节要求同一连接内 rid 不重复;Go、JS 现在复用原 rid)(审查 S-14,第二轮 SDK 审查补充 rid)
重连退避 四套用同一算法,不以任何一套现有实现为参照:第 n 次等待为 min(1 秒 × 2^(n-1), 30 秒) × 随机(0.7, 1.3),n 是连续失败计数(n ≥ 1)。只有应用调用 connect 后的第一次连接可以不等待;已建立的连接断开后,第一次重连也要按 n=1 等约 1 秒。连接尝试失败时 n 加 1;连上后不足 60 秒又断开,也按一次失败计(n 加 1,即 S1.3 的"闪断继续抬升");稳定在线满 60 秒后断开,n 重置为 1。只维护这一个计数,不能再另把 base 翻倍(JS 现在两处都翻倍,实际间隔约 1、4、16、30 秒;Go、Python、Java 断线后第一次重连为 0 秒)。连接超时覆盖等 CONNACK 的时间(审查 S-15,第二轮 SDK 审查核实后修订)
心跳 默认 30 秒(DEVELOPMENT §5,审查 S-11)
Receive Maximum CONNECT 不带该属性(B-01 的约束;目前四套都不带,保持)

改动文件

docs/DEVELOPMENT.md(第 9 节附录)、四套 SDK 的 README;实现改动在 K-01 至 K-04。

与其他问题的交互 / 冲突说明

  • 本 issue 只改文档;四套实现 issue 引用本约定,避免各自定语义。
  • 与 B-01(Receive Maximum)、L-03(0x8B 可重试)的服务端约束一致。
  • 2026-09-30 按第二轮 SDK 审查修订(总审查人已对照代码核实):"重连退避"改为明确算法(原写"统一采用 Go/JS 的 S1.3 状态机",但 JS 现有实现在 sdk/js/src/mqtt.ts:41-45 与 types.ts:174-215 两处同时翻倍,照搬会把错误带进四套);"rate_limited 重交退避"补充每次重新生成 rid;新增"logout"与"时长参数"两行。K-01 至 K-04 里"帧内容保持原样"指消息 id、正文与 send_at_ms,rid 按本表重新生成。

验收与测试

约定合入后,K-01 至 K-04 各自为每一项加同名用例(四套用例名一致,便于对照)。


问题明细(各区审查原文,证据含文件与行号)

以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。解决方案以本 issue 上方的"结论与统一方案"为准;原文里的方案与之不一致时,按上方执行。

[S-16] 四套对外语义不对齐(Go / JS / Python / Java)

  • 严重级:medium
  • 分类:设计
  • 现象与影响:
    1. 首次连接超时:Go 30 秒后调用 Close,该 Client 不能再用;JS 在服务器不可达时 connect() 永不返回;Python/Java 等 35 秒抛 busy,但后台继续重连。
    2. 认证失败错误码:Go/JS 为 auth_failed(原因丢失),Python/Java 为具体原因。
    3. 本地队列满:Go/JS 为 queue_full;Python/Java 为 quota_exceeded,与服务端同名错误码的含义不同。
    4. 返回结构:Go/JS 返回 data;Python/Java 返回整个 resp 信封(含 v/type/rid)。
    5. sendAt 和 delay 同时给:Python/Java 静默丢掉 delay。
    6. 停止重连后再 send:Python/Java 会入队并阻塞最长 3600 秒。
    7. max_receive_bytes 小于 1024:Go/JS 不在本地校验,只会报“连接超时”。
    8. 带路径前缀的 URL:各套处理规则不同。
  • 证据:Go connect.go:73-94;JS client.ts:168-193;Python client.py:197-211,260-264,292-299,345-421;Java Client.java:170-203,261-292,344-474。
  • 文档依据:PRD F19“四种 SDK 行为一致”;F11“定时与延迟同时使用则提交失败”;DEVELOPMENT 9“应用……不接触主题和 JSON”;6.1“不小于 1024”;6.10 错误码表。
  • 为何不是故意设计:没有偏差记录约定这些差异。
  • 解决方案:由总控定一份 SDK 行为约定(写入 DEVIATIONS 或 DEVELOPMENT 9 附录),逐项统一。建议:connect 超时即停止并返回 not_connected;错误码直接用原因;本地队列满统一 queue_full;返回值只给 data;sendAt 与 delay 同时给时本地报 bad_request;停止后 send 立即失败;max_receive_bytes 在本地校验。
  • 改动文件:四套主文件和 README
  • 与其他模块的交互/冲突风险:属于公开 API 变更,建议在 Z3 发布前完成,同时更新各自的清单测试。
  • 需补测试:为每一项在四套中加同名用例。
  • 置信度:代码阅读确定。

[S-24] 取消或超时后发送仍留在队列,稍后仍可能送达(Go / Python / Java)

  • 严重级:low
  • 分类:设计
  • 现象与影响:Go ctx 取消、Python 3600 秒超时、Java 1 小时超时后,调用方收到失败,但条目仍在队列里,之后仍可能发出。应用若换一个新消息号重发,接收方会收到两条相同内容。
  • 证据:Go send.go:126-131;Python client.py:323-324;Java Client.java:316。
  • 文档依据:文档未定义取消语义;PRD F05 要求重试使用同一消息号。
  • 为何不是故意设计:文档没有这方面的约定,现有行为容易导致重复。
  • 解决方案:未在途的条目在取消时出队;已在途的返回“结果未知”,并在文档说明应以同一消息号重试。
  • 改动文件:Go send.go;Python client.py;Java Client.java
  • 与其他模块的交互/冲突风险:与 S-16 的语义约定一起定。
  • 需补测试:取消未发出的条目后,断言重连时不再发出。
  • 置信度:代码阅读确定。

复审基线:main 4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。

**编号**:K-00 **严重级**:medium **工作线**:SDK(sdk/*) + 测试与文档(test/*、docs/*) **来源**:审查 S-16、S-24 **依赖**:无 **被依赖**:K-01 (#58)、K-02 (#59)、K-03 (#60)、K-04 (#61) 中涉及对外语义的条目 ### 结论与统一方案 四套 SDK 在多项对外语义上各行其是(审查 S-16、S-24,另见 S-11、S-14、S-15、S-18 的约定部分)。先由本 issue 定一份约定,写进 `docs/DEVELOPMENT.md` 第 9 节附录"SDK 行为约定",四套实现 issue(K-01 至 K-04)照此统一。约定如下(总审查人推荐值,负责人审核时可调整): | 项 | 约定 | |---|---| | 首次连接超时 | 默认 30 秒内没完成握手:停止重连,返回 `not_connected`;Client 可再次调用 connect | | 认证失败错误码 | 直接用原因:`bad_credentials`、`session_invalid`、`disabled`、`deleted`、`password_reset`、`taken_over`、`rate_limited`;事件 `auth_failed` / `kicked` 带同一原因 | | 顶号原因名 | 0x8E 一律报 `taken_over`,不报 `"0x8E"` | | 服务器关闭 | DISCONNECT 0x8B 按可重试处理(L-03 停机依赖这一点) | | 本地队列满 | 统一 `queue_full`,不用服务端的 `quota_exceeded` | | 请求返回值 | 只返回 resp 的 `data`,不返回整个信封 | | sendAt 与 delay 同时给 | 本地返回 `bad_request`(PRD F11) | | 停止重连后再 send | 立即以停止原因失败,不入队 | | logout | 服务端请求失败(含离线)时把错误返回给应用;本地照常停止重连、清空令牌、断开传输,队列里的发送按"停止重连后再 send"一行的停止原因结束,并发 offline 事件(Python、Java 现在吞掉请求失败;Go、JS 不结束队列也不发 offline,见各自的 S-13 条目)(第二轮 SDK 审查) | | 时长参数 | 毫秒时长和时间戳一律用 64 位整数(Java 用 `long` / `Long`)。Java `updateSelf` 的 `defaultDelayMs` 现在是 `Integer`,最多约 24.8 天,达不到 `max_schedule_seconds` 默认的 365 天(第二轮 SDK 审查) | | max_receive_bytes 小于 1024 | 本地返回 `bad_request` | | URL 映射 | http→ws、https→wss;ws/wss 原样;mqtt/mqtts 只在显式开启裸 TCP 选项时允许;路径为空或 "/" 时用 "/mqtt",否则按原路径 | | 取消与超时 | 尚未发出的条目取消即出队;已发出的返回"结果未知"错误,并在文档说明应以同一消息号重试(审查 S-24) | | rate_limited 重交退避 | 每条发送单独计数:1 秒起、翻倍、上限 30 秒、±30% 抖动,成功后清零;每次重交(包括断线后的重交)都重新生成 rid 并重新序列化帧,消息 id、正文和 `send_at_ms` 保持不变(DEVELOPMENT 第 6 节要求同一连接内 rid 不重复;Go、JS 现在复用原 rid)(审查 S-14,第二轮 SDK 审查补充 rid) | | 重连退避 | 四套用同一算法,不以任何一套现有实现为参照:第 n 次等待为 `min(1 秒 × 2^(n-1), 30 秒) × 随机(0.7, 1.3)`,n 是连续失败计数(n ≥ 1)。只有应用调用 connect 后的第一次连接可以不等待;已建立的连接断开后,第一次重连也要按 n=1 等约 1 秒。连接尝试失败时 n 加 1;连上后不足 60 秒又断开,也按一次失败计(n 加 1,即 S1.3 的"闪断继续抬升");稳定在线满 60 秒后断开,n 重置为 1。只维护这一个计数,不能再另把 base 翻倍(JS 现在两处都翻倍,实际间隔约 1、4、16、30 秒;Go、Python、Java 断线后第一次重连为 0 秒)。连接超时覆盖等 CONNACK 的时间(审查 S-15,第二轮 SDK 审查核实后修订) | | 心跳 | 默认 30 秒(DEVELOPMENT §5,审查 S-11) | | Receive Maximum | CONNECT 不带该属性(B-01 的约束;目前四套都不带,保持) | ### 改动文件 `docs/DEVELOPMENT.md`(第 9 节附录)、四套 SDK 的 README;实现改动在 K-01 至 K-04。 ### 与其他问题的交互 / 冲突说明 - 本 issue 只改文档;四套实现 issue 引用本约定,避免各自定语义。 - 与 B-01(Receive Maximum)、L-03(0x8B 可重试)的服务端约束一致。 - 2026-09-30 按第二轮 SDK 审查修订(总审查人已对照代码核实):"重连退避"改为明确算法(原写"统一采用 Go/JS 的 S1.3 状态机",但 JS 现有实现在 `sdk/js/src/mqtt.ts:41-45` 与 `types.ts:174-215` 两处同时翻倍,照搬会把错误带进四套);"rate_limited 重交退避"补充每次重新生成 rid;新增"logout"与"时长参数"两行。K-01 至 K-04 里"帧内容保持原样"指消息 id、正文与 `send_at_ms`,rid 按本表重新生成。 ### 验收与测试 约定合入后,K-01 至 K-04 各自为每一项加同名用例(四套用例名一致,便于对照)。 --- ### 问题明细(各区审查原文,证据含文件与行号) > 以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。**解决方案以本 issue 上方的"结论与统一方案"为准**;原文里的方案与之不一致时,按上方执行。 #### [S-16] 四套对外语义不对齐(Go / JS / Python / Java) - **严重级**:medium - **分类**:设计 - **现象与影响**: 1. **首次连接超时**:Go 30 秒后调用 Close,该 Client 不能再用;JS 在服务器不可达时 `connect()` 永不返回;Python/Java 等 35 秒抛 busy,但后台继续重连。 2. **认证失败错误码**:Go/JS 为 `auth_failed`(原因丢失),Python/Java 为具体原因。 3. **本地队列满**:Go/JS 为 `queue_full`;Python/Java 为 `quota_exceeded`,与服务端同名错误码的含义不同。 4. **返回结构**:Go/JS 返回 data;Python/Java 返回整个 resp 信封(含 v/type/rid)。 5. **sendAt 和 delay 同时给**:Python/Java 静默丢掉 delay。 6. **停止重连后再 send**:Python/Java 会入队并阻塞最长 3600 秒。 7. **max_receive_bytes 小于 1024**:Go/JS 不在本地校验,只会报“连接超时”。 8. **带路径前缀的 URL**:各套处理规则不同。 - **证据**:Go `connect.go:73-94`;JS `client.ts:168-193`;Python `client.py:197-211,260-264,292-299,345-421`;Java `Client.java:170-203,261-292,344-474`。 - **文档依据**:PRD F19“四种 SDK 行为一致”;F11“定时与延迟同时使用则提交失败”;DEVELOPMENT 9“应用……不接触主题和 JSON”;6.1“不小于 1024”;6.10 错误码表。 - **为何不是故意设计**:没有偏差记录约定这些差异。 - **解决方案**:由总控定一份 SDK 行为约定(写入 DEVIATIONS 或 DEVELOPMENT 9 附录),逐项统一。建议:connect 超时即停止并返回 not_connected;错误码直接用原因;本地队列满统一 `queue_full`;返回值只给 data;sendAt 与 delay 同时给时本地报 bad_request;停止后 send 立即失败;max_receive_bytes 在本地校验。 - **改动文件**:四套主文件和 README - **与其他模块的交互/冲突风险**:属于公开 API 变更,建议在 Z3 发布前完成,同时更新各自的清单测试。 - **需补测试**:为每一项在四套中加同名用例。 - **置信度**:代码阅读确定。 #### [S-24] 取消或超时后发送仍留在队列,稍后仍可能送达(Go / Python / Java) - **严重级**:low - **分类**:设计 - **现象与影响**:Go ctx 取消、Python 3600 秒超时、Java 1 小时超时后,调用方收到失败,但条目仍在队列里,之后仍可能发出。应用若换一个新消息号重发,接收方会收到两条相同内容。 - **证据**:Go `send.go:126-131`;Python `client.py:323-324`;Java `Client.java:316`。 - **文档依据**:文档未定义取消语义;PRD F05 要求重试使用同一消息号。 - **为何不是故意设计**:文档没有这方面的约定,现有行为容易导致重复。 - **解决方案**:未在途的条目在取消时出队;已在途的返回“结果未知”,并在文档说明应以同一消息号重试。 - **改动文件**:Go `send.go`;Python `client.py`;Java `Client.java` - **与其他模块的交互/冲突风险**:与 S-16 的语义约定一起定。 - **需补测试**:取消未发出的条目后,断言重连时不再发出。 - **置信度**:代码阅读确定。 --- <sub>复审基线:main `4059a15`(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。</sub>
nixevol added the P2-mediumlane/sdklane/test-docsreview-2026-09-30 labels 2026-09-30 13:57:06 +08:00
Author
Owner

按第二轮 SDK 审查修订了约定表(总审查人已对照代码核实):"重连退避"改为明确算法(原写法会把 JS 现有的双重翻倍带进四套),"rate_limited 重交退避"补充每次重新生成 rid,新增"logout"和"时长参数"两行。详见正文。

按第二轮 SDK 审查修订了约定表(总审查人已对照代码核实):"重连退避"改为明确算法(原写法会把 JS 现有的双重翻倍带进四套),"rate_limited 重交退避"补充每次重新生成 rid,新增"logout"和"时长参数"两行。详见正文。
Author
Owner

已合入 origin/main 0c9b459。落地提交 3749b9b docs: 写入 K-00 SDK 行为约定附录 (#57)。

已合入 origin/main `0c9b459`。落地提交 `3749b9b` docs: 写入 K-00 SDK 行为约定附录 (#57)。
Sign in to join this conversation.