Files
SQS-service/app/gosqs/send_message.go
T
Repinoid fd8ffc329d P1.1: per-queue блокировки вместо глобального лока на операции с очередью
Все операции с содержимым очереди (send/receive/delete/visibility/purge/attributes/
tags/admin/gauge/tick) переведены на per-queue RWMutex (QueueStore.GetLocked/
GetRLocked + queue.LockMu). Глобальный SyncQueues-лок остался только для map и
неизменяемых полей. DLQ-переносы берут лок DLQ отдельно (порядок main→DLQ).
marshalQueueMeta больше не мутирует очередь (DTO-снапшот). Устранён дедлок
create_queue (setQueueAttributesV1 вызывается вне глобального лока). Добавлены
конкурентные тесты; go test -race зелёный. Снимает боттлнек: синхронный Redis-I/O
больше не держит глобальный лок.
2026-10-06 18:58:16 +03:00

180 lines
7.4 KiB
Go

package gosqs
import (
"net/http"
"strings"
"time"
"github.com/google/uuid"
"shared-sqs/app/interfaces"
"shared-sqs/app/models"
"shared-sqs/app/persistence"
"shared-sqs/app/utils"
log "github.com/sirupsen/logrus"
"github.com/gorilla/mux"
)
// SendMessageV1 — добавляет сообщение в очередь тенанта.
//
// Логическая схема:
// 1. Разбор тела запроса (Query/JSON) → SendMessageRequest.
// 2. Аутентифицированный тенант из контекста (SigV4 → AccessKey).
// 3. Валидации:
// - MessageBody обязателен (пустой → MissingParameter);
// - DeduplicationId и GroupId ≤ 128 символов;
// - MessageAttributes ≤ 10;
// - FIFO-очередь БЕЗ MessageGroupId → MissingParameter (AWS требует GroupId
// для каждой отправки в FIFO);
// - размер тела ≤ MaximumMessageSize очереди → MessageTooBig;
// - число сообщений < лимита очереди (OOM-защита) → OverLimit.
// 4. Построение SqsMessage: MD5 тела и атрибутов, UUID, метки времени, задержка.
// 5. Атомарно под Lock:
// - FIFO: порядковый номер группы (NextSequenceNumber);
// - проверка дубликата по deduplicationId (дубль НЕ добавляется);
// - добавление в очередь + SaveMessage/SaveQueue в Redis.
// Примечание: сетевые Redis-записи асинхронны (asyncWrite), под Lock
// выполняется только json.Marshal — блокировка не удерживается на сетевом I/O.
// 6. Ответ: MessageId, MD5OfMessageBody/MD5OfMessageAttributes, SequenceNumber (FIFO).
func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
requestBody := models.NewSendMessageRequest()
ok := utils.REQUEST_TRANSFORMER(requestBody, req, false)
if !ok {
log.Error("Invalid Request - SendMessageV1")
return utils.CreateErrorResponseV1("InvalidParameterValue", true)
}
t := getTenantFromContext(req)
if t == nil {
return utils.CreateErrorResponseV1("InvalidClientTokenId", true)
}
messageBody := requestBody.MessageBody
if messageBody == "" {
return utils.CreateErrorResponseV1("MissingParameter", true)
}
messageGroupID := requestBody.MessageGroupId
messageDeduplicationID := requestBody.MessageDeduplicationId
// Валидация DeduplicationID и GroupID (макс 128 chars по AWS)
if err := ValidateDeduplicationID(messageDeduplicationID); err != nil {
return utils.CreateErrorResponseV1("InvalidParameterValue", true)
}
if err := ValidateGroupID(messageGroupID); err != nil {
return utils.CreateErrorResponseV1("InvalidParameterValue", true)
}
// Валидация количества message attributes (макс 10)
if len(requestBody.MessageAttributes) > MaxMessageAttributes {
return utils.CreateErrorResponseV1("InvalidParameterValue", true)
}
queueUrl := getQueueFromPath(requestBody.QueueUrl, req.URL.String())
queueName := ""
if queueUrl == "" {
vars := mux.Vars(req)
queueName = vars["queueName"]
} else {
// Ловушка #6: берём последний сегмент — это queueName, не tenantID
uriSegments := strings.Split(queueUrl, "/")
queueName = uriSegments[len(uriSegments)-1]
}
key := tenantQueueKey(t.AccessKey, queueName)
queue := models.SyncQueues.GetLocked(key)
if queue == nil {
return utils.CreateErrorResponseV1("QueueNotFound", true)
}
defer queue.UnlockMu()
maxMessageSize := queue.MaximumMessageSize
currentMsgCount := len(queue.Messages)
queueIsFIFO := queue.IsFIFO
delaySecs := queue.DelaySeconds
// FIFO-очередь БЕЗ MessageGroupId — ошибка: AWS требует GroupId для каждой
// отправки в FIFO (иначе невозможно гарантировать порядок внутри группы).
// Раньше сообщение просто попадало в группу с пустым именем.
if queueIsFIFO && messageGroupID == "" {
return utils.CreateErrorResponseV1("MissingParameter", true)
}
if maxMessageSize > 0 && len(messageBody) > maxMessageSize {
return utils.CreateErrorResponseV1("MessageTooBig", true)
}
// Fix #11: лимит сообщений в очереди — защита от OOM
if currentMsgCount >= MaxMessagesForQueue(queueIsFIFO) {
return utils.CreateErrorResponseV1("OverLimit", true)
}
if requestBody.DelaySeconds != 0 {
delaySecs = requestBody.DelaySeconds
}
log.Debugf("Putting Message in Queue: [%s] tenant: [%s]", queueName, t.ID)
msg := models.SqsMessage{MessageBody: messageBody}
if len(requestBody.MessageAttributes) > 0 {
msg.MessageAttributes = requestBody.MessageAttributes
msg.MD5OfMessageAttributes = utils.HashAttributes(requestBody.MessageAttributes)
}
msg.MD5OfMessageBody = utils.GetMD5Hash(messageBody)
msg.Uuid = uuid.NewString()
msg.GroupID = messageGroupID
msg.DeduplicationID = messageDeduplicationID
msg.SentTime = time.Now()
msg.DelaySecs = delaySecs
// Повторная проверка лимита под per-queue Lock (между снимком выше и текущим
// моментом другие горутины могли добавить сообщения — мягкий OOM-лимит).
if len(queue.Messages) >= MaxMessagesForQueue(queue.IsFIFO) {
return utils.CreateErrorResponseV1("OverLimit", true)
}
fifoSeqNumber := ""
if queue.IsFIFO {
fifoSeqNumber = queue.NextSequenceNumber(messageGroupID)
}
persistErr := false
if !queue.IsDuplicate(messageDeduplicationID) {
queue.Messages = append(queue.Messages, msg)
// Персистим сообщение СИНХРОННО до ответа клиенту: краш пода в окне
// между 200 OK и async-записью приводил бы к потере подтверждённого
// сообщения. При ошибке — откатываем append (не оставляем в памяти
// сообщение, которого нет в Redis).
if err := persistence.SaveMessageSync(key, &msg); err != nil {
log.Errorf("SendMessage: не удалось персистить сообщение %s в очередь %s: %v", msg.Uuid, key, err)
queue.Messages = queue.Messages[:len(queue.Messages)-1]
persistErr = true
}
} else {
log.Debugf("Duplicate message deduplicationId [%s] in queue [%s]", messageDeduplicationID, queueName)
}
queue.InitDuplicatation(messageDeduplicationID)
// Сохраняем метаданные очереди (FIFO state, duplicates) — без Messages
persistence.SaveQueue(key, queue)
if persistErr {
return utils.CreateErrorResponseV1("InternalError", true)
}
// Логируем только метаданные — тело сообщения не логируется (perf + security)
log.Infof("Queue: %s, MessageId: %s, Size: %d bytes", queueName, msg.Uuid, len(messageBody))
respStruct := models.SendMessageResponse{
Xmlns: models.BaseXmlns,
Result: models.SendMessageResult{
MD5OfMessageAttributes: msg.MD5OfMessageAttributes,
MD5OfMessageBody: msg.MD5OfMessageBody,
MessageId: msg.Uuid,
SequenceNumber: fifoSeqNumber,
},
Metadata: models.BaseResponseMetadata,
}
return http.StatusOK, respStruct
}