[T-01][medium] 测试 WS 客户端不按字节流拆包、并发写不加锁,被误判为服务端死锁;移除群事件 20ms 绕过 #3

Open
opened 2026-09-30 10:28:15 +08:00 by nixevol · 2 comments
Owner

编号:T-01 严重级:medium 工作线:测试与文档(test/harness、test/accept、docs) 来源:总审查人实证(复审 #3)
依赖:无 被依赖:所有依赖 WebSocket 集成测试的问题(建议最先合入)

本 issue 更正 #3 原先的"上行 worker 同步 PublishDown 与 mochi InlineClient 死锁"结论。该结论经实证不成立,下方"原始描述"保留供追溯。

结论

TestUplinkDMOfflineGroupRecall 等"resp 回不来"不是服务端死锁,是测试用 WebSocket 客户端 test/harness/mqtt.go 的两个缺陷:

  1. 接收不按字节流拆包:wsMQTT.Recv(test/harness/mqtt.go:173-192)把一个 WS 二进制帧当作恰好一个 MQTT 包返回。mochi 在客户端出站队列有积压时,会把多个包写进 outbuf 后一次 Write(mochi clients.go:602-631),经 coder/websocket 成为一个 WS 消息。MQTT 5.0 §6.0 [MQTT-6.0.0-3] 明确规定接收方不得假设控制包与 WS 帧对齐。客户端只解出第一个包,其余全部丢弃。
  2. 发送不是原子的、也不加锁:writeWSClientFrame(:249-277)把帧头和载荷分两次 Write,wsMQTT.Send(:169-171)没有锁。读循环回 PUBACK 与测试线程发请求并发时,两帧交错,发往服务端的 WS 流被写乱,服务端卡在读一个错乱长度的包上,而且不打任何报错日志。

实证(基线 main 4059a15,群事件改回同步下发、不带 20ms sleep)

条件 结果
原测试客户端,只加统计 6 次 1 失败;失败轮次记录到 WS frame carries 3 MQTT packets: [group_event group_event resp/rid=g1],随后 timeout waiting resp rid=g1
只修接收(按字节流拆包) 20 次 2 失败(g1、s4),无服务端报错
再修发送(加锁 + 整帧一次写) 20/20 通过,期间 125 个合包帧全部正确拆开;cmd/nixmsg、test/accept、internal/app/group、internal/broker 全部通过

通过的轮次里也一直在丢包(receipt+receipt、group_event+group_event 各丢第二个),只是现有断言没覆盖。

影响范围

所有经 harness.DialMQTTWebSocket 的测试客户端:cmd/nixmsg/*_test.go(mqttSess)、test/accept(MQTTSession)、test/chaos、test/load/q3_live_test.go、web/e2e/seedpending。此前 Q accept-rest 里"F15 带密拉人用独立短生命周期进程""group.add 带密码卡住"等绕行,以及 internal/app/group/app.go:659-686 的 goroutine + time.Sleep(20ms),都是这个缺陷的误诊产物。

解决方案(已验证)

  1. test/harness/mqtt.go:
    • wsMQTT 增加接收缓冲 buf []byte 与写锁 wmu sync.Mutex。
    • Recv 先从缓冲里按 MQTT 剩余长度切出一个完整包;不够时继续读 WS 帧,opcode 0x2 与续帧 0x0 的载荷都追加到缓冲;0x8 返回 EOF;0x9 在写锁内回 PONG;0xA 忽略。
    • Send 与回 PONG 都持写锁;writeWSClientFrame 把帧头和掩码后的载荷拼成一个切片,一次 Write。
  2. internal/app/group/app.go 的 emit:删除 goroutine 与 time.Sleep(20ms),改回在调用方上下文里同步 PublishDown(group_event 是 QoS 0,mochi 的 QoS 0 发布走非阻塞通道,不会卡住调用方)。
  3. test/accept/rest_accept_test.go:F15 去掉 runF15JoinWithPasswordFresh 的"独立短进程"绕行,在同一长会话里用 group.add + 当次对话密码断言成功。
  4. docs/DEVIATIONS.md:把「Q accept-rest」第 2–4 条和「死锁未修(issue #3)」整节标注为已被本 issue 取代,写明真实根因;不要删除历史文字。
  5. 放弃工作树 E:\code\NixMsg-wt\fix3 与分支 feat/fix-3-downlink-deadlock(基于错误假设,其 OnPublish 先写 PUBACK 的改法还改变了语义),不要合入。
已验证的测试客户端补丁要点(去掉了诊断日志)
type wsMQTT struct {
	conn net.Conn
	r    *bufio.Reader
	buf  []byte
	wmu  sync.Mutex
}

func splitMQTTPacket(b []byte) ([]byte, int, bool) {
	if len(b) < 2 {
		return nil, 0, false
	}
	rem, mult := 0, 1
	for i := 1; i < len(b) && i <= 4; i++ {
		rem += int(b[i]&127) * mult
		if b[i]&128 == 0 {
			total := 1 + i + rem
			if len(b) < total {
				return nil, 0, false
			}
			pkt := make([]byte, total)
			copy(pkt, b[:total])
			return pkt, total, true
		}
		mult *= 128
	}
	return nil, 0, false
}

func (c *wsMQTT) Send(packet []byte) error {
	c.wmu.Lock()
	defer c.wmu.Unlock()
	return writeWSClientBinary(c.conn, packet)
}

func (c *wsMQTT) Recv() ([]byte, error) {
	for {
		if pkt, n, ok := splitMQTTPacket(c.buf); ok {
			c.buf = c.buf[n:]
			return pkt, nil
		}
		payload, opcode, err := readWSFrame(c.r)
		if err != nil {
			return nil, err
		}
		switch opcode {
		case 0x0, 0x2:
			c.buf = append(c.buf, payload...)
		case 0x8:
			return nil, io.EOF
		case 0x9:
			c.wmu.Lock()
			_ = writeWSClientControl(c.conn, 0xA, payload)
			c.wmu.Unlock()
		}
	}
}

// writeWSClientFrame 末尾:帧头与掩码载荷拼成一个切片,一次写出
frame := make([]byte, len(header)+n)
copy(frame, header)
for i := 0; i < n; i++ {
	frame[len(header)+i] = payload[i] ^ mask[i%4]
}
_, err := w.Write(frame)
return err

改动文件

test/harness/mqtt.go、internal/app/group/app.go(仅 emit)、test/accept/rest_accept_test.go(仅 F15)、docs/DEVIATIONS.md。

交互 / 冲突说明

  • 只改测试基础设施和 emit 一个函数,与其他工作线没有文件冲突(身份线 U-02 改 group 其他函数时注意不要动 emit)。
  • 建议最先合入:其他问题的集成测试都依赖可靠的 WS 测试客户端,否则会出现与本问题相同的"随机收不到 resp"。

验收与测试

  • 新增 harness 单测:一个 WS 帧装 3 个 MQTT 包,Recv 依次返回 3 个;一个包跨两个帧;两个 goroutine 各并发 Send 1000 次,服务端全部正确解码。
  • go test ./cmd/nixmsg/ -run TestUplinkDMOfflineGroupRecall -count=20 在同步 emit 下全部通过。
  • task check 与 go test ./test/... -count=1 通过。

原始描述(已被推翻,保留追溯)

严重级

medium

分类

设计缺陷 / 竞态

现象

同一连接上 group.create / group.add 若在处理上行的 worker 里同步向本连接注入下行(server.Publish → InlineClient InjectPacket),会与 mochi InlineClient 互相等待,应用层 resp 发不回去。当前实现把 group_event 放到独立 goroutine 并 Sleep(20ms),让 worker 先 PublishDown resp。这是竞态窗口,不是根因修复:负载升高、调度延迟偏离 20ms、或其它仍同步的下行,仍可能卡住。

补测 TestUplinkDMOfflineGroupRecall 在清掉测试客户端 dial deadline 后可稳定复现(见 DEVIATIONS Q accept-rest 第 4 条)。

路径

  • internal/app/group/app.go 第 659–685 行:emit 异步 + 20ms 再 PublishDown(QoS 0)
  • internal/app/group/void.go 第 153–167 行:publishRevokes 仍同步 PublishDown;Leave/Remove/Dissolve(app.go 约 262、299、393 行)在返回给 uplink 发 resp 之前就会走到这里
  • internal/broker/hooks.go 第 114–118 行:InlineClient 必须放行才能分发给订阅者
  • internal/broker/broker.go 第 195–238 行:PublishDown → server.Publish
  • internal/broker/queue.go 第 41–44 行:每端串行 worker 调用 HandleUplink
  • cmd/nixmsg/uplink.go 第 62–75、268–295 行:业务返回后 replyOK 再 PublishDown resp
  • 旁证:internal/app/message/push.go WakePush 在 Submit 成功后异步 PushPending;自发自收时可能与 replyOK 并发 PublishDown 到同一连接

文档条款

  • DEVELOPMENT 第 5 节:装配 InlineClient = true;OnPublish 在客户端读循环同步执行;worker 不要同步等待「断开同一连接」
  • DEVIATIONS「Q accept-rest」第 4 条:写明原因是 InlineClient 互相等待,备选方案是 broker 对 Inline 发布做无锁队列;影响是 group_event 可能略晚于 resp
  • PRD F16:群事件尽力推送,没有「延迟 20ms」的产品要求

为何不是故意设计

20ms 不是 PRD/DEVELOPMENT 的协议语义,是补测为避开死锁加的睡眠。根因(上行 worker 调用栈上同步 Inline Publish)还在。group_event 绕开了,revoked 等同步路径没有。已知清单里的「群 emit 异步+20ms」是本波处理项,但死锁根因仍在,应按设计缺陷跟踪,而不是当作已修完。

建议方向(不实现)

不要用 time.Sleep 赌调度。正确修法例如:

  1. broker.PublishDown 对 Inline 走无锁出站队列,由独立循环 Inject;上行 worker 只入队、立刻返回;或
  2. 所有业务通知(group_event / revoked / presence)在 resp 发布之后由 uplink 层入队,且禁止在 HandleUplink 栈上对本连接同步 PublishDown。
    去掉 20ms 魔法数,并用原先会卡住的建群/退群+已推送投递用例做回归,而不是靠睡眠。

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

**编号**:T-01 **严重级**:medium **工作线**:测试与文档(test/harness、test/accept、docs) **来源**:总审查人实证(复审 #3) **依赖**:无 **被依赖**:所有依赖 WebSocket 集成测试的问题(建议最先合入) > 本 issue 更正 #3 原先的"上行 worker 同步 PublishDown 与 mochi InlineClient 死锁"结论。该结论经实证不成立,下方"原始描述"保留供追溯。 ### 结论 `TestUplinkDMOfflineGroupRecall` 等"resp 回不来"不是服务端死锁,是测试用 WebSocket 客户端 `test/harness/mqtt.go` 的两个缺陷: 1. **接收不按字节流拆包**:`wsMQTT.Recv`(`test/harness/mqtt.go:173-192`)把一个 WS 二进制帧当作恰好一个 MQTT 包返回。mochi 在客户端出站队列有积压时,会把多个包写进 `outbuf` 后一次 `Write`(mochi `clients.go:602-631`),经 coder/websocket 成为一个 WS 消息。MQTT 5.0 §6.0 [MQTT-6.0.0-3] 明确规定接收方不得假设控制包与 WS 帧对齐。客户端只解出第一个包,其余全部丢弃。 2. **发送不是原子的、也不加锁**:`writeWSClientFrame`(`:249-277`)把帧头和载荷分两次 `Write`,`wsMQTT.Send`(`:169-171`)没有锁。读循环回 PUBACK 与测试线程发请求并发时,两帧交错,发往服务端的 WS 流被写乱,服务端卡在读一个错乱长度的包上,而且不打任何报错日志。 ### 实证(基线 main 4059a15,群事件改回同步下发、不带 20ms sleep) | 条件 | 结果 | |---|---| | 原测试客户端,只加统计 | 6 次 1 失败;失败轮次记录到 `WS frame carries 3 MQTT packets: [group_event group_event resp/rid=g1]`,随后 `timeout waiting resp rid=g1` | | 只修接收(按字节流拆包) | 20 次 2 失败(`g1`、`s4`),无服务端报错 | | 再修发送(加锁 + 整帧一次写) | 20/20 通过,期间 125 个合包帧全部正确拆开;`cmd/nixmsg`、`test/accept`、`internal/app/group`、`internal/broker` 全部通过 | 通过的轮次里也一直在丢包(`receipt+receipt`、`group_event+group_event` 各丢第二个),只是现有断言没覆盖。 ### 影响范围 所有经 `harness.DialMQTTWebSocket` 的测试客户端:`cmd/nixmsg/*_test.go`(mqttSess)、`test/accept`(MQTTSession)、`test/chaos`、`test/load/q3_live_test.go`、`web/e2e/seedpending`。此前 Q accept-rest 里"F15 带密拉人用独立短生命周期进程""group.add 带密码卡住"等绕行,以及 `internal/app/group/app.go:659-686` 的 goroutine + `time.Sleep(20ms)`,都是这个缺陷的误诊产物。 ### 解决方案(已验证) 1. `test/harness/mqtt.go`: - `wsMQTT` 增加接收缓冲 `buf []byte` 与写锁 `wmu sync.Mutex`。 - `Recv` 先从缓冲里按 MQTT 剩余长度切出一个完整包;不够时继续读 WS 帧,opcode `0x2` 与续帧 `0x0` 的载荷都追加到缓冲;`0x8` 返回 EOF;`0x9` 在写锁内回 PONG;`0xA` 忽略。 - `Send` 与回 PONG 都持写锁;`writeWSClientFrame` 把帧头和掩码后的载荷拼成一个切片,一次 `Write`。 2. `internal/app/group/app.go` 的 `emit`:删除 goroutine 与 `time.Sleep(20ms)`,改回在调用方上下文里同步 `PublishDown`(`group_event` 是 QoS 0,mochi 的 QoS 0 发布走非阻塞通道,不会卡住调用方)。 3. `test/accept/rest_accept_test.go`:F15 去掉 `runF15JoinWithPasswordFresh` 的"独立短进程"绕行,在同一长会话里用 `group.add` + 当次对话密码断言成功。 4. `docs/DEVIATIONS.md`:把「Q accept-rest」第 2–4 条和「死锁未修(issue #3)」整节标注为已被本 issue 取代,写明真实根因;不要删除历史文字。 5. 放弃工作树 `E:\code\NixMsg-wt\fix3` 与分支 `feat/fix-3-downlink-deadlock`(基于错误假设,其 `OnPublish` 先写 PUBACK 的改法还改变了语义),不要合入。 <details><summary>已验证的测试客户端补丁要点(去掉了诊断日志)</summary> ```go type wsMQTT struct { conn net.Conn r *bufio.Reader buf []byte wmu sync.Mutex } func splitMQTTPacket(b []byte) ([]byte, int, bool) { if len(b) < 2 { return nil, 0, false } rem, mult := 0, 1 for i := 1; i < len(b) && i <= 4; i++ { rem += int(b[i]&127) * mult if b[i]&128 == 0 { total := 1 + i + rem if len(b) < total { return nil, 0, false } pkt := make([]byte, total) copy(pkt, b[:total]) return pkt, total, true } mult *= 128 } return nil, 0, false } func (c *wsMQTT) Send(packet []byte) error { c.wmu.Lock() defer c.wmu.Unlock() return writeWSClientBinary(c.conn, packet) } func (c *wsMQTT) Recv() ([]byte, error) { for { if pkt, n, ok := splitMQTTPacket(c.buf); ok { c.buf = c.buf[n:] return pkt, nil } payload, opcode, err := readWSFrame(c.r) if err != nil { return nil, err } switch opcode { case 0x0, 0x2: c.buf = append(c.buf, payload...) case 0x8: return nil, io.EOF case 0x9: c.wmu.Lock() _ = writeWSClientControl(c.conn, 0xA, payload) c.wmu.Unlock() } } } // writeWSClientFrame 末尾:帧头与掩码载荷拼成一个切片,一次写出 frame := make([]byte, len(header)+n) copy(frame, header) for i := 0; i < n; i++ { frame[len(header)+i] = payload[i] ^ mask[i%4] } _, err := w.Write(frame) return err ``` </details> ### 改动文件 `test/harness/mqtt.go`、`internal/app/group/app.go`(仅 `emit`)、`test/accept/rest_accept_test.go`(仅 F15)、`docs/DEVIATIONS.md`。 ### 交互 / 冲突说明 - 只改测试基础设施和 `emit` 一个函数,与其他工作线没有文件冲突(身份线 U-02 改 group 其他函数时注意不要动 `emit`)。 - 建议**最先合入**:其他问题的集成测试都依赖可靠的 WS 测试客户端,否则会出现与本问题相同的"随机收不到 resp"。 ### 验收与测试 - 新增 harness 单测:一个 WS 帧装 3 个 MQTT 包,`Recv` 依次返回 3 个;一个包跨两个帧;两个 goroutine 各并发 `Send` 1000 次,服务端全部正确解码。 - `go test ./cmd/nixmsg/ -run TestUplinkDMOfflineGroupRecall -count=20` 在同步 `emit` 下全部通过。 - `task check` 与 `go test ./test/... -count=1` 通过。 --- <details><summary>原始描述(已被推翻,保留追溯)</summary> ## 严重级 medium ## 分类 设计缺陷 / 竞态 ## 现象 同一连接上 `group.create` / `group.add` 若在处理上行的 worker 里同步向本连接注入下行(`server.Publish` → InlineClient `InjectPacket`),会与 mochi InlineClient 互相等待,应用层 `resp` 发不回去。当前实现把 `group_event` 放到独立 goroutine 并 `Sleep(20ms)`,让 worker 先 `PublishDown` resp。这是竞态窗口,不是根因修复:负载升高、调度延迟偏离 20ms、或其它仍同步的下行,仍可能卡住。 补测 `TestUplinkDMOfflineGroupRecall` 在清掉测试客户端 dial deadline 后可稳定复现(见 DEVIATIONS Q accept-rest 第 4 条)。 ## 路径 - `internal/app/group/app.go` 第 659–685 行:`emit` 异步 + 20ms 再 `PublishDown`(QoS 0) - `internal/app/group/void.go` 第 153–167 行:`publishRevokes` **仍同步** `PublishDown`;`Leave`/`Remove`/`Dissolve`(app.go 约 262、299、393 行)在返回给 uplink 发 resp **之前**就会走到这里 - `internal/broker/hooks.go` 第 114–118 行:InlineClient 必须放行才能分发给订阅者 - `internal/broker/broker.go` 第 195–238 行:`PublishDown` → `server.Publish` - `internal/broker/queue.go` 第 41–44 行:每端串行 worker 调用 `HandleUplink` - `cmd/nixmsg/uplink.go` 第 62–75、268–295 行:业务返回后 `replyOK` 再 `PublishDown` resp - 旁证:`internal/app/message/push.go` `WakePush` 在 `Submit` 成功后异步 `PushPending`;自发自收时可能与 `replyOK` 并发 `PublishDown` 到同一连接 ## 文档条款 - DEVELOPMENT 第 5 节:装配 `InlineClient = true`;`OnPublish` 在客户端读循环同步执行;worker 不要同步等待「断开同一连接」 - DEVIATIONS「Q accept-rest」第 4 条:写明原因是 InlineClient 互相等待,备选方案是 broker 对 Inline 发布做无锁队列;影响是 group_event 可能略晚于 resp - PRD F16:群事件尽力推送,没有「延迟 20ms」的产品要求 ## 为何不是故意设计 20ms 不是 PRD/DEVELOPMENT 的协议语义,是补测为避开死锁加的睡眠。根因(上行 worker 调用栈上同步 Inline `Publish`)还在。`group_event` 绕开了,`revoked` 等同步路径没有。已知清单里的「群 emit 异步+20ms」是本波处理项,但死锁根因仍在,应按设计缺陷跟踪,而不是当作已修完。 ## 建议方向(不实现) 不要用 `time.Sleep` 赌调度。正确修法例如: 1. `broker.PublishDown` 对 Inline 走无锁出站队列,由独立循环 Inject;上行 worker 只入队、立刻返回;或 2. 所有业务通知(group_event / revoked / presence)在 `resp` 发布之后由 uplink 层入队,且禁止在 `HandleUplink` 栈上对本连接同步 `PublishDown`。 去掉 20ms 魔法数,并用原先会卡住的建群/退群+已推送投递用例做回归,而不是靠睡眠。 </details> --- <sub>复审基线:main `4059a15`(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。</sub>
Author
Owner

未关闭,也未合入 feat/fix-3-downlink-deadlock。当前 main 是 4059a1576bc97879b840ec5200defd80bc9eb3f0,其中仍保留 20ms 绕过。分析已写入仓库 docs/DEVIATIONS.md 末尾「### 死锁未修(issue #3)」。工作树 e:\code\NixMsg-wt\fix3 保留给工程师,里面的未提交改动未验证、不进 main。

现象:每端上行串行 worker(internal/broker/queue.go 的 loop)同步调用 HandleUplink。群操作在同一调用栈里再对本连接 PublishDown group_event。PublishDown 走 mochi server.Publish → InlineClient InjectPacket,与读循环写 PUBACK 抢同一把 Client 锁,QoS 1 的 resp 回不去。清掉测试客户端 dial deadline 后,TestUplinkDMOfflineGroupRecall 在 group.create(rid=g1)稳定超时。

尝试 1:emit 立刻起 goroutine 再 PublishDown,仍与 resp 抢同一连接,仍然死锁。
尝试 2(已在 main,来自 479a08e):emit 的 goroutine 里 Sleep(20ms) 再下发,让 resp 先出去。当时 task check 通过。这是时间差绕过,不是根因修复;presence 等其他同步 PublishDown 仍可能卡。
尝试 3(叫停):分支停在 479a08e,没有可合的提交。未提交改动在 broker 层:OnPublish 先在读循环写完 PUBACK 再入队;该端 HandleUplink 期间对本连接的下行只入延后队列,handler 返回后再 server.Publish;group emit 改回同步并去掉 sleep。InlineClient 放行未改。本会话未对其跑 task check。

库约束:DEVELOPMENT 要求 InlineClient=true;OnPublish 对 InlineClient 必须放行,否则 PublishDown 送不到订阅者。
建议方向:broker 把对本连接的下行 InjectPacket 与上行 worker 解耦,不要靠固定 Sleep。

未关闭,也未合入 feat/fix-3-downlink-deadlock。当前 main 是 `4059a1576bc97879b840ec5200defd80bc9eb3f0`,其中仍保留 20ms 绕过。分析已写入仓库 `docs/DEVIATIONS.md` 末尾「### 死锁未修(issue #3)」。工作树 `e:\code\NixMsg-wt\fix3` 保留给工程师,里面的未提交改动未验证、不进 main。 现象:每端上行串行 worker(`internal/broker/queue.go` 的 loop)同步调用 HandleUplink。群操作在同一调用栈里再对本连接 PublishDown group_event。PublishDown 走 mochi `server.Publish` → InlineClient `InjectPacket`,与读循环写 PUBACK 抢同一把 Client 锁,QoS 1 的 resp 回不去。清掉测试客户端 dial deadline 后,`TestUplinkDMOfflineGroupRecall` 在 group.create(rid=g1)稳定超时。 尝试 1:emit 立刻起 goroutine 再 PublishDown,仍与 resp 抢同一连接,仍然死锁。 尝试 2(已在 main,来自 479a08e):emit 的 goroutine 里 Sleep(20ms) 再下发,让 resp 先出去。当时 task check 通过。这是时间差绕过,不是根因修复;presence 等其他同步 PublishDown 仍可能卡。 尝试 3(叫停):分支停在 479a08e,没有可合的提交。未提交改动在 broker 层:OnPublish 先在读循环写完 PUBACK 再入队;该端 HandleUplink 期间对本连接的下行只入延后队列,handler 返回后再 server.Publish;group emit 改回同步并去掉 sleep。InlineClient 放行未改。本会话未对其跑 task check。 库约束:DEVELOPMENT 要求 InlineClient=true;OnPublish 对 InlineClient 必须放行,否则 PublishDown 送不到订阅者。 建议方向:broker 把对本连接的下行 InjectPacket 与上行 worker 解耦,不要靠固定 Sleep。
nixevol changed title from 上行 worker 同步 PublishDown 与 mochi InlineClient 会死锁,20ms 只绕过 group_event to [T-01][medium] 测试 WS 客户端不按字节流拆包、并发写不加锁,被误判为服务端死锁;移除群事件 20ms 绕过 2026-09-30 13:57:22 +08:00
nixevol added the P2-mediumlane/test-docsreview-2026-09-30 labels 2026-09-30 13:57:22 +08:00
Author
Owner

已按 2026-09-30 全量复审更新本 issue:原先的"服务端死锁"结论不成立,真实根因是测试用 WebSocket 客户端不按字节流拆包、并发写不加锁(对照实验见正文)。本 issue 现为工作包 T-01,请按正文方案修复;原始描述保留在正文末尾。

已按 2026-09-30 全量复审更新本 issue:原先的"服务端死锁"结论不成立,真实根因是测试用 WebSocket 客户端不按字节流拆包、并发写不加锁(对照实验见正文)。本 issue 现为工作包 T-01,请按正文方案修复;原始描述保留在正文末尾。
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: nixevol/NixMsg#3