269 lines
8.6 KiB
Go
269 lines
8.6 KiB
Go
package inbound
|
|
|
|
import (
|
|
"context"
|
|
cmpp "github.com/bigwhite/gocmpp"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// This registry is the single in-process owner of authenticated downstream
|
|
// sessions. Delivery and recovery code must resolve sessions through it.
|
|
|
|
type downstreamConnectionEvent struct {
|
|
Account string `json:"account"`
|
|
ConnectionID string `json:"connectionId"`
|
|
Status string `json:"status"`
|
|
RemoteIP string `json:"remoteIp,omitempty"`
|
|
Protocol string `json:"protocol,omitempty"`
|
|
ConnectedAt string `json:"connectedAt,omitempty"`
|
|
ObservedAt string `json:"observedAt,omitempty"`
|
|
ErrorMessage string `json:"errorMessage,omitempty"`
|
|
}
|
|
|
|
type downstreamSession struct {
|
|
messageID string
|
|
account string
|
|
tenantID string
|
|
applicationID string
|
|
enterpriseCode string
|
|
protocol string
|
|
srcID string
|
|
phoneNumber string
|
|
gatewayMsgID uint64
|
|
remoteIP string
|
|
connectedAt time.Time
|
|
connectionID string
|
|
conn *cmpp.Conn
|
|
mu *sync.Mutex
|
|
presence PresenceStore
|
|
instanceID string
|
|
report func(*downstreamSession, string, string)
|
|
deliveryReport func(downstreamDeliveryLifecycleEvent)
|
|
protocolLog func(protocolLogEvent)
|
|
}
|
|
|
|
var downstreamRegistry = struct {
|
|
sync.RWMutex
|
|
byMessageID map[string]*downstreamSession
|
|
byAccount map[string]*downstreamSession
|
|
byConn map[*cmpp.Conn]*downstreamSession
|
|
}{
|
|
byMessageID: make(map[string]*downstreamSession),
|
|
byAccount: make(map[string]*downstreamSession),
|
|
byConn: make(map[*cmpp.Conn]*downstreamSession),
|
|
}
|
|
|
|
func (s Server) handleActivity(_ *cmpp.Response, packet *cmpp.Packet, logger *log.Logger) (bool, error) {
|
|
session := findSessionByConn(packet.Conn)
|
|
if session == nil {
|
|
return true, nil
|
|
}
|
|
switch response := packet.Packer.(type) {
|
|
case *cmpp.CmppActiveTestReqPkt, *cmpp.CmppActiveTestRspPkt:
|
|
if session.report != nil {
|
|
go s.reportConnectionOrDisconnect(session, "heartbeat", "")
|
|
}
|
|
case *cmpp.Cmpp2DeliverRspPkt:
|
|
handleDownstreamAcknowledgement(packet.Conn, response.SeqId, response.MsgId, uint32(response.Result), logger)
|
|
case *cmpp.Cmpp3DeliverRspPkt:
|
|
handleDownstreamAcknowledgement(packet.Conn, response.SeqId, response.MsgId, response.Result, logger)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (s Server) reportConnection(session *downstreamSession, status string, errorMessage string) {
|
|
_ = s.reportConnectionChecked(session, status, errorMessage)
|
|
}
|
|
|
|
func (s Server) reportConnectionChecked(session *downstreamSession, status string, errorMessage string) error {
|
|
if session == nil || strings.TrimSpace(session.account) == "" || strings.TrimSpace(session.connectionID) == "" {
|
|
return nil
|
|
}
|
|
event := downstreamConnectionEvent{
|
|
Account: session.account, ConnectionID: session.connectionID, Status: status,
|
|
RemoteIP: session.remoteIP, Protocol: session.protocol,
|
|
ConnectedAt: formatRFC3339Nano(session.connectedAt), ObservedAt: formatRFC3339Nano(time.Now()),
|
|
ErrorMessage: errorMessage,
|
|
}
|
|
if err := s.post(context.Background(), "/gateway/events/inbound/connection", event, nil); err != nil {
|
|
log.Printf("cmpp inbound connection state callback failed account=%s connection_id=%s status=%s err=%v", session.account, session.connectionID, status, err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s Server) reportConnectionOrDisconnect(session *downstreamSession, status string, errorMessage string) {
|
|
if err := s.reportConnectionChecked(session, status, errorMessage); err != nil && status != "disconnected" {
|
|
_ = s.reportConnectionChecked(session, "disconnected", err.Error())
|
|
forgetDownstream(session)
|
|
if session.conn != nil {
|
|
session.conn.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s Server) handleConnectionClosed(conn *cmpp.Conn) {
|
|
downstreamSubmitBarrier.Lock()
|
|
delete(downstreamSubmitBarrier.byConn, conn)
|
|
downstreamSubmitBarrier.Unlock()
|
|
session := findSessionByConn(conn)
|
|
if session == nil {
|
|
return
|
|
}
|
|
forgetDownstream(session)
|
|
_ = s.reportConnectionChecked(session, "disconnected", "CMPP client connection closed")
|
|
}
|
|
|
|
func findSessionByConn(conn *cmpp.Conn) *downstreamSession {
|
|
downstreamRegistry.RLock()
|
|
defer downstreamRegistry.RUnlock()
|
|
return downstreamRegistry.byConn[conn]
|
|
}
|
|
|
|
func rememberDownstream(session downstreamSession) {
|
|
if session.messageID == "" || session.conn == nil {
|
|
return
|
|
}
|
|
session.touchPresence("connected", true, false)
|
|
downstreamRegistry.Lock()
|
|
if existing := downstreamRegistry.byMessageID[session.messageID]; existing != nil && existing.conn == session.conn {
|
|
// Keep a stable message-level lookup for the live connection. Receipt
|
|
// delivery no longer uses this value for long-message fragments; it
|
|
// reconstructs every fragment Msg_Id from that fragment's Sequence_Id.
|
|
session.gatewayMsgID = existing.gatewayMsgID
|
|
}
|
|
downstreamRegistry.byMessageID[session.messageID] = &session
|
|
downstreamRegistry.byConn[session.conn] = &session
|
|
if session.account != "" {
|
|
downstreamRegistry.byAccount[session.account] = &session
|
|
}
|
|
downstreamRegistry.Unlock()
|
|
}
|
|
|
|
func rememberAccount(session *downstreamSession, maxConnections int) bool {
|
|
if session == nil || session.account == "" || session.conn == nil {
|
|
return false
|
|
}
|
|
if maxConnections <= 0 {
|
|
maxConnections = 1
|
|
}
|
|
session.touchPresence("connected", false, false)
|
|
downstreamRegistry.Lock()
|
|
defer downstreamRegistry.Unlock()
|
|
active := 0
|
|
for _, current := range downstreamRegistry.byConn {
|
|
if current != nil && current.account == session.account {
|
|
active++
|
|
}
|
|
}
|
|
if active >= maxConnections {
|
|
return false
|
|
}
|
|
downstreamRegistry.byAccount[session.account] = session
|
|
downstreamRegistry.byConn[session.conn] = session
|
|
return true
|
|
}
|
|
|
|
func forgetDownstream(session *downstreamSession) {
|
|
if session == nil {
|
|
return
|
|
}
|
|
downstreamRegistry.Lock()
|
|
for messageID, current := range downstreamRegistry.byMessageID {
|
|
if current != nil && current.conn == session.conn {
|
|
delete(downstreamRegistry.byMessageID, messageID)
|
|
}
|
|
}
|
|
if session.account != "" {
|
|
if current := downstreamRegistry.byAccount[session.account]; current == session {
|
|
delete(downstreamRegistry.byAccount, session.account)
|
|
}
|
|
}
|
|
if current := downstreamRegistry.byConn[session.conn]; current == session {
|
|
delete(downstreamRegistry.byConn, session.conn)
|
|
}
|
|
downstreamRegistry.Unlock()
|
|
_ = session.removePresence()
|
|
}
|
|
|
|
func onlineAccounts() []string {
|
|
downstreamRegistry.RLock()
|
|
defer downstreamRegistry.RUnlock()
|
|
accounts := make([]string, 0, len(downstreamRegistry.byAccount))
|
|
for account := range downstreamRegistry.byAccount {
|
|
if strings.TrimSpace(account) != "" {
|
|
accounts = append(accounts, account)
|
|
}
|
|
}
|
|
return accounts
|
|
}
|
|
|
|
// ActiveConnectionCount exposes only an aggregate gauge; account and remote-IP labels are intentionally excluded.
|
|
func ActiveConnectionCount() int {
|
|
downstreamRegistry.RLock()
|
|
defer downstreamRegistry.RUnlock()
|
|
return len(downstreamRegistry.byConn)
|
|
}
|
|
|
|
// DisconnectAccount closes every live downstream CMPP session for an
|
|
// application account. The normal connection-close callback removes registry
|
|
// and presence state and reports the disconnect to the API.
|
|
func DisconnectAccount(account string) int {
|
|
account = strings.TrimSpace(account)
|
|
if account == "" {
|
|
return 0
|
|
}
|
|
downstreamRegistry.RLock()
|
|
sessions := make([]*downstreamSession, 0)
|
|
for _, session := range downstreamRegistry.byConn {
|
|
if session != nil && session.account == account && session.conn != nil {
|
|
sessions = append(sessions, session)
|
|
}
|
|
}
|
|
downstreamRegistry.RUnlock()
|
|
for _, session := range sessions {
|
|
session.conn.Close()
|
|
}
|
|
return len(sessions)
|
|
}
|
|
|
|
func (s Server) gatewayInstanceID() string {
|
|
if strings.TrimSpace(s.GatewayInstanceID) != "" {
|
|
return strings.TrimSpace(s.GatewayInstanceID)
|
|
}
|
|
return "gateway-1"
|
|
}
|
|
|
|
func (session downstreamSession) touchPresence(state string, includeSubmit bool, includeDeliver bool) {
|
|
if session.presence == nil || strings.TrimSpace(session.account) == "" {
|
|
return
|
|
}
|
|
now := time.Now().UTC()
|
|
snapshot := DownstreamPresence{
|
|
Account: strings.TrimSpace(session.account),
|
|
SrcID: strings.TrimSpace(session.srcID),
|
|
RemoteIP: strings.TrimSpace(session.remoteIP),
|
|
GatewayInstanceID: strings.TrimSpace(session.instanceID),
|
|
State: defaultString(strings.TrimSpace(state), "connected"),
|
|
ConnectedAt: session.connectedAt,
|
|
UpdatedAt: now,
|
|
}
|
|
if includeSubmit {
|
|
snapshot.LastSubmitAt = now
|
|
}
|
|
if includeDeliver {
|
|
snapshot.LastDeliverAt = now
|
|
}
|
|
_ = session.presence.TouchAccount(context.Background(), snapshot)
|
|
}
|
|
|
|
func (session downstreamSession) removePresence() error {
|
|
if session.presence == nil || strings.TrimSpace(session.account) == "" {
|
|
return nil
|
|
}
|
|
return session.presence.RemoveAccount(context.Background(), strings.TrimSpace(session.account))
|
|
}
|