Repository navigation
[EE] CSGLite edge serving connector - #247
Conversation
| ctx, cancel := context.WithCancel(context.Background()) | ||
| m.cancel = cancel | ||
| m.done = make(chan struct{}) | ||
| m.draining = make(chan struct{}) |
There was a problem hiding this comment.
[P1] m.draining 无锁读写:并发开关会触发 data race,旧 loop 还可能读到新 channel 导致 drain 失效
现象与触发场景
run() 在 :284 持锁写入 m.draining,但 loop()(:330、:337、:373)和 session()(:466)都在 select 里不加锁直接读这个字段。用户在 Edge 页面连续切换开关时(PUT /api/edge → SetEnabled → Stop() / run()),旧 loop 或正在 drain 的 session 读到的是 run() 刚替换的新 channel,收不到关闭信号:Stop() 只能等 15s 超时后 cancel() 才返回;如果此时新 loop 已经起来,两个 loop 会各自建隧道,网关侧 previous.close("replaced") 把彼此顶掉,表现为连接反复闪断。go test -race 会直接报这一对读写。现有 manager_test.go 都是串行调用 SetEnabled,覆盖不到并发路径。
破坏的承诺
PR 描述承诺 "exponential backoff reconnect ... that resets after 30s stable",开关是用户可随时操作的控制面;draining 是这条控制面唯一的停止信号,读到旧/新 channel 的竞态会让"关闭"变成"15 秒后被迫取消"甚至"两个连接同时存在"。Risks 里没有覆盖并发开关场景,Non-goals 也没有排除。
修复建议
照 done 的做法:在 run() 里把 draining 取成局部变量传给 loop/session(done := m.done 已经是这个写法),并在 Stop() 持锁时把 m.draining 置 nil,禁止后续读取字段本身。
需要补的测试
并发 SetEnabled(true) / SetEnabled(false) 各若干次,在 -race 下运行;断言 Stop() 返回后不再有新的 Models() / 重连调用(例如统计 fakeHost 的 Models 调用次数在 stop 后不再增长)。
# Conflicts: # internal/license/features.go # internal/license/manager_test.go # internal/server/license_test.go
Summary
ee/edge: CSGLite edge serving connector. Lite dials the AI gateway over an outbound WebSocket (/v1/edge/connect), multiplexes streams with yamux, and serves approved local models through the tunnel./api/edgefor node status, model list and application review, gated by the newfeature.lite.edge_servingenterprise license feature.openapi/local-api.jsonand document the newly gated feature indocs/agent-guidelines/ee-features.md.Test plan
go test ./ee/edge ./internal/server ./internal/licensecd web && npx vitest run src/edge.test.ts