编号:K-03 严重级:critical 工作线:SDK(sdk/*) 来源:审查 S-01、S-03、S-05、S-06、S-10、S-12、S-13、S-14、S-15、S-17、S-18、S-19 依赖:K-00 (#57)(对外语义条目) 被依赖:无
本 issue 汇总涉及 Python SDK 的条目,由 Python SDK 负责人一次改完(集中在 client.py、transport.py、async_client.py)。下方"问题明细"是跨四套的原文,请只看 Python 的部分。
client.py
transport.py
async_client.py
reconnect_on_failure=False
transport.disconnect()
pending.rid=""
_cb_lock
_lock
ack()
send_at_ms
send_at
session_invalid
not_connected
queue_full
run_coroutine_threadsafe(...).result()
__init__.py
logout()
client.py:231-235
except Exception: pass
标"补充"的条目来自第二轮 SDK 审查,总审查人已对照代码核实,证据写在条目里(没有对应的原文段落)。
sdk/python/src/nixmsg/client.py、transport.py、async_client.py、protocol.py、__init__.py,sdk/python/tests/*,README。
sdk/python/src/nixmsg/client.py
protocol.py
sdk/python/tests/*
.venv
test_10_kick_no_reconnect
以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。解决方案以本 issue 上方的"结论与统一方案"为准;原文里的方案与之不一致时,按上方执行。
Send
wait(timeout=None)
reconnectPeriod:0
pumpSends
send.go:170-180
rf := <-ch
send.go:141-153
!it.inflight
connect.go:57-64
client.ts:494-543
client.ts:174-177
mqtt.ts:93-99
end
client.js:380-384
:882-904
:985-987
:490-496
client.py:861-873
rid==""
client.py:605-640
client.py:323
Client.java:1000-1006
Client.java:1019-1029
Transport.java:383-391
dispatchSend
select
ch
connGone
c.ctx.Done()
close
end(true)
sdk/go/send.go
connect.go
sdk/js/src/client.ts
mqtt.ts
sdk/java/.../Client.java
PahoClient(...)
transport.py:208-213
client.py:605-631
client.py:741
:2315-2328
:4018-4039
127.0.0.1:0
sdk/python/src/nixmsg/transport.py
threading.Lock
close()
change_login_password()
send()
_request
setState
lock
sendSync
.get()
request()
synchronized(lock)
transport.publish()
dispatchResp
cbMu
ChangeLoginPassword
Ack
emitRevoked
client.py:109
:192,220,573-578,605-640
_set_state
:951-959
:787-788
Client.java:659-665,913-915,1034-1054,1108-1120
Transport.java:377-391
MqttRxClient.publishes/publishUnsafe
observeOn(applicationScheduler)
Schedulers.computation()
receive.go:121-125,247-253
api.go:134-139
sdk/python/.../client.py
Transport.java
sdk/go/receive.go
api.go
-Drx2.computation-threads=1
client.py:756-758,798-803,836-851
Client.java:884-887,928-934,965-984
client.ts:352-360
receive.go:136-141,202-206
push.go:88-237,456-495,549-560
Client.java
client.ts
sendAtMs
clockSkewMs
Client.java:288-289
Types.java:90-100
client.py:292-299
send.go:83-87
client.ts:472-473
sendAt
java.util.Date
Types.java
Func(0)=0
auth_failed(session_invalid)
failAuth
"0x8E"
taken_over
session.go:316-336
broker.go:274-292
receive.go:38-42,75-87
connect.go:120,135-146
backoff.go:27-29
transport_mqtt.go:81-92
t.stopped
client.py:652-655,735-745
Client.java:752,863-875
client.ts:207,304-311
receive.go
transport_mqtt.go
Logout
logout
client.ts:413-437,718-725
receive.go:322-337
connect.go:232-245
client.py:913
Client.java:1055
client.py:682-689
Client.java:797-805
send.go:186-195
client.ts:528-533
PacketTimeout
backoff.go:24-39
net.go:43-52
transport_mqtt.go:115-133
client.go:186-188,272
client.py:458-499
Client.java:522-581
PacketTimeout = ConnectTimeout
backoff.go
async def on_message
async_client.py:32-33
client.py:786-796
connect
https://
mqtt://
transport.py:204-245
Transport.java:239-301
transport_mqtt.go:206-237
mqtt.ts:151-164
Protocol.java
client.ts:478-482
send.go:96-102
复审基线:main 4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。
4059a15
按第二轮 SDK 审查补充 1 条(logout() 吞掉请求失败);S-15 改为按 K-00 (#57) 修订后的退避算法实现,不再写"移植 Go/JS 的退避对象"(JS 现有实现有双重翻倍)。补充条目已由总审查人对照代码核实,详见正文。
已合入 origin/main 0c9b459。落地提交 537cef0 fix: 按 K-00 约定修复 Python SDK 断线重交与退避 (#60)。
0c9b459
537cef0
No dependencies set.
The note is not visible to the blocked user.
编号:K-03 严重级:critical 工作线:SDK(sdk/*) 来源:审查 S-01、S-03、S-05、S-06、S-10、S-12、S-13、S-14、S-15、S-17、S-18、S-19
依赖:K-00 (#57)(对外语义条目) 被依赖:无
结论与统一方案
本 issue 汇总涉及 Python SDK 的条目,由 Python SDK 负责人一次改完(集中在
client.py、transport.py、async_client.py)。下方"问题明细"是跨四套的原文,请只看 Python 的部分。reconnect_on_failure=False;在 taken_over 与认证失败分支里(异步)调transport.disconnect();修正 DEVIATIONS S2-PY/JAVA 1–3 第 6 条的错误描述pending.rid=""、重算在途数,按原 id 重交_cb_lock下执行,on_connection 还持_lock;存在 ABBA 死锁(已复现)ack()失败不报错send_at_ms原样透传,语义未说明send_at_ms是服务器时间,send_at是本机时间并做偏差校正session_invalid再报真实原因not_connected立即失败非发送请求queue_full、只返回 data、sendAt+delay 报错、停止后 send 立即失败、取消语义)run_coroutine_threadsafe(...).result()执行,抛错不 ack;同步 Client 收到协程返回值时报错__init__.py导出logout()吞掉请求失败(client.py:231-235的except Exception: pass),离线时服务端没有作废令牌,应用却以为已经退出标"补充"的条目来自第二轮 SDK 审查,总审查人已对照代码核实,证据写在条目里(没有对应的原文段落)。
改动文件
sdk/python/src/nixmsg/client.py、transport.py、async_client.py、protocol.py、__init__.py,sdk/python/tests/*,README。与其他问题的交互 / 冲突说明
.venv等产物。验收与测试
test_10_kick_no_reconnect改为被顶号后 5 秒内每 0.2 秒采样,状态始终不翻转;错误密码场景 10 秒内只有 1 次连接尝试。logout():抛出请求错误,之后不再重连。问题明细(各区审查原文,证据含文件与行号)
[S-01] 断线时已发出、未收到 resp 的发送永不重交,调用方挂起,在途额度泄漏(Go / JS / Python / Java)
Send在 ctx 没有截止时间时永久阻塞;JS 的 Promise 永远不会完成;Python 调用时若已在线,会wait(timeout=None)永久阻塞;Java 等 1 小时后抛 busy,但条目仍留在队列里。reconnectPeriod:0下,非主动断开时只清掉 volatile 回调,QoS1 publish 的回调永不触发,所以在 PUBACK 之前断线也会卡住。pumpSends直接把这条发送判为失败并出队,不会重交。send.go:170-180(publish 成功后rf := <-ch既无超时,也没有断线出口)、send.go:141-153(只挑!it.inflight)、connect.go:57-64(OnOffline 没有处理在途条目)。client.ts:494-543、client.ts:174-177、mqtt.ts:93-99(断线后新建 client,不end旧的);MQTT.jsclient.js:380-384、:882-904、:985-987、:490-496。client.py:861-873(只挑rid=="")、client.py:605-640、client.py:323。Client.java:1000-1006、Client.java:1019-1029(publish 抛异常就判失败并出队)、Transport.java:383-391。pending.rid=""),按实际状态重算在途数;帧内容保持原样(原 id、原send_at_ms)。服务器按消息号防重,会返回原结果。dispatchSend改为select三路:ch、每次连接一个的connGone、c.ctx.Done()。close事件里对旧 client 调end(true),并加 generation 编号,防止同一条被重复完成。sdk/go/send.go、connect.go;sdk/js/src/client.ts、mqtt.ts;sdk/python/src/nixmsg/client.py;sdk/java/.../Client.javasend_at_ms重发并完成;循环 150 次后仍能发送;Java 模拟 publish 抛异常,断言重交而非失败。[S-03] Python 没关 paho 自带的自动重连:被顶号后互踢,认证失败后后台持续重试(Python)
PahoClient(...)没有传reconnect_on_failure=False(paho 2.x 默认开启)。SDK 在被顶号或认证失败时只改自己的标志,不断开底层 paho client,于是 paho 会用创建时的凭据自己重连。transport.py:208-213、client.py:605-631;paho 2.1.0client.py:741(默认开启)、:2315-2328(loop_forever 自动重连)、:4018-4039(收到 DISCONNECT 不改状态)。127.0.0.1:0,数据放临时目录,结束后已删除):c1、c2 用同一编号先后登录后,状态约每 1.03 秒翻转一次,持续 8 秒直到关闭。test_10_kick_no_reconnect在 2.5 秒采样,恰好落在互踢周期回到初始状态的时刻,属于偶然通过。reconnect_on_failure=False;在 taken_over 和认证失败分支里(异步)调用transport.disconnect()。sdk/python/src/nixmsg/transport.py、client.py[S-05] 回调在 SDK 内部锁下执行,回调里调用 SDK 方法会死锁;Java 还持全局锁等 PUBACK(Go / Python / Java)
_cb_lock是不可重入的threading.Lock,所有回调都在它下面执行。在回调里调用close()、logout()、change_login_password(),或手动ack()时撤回先到,都会自锁。_lock:回调里调send()会永久卡住;调_request类方法会卡 60 秒,期间 paho 网络线程拿不到锁,心跳也停。_cb_lock与_lock交叉加锁(ABBA)的死锁。setState在lock内调用回调,而 ONLINE 回调就运行在 runLoop 线程上。回调里调sendSync会卡 1 小时,调.get()会永久卡住。request()在synchronized(lock)里调用transport.publish(),最多阻塞 10 秒等 PUBACK。结果所有 SDK 操作都被串行到网络往返上。dispatchResp的那条线程,就会死锁 10 秒,然后以 busy 失败,而服务器其实已经处理了请求。cbMu不可重入,每个回调期间都持有。ChangeLoginPassword和Ack(结果为撤回时会emitRevoked)都会再次加这把锁。手动确认模式下,在 OnMessage 末尾调Ack若遇上撤回先到(PRD F13 本身就有这个竞态),downLoop 会永久卡死。client.py:109、:192,220,573-578,605-640(持_lock调_set_state)、:951-959、:787-788。Client.java:659-665,913-915,1034-1054,1108-1120、Transport.java:377-391;javap 可见MqttRxClient.publishes/publishUnsafe用observeOn(applicationScheduler),默认为Schedulers.computation()。receive.go:121-125,247-253、api.go:134-139。close(),永不返回、锁一直被占;调send()同样卡死。Java 在 onConnection(ONLINE) 里调sendSync,5 秒后 nixmsg-client 线程仍处于 TIMED_WAITING。request()在锁内只登记 pending,锁外 publish,且不在调用线程里等待 PUBACK。sdk/python/.../client.py;sdk/java/.../Client.java、Transport.java;sdk/go/receive.go、connect.go、api.go-Drx2.computation-threads=1做并发 ack 压测。[S-06] 确认失败后没有恢复:重推被忽略、重复回执不再确认、手动 ack 吞掉失败(Python / Java / JS)
_request直接返回 NotConnected),这条回执就永远占着窗口;积满 64 条后,该发送方再也收不到新回执。ack()和 Java 的ack()失败时不报错(Java 的 future 正常完成),应用无从得知。client.py:756-758,798-803,836-851。Client.java:884-887,928-934,965-984。client.ts:352-360。receive.go:136-141,202-206。push.go:88-237,456-495,549-560。client.py、JavaClient.java、JSclient.ts[S-10] Java 定时发送不做时钟偏差校正(Java;Python 的
send_at_ms也原样透传)sendAtMs,写入帧时不加clockSkewMs。设备时钟偏差多少,定时消息就早到或晚到多少。Python 的send_at会校正,但send_at_ms原样透传,而且两者的语义没有说明。Client.java:288-289、Types.java:90-100;Pythonclient.py:292-299;对照 Gosend.go:83-87、JSclient.ts:472-473。send_at都做了校正。sendAt(epoch 毫秒或java.util.Date,Android API 24 没有 java.time),入队时加上偏差;在 Javadoc 明确sendAtMs表示服务器时间,两者同时设置时报 bad_request。Python 在文档里注明send_at_ms是服务器时间。Types.java、Client.java;Python 文档send_at_ms约等于本机时间加 62 秒。[S-12] fatal 与顶号的处理时序、原因上报不一致(Go / Python / Java;Go / JS 原因名)
Func(0)=0也立刻重连,拿作废令牌连上得到 0x86,于是先上报auth_failed(session_invalid);Python/Java 随后再报一次真实原因;Go 的failAuth调 cancel 后,downLoop 在 select 里可能直接退出,fatal 原因根本没上报。JS 在收包回调里同步处理 fatal,是正确的。"0x8E",应为taken_over。session.go:316-336、broker.go:274-292;Goreceive.go:38-42,75-87、connect.go:120,135-146、backoff.go:27-29、transport_mqtt.go:81-92(只看t.stopped,fatal 没有设置它);Pythonclient.py:652-655,735-745;JavaClient.java:752,863-875;JSclient.ts:207,304-311。taken_over。receive.go、connect.go、transport_mqtt.go;Pythonclient.py;JavaClient.java;JSclient.ts[S-13] 断线时非发送请求不失败、JS 请求无超时;Go / JS 的 logout 不收尾(Go / JS;Python / Java 部分)
Logout不失败发送队列、不停止传输、不发 offline 事件;JS 的logout不失败队列、不发 offline 事件。client.ts:413-437,718-725;Goreceive.go:322-337、connect.go:232-245;Pythonclient.py:913;JavaClient.java:1055。receive.go、connect.go;JSclient.ts;Pythonclient.py、JavaClient.java(断线失败挂起请求)[S-14] 发送收到 rate_limited 后重交没有退避(Python / Java 热循环;Go / JS 固定 1 秒)
client.py:682-689;JavaClient.java:797-805;Gosend.go:186-195;JSclient.ts:528-533。[S-15] 重连退避与连接超时四套不一致(Go / Python / Java)
Func(0)=0);paho 的PacketTimeout默认 10 秒,限制了等 CONNACK 的时间,SDK 设置的 30 秒连接超时实际不生效——这正是文档点名要改的情况。backoff.go:24-39、autopahonet.go:43-52、transport_mqtt.go:115-133(没设 PacketTimeout)、pahoclient.go:186-188,272;Pythonclient.py:458-499;JavaClient.java:522-581。PacketTimeout = ConnectTimeout。Python/Java 移植与 Go/JS 相同的退避对象。backoff.go、transport_mqtt.go;Pythonclient.py;JavaClient.java[S-17] Python AsyncClient 接受 async 回调却从不 await,消息被直接确认(Python)
async def on_message时,SDK 调用它只得到一个协程对象,没有 await,随即自动 ack。结果是消息从没被处理却被确认了,只留下一条 “coroutine never awaited” 警告。async_client.py:32-33、client.py:786-796。connect时记录当前事件循环;遇到协程函数用run_coroutine_threadsafe(...).result()执行,抛错则不 ack;同步 Client 收到协程返回值时报错。async_client.py、client.py[S-18] Python / Java 的 URL scheme 处理:https 走明文、mqtt:// 未开启也走裸 TCP(Python / Java)
https://会以明文连接(没写端口时连 80);服务器仅接受 TLS 时连接会失败(默认安全),但若服务器同时允许明文,凭据就会明文传输。mqtt://不需要开启选项就走裸 TCP。transport.py:204-245、Transport.java:239-301;对照 Gotransport_mqtt.go:206-237、JSmqtt.ts:151-164。transport.py、protocol.py;JavaTransport.java、Protocol.java[S-19] 本地整帧检查有缺口(四套)
client.ts:478-482;Gosend.go:96-102。复审基线:main
4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。按第二轮 SDK 审查补充 1 条(
logout()吞掉请求失败);S-15 改为按 K-00 (#57) 修订后的退避算法实现,不再写"移植 Go/JS 的退避对象"(JS 现有实现有双重翻倍)。补充条目已由总审查人对照代码核实,详见正文。已合入 origin/main
0c9b459。落地提交537cef0fix: 按 K-00 约定修复 Python SDK 断线重交与退避 (#60)。