fix: close receipt delivery workflows

This commit is contained in:
hectorzhao
2026-07-24 14:40:57 +08:00
parent 2ee39056eb
commit 91f04f5288
22 changed files with 829 additions and 48 deletions
@@ -4,8 +4,11 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
cmpp "github.com/bigwhite/gocmpp"
)
func TestSubmitResponseProtocolLoggerEmitsActualPacketDirection(t *testing.T) {
@@ -50,3 +53,38 @@ func TestSubmitResponseProtocolLoggerEmitsActualPacketDirection(t *testing.T) {
t.Fatal("timed out waiting for protocol event")
}
}
func TestDownstreamDeliverProtocolLoggerEmitsReceiptPacket(t *testing.T) {
events := make(chan protocolLogEvent, 1)
session := &downstreamSession{
account: "607532", tenantID: "tenant-1", applicationID: "app-1",
messageID: "MSG-LONG-1", phoneNumber: "18821203795", mu: &sync.Mutex{},
protocolLog: func(event protocolLogEvent) { events <- event },
}
session.recordDownstreamProtocol(
&cmpp.Cmpp2DeliverReqPkt{
MsgId: 736078096490905600, SrcTerminalId: "18821203795", RegisterDelivery: 1,
},
"delivery-1",
71,
736078096490905600,
"success",
"",
nil,
)
select {
case event := <-events:
if event.Protocol != "cmpp" || event.Direction != "platform_to_client" || event.EventType != "deliver_receipt" {
t.Fatalf("unexpected protocol event: %+v", event)
}
if event.TenantID != "tenant-1" || event.ApplicationID != "app-1" || event.Account != "607532" {
t.Fatalf("unexpected application identifiers: %+v", event)
}
if event.MessageID != "MSG-LONG-1" || event.GatewayMessageID != "736078096490905600" || event.Phone != "18821203795" {
t.Fatalf("unexpected message identifiers: %+v", event)
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for downstream deliver protocol event")
}
}
+81
View File
@@ -174,6 +174,8 @@ type downstreamConnectionEvent struct {
type downstreamSession struct {
messageID string
account string
tenantID string
applicationID string
enterpriseCode string
protocol string
srcID string
@@ -188,6 +190,7 @@ type downstreamSession struct {
instanceID string
report func(*downstreamSession, string, string)
deliveryReport func(downstreamDeliveryLifecycleEvent)
protocolLog func(protocolLogEvent)
}
var downstreamRegistry = struct {
@@ -244,6 +247,8 @@ func (s Server) handleLogin(response *cmpp.Response, packet *cmpp.Packet, logger
now := time.Now().UTC()
session := &downstreamSession{
account: strings.TrimSpace(defaultString(auth.Account, account)),
tenantID: strings.TrimSpace(auth.TenantID),
applicationID: strings.TrimSpace(auth.ApplicationID),
enterpriseCode: strings.TrimSpace(auth.EnterpriseCode),
protocol: cmppVersionName(req.Version),
srcID: strings.TrimSpace(auth.Account),
@@ -256,6 +261,7 @@ func (s Server) handleLogin(response *cmpp.Response, packet *cmpp.Packet, logger
instanceID: s.gatewayInstanceID(),
report: s.reportConnection,
deliveryReport: s.reportDownstreamDelivery,
protocolLog: s.emitProtocolLog,
}
if !rememberAccount(session, auth.MaxConnections) {
logger.Printf("cmpp inbound auth failed account=%s remote=%s err=connection limit exceeded max=%d", account, packet.Conn.Conn.RemoteAddr(), auth.MaxConnections)
@@ -372,6 +378,8 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
rememberDownstream(downstreamSession{
messageID: acceptedMessage.MessageID,
account: account,
tenantID: result.TenantID,
applicationID: result.ApplicationID,
enterpriseCode: session.enterpriseCode,
protocol: clientProtocol,
srcID: strings.TrimSpace(req.srcID),
@@ -386,6 +394,7 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
instanceID: s.gatewayInstanceID(),
report: session.report,
deliveryReport: session.deliveryReport,
protocolLog: session.protocolLog,
})
}
if current := findSessionByConn(packet.Conn); current != nil && current.report != nil {
@@ -1282,12 +1291,14 @@ func sendDownstream(session *downstreamSession, deliver cmpp.Packer, deliveryID
tracker := registerDownstreamAck(session, deliveryID, sequenceID, messageID, ackDeadlineAt)
if err := session.conn.SendPkt(deliver, sequenceID); err != nil {
removeDownstreamAck(tracker)
session.recordDownstreamProtocol(deliver, deliveryID, sequenceID, messageID, "failed", "SEND_FAILED", err)
if session.report != nil {
go session.report(session, "disconnected", err.Error())
}
forgetDownstream(session)
return DownstreamSendResult{}, err
}
session.recordDownstreamProtocol(deliver, deliveryID, sequenceID, messageID, "success", "", nil)
result := DownstreamSendResult{
Sent: true, ConnectionID: session.connectionID,
SequenceID: strconv.FormatUint(uint64(sequenceID), 10), MessageID: strconv.FormatUint(messageID, 10),
@@ -1306,6 +1317,56 @@ func sendDownstream(session *downstreamSession, deliver cmpp.Packer, deliveryID
return result, nil
}
func (session *downstreamSession) recordDownstreamProtocol(
deliver cmpp.Packer,
deliveryID string,
sequenceID uint32,
messageID uint64,
status string,
resultCode string,
sendErr error,
) {
if session == nil || session.protocolLog == nil {
return
}
eventType, phone := downstreamDeliverMetadata(deliver)
detail := map[string]any{"sequenceId": sequenceID, "deliveryId": deliveryID}
if sendErr != nil {
detail["error"] = sendErr.Error()
}
session.protocolLog(protocolLogEvent{
Protocol: "cmpp",
Direction: "platform_to_client",
EventType: eventType,
Status: status,
TenantID: session.tenantID,
ApplicationID: session.applicationID,
Account: session.account,
MessageID: session.messageID,
GatewayMessageID: strconv.FormatUint(messageID, 10),
Phone: defaultString(phone, session.phoneNumber),
ResultCode: resultCode,
Detail: detail,
})
}
func downstreamDeliverMetadata(deliver cmpp.Packer) (string, string) {
switch packet := deliver.(type) {
case *cmpp.Cmpp2DeliverReqPkt:
if packet.RegisterDelivery == 1 {
return "deliver_receipt", packet.SrcTerminalId
}
return "deliver_uplink", packet.SrcTerminalId
case *cmpp.Cmpp3DeliverReqPkt:
if packet.RegisterDelivery == 1 {
return "deliver_receipt", packet.SrcTerminalId
}
return "deliver_uplink", packet.SrcTerminalId
default:
return "deliver", ""
}
}
func downstreamDeliverMessageID(deliver cmpp.Packer) uint64 {
switch packet := deliver.(type) {
case *cmpp.Cmpp2DeliverReqPkt:
@@ -1389,6 +1450,26 @@ func handleDownstreamAcknowledgement(conn *cmpp.Conn, sequenceID uint32, message
SequenceID: sequenceID, MessageID: messageID, Result: result, ObservedAt: time.Now().UTC(),
})
}
if tracker.session != nil && tracker.session.protocolLog != nil {
status := "success"
if result != 0 {
status = "failed"
}
tracker.session.protocolLog(protocolLogEvent{
Protocol: "cmpp",
Direction: "client_to_platform",
EventType: "deliver_resp",
Status: status,
TenantID: tracker.session.tenantID,
ApplicationID: tracker.session.applicationID,
Account: tracker.session.account,
MessageID: tracker.session.messageID,
GatewayMessageID: strconv.FormatUint(messageID, 10),
Phone: tracker.session.phoneNumber,
ResultCode: strconv.FormatUint(uint64(result), 10),
Detail: map[string]any{"sequenceId": sequenceID, "deliveryId": tracker.deliveryID},
})
}
}
func downstreamAckTimeout() time.Duration {
+15
View File
@@ -912,10 +912,14 @@ func TestDownstreamDeliveryRequiresAcknowledgement(t *testing.T) {
defer resetDownstreamRegistry()
events := make(chan downstreamDeliveryLifecycleEvent, 1)
protocolEvents := make(chan protocolLogEvent, 1)
conn := &cmpp.Conn{}
session := &downstreamSession{
conn: conn, connectionID: "conn-1",
deliveryReport: func(event downstreamDeliveryLifecycleEvent) { events <- event },
tenantID: "tenant-1", applicationID: "app-1", account: "607532",
messageID: "MSG-LONG-1", phoneNumber: "18821203795",
protocolLog: func(event protocolLogEvent) { protocolEvents <- event },
}
registerDownstreamAck(session, "delivery-1", 37, 9016479179509871733, time.Now().Add(time.Second))
handleDownstreamAcknowledgement(conn, 37, 9016479179509871733, 0, log.Default())
@@ -928,6 +932,17 @@ func TestDownstreamDeliveryRequiresAcknowledgement(t *testing.T) {
case <-time.After(time.Second):
t.Fatal("timed out waiting acknowledgement event")
}
select {
case event := <-protocolEvents:
if event.Protocol != "cmpp" || event.Direction != "client_to_platform" || event.EventType != "deliver_resp" {
t.Fatalf("unexpected acknowledgement protocol event: %+v", event)
}
if event.MessageID != "MSG-LONG-1" || event.GatewayMessageID != "9016479179509871733" || event.ResultCode != "0" {
t.Fatalf("unexpected acknowledgement identifiers: %+v", event)
}
case <-time.After(time.Second):
t.Fatal("timed out waiting acknowledgement protocol event")
}
}
func TestReceiptLookupDoesNotFallbackToAccountBeforeSubmitMappingExists(t *testing.T) {