126 lines
6.1 KiB
Go
126 lines
6.1 KiB
Go
package gosqs
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
log "github.com/sirupsen/logrus"
|
||
|
||
"shared-sqs/app/models"
|
||
)
|
||
|
||
// setQueueAttributesV1 — применяет атрибуты очереди с валидацией по AWS-спецификации.
|
||
//
|
||
// Логическая схема:
|
||
// 1. Whitelist имён: неизвестное имя атрибута → InvalidAttributeName.
|
||
// Список имён (provided) доступен для Query-протокола (form-параметры
|
||
// Attribute.N.Name); для JSON-протокола provided == nil и проверка пропускается
|
||
// (структура уже разобрана из тела, неизвестные поля отброшены).
|
||
// 2. Применяются ТОЛЬКО явно переданные атрибуты (значение != 0).
|
||
// ВАЖНО (фикс): раньше условие `attr.X >= 0` срабатывало и для НЕ переданных
|
||
// полей (zero-value = 0) — вызов SetQueueAttributes с одним атрибутом
|
||
// обнулял все остальные атрибуты очереди.
|
||
// 3. Значения вне диапазонов AWS → ошибка InvalidParameterValue
|
||
// (раньше значения молча клэмпились в допустимый диапазон).
|
||
// 4. RedrivePolicy: ARN DLQ разбирается, DLQ ищется по TENANT-SCOPED ключу
|
||
// "{accessKey}:{queueName}" (раньше — по голому имени, а ключи в map
|
||
// tenant-scoped → DLQ никогда не находилась, RedrivePolicy не работал).
|
||
// DLQ должна принадлежать тому же тенанту.
|
||
func setQueueAttributesV1(q *models.Queue, attr models.QueueAttributes, provided map[string]string, tenantAccessKey string) error {
|
||
// Шаг 1: whitelist имён атрибутов (единый список — models.AttrNameWhitelist).
|
||
for name := range provided {
|
||
if !models.AttrNameWhitelist[name] {
|
||
return fmt.Errorf("InvalidAttributeName")
|
||
}
|
||
}
|
||
|
||
// Шаг 2: применяем только явно переданные атрибуты с проверкой диапазонов.
|
||
// Диапазоны AWS: DelaySeconds 0–900, MaximumMessageSize 1024–262144,
|
||
// MessageRetentionPeriod 60–1209600, ReceiveMessageWaitTimeSeconds 0–20,
|
||
// VisibilityTimeout 0–43200.
|
||
if attr.DelaySeconds != 0 {
|
||
if attr.DelaySeconds.Int() < 0 || attr.DelaySeconds.Int() > MaxDelaySeconds {
|
||
return fmt.Errorf("InvalidParameterValue")
|
||
}
|
||
q.DelaySeconds = attr.DelaySeconds.Int()
|
||
}
|
||
if attr.MaximumMessageSize != 0 {
|
||
if attr.MaximumMessageSize.Int() < MinMessageSizeLimit || attr.MaximumMessageSize.Int() > MaxMessageSizeLimit {
|
||
return fmt.Errorf("InvalidParameterValue")
|
||
}
|
||
q.MaximumMessageSize = attr.MaximumMessageSize.Int()
|
||
}
|
||
if attr.MessageRetentionPeriod != 0 {
|
||
if attr.MessageRetentionPeriod.Int() < MinMessageRetentionPeriod || attr.MessageRetentionPeriod.Int() > MaxMessageRetentionPeriod {
|
||
return fmt.Errorf("InvalidParameterValue")
|
||
}
|
||
q.MessageRetentionPeriod = attr.MessageRetentionPeriod.Int()
|
||
}
|
||
if attr.ReceiveMessageWaitTimeSeconds != 0 {
|
||
if attr.ReceiveMessageWaitTimeSeconds.Int() < 0 || attr.ReceiveMessageWaitTimeSeconds.Int() > MaxReceiveMessageWaitTimeSeconds {
|
||
return fmt.Errorf("InvalidParameterValue")
|
||
}
|
||
q.ReceiveMessageWaitTimeSeconds = attr.ReceiveMessageWaitTimeSeconds.Int()
|
||
}
|
||
if attr.VisibilityTimeout != 0 {
|
||
if attr.VisibilityTimeout.Int() < 0 || attr.VisibilityTimeout.Int() > MaxVisibilityTimeout {
|
||
return fmt.Errorf("InvalidParameterValue")
|
||
}
|
||
q.VisibilityTimeout = attr.VisibilityTimeout.Int()
|
||
}
|
||
|
||
// Шаг 3: RedrivePolicy (dead-letter queue).
|
||
if attr.RedrivePolicy != (models.RedrivePolicy{}) {
|
||
arnArray := strings.Split(attr.RedrivePolicy.DeadLetterTargetArn, ":")
|
||
queueName := arnArray[len(arnArray)-1]
|
||
// DLQ ищем по tenant-scoped ключу — как хранятся все очереди.
|
||
dlqKey := tenantAccessKey + ":" + queueName
|
||
models.SyncQueues.RLock()
|
||
deadLetterQueue, ok := models.SyncQueues.Queues[dlqKey]
|
||
models.SyncQueues.RUnlock()
|
||
if !ok {
|
||
log.Error("Invalid RedrivePolicy Attribute")
|
||
return fmt.Errorf("InvalidAttributeValue")
|
||
}
|
||
// Запрет self-DLQ (q → q) и циклов (A → B → A): при переносе в DLQ
|
||
// переносящий код берёт DeadLetterQueue.LockMu() под уже взятым q.mu —
|
||
// self/цикл даёт немедленный self-deadlock (или AB/BA-deadlock).
|
||
if deadLetterQueue == q {
|
||
log.Error("Invalid RedrivePolicy Attribute: self DLQ")
|
||
return fmt.Errorf("InvalidAttributeValue")
|
||
}
|
||
// Обход цепочки DLQ для детекта циклов. Лимит 100 — защита от уже
|
||
// существующего в данных цикла (если бы он там был).
|
||
for cur, steps := deadLetterQueue, 0; cur != nil && steps < 100; cur, steps = cur.DeadLetterQueue, steps+1 {
|
||
if cur == q {
|
||
log.Error("Invalid RedrivePolicy Attribute: cyclic DLQ")
|
||
return fmt.Errorf("InvalidAttributeValue")
|
||
}
|
||
}
|
||
q.DeadLetterQueue = deadLetterQueue
|
||
q.MaxReceiveCount = attr.RedrivePolicy.MaxReceiveCount.Int()
|
||
}
|
||
|
||
// Шаг 4: FIFO-атрибуты и Policy — храним как есть (совместимость с Terraform).
|
||
// FifoQueue только включаем (выключить FIFO у существующей очереди нельзя —
|
||
// как и в AWS). ContentBasedDeduplication и Policy не исполняются,
|
||
// но должны возвращаться в GetQueueAttributes, иначе drift в Terraform.
|
||
if attr.FifoQueue.Bool() {
|
||
q.IsFIFO = true
|
||
}
|
||
if attr.ContentBasedDeduplication.Bool() {
|
||
q.ContentBasedDeduplication = true
|
||
}
|
||
if attr.Policy != nil {
|
||
// Сохраняем канонический JSON (map → marshal сортирует ключи) —
|
||
// детерминированно, без drift при чтении.
|
||
if data, err := json.Marshal(attr.Policy); err == nil {
|
||
q.Policy = string(data)
|
||
}
|
||
}
|
||
q.LastModifiedAt = time.Now().UTC()
|
||
return nil
|
||
}
|