fix(batch): SendMessageBatch пишет в Redis синхронно (как SendMessage) + не персистит дубли; SaveMessagesSync

This commit is contained in:
Repinoid
2026-10-10 16:18:54 +03:00
parent 064ed6e5d7
commit a341508b6d
2 changed files with 43 additions and 4 deletions
+12 -4
View File
@@ -113,6 +113,7 @@ func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody
} }
newMsgs := make([]models.SqsMessage, 0, len(sendEntries)) newMsgs := make([]models.SqsMessage, 0, len(sendEntries))
appendedIdx := make([]int, 0, len(sendEntries))
for _, sendEntry := range sendEntries { for _, sendEntry := range sendEntries {
msg := models.SqsMessage{MessageBody: sendEntry.MessageBody} msg := models.SqsMessage{MessageBody: sendEntry.MessageBody}
if len(sendEntry.MessageAttributes) > 0 { if len(sendEntry.MessageAttributes) > 0 {
@@ -130,7 +131,8 @@ func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody
fifoSeqNumber = queue.NextSequenceNumber(sendEntry.MessageGroupId) fifoSeqNumber = queue.NextSequenceNumber(sendEntry.MessageGroupId)
} }
if !queue.IsDuplicate(sendEntry.MessageDeduplicationId) { if !queue.IsDuplicate(sendEntry.MessageDeduplicationId) {
queue.AppendMessage(msg) appendedIdx = append(appendedIdx, queue.AppendMessage(msg))
newMsgs = append(newMsgs, msg)
} else { } else {
log.Debugf("Duplicate deduplicationId [%s] in queue [%s]", sendEntry.MessageDeduplicationId, queueName) log.Debugf("Duplicate deduplicationId [%s] in queue [%s]", sendEntry.MessageDeduplicationId, queueName)
} }
@@ -143,12 +145,18 @@ func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody
MD5OfMessageAttributes: msg.MD5OfMessageAttributes, MD5OfMessageAttributes: msg.MD5OfMessageAttributes,
SequenceNumber: fifoSeqNumber, SequenceNumber: fifoSeqNumber,
}) })
newMsgs = append(newMsgs, msg)
// Логируем только метаданные — тело сообщения не логируется (perf + security) // Логируем только метаданные — тело сообщения не логируется (perf + security)
log.Debugf("Queue: %s, MessageId: %s, Size: %d bytes", queueName, msg.Uuid, len(sendEntry.MessageBody)) log.Debugf("Queue: %s, MessageId: %s, Size: %d bytes", queueName, msg.Uuid, len(sendEntry.MessageBody))
} }
// Персистим новые сообщения отдельно — O(batch_size*msg_size) вместо O(N*msg_size) // Персистим СИНХРОННО до ответа (как одиночный SendMessage): иначе краш в окне
persistence.SaveMessages(key, newMsgs) // между 200 OK и async-записью потеряет подтверждённые сообщения.
if err := persistence.SaveMessagesSync(key, newMsgs); err != nil {
log.Errorf("SendMessageBatch: не удалось персистить батч в %s: %v", key, err)
for _, idx := range appendedIdx {
queue.RemoveIndex(idx)
}
return utils.CreateErrorResponseV1("InternalError", true)
}
// Метаданные очереди (FIFO state, duplicates) — без Messages // Метаданные очереди (FIFO state, duplicates) — без Messages
persistence.SaveQueue(key, queue) persistence.SaveQueue(key, queue)
+31
View File
@@ -314,6 +314,37 @@ func SaveMessages(queueKey string, msgs []models.SqsMessage) {
}) })
} }
// SaveMessagesSync — сохраняет несколько сообщений в Redis СИНХРОННО (pipeline).
// Используется на пути SendMessageBatch: ответ 200 выдаётся только после записи
// всех сообщений (как в одиночном SendMessage). Возвращает ошибку для отката.
func SaveMessagesSync(queueKey string, msgs []models.SqsMessage) error {
if Client == nil || len(msgs) == 0 {
return nil
}
fields := make(map[string]string, len(msgs))
for i := range msgs {
data, err := json.Marshal(&msgs[i])
if err != nil {
return fmt.Errorf("marshal message %s/%s: %w", queueKey, msgs[i].Uuid, err)
}
fields[msgs[i].Uuid] = string(data)
}
if len(fields) == 0 {
return nil
}
hashKey := redisMsgHashPrefix + queueKey
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
args := make([]interface{}, 0, len(fields)*2)
for k, v := range fields {
args = append(args, k, v)
}
if err := Client.HSet(ctx, hashKey, args...).Err(); err != nil {
return fmt.Errorf("HSet messages %s (%d msgs): %w", queueKey, len(fields), err)
}
return nil
}
// DeleteMessagePersist — удаляет одно сообщение из Redis СИНХРОННО. // DeleteMessagePersist — удаляет одно сообщение из Redis СИНХРОННО.
// Синхронность обязательна: если удаление потеряется при рестарте пода, // Синхронность обязательна: если удаление потеряется при рестарте пода,
// сообщение «воскреснет» из Redis (факт: удалённые очереди возвращались // сообщение «воскреснет» из Redis (факт: удалённые очереди возвращались