Files
SQS-service/app/gosqs/change_message_visibility_batch.go
T
Repinoid 2e1c594892 fix(P1.2-revue): батч-видимость не заводила в DLQ + TOCTOU лимит в SendMessageBatch
1. ChangeMessageVisibilityBatch при VisibilityTimeout=0 лишь инкрементировал
   Retry и НЕ переносил сообщение в DLQ (логика была только в одиночной версии).
   Вынесен единый метод Queue.ResetVisibilityAndTryDLQ — сброс видимости +
   DLQ-перенос. Обе версии привели к одному пути.
2. SendMessageBatch: проверка OOM-лимита была только по устаревшему снимку
   (RLock → Lock) — добавлена повторная проверка под per-queue Lock (TOCTOU).
3. Инварианты InFlightIndexes/ensureIndexes зафиксированы в док-комментариях.

Тест: TestChangeVisibilityBatch_MovesToDLQ (падает на старой логике, зелёный
на новой).
2026-10-07 08:35:52 +03:00

133 lines
4.5 KiB
Go

// Создано: 2026-04-11, Изменено: 2026-04-11 — добавлена Redis persistence
// ChangeMessageVisibilityBatchV1 — пакетная смена таймаута видимости (до 10 сообщений).
// Паттерн аналогичен DeleteMessageBatchV1: валидация Id, цикл по записям, partial success.
package gosqs
import (
"net/http"
"strings"
"time"
"shared-sqs/app/interfaces"
"shared-sqs/app/models"
"shared-sqs/app/persistence"
"shared-sqs/app/utils"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)
func ChangeMessageVisibilityBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
requestBody := models.NewChangeMessageVisibilityBatchRequest()
ok := utils.REQUEST_TRANSFORMER(requestBody, req, false)
if !ok {
log.Error("Invalid Request - ChangeMessageVisibilityBatchV1")
return utils.CreateErrorResponseV1("InvalidParameterValue", true)
}
t := getTenantFromContext(req)
if t == nil {
return utils.CreateErrorResponseV1("InvalidClientTokenId", true)
}
queueUrl := requestBody.QueueUrl
queueName := ""
if queueUrl == "" {
vars := mux.Vars(req)
queueName = vars["queueName"]
} else {
uriSegments := strings.Split(queueUrl, "/")
queueName = uriSegments[len(uriSegments)-1]
}
key := tenantQueueKey(t.AccessKey, queueName)
if len(requestBody.Entries) == 0 {
return utils.CreateErrorResponseV1("EmptyBatchRequest", true)
}
if len(requestBody.Entries) > 10 {
return utils.CreateErrorResponseV1("TooManyEntriesInBatchRequest", true)
}
// Проверка уникальности Id в пределах запроса
ids := map[string]bool{}
for _, entry := range requestBody.Entries {
if _, found := ids[entry.Id]; found {
return utils.CreateErrorResponseV1("BatchEntryIdsNotDistinct", true)
}
ids[entry.Id] = true
}
queue := models.SyncQueues.GetLocked(key)
if queue == nil {
return utils.CreateErrorResponseV1("QueueNotFound", true)
}
defer queue.UnlockMu()
successEntries := make([]models.ChangeMessageVisibilityBatchResultEntry, 0)
failedEntries := make([]models.BatchResultErrorEntry, 0)
for _, entry := range requestBody.Entries {
if entry.VisibilityTimeout > 43200 {
failedEntries = append(failedEntries, models.BatchResultErrorEntry{
Code: "InvalidParameterValue",
Id: entry.Id,
Message: "VisibilityTimeout must be between 0 and 43200",
SenderFault: true,
})
continue
}
messageFound := false
// O(1)-поиск по индексу ReceiptHandle (P1.2) вместо скана всего среза.
if i, ok := queue.IndexOfReceipt(entry.ReceiptHandle); ok {
if entry.VisibilityTimeout == 0 {
// Сброс + DLQ-перенос при исчерпании MaxReceiveCount — единый путь
// с одиночной версией (раньше батч лишь инкрементировал Retry и не
// переносил в DLQ — счётчик и перенос могли расходиться).
uuid, moved := queue.ResetVisibilityAndTryDLQ(i, entry.ReceiptHandle)
if moved {
// Сообщение перенесено в DLQ и удалено из очереди — чистим Redis.
persistence.DeleteMessagePersist(key, uuid)
} else if j, ok := queue.IndexOfUUID(uuid); ok {
persistence.SaveMessage(key, &queue.Messages[j])
}
} else {
queue.Messages[i].VisibilityTimeout = time.Now().Add(time.Duration(entry.VisibilityTimeout) * time.Second)
// Персистим изменённое сообщение отдельно
persistence.SaveMessage(key, &queue.Messages[i])
}
messageFound = true
}
if messageFound {
successEntries = append(successEntries, models.ChangeMessageVisibilityBatchResultEntry{Id: entry.Id})
} else {
failedEntries = append(failedEntries, models.BatchResultErrorEntry{
Code: "ReceiptHandleIsInvalid",
Id: entry.Id,
Message: "Message not found",
SenderFault: true,
})
}
}
// Персистим изменения в Redis под Lock
if len(successEntries) > 0 {
persistence.SaveQueue(key, queue)
}
respStruct := models.ChangeMessageVisibilityBatchResponse{
Xmlns: models.BaseXmlns,
Result: models.ChangeMessageVisibilityBatchResult{
Successful: successEntries,
Failed: failedEntries,
},
Metadata: models.BaseResponseMetadata,
}
log.Debugf("ChangeMessageVisibilityBatch: %s — %d ok, %d failed", queueName, len(successEntries), len(failedEntries))
return http.StatusOK, respStruct
}