feat: add 4 missing SQS API commands for Yandex/AWS compatibility

- ChangeMessageVisibilityBatch: batch visibility timeout (up to 10 msgs)
- TagQueue: add/update queue tags
- UntagQueue: remove queue tags by keys
- ListQueueTags: list all queue tags
- Added Tags field to Queue struct
- Request/Response models for all 4 commands
- Registered in router (17 total API commands now)
- Yandex Message Queue API reference doc
This commit is contained in:
Naeel
2026-04-11 07:51:17 +03:00
parent c9d9231efc
commit fbecde72eb
10 changed files with 1250 additions and 0 deletions
@@ -0,0 +1,123 @@
// Создано: 2026-04-11
// ChangeMessageVisibilityBatchV1 — пакетная смена таймаута видимости (до 10 сообщений).
// Паттерн аналогичен DeleteMessageBatchV1: валидация Id, цикл по записям, partial success.
package gosqs
import (
"net/http"
"strings"
"time"
"shared-sqs/app/interfaces"
"shared-sqs/app/models"
"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 _, ok := models.SyncQueues.Queues[key]; !ok {
return utils.CreateErrorResponseV1("QueueNotFound", true)
}
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
}
models.SyncQueues.Lock()
defer models.SyncQueues.Unlock()
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
queue := models.SyncQueues.Queues[key]
for i := 0; i < len(queue.Messages); i++ {
if queue.Messages[i].ReceiptHandle == entry.ReceiptHandle {
if entry.VisibilityTimeout == 0 {
// Сброс: сообщение снова видимо, счётчик retry++
queue.Messages[i].ReceiptTime = time.Now().UTC()
queue.Messages[i].ReceiptHandle = ""
queue.Messages[i].VisibilityTimeout = time.Now().Add(time.Duration(queue.VisibilityTimeout) * time.Second)
queue.Messages[i].Retry++
} else {
queue.Messages[i].VisibilityTimeout = time.Now().Add(time.Duration(entry.VisibilityTimeout) * time.Second)
}
messageFound = true
break
}
}
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,
})
}
}
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
}
+65
View File
@@ -0,0 +1,65 @@
// Создано: 2026-04-11
// ListQueueTagsV1 — возвращает все метки (tags) очереди тенанта.
// Совместимость с AWS SQS ListQueueTags.
package gosqs
import (
"net/http"
"strings"
"shared-sqs/app/interfaces"
"shared-sqs/app/models"
"shared-sqs/app/utils"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)
func ListQueueTagsV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
requestBody := models.NewListQueueTagsRequest()
ok := utils.REQUEST_TRANSFORMER(requestBody, req, false)
if !ok {
log.Error("Invalid Request - ListQueueTagsV1")
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)
models.SyncQueues.RLock()
defer models.SyncQueues.RUnlock()
queue, exists := models.SyncQueues.Queues[key]
if !exists {
return utils.CreateErrorResponseV1("QueueNotFound", true)
}
tags := queue.Tags
if tags == nil {
tags = make(map[string]string)
}
log.Debugf("ListQueueTags: %s — %d tags", queueName, len(tags))
respStruct := models.ListQueueTagsResponse{
Xmlns: models.BaseXmlns,
Result: models.ListQueueTagsResult{
Tags: tags,
},
Metadata: models.BaseResponseMetadata,
}
return http.StatusOK, respStruct
}
+70
View File
@@ -0,0 +1,70 @@
// Создано: 2026-04-11
// TagQueueV1 — добавляет или обновляет метки (tags) очереди тенанта.
// Совместимость с AWS SQS TagQueue: Tag.N.Key / Tag.N.Value.
package gosqs
import (
"net/http"
"strings"
"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 TagQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
requestBody := models.NewTagQueueRequest()
ok := utils.REQUEST_TRANSFORMER(requestBody, req, false)
if !ok {
log.Error("Invalid Request - TagQueueV1")
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)
models.SyncQueues.Lock()
defer models.SyncQueues.Unlock()
queue, exists := models.SyncQueues.Queues[key]
if !exists {
return utils.CreateErrorResponseV1("QueueNotFound", true)
}
// Инициализируем Tags если nil (для старых очередей, созданных без Tags)
if queue.Tags == nil {
queue.Tags = make(map[string]string)
}
// Merge: новая метка с совпадающим ключом заменяет существующую
for k, v := range requestBody.Tags {
queue.Tags[k] = v
}
persistence.SaveQueue(key, queue)
log.Debugf("TagQueue: %s — added %d tags", queueName, len(requestBody.Tags))
respStruct := models.TagQueueResponse{
Xmlns: models.BaseXmlns,
Metadata: models.BaseResponseMetadata,
}
return http.StatusOK, respStruct
}
+66
View File
@@ -0,0 +1,66 @@
// Создано: 2026-04-11
// UntagQueueV1 — удаляет метки (tags) очереди тенанта по ключам.
// Совместимость с AWS SQS UntagQueue: TagKey.N.
package gosqs
import (
"net/http"
"strings"
"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 UntagQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
requestBody := models.NewUntagQueueRequest()
ok := utils.REQUEST_TRANSFORMER(requestBody, req, false)
if !ok {
log.Error("Invalid Request - UntagQueueV1")
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)
models.SyncQueues.Lock()
defer models.SyncQueues.Unlock()
queue, exists := models.SyncQueues.Queues[key]
if !exists {
return utils.CreateErrorResponseV1("QueueNotFound", true)
}
if queue.Tags != nil {
for _, tagKey := range requestBody.TagKeys {
delete(queue.Tags, tagKey)
}
}
persistence.SaveQueue(key, queue)
log.Debugf("UntagQueue: %s — removed %d tag keys", queueName, len(requestBody.TagKeys))
respStruct := models.UntagQueueResponse{
Xmlns: models.BaseXmlns,
Metadata: models.BaseResponseMetadata,
}
return http.StatusOK, respStruct
}