// Изменено: 2026-04-11 — добавлена Redis persistence // ChangeMessageVisibilityV1 — меняет visibility timeout сообщения в очереди тенанта. 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 ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractResponseBody) { requestBody := models.NewChangeMessageVisibilityRequest() ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) if !ok { log.Error("Invalid Request - ChangeMessageVisibilityV1") return utils.CreateErrorResponseV1("InvalidParameterValue", true) } t := getTenantFromContext(req) if t == nil { return utils.CreateErrorResponseV1("InvalidClientTokenId", true) } vars := mux.Vars(req) queueUrl := requestBody.QueueUrl queueName := "" if queueUrl == "" { queueName = vars["queueName"] } else { uriSegments := strings.Split(queueUrl, "/") queueName = uriSegments[len(uriSegments)-1] } key := tenantQueueKey(t.AccessKey, queueName) receiptHandle := requestBody.ReceiptHandle visibilityTimeout := requestBody.VisibilityTimeout if visibilityTimeout < 0 || visibilityTimeout > 43200 { return utils.CreateErrorResponseV1("InvalidParameterValue", true) } queue := models.SyncQueues.GetLocked(key) if queue == nil { return utils.CreateErrorResponseV1("QueueNotFound", true) } defer queue.UnlockMu() messageFound := false var changedMsgUuid string var msgRemoved bool // O(1)-поиск по индексу ReceiptHandle (P1.2) вместо скана всего среза. if i, ok := queue.IndexOfReceipt(receiptHandle); ok { msg := &queue.Messages[i] timeout := queue.VisibilityTimeout if visibilityTimeout == 0 { // Возврат видимости: сброс handle + снятие с индекса in-flight. queue.ClearInFlight(i, receiptHandle) msg.ReceiptTime = time.Now().UTC() msg.ReceiptHandle = "" msg.VisibilityTimeout = time.Now().Add(time.Duration(timeout) * time.Second) msg.Retry++ changedMsgUuid = msg.Uuid if queue.MaxReceiveCount > 0 && queue.DeadLetterQueue != nil && msg.Retry >= queue.MaxReceiveCount { // Перенос в DLQ + удаление из исходной очереди (tombstone, O(1)). dlqMsg := *msg queue.DeadLetterQueue.LockMu() queue.DeadLetterQueue.AppendMessage(dlqMsg) queue.DeadLetterQueue.UnlockMu() queue.RemoveIndex(i) msgRemoved = true } } else { msg.VisibilityTimeout = time.Now().Add(time.Duration(visibilityTimeout) * time.Second) changedMsgUuid = msg.Uuid } messageFound = true } // Персистим изменения в Redis под Lock (сетевые записи асинхронны). if messageFound { if msgRemoved { // Сообщение удалено (перемещено в DLQ) — удаляем из Redis persistence.DeleteMessagePersist(key, changedMsgUuid) } else if i, ok := queue.IndexOfUUID(changedMsgUuid); ok { // Сообщение обновлено — перезаписываем в Redis persistence.SaveMessage(key, &queue.Messages[i]) } persistence.SaveQueue(key, queue) } if !messageFound { return utils.CreateErrorResponseV1("MessageNotInFlight", true) } respStruct := models.ChangeMessageVisibilityResult{ Xmlns: models.BaseXmlns, Metadata: models.BaseResponseMetadata, } return http.StatusOK, &respStruct }