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) models.SyncQueues.RLock() queue, exists := models.SyncQueues.Queues[key] if !exists { models.SyncQueues.RUnlock() return utils.CreateErrorResponseV1("QueueNotFound", true) } maxMessageSize := queue.MaximumMessageSize currentMsgCount := len(queue.Messages) queueIsFIFO := queue.IsFIFO delaySecs := queue.DelaySeconds models.SyncQueues.RUnlock() // 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 models.SyncQueues.Lock() // Перепроверка под Lock: очередь могла быть удалена между RLock-проверкой // выше и захватом Lock (иначе nil-pointer deref). queue = models.SyncQueues.Queues[key] if queue == nil { models.SyncQueues.Unlock() return utils.CreateErrorResponseV1("QueueNotFound", true) } // Повторная проверка лимита под Lock: между RLock-снимком и захватом Lock // другие горутины могли добавить сообщения (мягкий OOM-лимит). if len(queue.Messages) >= MaxMessagesForQueue(queue.IsFIFO) { models.SyncQueues.Unlock() return utils.CreateErrorResponseV1("OverLimit", true) } fifoSeqNumber := "" if queue.IsFIFO { fifoSeqNumber = queue.NextSequenceNumber(messageGroupID) } if !queue.IsDuplicate(messageDeduplicationID) { queue.Messages = append(queue.Messages, msg) // Персистим одно сообщение отдельно — O(msg_size) вместо O(N*msg_size) persistence.SaveMessage(key, &msg) } else { log.Debugf("Duplicate message deduplicationId [%s] in queue [%s]", messageDeduplicationID, queueName) } queue.InitDuplicatation(messageDeduplicationID) // Сохраняем метаданные очереди (FIFO state, duplicates) — без Messages persistence.SaveQueue(key, queue) models.SyncQueues.Unlock() // Логируем только метаданные — тело сообщения не логируется (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 }