fix: 按 K-00 约定修复 Go SDK 断线重交与退避
This commit is contained in:
+98
-27
@@ -11,11 +11,16 @@ func (c *Client) Send(ctx context.Context, to Target, body Body, opt SendOptions
|
||||
c.mu.Lock()
|
||||
limits := c.limits
|
||||
skew := c.clockSkew
|
||||
handshook := c.handshook
|
||||
maxQ := c.opts.SendQueueSize
|
||||
qLen := len(c.sendQ)
|
||||
stopped := c.stopReconnect || c.closed
|
||||
stopErr := c.stopErrLocked()
|
||||
c.mu.Unlock()
|
||||
|
||||
if stopped {
|
||||
return SendResult{}, stopErr
|
||||
}
|
||||
|
||||
if body.Enc == "" {
|
||||
body.Enc = "utf8"
|
||||
}
|
||||
@@ -39,10 +44,7 @@ func (c *Client) Send(ctx context.Context, to Target, body Body, opt SendOptions
|
||||
if maxBody <= 0 {
|
||||
maxBody = 262144
|
||||
}
|
||||
if handshook && n > maxBody {
|
||||
return SendResult{}, apiErr(CodeBodyTooLarge, "正文超限")
|
||||
}
|
||||
if !handshook && n > 262144 {
|
||||
if n > maxBody {
|
||||
return SendResult{}, apiErr(CodeBodyTooLarge, "正文超限")
|
||||
}
|
||||
|
||||
@@ -81,7 +83,6 @@ func (c *Client) Send(ctx context.Context, to Target, body Body, opt SendOptions
|
||||
return SendResult{}, apiErr(CodeBadRequest, "sendAt 与 delay 互斥")
|
||||
}
|
||||
if opt.SendAt != nil {
|
||||
// sendAt 使用本机时间 + 服务器偏差,换算后写入,重交不重算
|
||||
ms := opt.SendAt.UnixMilli() + skew
|
||||
sendAtMs = &ms
|
||||
frame["send_at_ms"] = ms
|
||||
@@ -97,7 +98,7 @@ func (c *Client) Send(ctx context.Context, to Target, body Body, opt SendOptions
|
||||
if maxFrame <= 0 {
|
||||
maxFrame = 786432
|
||||
}
|
||||
if handshook && len(payload) > maxFrame {
|
||||
if len(payload) > maxFrame {
|
||||
return SendResult{}, apiErr(CodeFrameTooLarge, "整帧超限")
|
||||
}
|
||||
|
||||
@@ -111,10 +112,11 @@ func (c *Client) Send(ctx context.Context, to Target, body Body, opt SendOptions
|
||||
|
||||
c.mu.Lock()
|
||||
if c.closed || c.stopReconnect {
|
||||
err := c.stopErrLocked()
|
||||
c.mu.Unlock()
|
||||
return SendResult{}, apiErr(CodeClosed, "已关闭")
|
||||
return SendResult{}, err
|
||||
}
|
||||
if qLen >= maxQ {
|
||||
if qLen >= maxQ || len(c.sendQ) >= maxQ {
|
||||
c.mu.Unlock()
|
||||
return SendResult{}, apiErr(CodeQueueFull, "发送队列已满")
|
||||
}
|
||||
@@ -125,7 +127,21 @@ func (c *Client) Send(ctx context.Context, to Target, body Body, opt SendOptions
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return SendResult{}, ctx.Err()
|
||||
c.mu.Lock()
|
||||
if !item.inflight && !item.abandoned {
|
||||
nq := c.sendQ[:0]
|
||||
for _, it := range c.sendQ {
|
||||
if it != item {
|
||||
nq = append(nq, it)
|
||||
}
|
||||
}
|
||||
c.sendQ = nq
|
||||
c.mu.Unlock()
|
||||
return SendResult{}, ctx.Err()
|
||||
}
|
||||
item.abandoned = true
|
||||
c.mu.Unlock()
|
||||
return SendResult{}, apiErr(CodeResultUnknown, "结果未知,请用同一消息号重试")
|
||||
case out := <-item.result:
|
||||
return out.res, out.err
|
||||
}
|
||||
@@ -134,13 +150,17 @@ func (c *Client) Send(ctx context.Context, to Target, body Body, opt SendOptions
|
||||
func (c *Client) drainSendQueue() {
|
||||
for {
|
||||
c.mu.Lock()
|
||||
if !c.handshook || c.transport == nil {
|
||||
if !c.handshook || c.transport == nil || c.stopReconnect {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
maxFrame := c.limits.MaxFrameBytes
|
||||
if maxFrame <= 0 {
|
||||
maxFrame = 786432
|
||||
}
|
||||
var next *sendItem
|
||||
for _, it := range c.sendQ {
|
||||
if !it.inflight {
|
||||
if !it.inflight && !it.abandoned {
|
||||
next = it
|
||||
break
|
||||
}
|
||||
@@ -149,48 +169,93 @@ func (c *Client) drainSendQueue() {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if len(next.payload) > maxFrame {
|
||||
c.mu.Unlock()
|
||||
c.finishSend(next, SendResult{}, apiErr(CodeFrameTooLarge, "整帧超限"))
|
||||
continue
|
||||
}
|
||||
next.epoch++
|
||||
captured := next.epoch
|
||||
next.inflight = true
|
||||
c.inflight++
|
||||
tr := c.transport
|
||||
payload := next.payload
|
||||
rid, _ := next.frame["rid"].(string)
|
||||
item := next
|
||||
lost := c.connLost
|
||||
c.mu.Unlock()
|
||||
|
||||
go c.dispatchSend(tr, item, rid, payload)
|
||||
go c.dispatchSend(tr, item, rid, payload, captured, lost)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) dispatchSend(tr transport, item *sendItem, rid string, payload []byte) {
|
||||
func (c *Client) dispatchSend(tr transport, item *sendItem, rid string, payload []byte, epoch uint64, lost <-chan struct{}) {
|
||||
ch := make(chan respFrame, 1)
|
||||
c.mu.Lock()
|
||||
c.pending[rid] = &pendingReq{rid: rid, ch: ch}
|
||||
if item.epoch != epoch || !item.inflight {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.pending[rid] = &pendingReq{rid: rid, ch: ch, isSend: true}
|
||||
c.mu.Unlock()
|
||||
|
||||
if err := tr.PublishUp(payload); err != nil {
|
||||
c.mu.Lock()
|
||||
delete(c.pending, rid)
|
||||
item.inflight = false
|
||||
c.inflight--
|
||||
if item.epoch == epoch && item.inflight {
|
||||
item.inflight = false
|
||||
if c.inflight > 0 {
|
||||
c.inflight--
|
||||
}
|
||||
if c.stopReconnect {
|
||||
errStop := c.stopErrLocked()
|
||||
c.mu.Unlock()
|
||||
c.finishSend(item, SendResult{}, errStop)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
// 网络错误:保留队列等重连
|
||||
return
|
||||
}
|
||||
|
||||
rf := <-ch
|
||||
if lost == nil {
|
||||
lost = make(chan struct{})
|
||||
}
|
||||
|
||||
var rf respFrame
|
||||
select {
|
||||
case rf = <-ch:
|
||||
case <-lost:
|
||||
return
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
valid := item.epoch == epoch && item.inflight
|
||||
c.mu.Unlock()
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
|
||||
if !rf.OK {
|
||||
code, msg := CodeBadRequest, "发送失败"
|
||||
if rf.Error != nil {
|
||||
code, msg = rf.Error.Code, rf.Error.Message
|
||||
}
|
||||
if code == CodeRateLimited {
|
||||
// 自动重交:保持同一 payload(含 id / send_at_ms)
|
||||
c.mu.Lock()
|
||||
item.inflight = false
|
||||
c.inflight--
|
||||
if c.inflight > 0 {
|
||||
c.inflight--
|
||||
}
|
||||
delete(c.pending, rid)
|
||||
item.rateN++
|
||||
n := item.rateN
|
||||
c.regenerateSendLocked(item)
|
||||
c.mu.Unlock()
|
||||
time.AfterFunc(time.Second, func() { c.drainSendQueue() })
|
||||
d := backoffJitter(nominalDelay(n))
|
||||
time.AfterFunc(d, func() { c.drainSendQueue() })
|
||||
return
|
||||
}
|
||||
c.finishSend(item, SendResult{}, apiErr(code, msg))
|
||||
@@ -206,8 +271,8 @@ func (c *Client) dispatchSend(tr transport, item *sendItem, rid string, payload
|
||||
|
||||
func (c *Client) finishSend(item *sendItem, res SendResult, err error) {
|
||||
c.mu.Lock()
|
||||
// 从队列移除
|
||||
out := item.result
|
||||
abandoned := item.abandoned
|
||||
nq := c.sendQ[:0]
|
||||
for _, it := range c.sendQ {
|
||||
if it != item {
|
||||
@@ -216,13 +281,19 @@ func (c *Client) finishSend(item *sendItem, res SendResult, err error) {
|
||||
}
|
||||
c.sendQ = nq
|
||||
if item.inflight {
|
||||
c.inflight--
|
||||
if c.inflight > 0 {
|
||||
c.inflight--
|
||||
}
|
||||
item.inflight = false
|
||||
}
|
||||
rid, _ := item.frame["rid"].(string)
|
||||
delete(c.pending, rid)
|
||||
c.mu.Unlock()
|
||||
select {
|
||||
case out <- sendOutcome{res: res, err: err}:
|
||||
default:
|
||||
if !abandoned {
|
||||
select {
|
||||
case out <- sendOutcome{res: res, err: err}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
c.drainSendQueue()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user