perf: expand gateway capacity and prevent receipt replay

This commit is contained in:
hectorzhao
2026-08-25 16:08:30 +08:00
parent 761c123b65
commit 9292352be1
48 changed files with 2001 additions and 144 deletions
+41 -18
View File
@@ -1,6 +1,7 @@
package upstream
import (
"cmpp-platform/gateway/internal/protocollog"
"cmpp-platform/gateway/internal/queue"
"context"
"fmt"
@@ -16,24 +17,38 @@ import (
// the connection must wake pending submitters before scheduling pool recovery.
type connection struct {
channelID string
config queue.UpstreamConfig
index int
pool *connectionPool
apiBaseURL string
httpClient *http.Client
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{}
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
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 {
@@ -161,6 +176,14 @@ func (c *connection) identity() string {
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"))
}
@@ -191,7 +214,7 @@ func (c *connection) handleConnectionLoss(err error) {
default:
}
}
if c.pool != nil {
if c.pool != nil && !c.retired {
status := "disconnected"
if c.pool.countActiveConnections() > 0 {
status = "reconnecting"
+20 -2
View File
@@ -88,7 +88,16 @@ func (c *connection) handleDeliver(pkt deliverPacket) error {
DeliveredAt: time.Now().UTC(),
ConnectionID: c.identity(),
}
if err := postJSON(context.Background(), c.httpClient, c.apiBaseURL, "/gateway/events/receipt/intake", event); err != nil {
if c.protocolLogPublisher != nil {
c.emitProtocolLog(protocolLogEvent{Protocol: "cmpp", Direction: "channel_to_platform", EventType: "deliver_receipt", Status: "success", ChannelID: channelID, Account: c.config.Account, MessageID: messageID, GatewayMessageID: fmt.Sprint(receipt.MsgId), Phone: strings.TrimSpace(receipt.DestTerminalId), ResultCode: strings.TrimSpace(receipt.Stat), Detail: map[string]any{"sequenceId": pkt.seqID}})
}
var publishErr error
if c.eventPublisher != nil {
publishErr = c.eventPublisher.PublishReceipt(context.Background(), event)
} else {
publishErr = postJSON(context.Background(), c.httpClient, c.apiBaseURL, "/gateway/events/receipt/intake", event)
}
if err := publishErr; err != nil {
log.Printf("protocol_event protocol=cmpp direction=gateway_to_api event=deliver_receipt status=forward_failed channel_id=%s sequence_id=%d gateway_message_id=%d error=%q", c.channelID, pkt.seqID, receipt.MsgId, err)
return err
} else {
@@ -121,7 +130,16 @@ func (c *connection) handleDeliver(pkt deliverPacket) error {
Content: content,
ReceivedAt: time.Now().UTC(),
}
if err := postJSON(context.Background(), c.httpClient, c.apiBaseURL, "/gateway/events/uplink", event); err != nil {
if c.protocolLogPublisher != nil {
c.emitProtocolLog(protocolLogEvent{Protocol: "cmpp", Direction: "channel_to_platform", EventType: "deliver_uplink", Status: "success", ChannelID: c.channelID, Account: c.config.Account, MessageID: cmd.MessageID, Phone: strings.TrimSpace(pkt.srcTerminalID), Detail: map[string]any{"sequenceId": pkt.seqID}})
}
var publishErr error
if c.eventPublisher != nil {
publishErr = c.eventPublisher.PublishUplink(context.Background(), event)
} else {
publishErr = postJSON(context.Background(), c.httpClient, c.apiBaseURL, "/gateway/events/uplink", event)
}
if err := publishErr; err != nil {
log.Printf("protocol_event protocol=cmpp direction=gateway_to_api event=deliver_uplink status=forward_failed channel_id=%s sequence_id=%d packet_msg_id=%d error=%q", c.channelID, pkt.seqID, pkt.msgID, err)
} else {
log.Printf("protocol_event protocol=cmpp direction=gateway_to_api event=deliver_uplink status=forwarded channel_id=%s sequence_id=%d packet_msg_id=%d", c.channelID, pkt.seqID, pkt.msgID)
+54 -5
View File
@@ -45,11 +45,20 @@ func (p *connectionPool) tryAcquireConnection() (*connection, func()) {
if len(p.conns) == 0 {
return nil, nil
}
for i := 0; i < len(p.conns); i++ {
index := (p.next + i) % len(p.conns)
conn := p.conns[index]
bestIndex := -1
bestInFlight := int(^uint(0) >> 1)
var bestRTT time.Duration
for index, conn := range p.conns {
inFlight, rtt, usable := conn.capacitySnapshot()
if !usable || inFlight > bestInFlight || (inFlight == bestInFlight && bestIndex >= 0 && bestRTT > 0 && rtt >= bestRTT) {
continue
}
bestIndex, bestInFlight, bestRTT = index, inFlight, rtt
}
if bestIndex >= 0 {
conn := p.conns[bestIndex]
if conn.tryAcquireWindow() {
p.next = (index + 1) % len(p.conns)
p.next = (bestIndex + 1) % len(p.conns)
return conn, conn.releaseWindow
}
}
@@ -57,8 +66,17 @@ func (p *connectionPool) tryAcquireConnection() (*connection, func()) {
}
func (c *connection) tryAcquireWindow() bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.window == nil {
c.window = make(chan struct{}, defaultWindowSize)
c.window = make(chan struct{}, maximumWindowSize)
}
limit := c.windowLimit
if limit < 1 {
limit = defaultWindowSize
}
if c.closed || c.draining || c.client == nil || len(c.window) >= limit {
return false
}
select {
case c.window <- struct{}{}:
@@ -68,6 +86,37 @@ func (c *connection) tryAcquireWindow() bool {
}
}
func (c *connection) capacitySnapshot() (int, time.Duration, bool) {
c.mu.Lock()
defer c.mu.Unlock()
limit := c.windowLimit
if limit < 1 {
limit = defaultWindowSize
}
inFlight := len(c.window)
return inFlight, c.lastSubmitRTT, !c.closed && !c.draining && c.client != nil && inFlight < limit && !time.Now().Before(c.cooldownUntil)
}
func (c *connection) markSubmitFailure() {
c.mu.Lock()
defer c.mu.Unlock()
c.consecutiveFailures++
if c.consecutiveFailures >= 3 {
seconds := c.config.FailureCooldownSeconds
if seconds <= 0 {
seconds = 30
}
c.cooldownUntil = time.Now().Add(time.Duration(seconds) * time.Second)
}
}
func (c *connection) markSubmitSuccess() {
c.mu.Lock()
defer c.mu.Unlock()
c.consecutiveFailures = 0
c.cooldownUntil = time.Time{}
}
func (c *connection) releaseWindow() {
if c.window == nil {
return
+65 -4
View File
@@ -1,6 +1,7 @@
package upstream
import (
"cmpp-platform/gateway/internal/protocollog"
"cmpp-platform/gateway/internal/queue"
"context"
"fmt"
@@ -19,6 +20,8 @@ const (
defaultReconnectInitialDelay = 5 * time.Second
defaultReconnectMaximumDelay = 5 * time.Minute
defaultAuthReconnectDelay = 5 * time.Minute
maximumConnections = 8
maximumWindowSize = 64
)
type Manager struct {
@@ -26,6 +29,14 @@ type Manager struct {
EventAPIBaseURL string
HTTPClient *http.Client
SubmitSegmentPublisher SubmitSegmentPublisher
EventPublisher interface {
PublishReceipt(context.Context, queue.ReceiptEvent) error
PublishUplink(context.Context, queue.UplinkEvent) error
}
ProtocolLogPublisher interface {
Publish(context.Context, protocollog.Event) error
}
GatewayInstanceID string
mu sync.Mutex
conns map[string]*connectionPool
@@ -72,8 +83,14 @@ func (m *Manager) ConnectChannel(ctx context.Context, command queue.ConnectChann
WindowSize: command.Channel.WindowSize,
HeartbeatIntervalSeconds: command.Channel.HeartbeatIntervalSeconds,
HeartbeatMissThreshold: command.Channel.HeartbeatMissThreshold,
ConnectionWarmupSeconds: command.Channel.ConnectionWarmupSeconds,
ConnectionDrainSeconds: command.Channel.ConnectionDrainSeconds,
SubmitTimeoutSeconds: command.Channel.SubmitTimeoutSeconds,
FailureCooldownSeconds: command.Channel.FailureCooldownSeconds,
})
if pool == nil || !pool.matches(config) {
if pool != nil && !pool.matches(config) && pool.sameEndpoint(config) {
pool.reconfigure(config)
} else if pool == nil || !pool.matches(config) {
if pool != nil {
pool.close()
}
@@ -130,7 +147,9 @@ func (m *Manager) connectionFor(cmd queue.SubmitCommand) (*connectionPool, error
m.ensureDefaultsLocked()
pool := m.conns[cmd.ChannelID]
if pool == nil || !pool.matches(cmd.Upstream) {
if pool != nil && !pool.matches(cmd.Upstream) && pool.sameEndpoint(cmd.Upstream) {
pool.reconfigure(cmd.Upstream)
} else if pool == nil || !pool.matches(cmd.Upstream) {
if pool != nil {
pool.close()
}
@@ -170,6 +189,27 @@ func (m *Manager) ConnectionCounts() (desired int, connected int) {
return desired, connected
}
func (m *Manager) WindowCounts() (configured int, inFlight int) {
m.mu.Lock()
pools := make([]*connectionPool, 0, len(m.conns))
for _, pool := range m.conns {
pools = append(pools, pool)
}
m.mu.Unlock()
for _, pool := range pools {
pool.mu.Lock()
conns := append([]*connection(nil), pool.conns...)
pool.mu.Unlock()
for _, conn := range conns {
conn.mu.Lock()
configured += max(1, conn.windowLimit)
inFlight += len(conn.window)
conn.mu.Unlock()
}
}
return configured, inFlight
}
func (m *Manager) newConnectionPool(channelID string, connectionID string, config queue.UpstreamConfig) *connectionPool {
eventAPIBaseURL := m.EventAPIBaseURL
if eventAPIBaseURL == "" {
@@ -184,8 +224,11 @@ func (m *Manager) newConnectionPool(channelID string, connectionID string, confi
reporter: func(ctx context.Context, state ConnectionState) error {
return m.post(ctx, "/admin/gateway/connections", state)
},
reconnectSignal: make(chan struct{}, 1),
stopCh: make(chan struct{}),
protocolLogPublisher: m.ProtocolLogPublisher,
gatewayInstanceID: m.GatewayInstanceID,
eventPublisher: m.EventPublisher,
reconnectSignal: make(chan struct{}, 1),
stopCh: make(chan struct{}),
}
}
@@ -202,6 +245,12 @@ func validateConnectChannelCommand(command queue.ConnectChannelCommand) error {
if command.Channel.Account == "" || command.Channel.PasswordCipher == "" {
return fmt.Errorf("account and passwordCipher are required")
}
if command.DesiredConnections < 1 || command.DesiredConnections > maximumConnections {
return fmt.Errorf("desiredConnections must be between 1 and %d", maximumConnections)
}
if command.Channel.WindowSize != 0 && (command.Channel.WindowSize < 1 || command.Channel.WindowSize > maximumWindowSize) {
return fmt.Errorf("windowSize must be between 1 and %d", maximumWindowSize)
}
return nil
}
@@ -222,5 +271,17 @@ func normalizeUpstreamConfig(config queue.UpstreamConfig) queue.UpstreamConfig {
if config.HeartbeatMissThreshold <= 0 {
config.HeartbeatMissThreshold = defaultHeartbeatMissThreshold
}
if config.ConnectionWarmupSeconds < 0 {
config.ConnectionWarmupSeconds = 30
}
if config.ConnectionDrainSeconds <= 0 {
config.ConnectionDrainSeconds = 60
}
if config.SubmitTimeoutSeconds <= 0 {
config.SubmitTimeoutSeconds = 60
}
if config.FailureCooldownSeconds <= 0 {
config.FailureCooldownSeconds = 30
}
return config
}
+118 -19
View File
@@ -1,6 +1,7 @@
package upstream
import (
"cmpp-platform/gateway/internal/protocollog"
"cmpp-platform/gateway/internal/queue"
"context"
"net/http"
@@ -9,12 +10,20 @@ import (
)
type connectionPool struct {
channelID string
connectionID string
config queue.UpstreamConfig
apiBaseURL string
httpClient *http.Client
reporter func(context.Context, ConnectionState) error
channelID string
connectionID string
config queue.UpstreamConfig
apiBaseURL string
httpClient *http.Client
reporter func(context.Context, ConnectionState) error
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
connectMu sync.Mutex
@@ -34,6 +43,103 @@ func (p *connectionPool) matches(config queue.UpstreamConfig) bool {
return p.config == normalizeUpstreamConfig(config)
}
func (p *connectionPool) sameEndpoint(config queue.UpstreamConfig) bool {
next := normalizeUpstreamConfig(config)
current := p.config
return current.GatewayHost == next.GatewayHost && current.GatewayPort == next.GatewayPort &&
current.Account == next.Account && current.PasswordCipher == next.PasswordCipher && current.CMPPVersion == next.CMPPVersion
}
// reconfigure changes only runtime capacity on the existing pool. Existing
// sequence mappings remain attached to their physical connection. Scale down
// marks surplus connections draining before closing them; scale up is serialized.
func (p *connectionPool) reconfigure(config queue.UpstreamConfig) {
next := normalizeUpstreamConfig(config)
p.mu.Lock()
p.config = next
for _, conn := range p.conns {
conn.mu.Lock()
conn.config = next
conn.windowLimit = next.WindowSize
conn.mu.Unlock()
}
p.mu.Unlock()
go p.reconcileCapacity()
}
func (p *connectionPool) reconcileCapacity() {
p.connectMu.Lock()
defer p.connectMu.Unlock()
if p.stopped() {
return
}
p.mu.Lock()
desired := max(1, min(maximumConnections, p.config.DesiredConnections))
if len(p.conns) > desired {
retiring := append([]*connection(nil), p.conns[desired:]...)
p.conns = p.conns[:desired]
for _, conn := range retiring {
conn.mu.Lock()
conn.draining = true
conn.mu.Unlock()
}
p.mu.Unlock()
deadline := time.Now().Add(time.Duration(p.config.ConnectionDrainSeconds) * time.Second)
for _, conn := range retiring {
for len(conn.window) > 0 && time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond)
}
conn.retire()
}
_ = p.reportState(context.Background(), "connected", nil)
return
}
p.mu.Unlock()
for {
p.mu.Lock()
if len(p.conns) >= desired || p.stopped() {
p.mu.Unlock()
break
}
index := len(p.conns)
config := p.config
p.mu.Unlock()
conn := p.newConnection(index, config)
if _, err := conn.ensureConnected(); err != nil {
p.scheduleReconnect(err)
_ = p.reportState(context.Background(), "failed", err)
return
}
p.mu.Lock()
p.conns = append(p.conns, conn)
p.mu.Unlock()
_ = p.reportState(context.Background(), "connected", nil)
if len(p.conns) < desired && config.ConnectionWarmupSeconds > 0 {
timer := time.NewTimer(time.Duration(config.ConnectionWarmupSeconds) * time.Second)
select {
case <-p.stopCh:
timer.Stop()
return
case <-timer.C:
}
}
}
}
func (p *connectionPool) newConnection(index int, config queue.UpstreamConfig) *connection {
return &connection{
channelID: p.channelID, config: config, index: index, pool: p,
apiBaseURL: p.apiBaseURL, httpClient: p.httpClient,
protocolLogPublisher: p.protocolLogPublisher, gatewayInstanceID: p.gatewayInstanceID,
eventPublisher: p.eventPublisher,
window: make(chan struct{}, maximumWindowSize), windowLimit: config.WindowSize,
pending: make(map[uint32]chan submitPartResponse), tracker: make(map[uint64]queue.SubmitCommand),
longUplink: make(map[string]*longUplinkAssembly), heartbeatPending: make(map[uint32]time.Time),
}
}
func (p *connectionPool) ensureConnected() error {
p.connectMu.Lock()
defer p.connectMu.Unlock()
@@ -58,20 +164,12 @@ func (p *connectionPool) ensureConnected() error {
p.mu.Unlock()
break
}
index := len(p.conns)
conn := &connection{
channelID: p.channelID,
config: p.config,
index: index,
pool: p,
apiBaseURL: p.apiBaseURL,
httpClient: p.httpClient,
window: make(chan struct{}, p.config.WindowSize),
pending: make(map[uint32]chan submitPartResponse),
tracker: make(map[uint64]queue.SubmitCommand),
longUplink: make(map[string]*longUplinkAssembly),
heartbeatPending: make(map[uint32]time.Time),
if connectedAny {
p.mu.Unlock()
break
}
index := len(p.conns)
conn := p.newConnection(index, p.config)
p.mu.Unlock()
connected, err := conn.ensureConnected()
@@ -87,6 +185,7 @@ func (p *connectionPool) ensureConnected() error {
if connectedAny {
_ = p.reportState(context.Background(), "connected", nil)
}
go p.reconcileCapacity()
return nil
}
+49 -2
View File
@@ -2,7 +2,9 @@ package upstream
import (
"context"
cmpp "github.com/bigwhite/gocmpp"
"testing"
"time"
"cmpp-platform/gateway/internal/queue"
)
@@ -10,8 +12,8 @@ import (
func TestConnectionPoolAcquiresAcrossConnections(t *testing.T) {
pool := &connectionPool{
conns: []*connection{
{window: make(chan struct{}, 1)},
{window: make(chan struct{}, 1)},
{client: &cmpp.Client{}, window: make(chan struct{}, 64), windowLimit: 1},
{client: &cmpp.Client{}, window: make(chan struct{}, 64), windowLimit: 1},
},
}
@@ -40,6 +42,51 @@ func TestConnectionPoolAcquiresAcrossConnections(t *testing.T) {
releaseSecond()
}
func TestPoolContinuesOnHealthyConnectionWhenPeerIsDraining(t *testing.T) {
draining := &connection{client: &cmpp.Client{}, window: make(chan struct{}, 64), windowLimit: 16, draining: true}
healthy := &connection{client: &cmpp.Client{}, window: make(chan struct{}, 64), windowLimit: 16}
pool := &connectionPool{conns: []*connection{draining, healthy}}
selected, release := pool.tryAcquireConnection()
if selected != healthy {
t.Fatal("expected healthy peer connection")
}
release()
}
func TestRuntimeCapacityMatrixAndSmoothScaleDown(t *testing.T) {
for _, connections := range []int{1, 2, 4, 8} {
for _, window := range []int{1, 16, 32, 64} {
config := normalizeUpstreamConfig(queue.UpstreamConfig{GatewayHost: "127.0.0.1", GatewayPort: 17890, Account: "a", PasswordCipher: "p", CMPPVersion: "3.0", DesiredConnections: connections, WindowSize: window})
if config.DesiredConnections != connections || config.WindowSize != window {
t.Fatalf("matrix normalized incorrectly: %+v", config)
}
}
}
pool := &connectionPool{channelID: "channel-1", connectionID: "primary", config: normalizeUpstreamConfig(queue.UpstreamConfig{DesiredConnections: 2, WindowSize: 16, ConnectionDrainSeconds: 1}), stopCh: make(chan struct{}), reconnectSignal: make(chan struct{}, 1)}
pool.conns = []*connection{{pool: pool, window: make(chan struct{}, 64), windowLimit: 16}, {pool: pool, window: make(chan struct{}, 64), windowLimit: 16}}
pool.reconfigure(queue.UpstreamConfig{DesiredConnections: 1, WindowSize: 32, ConnectionDrainSeconds: 1})
deadline := time.Now().Add(time.Second)
for {
pool.mu.Lock()
count := len(pool.conns)
first := pool.conns[0]
pool.mu.Unlock()
if count == 1 {
first.mu.Lock()
limit := first.windowLimit
first.mu.Unlock()
if limit != 32 {
t.Fatalf("window limit=%d want32", limit)
}
break
}
if time.Now().After(deadline) {
t.Fatal("scale down did not complete")
}
time.Sleep(time.Millisecond)
}
}
func TestManagerDisconnectChannelRemovesPoolAndStopsReconnects(t *testing.T) {
pool := &connectionPool{
channelID: "channel-1",
+12 -16
View File
@@ -1,6 +1,7 @@
package upstream
import (
"cmpp-platform/gateway/internal/protocollog"
"context"
"fmt"
cmpp "github.com/bigwhite/gocmpp"
@@ -8,22 +9,7 @@ import (
"strings"
)
type protocolLogEvent struct {
Protocol string `json:"protocol"`
Direction string `json:"direction"`
EventType string `json:"eventType"`
Status string `json:"status"`
TenantID string `json:"tenantId,omitempty"`
ApplicationID string `json:"applicationId,omitempty"`
ChannelID string `json:"channelId,omitempty"`
Account string `json:"account,omitempty"`
MessageID string `json:"messageId,omitempty"`
GatewayMessageID string `json:"gatewayMessageId,omitempty"`
Phone string `json:"phone,omitempty"`
ResultCode string `json:"resultCode,omitempty"`
PayloadBytes int `json:"payloadBytes,omitempty"`
Detail map[string]any `json:"detail,omitempty"`
}
type protocolLogEvent = protocollog.Event
func (c *connection) emitDeliverResponse(pkt deliverPacket, responseErr error) {
status := "success"
@@ -71,6 +57,16 @@ func (c *connection) emitDeliverResponse(pkt deliverPacket, responseErr error) {
}
func (c *connection) emitProtocolLog(event protocolLogEvent) {
event.ConnectionID = c.identity()
event.GatewayInstanceID = c.gatewayInstanceID
if c.protocolLogPublisher != nil {
go func() {
if err := c.protocolLogPublisher.Publish(context.Background(), event); err != nil {
log.Printf("protocol log Redis publish failed channel_id=%s message_id=%s error=%q", event.ChannelID, event.MessageID, err)
}
}()
return
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), defaultHTTPTimeout)
defer cancel()
+20 -1
View File
@@ -97,6 +97,12 @@ func (p *connectionPool) submit(
}
func (c *connection) submitPart(ctx context.Context, cmd queue.SubmitCommand, part submitPart) (uint32, string, queue.SubmitResult, error) {
startedAt := time.Now()
defer func() {
c.mu.Lock()
c.lastSubmitRTT = time.Since(startedAt)
c.mu.Unlock()
}()
rspCh := make(chan submitPartResponse, 1)
pkt := c.submitRequestPacket(cmd, part)
@@ -163,14 +169,20 @@ func (c *connection) submitPart(ctx context.Context, cmd queue.SubmitCommand, pa
c.mu.Unlock()
}()
waitCtx, cancel := context.WithTimeout(ctx, defaultSubmitTimeout)
timeout := time.Duration(c.config.SubmitTimeoutSeconds) * time.Second
if timeout <= 0 {
timeout = defaultSubmitTimeout
}
waitCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
select {
case <-waitCtx.Done():
c.markSubmitFailure()
result := submitResult(cmd, seq, "", "timeout", "SUBMIT_TIMEOUT", waitCtx.Err().Error())
return seq, "", result, waitCtx.Err()
case rsp := <-rspCh:
if rsp.err != nil {
c.markSubmitFailure()
result := submitResult(cmd, seq, "", "timeout", "CONNECTION_LOST", rsp.err.Error())
return seq, "", result, rsp.err
}
@@ -203,6 +215,7 @@ func (c *connection) submitPart(ctx context.Context, cmd queue.SubmitCommand, pa
},
})
if rsp.result == 0 {
c.markSubmitSuccess()
c.mu.Lock()
c.tracker[rsp.msgID] = cmd
c.mu.Unlock()
@@ -345,6 +358,12 @@ func validateSubmitCommand(cmd queue.SubmitCommand) error {
if cmd.Upstream.Account == "" || cmd.Upstream.PasswordCipher == "" {
return fmt.Errorf("upstream account and passwordCipher are required")
}
if cmd.Upstream.DesiredConnections != 0 && (cmd.Upstream.DesiredConnections < 1 || cmd.Upstream.DesiredConnections > maximumConnections) {
return fmt.Errorf("upstream desiredConnections must be between 1 and %d", maximumConnections)
}
if cmd.Upstream.WindowSize != 0 && (cmd.Upstream.WindowSize < 1 || cmd.Upstream.WindowSize > maximumWindowSize) {
return fmt.Errorf("upstream windowSize must be between 1 and %d", maximumWindowSize)
}
if len(cmd.PhoneNumber) == 0 || len(cmd.Content) == 0 {
return fmt.Errorf("phoneNumber and content are required")
}