Files
hectorzhao 001d5f2cbd
CSS quality / css-quality (push) Has been cancelled
fix: 修复 CMPP 协议字段容量与版本兼容性
2026-09-20 15:49:37 +08:00

233 lines
6.7 KiB
Go

package upstream
import (
"cmpp-platform/gateway/internal/protocollog"
"cmpp-platform/gateway/internal/queue"
"context"
"fmt"
cmpp "github.com/bigwhite/gocmpp"
"log"
"net/http"
"strings"
"sync"
"time"
)
// The reader and heartbeat goroutines share one connection lifecycle. Closing
// the connection must wake pending submitters before scheduling pool recovery.
// Caller holds c.mu across sequence selection, wire write and pending registration.
func (c *connection) sequenceAvailable(sequence uint32) bool {
_, submit := c.pending[sequence]
_, heartbeat := c.heartbeatPending[sequence]
return !submit && !heartbeat
}
type connection struct {
channelID string
config queue.UpstreamConfig
index int
pool *connectionPool
apiBaseURL string
httpClient *http.Client
protocolLogPublisher interface {
Publish(context.Context, protocollog.Event) error
}
gatewayInstanceID string
eventPublisher interface {
PublishReceipt(context.Context, queue.ReceiptEvent) error
PublishUplink(context.Context, queue.UplinkEvent) error
}
mu sync.Mutex
sendMu sync.Mutex
client *cmpp.Client
window chan struct{}
windowLimit int
draining bool
retired bool
lastSubmitRTT time.Duration
consecutiveFailures int
cooldownUntil time.Time
pending map[uint32]chan submitPartResponse
tracker map[uint64]queue.SubmitCommand
longUplink map[string]*longUplinkAssembly
readOnce sync.Once
closed bool
heartbeatCancel context.CancelFunc
heartbeatPending map[uint32]time.Time
}
type submitPartResponse struct {
seqID uint32
msgID uint64
result uint32
err error
}
func (c *connection) matches(config queue.UpstreamConfig) bool {
return c.config == normalizeUpstreamConfig(config)
}
func (c *connection) ensureConnected() (bool, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.client != nil && !c.closed {
return false, nil
}
client := cmpp.NewClient(protocolVersion(c.config.CMPPVersion))
addr := fmt.Sprintf("%s:%d", c.config.GatewayHost, c.config.GatewayPort)
if err := client.Connect(addr, c.config.Account, c.config.PasswordCipher, defaultConnectTimeout); err != nil {
client.Disconnect()
return false, err
}
c.client = client
c.closed = false
c.heartbeatPending = make(map[uint32]time.Time)
heartbeatCtx, cancelHeartbeat := context.WithCancel(context.Background())
c.heartbeatCancel = cancelHeartbeat
go c.readLoop()
go c.heartbeatLoop(heartbeatCtx)
return true, nil
}
func (c *connection) readLoop() {
for {
c.mu.Lock()
client := c.client
closed := c.closed
c.mu.Unlock()
if closed || client == nil {
return
}
pkt, err := client.RecvAndUnpackPkt(time.Second)
if err != nil {
c.mu.Lock()
closed = c.closed
c.mu.Unlock()
if closed {
return
}
if isTemporaryReadTimeout(err) {
continue
}
c.handleConnectionLoss(err)
return
}
switch p := pkt.(type) {
case *cmpp.Cmpp2SubmitRspPkt:
c.mu.Lock()
ch := c.pending[p.SeqId]
c.mu.Unlock()
if ch != nil {
ch <- submitPartResponse{seqID: p.SeqId, msgID: p.MsgId, result: uint32(p.Result)}
}
case *cmpp.Cmpp3SubmitRspPkt:
c.mu.Lock()
ch := c.pending[p.SeqId]
c.mu.Unlock()
if ch != nil {
ch <- submitPartResponse{seqID: p.SeqId, msgID: p.MsgId, result: p.Result}
}
case *cmpp.Cmpp2DeliverReqPkt:
deliver := deliverPacketFromCMPP2(p)
if deliver.registerDelivery == 1 {
if err := c.handleDeliver(deliver); err != nil {
log.Printf("protocol_event protocol=cmpp direction=gateway_to_api event=deliver_receipt status=intake_failed channel_id=%s sequence_id=%d error=%q", c.channelID, deliver.seqID, err)
c.handleConnectionLoss(fmt.Errorf("persist upstream receipt before DELIVER_RESP: %w", err))
return
}
responseErr := c.sendResponse(client, &cmpp.Cmpp2DeliverRspPkt{MsgId: p.MsgId, Result: 0}, p.SeqId)
c.emitDeliverResponse(deliver, responseErr)
} else {
responseErr := c.sendResponse(client, &cmpp.Cmpp2DeliverRspPkt{MsgId: p.MsgId, Result: 0}, p.SeqId)
c.emitDeliverResponse(deliver, responseErr)
_ = c.handleDeliver(deliver)
}
case *cmpp.Cmpp3DeliverReqPkt:
deliver := deliverPacketFromCMPP3(p)
if deliver.registerDelivery == 1 {
if err := c.handleDeliver(deliver); err != nil {
log.Printf("protocol_event protocol=cmpp direction=gateway_to_api event=deliver_receipt status=intake_failed channel_id=%s sequence_id=%d error=%q", c.channelID, deliver.seqID, err)
c.handleConnectionLoss(fmt.Errorf("persist upstream receipt before DELIVER_RESP: %w", err))
return
}
responseErr := c.sendResponse(client, &cmpp.Cmpp3DeliverRspPkt{MsgId: p.MsgId, Result: 0}, p.SeqId)
c.emitDeliverResponse(deliver, responseErr)
} else {
responseErr := c.sendResponse(client, &cmpp.Cmpp3DeliverRspPkt{MsgId: p.MsgId, Result: 0}, p.SeqId)
c.emitDeliverResponse(deliver, responseErr)
_ = c.handleDeliver(deliver)
}
case *cmpp.CmppActiveTestReqPkt:
_ = c.sendResponse(client, &cmpp.CmppActiveTestRspPkt{}, p.SeqId)
_ = c.pool.reportState(context.Background(), "heartbeat", nil)
case *cmpp.CmppActiveTestRspPkt:
c.handleHeartbeatResponse(p.SeqId)
_ = c.pool.reportState(context.Background(), "heartbeat", nil)
}
}
}
func (c *connection) sendResponse(client *cmpp.Client, packet cmpp.Packer, sequenceID uint32) error {
c.sendMu.Lock()
defer c.sendMu.Unlock()
return client.SendRspPkt(packet, sequenceID)
}
func (c *connection) identity() string {
if c.pool != nil && strings.TrimSpace(c.pool.connectionID) != "" {
return fmt.Sprintf("%s-%d", c.pool.connectionID, c.index)
}
return fmt.Sprintf("%s-%d", c.channelID, c.index)
}
func (c *connection) retire() {
c.mu.Lock()
c.draining = true
c.retired = true
c.mu.Unlock()
c.handleConnectionLoss(fmt.Errorf("connection retired after drain"))
}
func (c *connection) close() {
c.handleConnectionLoss(fmt.Errorf("connection closed"))
}
func (c *connection) handleConnectionLoss(err error) {
c.mu.Lock()
if c.closed && c.client == nil {
c.mu.Unlock()
return
}
c.closed = true
if c.heartbeatCancel != nil {
c.heartbeatCancel()
c.heartbeatCancel = nil
}
pending := c.pending
c.pending = make(map[uint32]chan submitPartResponse)
c.heartbeatPending = make(map[uint32]time.Time)
if c.client != nil {
c.client.Disconnect()
c.client = nil
}
c.mu.Unlock()
for _, ch := range pending {
select {
case ch <- submitPartResponse{err: err}:
default:
}
}
if c.pool != nil && !c.retired {
status := "disconnected"
if c.pool.countActiveConnections() > 0 {
status = "reconnecting"
}
c.pool.scheduleReconnect(err)
_ = c.pool.reportState(context.Background(), status, err)
}
}