fix: 按 K-00 约定修复 Go SDK 断线重交与退避

This commit is contained in:
Nixevol
2026-09-30 16:24:13 +08:00
parent 3749b9bdf1
commit f6f8ccf269
20 changed files with 1544 additions and 324 deletions
+172 -23
View File
@@ -1,4 +1,4 @@
package nixmsg
package nixmsg
import (
"context"
@@ -12,17 +12,21 @@ import (
)
type pendingReq struct {
rid string
ch chan respFrame
rid string
ch chan respFrame
isSend bool
}
type sendItem struct {
frame map[string]any
payload []byte
id string
sendAtMs *int64
result chan sendOutcome
inflight bool
frame map[string]any
payload []byte
id string
sendAtMs *int64
result chan sendOutcome
inflight bool
rateN int
epoch uint64
abandoned bool
}
type sendOutcome struct {
@@ -35,6 +39,7 @@ type dedupState int
const (
dedupDelivered dedupState = iota + 1
dedupAcked
dedupRevoked
)
type dedupEntry struct {
@@ -43,6 +48,13 @@ type dedupEntry struct {
id string
}
type queuedFrame struct {
payload []byte
kind string
}
const eventQueueCap = 10000
// Client NixMsg 客户端。
type Client struct {
opts Options
@@ -60,18 +72,15 @@ type Client struct {
session string
stopReconnect bool
closed bool
lastStopCode string
lastStopErr error
ridSeq atomic.Uint64
pending map[string]*pendingReq
sendQ []*sendItem
inflight int
dedup map[string]*dedupEntry
dedupOrd []string
receiptSeen map[string]struct{}
cbMu sync.Mutex
store *lruCache
onSession func(token string)
onMessage func(msg Message) error
@@ -85,19 +94,22 @@ type Client struct {
ctx context.Context
cancel context.CancelFunc
// downCh 串行处理非 resp 下行,避免在 MQTT 收包回调里同步 request 死锁。
downCh chan []byte
incoming chan queuedFrame
cbQ chan func()
connLost chan struct{}
watchIDs []string
watchAll bool
watchSet bool
}
// New 创建客户端(尚未连接)。
func New() *Client {
return &Client{
pending: make(map[string]*pendingReq),
dedup: make(map[string]*dedupEntry),
receiptSeen: make(map[string]struct{}),
state: StateOffline,
backoff: newReconnectBackoff(),
downCh: make(chan []byte, 256),
pending: make(map[string]*pendingReq),
store: newLRU(10000),
state: StateOffline,
backoff: newReconnectBackoff(),
}
}
@@ -142,3 +154,140 @@ type respFrame struct {
Message string `json:"message"`
} `json:"error"`
}
func (c *Client) dispatch(fn func()) {
if fn == nil {
return
}
c.mu.Lock()
c.dispatchLocked(fn)
c.mu.Unlock()
}
func (c *Client) dispatchLocked(fn func()) {
if fn == nil {
return
}
ch := c.cbQ
ctx := c.ctx
if ch == nil {
go fn()
return
}
select {
case ch <- fn:
default:
go func() {
if ctx == nil {
ch <- fn
return
}
select {
case ch <- fn:
case <-ctx.Done():
}
}()
}
}
func (c *Client) cbLoop(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case fn := <-c.cbQ:
if fn != nil {
fn()
}
}
}
}
func (c *Client) stopErrLocked() error {
if c.lastStopErr != nil {
return c.lastStopErr
}
if c.lastStopCode != "" {
return apiErr(c.lastStopCode, "已停止重连")
}
return apiErr(CodeClosed, "已关闭")
}
func (c *Client) failPendingLocked(err error, sendsToo bool) {
for rid, p := range c.pending {
if p == nil {
continue
}
if p.isSend && !sendsToo {
continue
}
select {
case p.ch <- respFrame{OK: false, Error: &struct {
Code string `json:"code"`
Message string `json:"message"`
}{Code: errCode(err), Message: err.Error()}}:
default:
}
delete(c.pending, rid)
}
}
func errCode(err error) string {
if err == nil {
return CodeNotConnected
}
if ae, ok := err.(*APIError); ok {
return ae.Code
}
return CodeNotConnected
}
func (c *Client) closeConnLostLocked() {
if c.connLost != nil {
select {
case <-c.connLost:
default:
close(c.connLost)
}
c.connLost = nil
}
}
func (c *Client) regenerateSendLocked(it *sendItem) {
rid := c.nextRID()
it.frame["rid"] = rid
payload, err := marshalJSON(it.frame)
if err != nil {
return
}
it.payload = payload
}
func (c *Client) requeueInflightLocked() {
for _, it := range c.sendQ {
if !it.inflight {
continue
}
it.epoch++
it.inflight = false
rid, _ := it.frame["rid"].(string)
delete(c.pending, rid)
c.regenerateSendLocked(it)
}
c.inflight = 0
}
func (c *Client) onTransportOffline() {
c.mu.Lock()
c.handshook = false
c.closeConnLostLocked()
if c.stopReconnect || c.closed {
c.failPendingLocked(apiErr(CodeNotConnected, "未连接"), true)
c.mu.Unlock()
return
}
c.requeueInflightLocked()
c.failPendingLocked(apiErr(CodeNotConnected, "未连接"), false)
c.setStateLocked(StateReconnecting, "")
c.mu.Unlock()
}