fix: 按 K-00 约定修复 Go SDK 断线重交与退避
This commit is contained in:
+172
-23
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user