// Создано: 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, dlqMsg := queue.ResetVisibilityAndTryDLQ(i, entry.ReceiptHandle) if moved { // Сообщение перенесено в DLQ и удалено из очереди — чистим Redis // исходной и сохраняем в DLQ (иначе пропадёт после рестарта). _ = persistence.DeleteMessagePersist(key, uuid) if dlqMsg != nil { persistence.SaveMessage(dlqKeyFor(key, queue.DeadLetterQueue.Name), dlqMsg) } } 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 }