fix: correlate downstream receipts with submit responses
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -75,6 +76,7 @@ type DownstreamReceipt struct {
|
||||
ReceiptStatus string `json:"receiptStatus"`
|
||||
RawStatus string `json:"rawStatus,omitempty"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
SubmitSequenceID uint32 `json:"submitSequenceId,omitempty"`
|
||||
DeliveredAt string `json:"deliveredAt,omitempty"`
|
||||
}
|
||||
|
||||
@@ -220,7 +222,11 @@ func (s Server) handleLogin(response *cmpp.Response, packet *cmpp.Packet, logger
|
||||
}
|
||||
rememberAccount(session)
|
||||
go s.reportConnection(&session, "connected", "")
|
||||
go s.flushPending(defaultString(auth.Account, account), logger)
|
||||
response.AfterSend = func(sendErr error) {
|
||||
if sendErr == nil {
|
||||
go s.flushPending(defaultString(auth.Account, account), logger)
|
||||
}
|
||||
}
|
||||
logger.Printf(
|
||||
"cmpp inbound event=login_accepted protocol=%s requested_version=0x%02x response_version=0x%02x account=%s remote=%s",
|
||||
cmppVersionName(req.Version), uint8(req.Version), uint8(req.Version), account, packet.Conn.Conn.RemoteAddr(),
|
||||
@@ -319,11 +325,16 @@ func (s Server) handleSubmit(response *cmpp.Response, packet *cmpp.Packet, logge
|
||||
if current := findSessionByConn(packet.Conn); current != nil && current.report != nil {
|
||||
go current.report(current, "submit", "")
|
||||
}
|
||||
go func() {
|
||||
if _, err := s.flushPending(account, logger); err != nil {
|
||||
logger.Printf("cmpp inbound event=post_submit_pending_flush_failed account=%s message_id=%s error=%q", account, result.MessageID, err.Error())
|
||||
response.AfterSend = func(sendErr error) {
|
||||
if sendErr != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
if _, err := s.flushPending(account, logger); err != nil {
|
||||
logger.Printf("cmpp inbound event=post_submit_pending_flush_failed account=%s message_id=%s error=%q", account, result.MessageID, err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
logger.Printf(
|
||||
"cmpp inbound event=submit_accepted protocol=%s packet_type=%s account=%s remote=%s seq=%d phone=%s result=0 message_id=%s gateway_message_id=%d duration_ms=%d content_chars=%d content_hash=%s",
|
||||
clientProtocol, req.protocol, account, remote, req.sequenceID, phone, result.MessageID, gatewayMsgID, time.Since(startedAt).Milliseconds(), len([]rune(content)), contentHash,
|
||||
@@ -493,6 +504,7 @@ type pendingDelivery struct {
|
||||
ID string `json:"id"`
|
||||
DeliveryType string `json:"deliveryType"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type pendingFlushResult struct {
|
||||
@@ -546,7 +558,8 @@ func (s Server) pushPendingDelivery(account string, delivery pendingDelivery) (D
|
||||
}
|
||||
event.DeliveryID = delivery.ID
|
||||
event.Account = defaultString(event.Account, account)
|
||||
return PushReceiptWithResult(event)
|
||||
allowRecovery := !delivery.CreatedAt.IsZero() && time.Since(delivery.CreatedAt) >= 5*time.Second
|
||||
return pushReceiptWithResult(event, allowRecovery)
|
||||
case "uplink":
|
||||
var event DownstreamUplink
|
||||
if err := json.Unmarshal(delivery.Payload, &event); err != nil {
|
||||
@@ -848,7 +861,14 @@ func PushReceipt(event DownstreamReceipt) (bool, error) {
|
||||
}
|
||||
|
||||
func PushReceiptWithResult(event DownstreamReceipt) (DownstreamSendResult, error) {
|
||||
session := findSession(event.MessageID, event.Account)
|
||||
return pushReceiptWithResult(event, false)
|
||||
}
|
||||
|
||||
func pushReceiptWithResult(event DownstreamReceipt, allowRecovery bool) (DownstreamSendResult, error) {
|
||||
session := findReceiptSession(event.MessageID, event.Account)
|
||||
if session == nil && allowRecovery {
|
||||
session = recoverReceiptSession(event)
|
||||
}
|
||||
if session == nil {
|
||||
return DownstreamSendResult{}, nil
|
||||
}
|
||||
@@ -878,6 +898,34 @@ func PushReceiptWithResult(event DownstreamReceipt) (DownstreamSendResult, error
|
||||
return sendDownstream(session, deliver, event.DeliveryID)
|
||||
}
|
||||
|
||||
func findReceiptSession(messageID string, account string) *downstreamSession {
|
||||
downstreamRegistry.RLock()
|
||||
defer downstreamRegistry.RUnlock()
|
||||
if messageID != "" {
|
||||
return downstreamRegistry.byMessageID[messageID]
|
||||
}
|
||||
if account != "" {
|
||||
return downstreamRegistry.byAccount[account]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func recoverReceiptSession(event DownstreamReceipt) *downstreamSession {
|
||||
if event.MessageID == "" || event.SubmitSequenceID == 0 || event.Account == "" {
|
||||
return nil
|
||||
}
|
||||
downstreamRegistry.RLock()
|
||||
accountSession := downstreamRegistry.byAccount[event.Account]
|
||||
downstreamRegistry.RUnlock()
|
||||
if accountSession == nil || accountSession.conn == nil {
|
||||
return nil
|
||||
}
|
||||
recovered := *accountSession
|
||||
recovered.messageID = event.MessageID
|
||||
recovered.gatewayMsgID = messageIDFrom(event.MessageID, event.SubmitSequenceID)
|
||||
return &recovered
|
||||
}
|
||||
|
||||
func PushUplink(event DownstreamUplink) (bool, error) {
|
||||
result, err := PushUplinkWithResult(event)
|
||||
return result.Sent, err
|
||||
@@ -936,8 +984,11 @@ func findSession(messageID string, account string) *downstreamSession {
|
||||
func sendDownstream(session *downstreamSession, deliver cmpp.Packer, deliveryID string) (DownstreamSendResult, error) {
|
||||
session.mu.Lock()
|
||||
defer session.mu.Unlock()
|
||||
sequenceID := <-session.conn.SeqId
|
||||
messageID := downstreamDeliverMessageID(deliver)
|
||||
if messageID == 0 {
|
||||
return DownstreamSendResult{}, errors.New("refusing downstream CMPP_DELIVER with Msg_Id=0")
|
||||
}
|
||||
sequenceID := <-session.conn.SeqId
|
||||
sentAt := time.Now().UTC()
|
||||
ackDeadlineAt := sentAt.Add(downstreamAckTimeout())
|
||||
tracker := registerDownstreamAck(session, deliveryID, sequenceID, messageID, ackDeadlineAt)
|
||||
|
||||
@@ -226,6 +226,82 @@ func TestInboundServerAuthenticatesAndSubmits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitResponsePrecedesQueuedFailureReceipt(t *testing.T) {
|
||||
resetDownstreamRegistry()
|
||||
defer resetDownstreamRegistry()
|
||||
account := "100001"
|
||||
password := "secret-hash"
|
||||
var mu sync.Mutex
|
||||
var submit submitRequest
|
||||
pendingReturned := false
|
||||
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/gateway/events/inbound/authenticate":
|
||||
_ = json.NewEncoder(w).Encode(authResponse{PasswordCipher: password, Account: account, EnterpriseCode: account})
|
||||
case "/api/gateway/events/inbound/submit":
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err := json.NewDecoder(r.Body).Decode(&submit); err != nil {
|
||||
t.Fatalf("decode submit: %v", err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(submitResponse{Accepted: true, MessageID: "MSG-ORDER"})
|
||||
case "/api/gateway/events/downstream/pending":
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if submit.SequenceID == 0 || pendingReturned {
|
||||
_ = json.NewEncoder(w).Encode([]pendingDelivery{})
|
||||
return
|
||||
}
|
||||
payload, _ := json.Marshal(DownstreamReceipt{
|
||||
Account: account, MessageID: "MSG-ORDER", PhoneNumber: "13500002696",
|
||||
ReceiptStatus: "undelivered", RawStatus: "REJECTD", SubmitSequenceID: submit.SequenceID,
|
||||
})
|
||||
pendingReturned = true
|
||||
_ = json.NewEncoder(w).Encode([]pendingDelivery{{
|
||||
ID: "delivery-order", DeliveryType: "receipt", Payload: payload, CreatedAt: time.Now().UTC(),
|
||||
}})
|
||||
case "/api/gateway/events/inbound/connection", "/api/gateway/events/downstream/sent":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
t.Fatalf("unexpected api path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
addr := reserveTCPAddr(t)
|
||||
go func() { _ = (Server{Addr: addr, APIBaseURL: api.URL + "/api"}).ListenAndServe() }()
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
client := cmpp.NewClient(cmpp.V20)
|
||||
defer client.Disconnect()
|
||||
if err := client.Connect(addr, account, password, 2*time.Second); err != nil {
|
||||
t.Fatalf("connect CMPP2 inbound: %v", err)
|
||||
}
|
||||
content, _ := cmpputils.Utf8ToUcs2("测试回执顺序")
|
||||
if _, err := client.SendReqPkt(&cmpp.Cmpp2SubmitReqPkt{
|
||||
PkTotal: 1, PkNumber: 1, RegisteredDelivery: 1, MsgLevel: 1,
|
||||
ServiceId: "cmpp", FeeUserType: 2, FeeTerminalId: "13500002696",
|
||||
MsgFmt: 8, MsgSrc: account, FeeType: "02", FeeCode: "0", SrcId: "10690000",
|
||||
DestUsrTl: 1, DestTerminalId: []string{"13500002696"}, MsgLength: uint8(len(content)), MsgContent: content,
|
||||
}); err != nil {
|
||||
t.Fatalf("send submit: %v", err)
|
||||
}
|
||||
|
||||
first, err := client.RecvAndUnpackPkt(2 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("receive first packet: %v", err)
|
||||
}
|
||||
submitResponse, ok := first.(*cmpp.Cmpp2SubmitRspPkt)
|
||||
if !ok || submitResponse.Result != 0 || submitResponse.MsgId == 0 {
|
||||
t.Fatalf("first packet must be successful SUBMIT_RESP, got %T %+v", first, first)
|
||||
}
|
||||
deliver := recvDeliver20(t, client)
|
||||
if deliver.MsgId != submitResponse.MsgId {
|
||||
t.Fatalf("receipt Msg_Id=%d does not match SUBMIT_RESP Msg_Id=%d", deliver.MsgId, submitResponse.MsgId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInboundServerNegotiatesCMPP2AndUsesAuthenticatedAccountForSubmit(t *testing.T) {
|
||||
resetDownstreamRegistry()
|
||||
defer resetDownstreamRegistry()
|
||||
@@ -582,6 +658,40 @@ func TestDownstreamDeliveryRequiresAcknowledgement(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReceiptLookupDoesNotFallbackToAccountBeforeSubmitMappingExists(t *testing.T) {
|
||||
resetDownstreamRegistry()
|
||||
defer resetDownstreamRegistry()
|
||||
|
||||
conn := &cmpp.Conn{}
|
||||
rememberAccount(downstreamSession{
|
||||
account: "100001", protocol: "cmpp20", conn: conn, mu: &sync.Mutex{}, connectionID: "conn-1",
|
||||
})
|
||||
if session := findReceiptSession("MSG-NOT-REMEMBERED", "100001"); session != nil {
|
||||
t.Fatalf("receipt unexpectedly fell back to account session: %+v", session)
|
||||
}
|
||||
|
||||
recovered := recoverReceiptSession(DownstreamReceipt{
|
||||
MessageID: "MSG-NOT-REMEMBERED", Account: "100001", SubmitSequenceID: 1216579149,
|
||||
})
|
||||
if recovered == nil {
|
||||
t.Fatal("expected persisted submit sequence to recover receipt session")
|
||||
}
|
||||
if recovered.gatewayMsgID != messageIDFrom("MSG-NOT-REMEMBERED", 1216579149) || recovered.gatewayMsgID == 0 {
|
||||
t.Fatalf("unexpected recovered Msg_Id: %d", recovered.gatewayMsgID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendDownstreamRejectsZeroMessageID(t *testing.T) {
|
||||
_, err := sendDownstream(
|
||||
&downstreamSession{mu: &sync.Mutex{}},
|
||||
&cmpp.Cmpp2DeliverReqPkt{MsgId: 0},
|
||||
"delivery-zero",
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), "Msg_Id=0") {
|
||||
t.Fatalf("expected zero Msg_Id rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownstreamDeliveryReportsAckTimeout(t *testing.T) {
|
||||
resetDownstreamRegistry()
|
||||
defer resetDownstreamRegistry()
|
||||
|
||||
Vendored
+7
-2
@@ -39,7 +39,8 @@ type Packet struct {
|
||||
type Response struct {
|
||||
*Packet
|
||||
Packer
|
||||
SeqId uint32
|
||||
SeqId uint32
|
||||
AfterSend func(error)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
@@ -420,7 +421,11 @@ func (c *conn) serve() {
|
||||
}
|
||||
|
||||
_, err = c.server.Handler.ServeCmpp(r, r.Packet, c.server.ErrorLog)
|
||||
if err1 := c.finishPacket(r); err1 != nil {
|
||||
err1 := c.finishPacket(r)
|
||||
if r.AfterSend != nil {
|
||||
r.AfterSend(err1)
|
||||
}
|
||||
if err1 != nil {
|
||||
c.server.ErrorLog.Printf(
|
||||
"send response packet failed remote=%v protocol=%s packet_type=%T seq=%d err_type=%T err=%v",
|
||||
c.Conn.RemoteAddr(), c.Conn.Typ, r.Packer, r.SeqId, err1, err1,
|
||||
|
||||
Reference in New Issue
Block a user