264 lines
8.9 KiB
Go
264 lines
8.9 KiB
Go
package tmobile
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"fiskerinc.com/modules/grpc/kafka_grpc"
|
|
|
|
"fiskerinc.com/modules/kafka"
|
|
"fiskerinc.com/modules/logger"
|
|
"fiskerinc.com/modules/utils/envtool"
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
var (
|
|
TMOBILE_MESSAGE_RETRY_MILLISECONDS int = envtool.GetEnvInt("TMOBILE_MESSAGE_RETRY_MILLISECONDS", 300)
|
|
TMOBILE_MESSAGE_RETRY_ATTEMPTS int = envtool.GetEnvInt("TMOBILE_MESSAGE_RETRY_ATTEMPTS", 3) // Number of times we will try to send a message before giving up
|
|
TMOBILE_MESSAGE_TIMEOUT_SECONDS int = envtool.GetEnvInt("TMOBILE_MESSAGE_TIMEOUT_SECONDS", 10)
|
|
TMOBILE_MESSAGE_CHECK_MILLISECONDS int = envtool.GetEnvInt("TMOBILE_MESSAGE_CHECK_MILLISECONDS", 500) // About how often we want to check on a message
|
|
|
|
// If we need to send even more messages at one time, I would suggest using an external message queue from either redis or maybe even kafka
|
|
TMOBILE_MESSAGE_QUEUE_SIZE int = envtool.GetEnvInt("TMOBILE_MESSAGE_QUEUE_SIZE", 5000) // Number of messages we can wait on at one time
|
|
TMOBILE_READ_THREADS int = envtool.GetEnvInt("TMOBILE_READ_THREADS", 3) // Number of goroutines that will be used to check on tmboile messages
|
|
)
|
|
|
|
// When we send an sms, we want to send it off, and then check on it every second to see if it has been delivered yet.
|
|
// After it has been delivered, we send a message in kafka to say that that message is done, and continue on
|
|
// This is for messages that we don't care if we deliver the message right away.
|
|
// Like if we are waking the car up for an update, we don't we can wait for the car to wake up nicely
|
|
// for remote commands, we want the car to be awoken and we return right away. Can maybe be its own faster queue
|
|
|
|
type RunningQueue struct {
|
|
client TMobClienter
|
|
messageQueues []chan messageTimeout // Need to lock and unlock this array
|
|
toSendThread int // which thread we will try to send to
|
|
kafka kafka.ProducerInterface
|
|
}
|
|
|
|
type messageTimeout struct {
|
|
MessageID string
|
|
Timeout time.Time
|
|
NextCheck time.Time
|
|
}
|
|
|
|
func NewQueue(client TMobClienter, kafkaProducer kafka.ProducerInterface) (rq *RunningQueue) {
|
|
rq = &RunningQueue{}
|
|
rq.client = client
|
|
rq.kafka = kafkaProducer
|
|
|
|
for x := 0; x < TMOBILE_READ_THREADS; x++ {
|
|
rq.messageQueues = append(rq.messageQueues, make(chan messageTimeout, TMOBILE_MESSAGE_QUEUE_SIZE))
|
|
go rq.queueChecker(x)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
type SendSMS struct {
|
|
Message string
|
|
ICCID string // The target of our message
|
|
CheckDelivery bool // Do we care if this message is delivered or not
|
|
KafkaServiceTarget string // The kafka service that will get the sms delivery status
|
|
}
|
|
|
|
// try to send message, if we fail to even send the message, that can be notified back right away
|
|
// then we thread out of here to wait for actual message delivery checks
|
|
func (rq *RunningQueue) SendSMS(ctx context.Context, req *SendSMSRequest) (messageID string, err error) {
|
|
msg := SendSMS{
|
|
Message: req.MessageText,
|
|
ICCID: req.ICCID,
|
|
CheckDelivery: req.await,
|
|
KafkaServiceTarget: req.KafkaServiceTarget,
|
|
}
|
|
messageID, err = rq.sendSMS(msg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// If we don't want to await for the message delivery, we will skip checking on the message
|
|
if !req.await {
|
|
return
|
|
}
|
|
|
|
queueMSG := messageTimeout{
|
|
MessageID: messageID,
|
|
Timeout: time.Now().Add(time.Second * time.Duration(TMOBILE_MESSAGE_TIMEOUT_SECONDS)),
|
|
NextCheck: time.Now().Add(time.Millisecond * time.Duration(TMOBILE_MESSAGE_CHECK_MILLISECONDS)),
|
|
}
|
|
rq.addToQueue(queueMSG)
|
|
|
|
return
|
|
}
|
|
|
|
func (rq *RunningQueue) addToQueue(msg messageTimeout) {
|
|
rq.messageQueues[rq.toSendThread] <- msg
|
|
|
|
// Lightweight round robin, due to the multithreadedness, we might not send one message to each queue, but I am fine with that
|
|
rq.toSendThread = (rq.toSendThread + 1) % TMOBILE_READ_THREADS
|
|
}
|
|
|
|
// Send down the kafka line if this message has been successfully delivered or not
|
|
// This should probably be moved outside of tmobile and into the sms service
|
|
func (rq *RunningQueue) UpdateMessageStatusKafka(messageID string, status MessageStatusEnum) {
|
|
// We want to use grpc, but the other attendant services would also need a conversion
|
|
/* msgGRPC := kafka_grpc.SMSStatus{
|
|
MessageID: messageID,
|
|
Status: 0,
|
|
}
|
|
//grpcCanSignal := cansignal.ToGrpc(batch)
|
|
grpcData, err := proto.Marshal(&msgGRPC)
|
|
if err != nil {
|
|
logger.Err(err).Msgf("failed to protobuff sms message id %s", messageID)
|
|
return
|
|
} */
|
|
|
|
msg := &kafka_grpc.GRPC_AttendantPayload_MessageStatus{
|
|
MessageStatus: &kafka_grpc.MessageStatus{
|
|
MessageId: messageID,
|
|
Status: kafka_grpc.EmumStatus(kafka_grpc.EmumStatus_value[string(status)]),
|
|
},
|
|
}
|
|
kafkaMSG := kafka_grpc.GRPC_AttendantPayload{
|
|
Handler: "sms_delivery_status_manifest",
|
|
Data: msg,
|
|
}
|
|
|
|
binaryPayload, _ := proto.Marshal(&kafkaMSG)
|
|
err := rq.kafka.ProduceBinary(kafka.AttendantServiceGRPCKafka, "", binaryPayload, nil)
|
|
if err != nil {
|
|
err = errors.WithStack(err)
|
|
logger.Err(err).Msgf("failed to produce kafka message for sms id %s", messageID)
|
|
}
|
|
}
|
|
|
|
func (rq *RunningQueue) sendSMS(msg SendSMS) (messageID string, err error) {
|
|
in := SendSMSRequest{
|
|
ICCID: msg.ICCID,
|
|
MessageText: msg.Message,
|
|
}
|
|
|
|
var smr *SendSMSResponse
|
|
// Giving 3 tries for a gateway failure
|
|
// Sometimes we fail to deliver a message to tmobile for some reason, so we have to retry sending the message
|
|
for x := 0; x < 3; x++ {
|
|
smr, err = rq.client.SendSMS(context.Background(), &in)
|
|
if err != nil {
|
|
if errors.Is(err, ErrBadGatewayCode) {
|
|
logger.Warn().Err(err).Msgf("ICCID %s gateway failure sending sms", in.ICCID)
|
|
time.Sleep(time.Millisecond * time.Duration(TMOBILE_MESSAGE_RETRY_MILLISECONDS)) // 300 milliseconds seems to work best for me
|
|
continue
|
|
}
|
|
if errors.Is(err, ErrAccessTokenExpired) {
|
|
logger.Err(err).Msgf("Access token for sms expired, should never happen")
|
|
}
|
|
return "", errors.WithMessagef(err, "failed to send SMS to ICCID: %s", in.ICCID)
|
|
}
|
|
break
|
|
}
|
|
|
|
if smr == nil {
|
|
return "", errors.WithMessagef(err, "failed to send SMS to ICCID: %s", in.ICCID)
|
|
}
|
|
|
|
messageID = smr.SmsMessageID
|
|
return
|
|
}
|
|
|
|
// Continually read from the queue and check the status of the given message
|
|
func (rq *RunningQueue) queueChecker(index int) {
|
|
messageQueue := rq.messageQueues[index]
|
|
for msg := range messageQueue {
|
|
// This will only happen when we are not sending messages
|
|
if !time.Now().After(msg.NextCheck) {
|
|
time.Sleep(time.Until(msg.NextCheck))
|
|
}
|
|
logger.Debug().Msgf("Checking msgID %s for delivery status", msg.MessageID)
|
|
go func(msg messageTimeout) {
|
|
delivered, err := rq.checkIfMessageDelivered(msg.MessageID)
|
|
if err == nil {
|
|
if delivered {
|
|
rq.UpdateMessageStatusKafka(msg.MessageID, MESSAGE_STATUS_DELIVERED)
|
|
return
|
|
}
|
|
// Not delivered, but not an error
|
|
if time.Now().After(msg.Timeout) {
|
|
rq.UpdateMessageStatusKafka(msg.MessageID, MESSAGE_STATUS_TIMEOUT)
|
|
return
|
|
}
|
|
// If we get here, we need to check on the message again
|
|
msg.NextCheck = time.Now().Add(time.Millisecond * time.Duration(TMOBILE_MESSAGE_CHECK_MILLISECONDS))
|
|
rq.addToQueue(msg)
|
|
} else {
|
|
rq.UpdateMessageStatusKafka(msg.MessageID, MESSAGE_STATUS_FAILED)
|
|
}
|
|
}(msg)
|
|
}
|
|
}
|
|
|
|
func (rq *RunningQueue) checkIfMessageDelivered(msgID string) (delivered bool, err error) {
|
|
if !IsRealSMSID(msgID) {
|
|
return true, nil
|
|
}
|
|
|
|
var out *SMSDetailsResponse
|
|
for x := 0; x < 3; x++ {
|
|
out, err = rq.client.Details(context.Background(), msgID)
|
|
if err != nil {
|
|
if errors.Is(err, ErrBadGatewayCode) {
|
|
logger.Warn().Err(err).Msgf("MSG id %s gateway failure sending sms", msgID)
|
|
time.Sleep(time.Millisecond * time.Duration(TMOBILE_MESSAGE_RETRY_MILLISECONDS))
|
|
continue
|
|
}
|
|
if errors.Is(err, ErrAccessTokenExpired) {
|
|
logger.Err(err).Msgf("Access token for sms expired, should never happen")
|
|
}
|
|
|
|
return false, errors.WithMessagef(err, "failed to check status of SMS: %s", msgID)
|
|
}
|
|
}
|
|
|
|
switch out.Status {
|
|
case Pending, CancelPending:
|
|
return false, nil
|
|
case Delivered:
|
|
return true, nil
|
|
default:
|
|
logger.Warn().Msgf("Fell through to default of sms status id: %s status: %s", msgID, out.Status)
|
|
return false, errors.WithMessagef(
|
|
ErrBadMsgStatus,
|
|
"message with id %s failed with status %s",
|
|
out.SmsMsgID,
|
|
out.Status,
|
|
)
|
|
}
|
|
}
|
|
|
|
type MessageStatus struct {
|
|
MessageID string `json:"MessageID"`
|
|
Status MessageStatusEnum `json:"Status"`
|
|
}
|
|
|
|
func GRPCToTMessage(payload *kafka_grpc.GRPC_AttendantPayload) []byte {
|
|
if payload.Data == nil {
|
|
return nil
|
|
}
|
|
data := payload.Data.(*kafka_grpc.GRPC_AttendantPayload_MessageStatus)
|
|
msg := MessageStatus{
|
|
MessageID: data.MessageStatus.GetMessageId(),
|
|
Status: MessageStatusEnum(data.MessageStatus.GetStatus().String()),
|
|
}
|
|
bytes, _ := json.Marshal(msg)
|
|
return bytes
|
|
}
|
|
|
|
type MessageStatusEnum string
|
|
|
|
const (
|
|
MESSAGE_STATUS_DELIVERED MessageStatusEnum = "DELIVERED"
|
|
MESSAGE_STATUS_TIMEOUT MessageStatusEnum = "TIMEOUT"
|
|
MESSAGE_STATUS_FAILED MessageStatusEnum = "FAILED"
|
|
)
|