fix: 按 K-00 约定修复 Go SDK 断线重交与退避
This commit is contained in:
+148
-104
@@ -15,7 +15,6 @@ func (c *Client) handleDown(payload []byte) {
|
||||
if err := unmarshalJSON(payload, &head); err != nil {
|
||||
return
|
||||
}
|
||||
// resp 必须在收包路径同步处理,否则 request/ack 在 downLoop 里等待时会死锁。
|
||||
if head.Type == "resp" {
|
||||
var rf respFrame
|
||||
if err := unmarshalJSON(payload, &rf); err != nil {
|
||||
@@ -35,10 +34,45 @@ func (c *Client) handleDown(payload []byte) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if head.Type == "fatal" {
|
||||
var f struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
_ = unmarshalJSON(payload, &f)
|
||||
c.handleFatal(f.Reason)
|
||||
return
|
||||
}
|
||||
kind := head.Type
|
||||
cp := append([]byte(nil), payload...)
|
||||
c.enqueueDown(queuedFrame{payload: cp, kind: kind})
|
||||
}
|
||||
|
||||
func (c *Client) enqueueDown(fr queuedFrame) {
|
||||
c.mu.Lock()
|
||||
ch := c.incoming
|
||||
ctx := c.ctx
|
||||
c.mu.Unlock()
|
||||
if ch == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
if fr.kind == "presence" || fr.kind == "group_event" {
|
||||
select {
|
||||
case ch <- fr:
|
||||
default:
|
||||
// 超阈值丢最旧:缓冲满则丢弃本条事件
|
||||
}
|
||||
return
|
||||
}
|
||||
select {
|
||||
case c.downCh <- cp:
|
||||
case <-c.ctx.Done():
|
||||
case ch <- fr:
|
||||
case <-ctx.Done():
|
||||
default:
|
||||
go func() {
|
||||
select {
|
||||
case ch <- fr:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +81,8 @@ func (c *Client) downLoop(ctx context.Context) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case payload := <-c.downCh:
|
||||
c.handleDownApp(payload)
|
||||
case fr := <-c.incoming:
|
||||
c.handleDownApp(fr.payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,23 +105,17 @@ func (c *Client) handleDownApp(payload []byte) {
|
||||
c.handlePresence(payload)
|
||||
case "group_event":
|
||||
c.handleGroupEvent(payload)
|
||||
case "fatal":
|
||||
var f struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
_ = unmarshalJSON(payload, &f)
|
||||
c.mu.Lock()
|
||||
c.stopReconnect = true
|
||||
c.setStateLocked(StateAuthFailed, f.Reason)
|
||||
c.failQueuedLocked(apiErr("fatal", f.Reason))
|
||||
cancel := c.cancel
|
||||
c.mu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func msgKey(from, id string) string {
|
||||
return "m\x00" + from + "\x00" + id
|
||||
}
|
||||
|
||||
func receiptKey(id string) string {
|
||||
return "r\x00" + id
|
||||
}
|
||||
|
||||
func (c *Client) handleMsg(payload []byte) {
|
||||
var m struct {
|
||||
ID string `json:"id"`
|
||||
@@ -100,57 +128,57 @@ func (c *Client) handleMsg(payload []byte) {
|
||||
if err := unmarshalJSON(payload, &m); err != nil {
|
||||
return
|
||||
}
|
||||
key := m.From + "\x00" + m.ID
|
||||
key := msgKey(m.From, m.ID)
|
||||
c.mu.Lock()
|
||||
ent := c.dedup[key]
|
||||
manual := c.opts.ManualAck
|
||||
if ent != nil && ent.state == dedupAcked {
|
||||
c.mu.Unlock()
|
||||
_ = c.sendAckFrame(m.From, m.ID)
|
||||
return
|
||||
raw, ok := c.store.Get(key)
|
||||
if ok {
|
||||
ent := raw.(*dedupEntry)
|
||||
if ent.state == dedupAcked {
|
||||
c.mu.Unlock()
|
||||
go func() { _ = c.sendAckFrame(m.From, m.ID) }()
|
||||
return
|
||||
}
|
||||
if ent.state == dedupDelivered || ent.state == dedupRevoked {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
if ent != nil && ent.state == dedupDelivered {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.rememberDedupLocked(key, m.From, m.ID, dedupDelivered)
|
||||
c.store.Put(key, &dedupEntry{state: dedupDelivered, from: m.From, id: m.ID})
|
||||
c.mu.Unlock()
|
||||
|
||||
msg := Message{ID: m.ID, From: m.From, To: m.To, Body: m.Body, Meta: m.Meta, SendAtMs: m.SendAtMs}
|
||||
var cbErr error
|
||||
c.cbMu.Lock()
|
||||
if c.onMessage != nil {
|
||||
cbErr = c.onMessage(msg)
|
||||
}
|
||||
c.cbMu.Unlock()
|
||||
|
||||
if manual {
|
||||
return
|
||||
}
|
||||
if cbErr != nil {
|
||||
c.mu.Lock()
|
||||
delete(c.dedup, key)
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
_ = c.sendAckFrame(m.From, m.ID)
|
||||
c.mu.Lock()
|
||||
if e := c.dedup[key]; e != nil {
|
||||
e.state = dedupAcked
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) rememberDedupLocked(key, from, id string, st dedupState) {
|
||||
if _, ok := c.dedup[key]; !ok {
|
||||
c.dedupOrd = append(c.dedupOrd, key)
|
||||
for len(c.dedupOrd) > c.opts.DedupCapacity {
|
||||
old := c.dedupOrd[0]
|
||||
c.dedupOrd = c.dedupOrd[1:]
|
||||
delete(c.dedup, old)
|
||||
done := make(chan struct{})
|
||||
c.dispatch(func() {
|
||||
defer close(done)
|
||||
var cbErr error
|
||||
if c.onMessage != nil {
|
||||
cbErr = c.onMessage(msg)
|
||||
}
|
||||
if manual {
|
||||
return
|
||||
}
|
||||
if cbErr != nil {
|
||||
c.mu.Lock()
|
||||
c.store.Delete(key)
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
_ = c.sendAckFrame(m.From, m.ID)
|
||||
c.mu.Lock()
|
||||
if raw, ok := c.store.Get(key); ok {
|
||||
if ent, ok := raw.(*dedupEntry); ok {
|
||||
ent.state = dedupAcked
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}()
|
||||
})
|
||||
select {
|
||||
case <-done:
|
||||
case <-c.ctx.Done():
|
||||
}
|
||||
c.dedup[key] = &dedupEntry{state: st, from: from, id: id}
|
||||
}
|
||||
|
||||
// Ack 手动确认。
|
||||
@@ -158,9 +186,9 @@ func (c *Client) Ack(msg Message) error {
|
||||
if err := c.sendAckFrame(msg.From, msg.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
key := msg.From + "\x00" + msg.ID
|
||||
key := msgKey(msg.From, msg.ID)
|
||||
c.mu.Lock()
|
||||
c.rememberDedupLocked(key, msg.From, msg.ID, dedupAcked)
|
||||
c.store.Put(key, &dedupEntry{state: dedupAcked, from: msg.From, id: msg.ID})
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
@@ -198,21 +226,22 @@ func (c *Client) handleReceipt(payload []byte) {
|
||||
if err := unmarshalJSON(payload, &r); err != nil {
|
||||
return
|
||||
}
|
||||
key := receiptKey(r.ReceiptID)
|
||||
c.mu.Lock()
|
||||
if _, ok := c.receiptSeen[r.ReceiptID]; ok {
|
||||
if c.store.Has(key) {
|
||||
c.mu.Unlock()
|
||||
_ = c.sendReceiptAck(r.ReceiptID)
|
||||
go func() { _ = c.sendReceiptAck(r.ReceiptID) }()
|
||||
return
|
||||
}
|
||||
c.receiptSeen[r.ReceiptID] = struct{}{}
|
||||
c.store.Put(key, struct{}{})
|
||||
c.mu.Unlock()
|
||||
ev := Receipt{ReceiptID: r.ReceiptID, ID: r.ID, EndpointID: r.EndpointID, State: r.State, Reason: r.Reason, AtMs: r.AtMs}
|
||||
c.cbMu.Lock()
|
||||
if c.onReceipt != nil {
|
||||
c.onReceipt(ev)
|
||||
}
|
||||
c.cbMu.Unlock()
|
||||
_ = c.sendReceiptAck(r.ReceiptID)
|
||||
c.dispatch(func() {
|
||||
if c.onReceipt != nil {
|
||||
c.onReceipt(ev)
|
||||
}
|
||||
})
|
||||
go func() { _ = c.sendReceiptAck(r.ReceiptID) }()
|
||||
}
|
||||
|
||||
func (c *Client) sendReceiptAck(receiptID string) error {
|
||||
@@ -232,24 +261,27 @@ func (c *Client) handleRevoked(payload []byte) {
|
||||
if err := unmarshalJSON(payload, &r); err != nil {
|
||||
return
|
||||
}
|
||||
key := r.From + "\x00" + r.ID
|
||||
key := msgKey(r.From, r.ID)
|
||||
c.mu.Lock()
|
||||
ent := c.dedup[key]
|
||||
if ent == nil || ent.state == dedupAcked {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
raw, ok := c.store.Get(key)
|
||||
if ok {
|
||||
ent := raw.(*dedupEntry)
|
||||
if ent.state == dedupAcked || ent.state == dedupRevoked {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
delete(c.dedup, key)
|
||||
c.store.Put(key, &dedupEntry{state: dedupRevoked, from: r.From, id: r.ID})
|
||||
c.mu.Unlock()
|
||||
c.emitRevoked(RevokedEvent{ID: r.ID, From: r.From, Reason: r.Reason})
|
||||
}
|
||||
|
||||
func (c *Client) emitRevoked(e RevokedEvent) {
|
||||
c.cbMu.Lock()
|
||||
defer c.cbMu.Unlock()
|
||||
if c.onRevoked != nil {
|
||||
c.onRevoked(e)
|
||||
}
|
||||
c.dispatch(func() {
|
||||
if c.onRevoked != nil {
|
||||
c.onRevoked(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) handlePresence(payload []byte) {
|
||||
@@ -261,11 +293,11 @@ func (c *Client) handlePresence(payload []byte) {
|
||||
if err := unmarshalJSON(payload, &p); err != nil {
|
||||
return
|
||||
}
|
||||
c.cbMu.Lock()
|
||||
defer c.cbMu.Unlock()
|
||||
if c.onPresence != nil {
|
||||
c.onPresence(PresenceEvent{ID: p.ID, Online: p.Online, AtMs: p.AtMs})
|
||||
}
|
||||
c.dispatch(func() {
|
||||
if c.onPresence != nil {
|
||||
c.onPresence(PresenceEvent{ID: p.ID, Online: p.Online, AtMs: p.AtMs})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) handleGroupEvent(payload []byte) {
|
||||
@@ -278,14 +310,19 @@ func (c *Client) handleGroupEvent(payload []byte) {
|
||||
if err := unmarshalJSON(payload, &g); err != nil {
|
||||
return
|
||||
}
|
||||
c.cbMu.Lock()
|
||||
defer c.cbMu.Unlock()
|
||||
if c.onGroupEvent != nil {
|
||||
c.onGroupEvent(GroupEvent{GroupID: g.GroupID, Event: g.Event, EndpointID: g.EndpointID, AtMs: g.AtMs})
|
||||
}
|
||||
c.dispatch(func() {
|
||||
if c.onGroupEvent != nil {
|
||||
c.onGroupEvent(GroupEvent{GroupID: g.GroupID, Event: g.Event, EndpointID: g.EndpointID, AtMs: g.AtMs})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) request(ctx context.Context, frame map[string]any, allowUnready bool) (json.RawMessage, error) {
|
||||
if _, ok := ctx.Deadline(); !ok {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
@@ -296,6 +333,7 @@ func (c *Client) request(ctx context.Context, frame map[string]any, allowUnready
|
||||
return nil, apiErr(CodeNotConnected, "未握手")
|
||||
}
|
||||
tr := c.transport
|
||||
clientCtx := c.ctx
|
||||
c.mu.Unlock()
|
||||
if tr == nil {
|
||||
return nil, apiErr(CodeNotConnected, "未连接")
|
||||
@@ -311,7 +349,7 @@ func (c *Client) request(ctx context.Context, frame map[string]any, allowUnready
|
||||
}
|
||||
ch := make(chan respFrame, 1)
|
||||
c.mu.Lock()
|
||||
c.pending[rid] = &pendingReq{rid: rid, ch: ch}
|
||||
c.pending[rid] = &pendingReq{rid: rid, ch: ch, isSend: false}
|
||||
c.mu.Unlock()
|
||||
if err := tr.PublishUp(payload); err != nil {
|
||||
c.mu.Lock()
|
||||
@@ -319,22 +357,28 @@ func (c *Client) request(ctx context.Context, frame map[string]any, allowUnready
|
||||
c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
var rf respFrame
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.mu.Lock()
|
||||
delete(c.pending, rid)
|
||||
c.mu.Unlock()
|
||||
return nil, ctx.Err()
|
||||
case rf := <-ch:
|
||||
if !rf.OK {
|
||||
code, msg := CodeBadRequest, "请求失败"
|
||||
if rf.Error != nil {
|
||||
code, msg = rf.Error.Code, rf.Error.Message
|
||||
}
|
||||
return nil, apiErr(code, msg)
|
||||
}
|
||||
return rf.Data, nil
|
||||
case <-clientCtx.Done():
|
||||
c.mu.Lock()
|
||||
delete(c.pending, rid)
|
||||
c.mu.Unlock()
|
||||
return nil, apiErr(CodeNotConnected, "未连接")
|
||||
case rf = <-ch:
|
||||
}
|
||||
if !rf.OK {
|
||||
code, msg := CodeBadRequest, "请求失败"
|
||||
if rf.Error != nil {
|
||||
code, msg = rf.Error.Code, rf.Error.Message
|
||||
}
|
||||
return nil, apiErr(code, msg)
|
||||
}
|
||||
return rf.Data, nil
|
||||
}
|
||||
|
||||
// bodyDecodedLen 按解码后字节计正文大小。
|
||||
|
||||
Reference in New Issue
Block a user