P1.1: per-queue блокировки вместо глобального лока на операции с очередью
Все операции с содержимым очереди (send/receive/delete/visibility/purge/attributes/ tags/admin/gauge/tick) переведены на per-queue RWMutex (QueueStore.GetLocked/ GetRLocked + queue.LockMu). Глобальный SyncQueues-лок остался только для map и неизменяемых полей. DLQ-переносы берут лок DLQ отдельно (порядок main→DLQ). marshalQueueMeta больше не мутирует очередь (DTO-снапшот). Устранён дедлок create_queue (setQueueAttributesV1 вызывается вне глобального лока). Добавлены конкурентные тесты; go test -race зелёный. Снимает боттлнек: синхронный Redis-I/O больше не держит глобальный лок.
This commit is contained in:
+44
-24
@@ -445,15 +445,23 @@ func (h *Handler) listTenantQueues(w http.ResponseWriter, r *http.Request) {
|
||||
RetentionPeriod int `json:"retention_period"`
|
||||
}
|
||||
queues := make([]queueInfo, 0)
|
||||
models.SyncQueues.RLock()
|
||||
for _, name := range models.SyncQueues.TenantQueueNames(t.AccessKey) {
|
||||
q := models.SyncQueues.Queues[prefix+name]
|
||||
if q == nil {
|
||||
continue
|
||||
type nameQueue struct {
|
||||
name string
|
||||
q *models.Queue
|
||||
}
|
||||
models.SyncQueues.RLock()
|
||||
pairs := make([]nameQueue, 0)
|
||||
for _, name := range models.SyncQueues.TenantQueueNames(t.AccessKey) {
|
||||
if q := models.SyncQueues.Queues[prefix+name]; q != nil {
|
||||
pairs = append(pairs, nameQueue{name, q})
|
||||
}
|
||||
}
|
||||
models.SyncQueues.RUnlock()
|
||||
for _, p := range pairs {
|
||||
p.q.RLockMu()
|
||||
visible := 0
|
||||
notVisible := 0
|
||||
for _, msg := range q.Messages {
|
||||
for _, msg := range p.q.Messages {
|
||||
if msg.ReceiptHandle != "" {
|
||||
notVisible++
|
||||
} else {
|
||||
@@ -461,15 +469,15 @@ func (h *Handler) listTenantQueues(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
queues = append(queues, queueInfo{
|
||||
Name: name,
|
||||
Name: p.name,
|
||||
Messages: visible,
|
||||
NotVisible: notVisible,
|
||||
VisibilityTimeout: q.VisibilityTimeout,
|
||||
MaxMessageSize: q.MaximumMessageSize,
|
||||
RetentionPeriod: q.MessageRetentionPeriod,
|
||||
VisibilityTimeout: p.q.VisibilityTimeout,
|
||||
MaxMessageSize: p.q.MaximumMessageSize,
|
||||
RetentionPeriod: p.q.MessageRetentionPeriod,
|
||||
})
|
||||
p.q.RUnlockMu()
|
||||
}
|
||||
models.SyncQueues.RUnlock()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(queues)
|
||||
}
|
||||
@@ -594,7 +602,7 @@ func (h *Handler) peekQueueMessages(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
models.SyncQueues.RLock()
|
||||
q.RLockMu()
|
||||
result := make([]peekMessageItem, 0, len(q.Messages))
|
||||
for i, msg := range q.Messages {
|
||||
if i >= limit {
|
||||
@@ -609,7 +617,7 @@ func (h *Handler) peekQueueMessages(w http.ResponseWriter, r *http.Request) {
|
||||
InFlight: msg.ReceiptHandle != "",
|
||||
})
|
||||
}
|
||||
models.SyncQueues.RUnlock()
|
||||
q.RUnlockMu()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
@@ -645,12 +653,12 @@ func (h *Handler) sendMessageToQueue(w http.ResponseWriter, r *http.Request) {
|
||||
Uuid: uuid.NewString(),
|
||||
SentTime: time.Now(),
|
||||
}
|
||||
models.SyncQueues.Lock()
|
||||
models.SyncQueues.Queues[key].Messages = append(models.SyncQueues.Queues[key].Messages, msg)
|
||||
q.LockMu()
|
||||
q.Messages = append(q.Messages, msg)
|
||||
// Персистим одно сообщение отдельно
|
||||
persistence.SaveMessage(key, &msg)
|
||||
persistence.SaveQueue(key, models.SyncQueues.Queues[key])
|
||||
models.SyncQueues.Unlock()
|
||||
persistence.SaveQueue(key, q)
|
||||
q.UnlockMu()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]string{"id": msg.Uuid, "status": "sent"})
|
||||
@@ -671,12 +679,12 @@ func (h *Handler) purgeQueue(w http.ResponseWriter, r *http.Request) {
|
||||
jsonErr(w, http.StatusNotFound, "queue not found")
|
||||
return
|
||||
}
|
||||
models.SyncQueues.Lock()
|
||||
models.SyncQueues.Queues[key].Messages = models.SyncQueues.Queues[key].Messages[:0]
|
||||
q.LockMu()
|
||||
q.Messages = q.Messages[:0]
|
||||
// Удаляем все сообщения из Redis одной командой
|
||||
persistence.PurgeMessagesPersist(key)
|
||||
persistence.SaveQueue(key, models.SyncQueues.Queues[key])
|
||||
models.SyncQueues.Unlock()
|
||||
persistence.SaveQueue(key, q)
|
||||
q.UnlockMu()
|
||||
log.Infof("admin: purged queue %s for tenant %s", queueName, t.ID)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -704,13 +712,19 @@ func (h *Handler) detailedHealth(w http.ResponseWriter, r *http.Request) {
|
||||
queueCount := 0
|
||||
msgCount := 0
|
||||
models.SyncQueues.RLock()
|
||||
uiQueues := make([]*models.Queue, 0)
|
||||
for key, q := range models.SyncQueues.Queues {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
queueCount++
|
||||
msgCount += len(q.Messages)
|
||||
uiQueues = append(uiQueues, q)
|
||||
}
|
||||
}
|
||||
models.SyncQueues.RUnlock()
|
||||
for _, q := range uiQueues {
|
||||
q.RLockMu()
|
||||
msgCount += len(q.Messages)
|
||||
q.RUnlockMu()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(adminHealthDetail{
|
||||
@@ -725,11 +739,17 @@ func (h *Handler) detailedHealth(w http.ResponseWriter, r *http.Request) {
|
||||
tenants := h.store.List()
|
||||
models.SyncQueues.RLock()
|
||||
queueCount := len(models.SyncQueues.Queues)
|
||||
msgCount := 0
|
||||
allQueues := make([]*models.Queue, 0, queueCount)
|
||||
for _, q := range models.SyncQueues.Queues {
|
||||
msgCount += len(q.Messages)
|
||||
allQueues = append(allQueues, q)
|
||||
}
|
||||
models.SyncQueues.RUnlock()
|
||||
msgCount := 0
|
||||
for _, q := range allQueues {
|
||||
q.RLockMu()
|
||||
msgCount += len(q.Messages)
|
||||
q.RUnlockMu()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(adminHealthDetail{
|
||||
|
||||
@@ -47,18 +47,11 @@ func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractRespo
|
||||
return utils.CreateErrorResponseV1("InvalidParameterValue", true)
|
||||
}
|
||||
|
||||
// ВСЯ работа с очередью — под одним Lock.
|
||||
// Раньше проверка существования шла БЕЗ блокировки (чтение map) —
|
||||
// concurrent map read and map write при параллельном DeleteQueue
|
||||
// (fatal error, под падал под нагрузкой).
|
||||
// defer Unlock: при любой панике в секции мьютекс не останется захваченным.
|
||||
models.SyncQueues.Lock()
|
||||
defer models.SyncQueues.Unlock()
|
||||
|
||||
queue, exists := models.SyncQueues.Queues[key]
|
||||
if !exists {
|
||||
queue := models.SyncQueues.GetLocked(key)
|
||||
if queue == nil {
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
defer queue.UnlockMu()
|
||||
|
||||
messageFound := false
|
||||
var changedMsgUuid string
|
||||
@@ -76,7 +69,9 @@ func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractRespo
|
||||
queue.DeadLetterQueue != nil &&
|
||||
msgs[i].Retry >= queue.MaxReceiveCount {
|
||||
changedMsgUuid = msgs[i].Uuid
|
||||
queue.DeadLetterQueue.LockMu()
|
||||
queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, msgs[i])
|
||||
queue.DeadLetterQueue.UnlockMu()
|
||||
queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...)
|
||||
msgRemoved = true
|
||||
} else {
|
||||
|
||||
@@ -59,15 +59,11 @@ func ChangeMessageVisibilityBatchV1(req *http.Request) (int, interfaces.Abstract
|
||||
ids[entry.Id] = true
|
||||
}
|
||||
|
||||
models.SyncQueues.Lock()
|
||||
defer models.SyncQueues.Unlock()
|
||||
|
||||
// Проверка существования очереди ВНУТРИ Lock: раньше была БЕЗ блокировки
|
||||
// (чтение map) — concurrent map read/write при параллельном DeleteQueue.
|
||||
queue, exists := models.SyncQueues.Queues[key]
|
||||
if !exists {
|
||||
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)
|
||||
|
||||
@@ -49,14 +49,9 @@ func CreateQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
queueUrl := tenantQueueURL(t, queueName)
|
||||
queueArn := tenantQueueARN(t, queueName)
|
||||
|
||||
models.SyncQueues.Lock()
|
||||
if _, exists := models.SyncQueues.Queues[key]; !exists {
|
||||
// Проверка лимита очередей тенанта
|
||||
if t.MaxQueues > 0 && countTenantQueues(t.AccessKey) >= t.MaxQueues {
|
||||
models.SyncQueues.Unlock()
|
||||
return utils.CreateErrorResponseV1("LimitExceeded", true)
|
||||
}
|
||||
log.Infof("Creating Queue: %s (tenant: %s)", queueName, t.ID)
|
||||
// Готовим очередь и применяем атрибуты ВНЕ глобального лока: setQueueAttributesV1
|
||||
// берёт SyncQueues.RLock (поиск DLQ), а RWMutex не реентрантен — вызов под
|
||||
// уже захваченным SyncQueues.Lock дал бы дедлок.
|
||||
queue := &models.Queue{
|
||||
Name: queueName,
|
||||
URL: queueUrl,
|
||||
@@ -72,9 +67,17 @@ func CreateQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
provided = utils.ExtractQueueAttributes(req.PostForm)
|
||||
}
|
||||
if err := setQueueAttributesV1(queue, requestBody.Attributes, provided, t.AccessKey); err != nil {
|
||||
models.SyncQueues.Unlock()
|
||||
return utils.CreateErrorResponseV1(err.Error(), true)
|
||||
}
|
||||
|
||||
models.SyncQueues.Lock()
|
||||
if _, exists := models.SyncQueues.Queues[key]; !exists {
|
||||
// Проверка лимита очередей тенанта
|
||||
if t.MaxQueues > 0 && countTenantQueues(t.AccessKey) >= t.MaxQueues {
|
||||
models.SyncQueues.Unlock()
|
||||
return utils.CreateErrorResponseV1("LimitExceeded", true)
|
||||
}
|
||||
log.Infof("Creating Queue: %s (tenant: %s)", queueName, t.ID)
|
||||
models.SyncQueues.AddQueue(key, queue)
|
||||
}
|
||||
// Сохраняем очередь в Redis пока держим Lock — консистентный снапшот
|
||||
|
||||
+12
-11
@@ -42,19 +42,23 @@ func DeleteMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
key := tenantQueueKey(t.AccessKey, queueName)
|
||||
log.Info("Deleting Message, Queue:", queueName, ", ReceiptHandle:", receiptHandle)
|
||||
|
||||
models.SyncQueues.Lock()
|
||||
defer models.SyncQueues.Unlock()
|
||||
if _, ok := models.SyncQueues.Queues[key]; ok {
|
||||
for i, msg := range models.SyncQueues.Queues[key].Messages {
|
||||
queue := models.SyncQueues.GetLocked(key)
|
||||
if queue == nil {
|
||||
log.Warning("Queue not found")
|
||||
return utils.CreateErrorResponseV1("MessageDoesNotExist", true)
|
||||
}
|
||||
defer queue.UnlockMu()
|
||||
|
||||
for i, msg := range queue.Messages {
|
||||
if msg.ReceiptHandle == receiptHandle {
|
||||
msgUuid := msg.Uuid
|
||||
models.SyncQueues.Queues[key].UnlockGroup(msg.GroupID)
|
||||
models.SyncQueues.Queues[key].Messages = append(models.SyncQueues.Queues[key].Messages[:i], models.SyncQueues.Queues[key].Messages[i+1:]...)
|
||||
delete(models.SyncQueues.Queues[key].Duplicates, msg.DeduplicationID)
|
||||
queue.UnlockGroup(msg.GroupID)
|
||||
queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...)
|
||||
delete(queue.Duplicates, msg.DeduplicationID)
|
||||
// Удаляем одно сообщение из Redis — O(1)
|
||||
persistence.DeleteMessagePersist(key, msgUuid)
|
||||
// Метаданные очереди (duplicates, FIFO state)
|
||||
persistence.SaveQueue(key, models.SyncQueues.Queues[key])
|
||||
persistence.SaveQueue(key, queue)
|
||||
respStruct := models.DeleteMessageResponse{
|
||||
Xmlns: models.BaseXmlns,
|
||||
Metadata: models.BaseResponseMetadata,
|
||||
@@ -63,9 +67,6 @@ func DeleteMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
}
|
||||
}
|
||||
log.Warning("Receipt Handle not found")
|
||||
} else {
|
||||
log.Warning("Queue not found")
|
||||
}
|
||||
|
||||
return utils.CreateErrorResponseV1("MessageDoesNotExist", true)
|
||||
}
|
||||
|
||||
@@ -56,15 +56,11 @@ func DeleteMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBo
|
||||
ids[v.Id] = true
|
||||
}
|
||||
|
||||
models.SyncQueues.Lock()
|
||||
defer models.SyncQueues.Unlock()
|
||||
|
||||
// Проверка существования очереди ВНУТРИ Lock: раньше была БЕЗ блокировки
|
||||
// (чтение map) — concurrent map read/write при параллельном DeleteQueue.
|
||||
queue, exists := models.SyncQueues.Queues[key]
|
||||
if !exists {
|
||||
queue := models.SyncQueues.GetLocked(key)
|
||||
if queue == nil {
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
defer queue.UnlockMu()
|
||||
|
||||
deleteMessageMap := make(map[string]*deleteEntry)
|
||||
for _, entry := range requestBody.Entries {
|
||||
|
||||
@@ -64,13 +64,12 @@ func GetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo
|
||||
log.Infof("Get Queue Attributes: %s (tenant: %s)", queueName, t.ID)
|
||||
queueAttributes := make([]models.Attribute, 0)
|
||||
|
||||
models.SyncQueues.RLock()
|
||||
defer models.SyncQueues.RUnlock()
|
||||
queue, ok := models.SyncQueues.Queues[key]
|
||||
if !ok {
|
||||
queue := models.SyncQueues.GetRLocked(key)
|
||||
if queue == nil {
|
||||
log.Errorf("Get Queue Attributes: %s queue does not exist for tenant %s", queueName, t.ID)
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
defer queue.RUnlockMu()
|
||||
|
||||
if shouldInclude("DelaySeconds") {
|
||||
queueAttributes = append(queueAttributes, models.Attribute{Name: "DelaySeconds", Value: strconv.Itoa(queue.DelaySeconds)})
|
||||
|
||||
+5
-3
@@ -48,9 +48,9 @@ func PeriodicTasks(d time.Duration, quit chan bool) {
|
||||
// Каждую очередь обрабатываем под ОТДЕЛЬНЫМ Lock — блокировка одной
|
||||
// очереди не мешает обработке остальных и HTTP-запросам.
|
||||
for _, qi := range queues {
|
||||
models.SyncQueues.Lock()
|
||||
qi.queue.LockMu()
|
||||
processQueueTick(qi.key, qi.queue)
|
||||
models.SyncQueues.Unlock()
|
||||
qi.queue.UnlockMu()
|
||||
}
|
||||
case <-quit:
|
||||
ticker.Stop()
|
||||
@@ -61,7 +61,7 @@ func PeriodicTasks(d time.Duration, quit chan bool) {
|
||||
|
||||
// processQueueTick — обработка ОДНОЙ очереди за один такт.
|
||||
//
|
||||
// ВЫЗЫВАТЬ ТОЛЬКО под удержанием SyncQueues.Lock.
|
||||
// ВЫЗЫВАТЬ под per-queue блокировкой этой очереди (queue.LockMu).
|
||||
//
|
||||
// Логическая схема:
|
||||
// 0. Эвикция сообщений по MessageRetentionPeriod (старше retention удаляются).
|
||||
@@ -129,7 +129,9 @@ func processQueueTick(key string, queue *models.Queue) {
|
||||
if queue.MaxReceiveCount > 0 &&
|
||||
queue.DeadLetterQueue != nil &&
|
||||
msg.Retry >= queue.MaxReceiveCount {
|
||||
queue.DeadLetterQueue.LockMu()
|
||||
queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, *msg)
|
||||
queue.DeadLetterQueue.UnlockMu()
|
||||
queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...)
|
||||
i-- // элемент на позиции i удалён — повторно проверяем новый элемент на ней
|
||||
}
|
||||
|
||||
@@ -40,13 +40,11 @@ func ListQueueTagsV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
|
||||
key := tenantQueueKey(t.AccessKey, queueName)
|
||||
|
||||
models.SyncQueues.RLock()
|
||||
defer models.SyncQueues.RUnlock()
|
||||
|
||||
queue, exists := models.SyncQueues.Queues[key]
|
||||
if !exists {
|
||||
queue := models.SyncQueues.GetRLocked(key)
|
||||
if queue == nil {
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
defer queue.RUnlockMu()
|
||||
|
||||
tags := queue.Tags
|
||||
if tags == nil {
|
||||
|
||||
@@ -45,29 +45,29 @@ func PurgeQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
queueName := uriSegments[len(uriSegments)-1]
|
||||
key := tenantQueueKey(t.AccessKey, queueName)
|
||||
|
||||
models.SyncQueues.Lock()
|
||||
defer models.SyncQueues.Unlock()
|
||||
if _, ok := models.SyncQueues.Queues[key]; !ok {
|
||||
queue := models.SyncQueues.GetLocked(key)
|
||||
if queue == nil {
|
||||
log.Errorf("Purge Queue: %s, queue does not exist for tenant %s", queueName, t.ID)
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
defer queue.UnlockMu()
|
||||
|
||||
// AWS: PurgeQueue допускается не чаще одного раза в 60 секунд на очередь.
|
||||
// Нулевой LastPurgeTime (purge ещё не выполнялся) → запрет не срабатывает.
|
||||
if time.Since(models.SyncQueues.Queues[key].LastPurgeTime) < 60*time.Second {
|
||||
log.Warnf("Purge Queue: %s rejected — purge in progress (last: %s)", queueName, models.SyncQueues.Queues[key].LastPurgeTime.Format(time.RFC3339))
|
||||
if time.Since(queue.LastPurgeTime) < 60*time.Second {
|
||||
log.Warnf("Purge Queue: %s rejected — purge in progress (last: %s)", queueName, queue.LastPurgeTime.Format(time.RFC3339))
|
||||
return utils.CreateErrorResponseV1("PurgeQueueInProgress", true)
|
||||
}
|
||||
|
||||
log.Infof("Purging Queue: %s (tenant: %s)", queueName, t.ID)
|
||||
models.SyncQueues.Queues[key].Messages = nil
|
||||
models.SyncQueues.Queues[key].Duplicates = make(map[string]time.Time)
|
||||
queue.Messages = nil
|
||||
queue.Duplicates = make(map[string]time.Time)
|
||||
// Фиксируем время purge — защита от повторного вызова в течение 60 секунд.
|
||||
models.SyncQueues.Queues[key].LastPurgeTime = time.Now()
|
||||
queue.LastPurgeTime = time.Now()
|
||||
// Удаляем все сообщения из Redis одной командой DEL
|
||||
persistence.PurgeMessagesPersist(key)
|
||||
// Сохраняем пустые метаданные очереди
|
||||
persistence.SaveQueue(key, models.SyncQueues.Queues[key])
|
||||
persistence.SaveQueue(key, queue)
|
||||
|
||||
respStruct := models.PurgeQueueResponse{
|
||||
Xmlns: models.BaseXmlns,
|
||||
|
||||
@@ -75,7 +75,9 @@ func setQueueAttributesV1(q *models.Queue, attr models.QueueAttributes, provided
|
||||
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")
|
||||
|
||||
@@ -86,17 +86,15 @@ func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody)
|
||||
// ОДИН RLock на проверку существования И чтение атрибута очереди:
|
||||
// раньше было два отдельных RLock — между ними очередь могла быть удалена
|
||||
// (nil-deref при чтении ReceiveMessageWaitTimeSeconds).
|
||||
models.SyncQueues.RLock()
|
||||
queue, queueExists := models.SyncQueues.Queues[key]
|
||||
if !queueExists {
|
||||
models.SyncQueues.RUnlock()
|
||||
queue := models.SyncQueues.GetRLocked(key)
|
||||
if queue == nil {
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
waitTimeSeconds := requestBody.WaitTimeSeconds
|
||||
if waitTimeSeconds == 0 {
|
||||
waitTimeSeconds = queue.ReceiveMessageWaitTimeSeconds
|
||||
}
|
||||
models.SyncQueues.RUnlock()
|
||||
queue.RUnlockMu()
|
||||
if waitTimeSeconds < 0 || waitTimeSeconds > MaxReceiveMessageWaitTimeSeconds {
|
||||
return utils.CreateErrorResponseV1("InvalidParameterValue", true)
|
||||
}
|
||||
@@ -155,13 +153,11 @@ func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody)
|
||||
// (ApproximateReceiveCount в ответе);
|
||||
// - для FIFO-очереди блокируется группа (LockGroup) до DeleteMessage/таймаута.
|
||||
func receiveMessagesUnderLock(key string, visibilityTimeout, maxNumberOfMessages int) ([]*models.ResultMessage, bool) {
|
||||
models.SyncQueues.Lock()
|
||||
defer models.SyncQueues.Unlock()
|
||||
|
||||
queue, queueFound := models.SyncQueues.Queues[key]
|
||||
if !queueFound || len(queue.Messages) == 0 {
|
||||
queue := models.SyncQueues.GetLocked(key)
|
||||
if queue == nil || len(queue.Messages) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
defer queue.UnlockMu()
|
||||
// Быстрая предпроверка: есть ли вообще хотя бы одно готовое сообщение.
|
||||
if !queueHasReceivableMessages(queue) {
|
||||
return nil, false
|
||||
|
||||
@@ -83,17 +83,16 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
|
||||
key := tenantQueueKey(t.AccessKey, queueName)
|
||||
|
||||
models.SyncQueues.RLock()
|
||||
queue, exists := models.SyncQueues.Queues[key]
|
||||
if !exists {
|
||||
models.SyncQueues.RUnlock()
|
||||
queue := models.SyncQueues.GetLocked(key)
|
||||
if queue == nil {
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
defer queue.UnlockMu()
|
||||
|
||||
maxMessageSize := queue.MaximumMessageSize
|
||||
currentMsgCount := len(queue.Messages)
|
||||
queueIsFIFO := queue.IsFIFO
|
||||
delaySecs := queue.DelaySeconds
|
||||
models.SyncQueues.RUnlock()
|
||||
|
||||
// FIFO-очередь БЕЗ MessageGroupId — ошибка: AWS требует GroupId для каждой
|
||||
// отправки в FIFO (иначе невозможно гарантировать порядок внутри группы).
|
||||
@@ -128,18 +127,9 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
msg.SentTime = time.Now()
|
||||
msg.DelaySecs = delaySecs
|
||||
|
||||
models.SyncQueues.Lock()
|
||||
// Перепроверка под Lock: очередь могла быть удалена между RLock-проверкой
|
||||
// выше и захватом Lock (иначе nil-pointer deref).
|
||||
queue = models.SyncQueues.Queues[key]
|
||||
if queue == nil {
|
||||
models.SyncQueues.Unlock()
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
// Повторная проверка лимита под Lock: между RLock-снимком и захватом Lock
|
||||
// другие горутины могли добавить сообщения (мягкий OOM-лимит).
|
||||
// Повторная проверка лимита под per-queue Lock (между снимком выше и текущим
|
||||
// моментом другие горутины могли добавить сообщения — мягкий OOM-лимит).
|
||||
if len(queue.Messages) >= MaxMessagesForQueue(queue.IsFIFO) {
|
||||
models.SyncQueues.Unlock()
|
||||
return utils.CreateErrorResponseV1("OverLimit", true)
|
||||
}
|
||||
|
||||
@@ -167,7 +157,6 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
queue.InitDuplicatation(messageDeduplicationID)
|
||||
// Сохраняем метаданные очереди (FIFO state, duplicates) — без Messages
|
||||
persistence.SaveQueue(key, queue)
|
||||
models.SyncQueues.Unlock()
|
||||
|
||||
if persistErr {
|
||||
return utils.CreateErrorResponseV1("InternalError", true)
|
||||
|
||||
@@ -43,16 +43,14 @@ func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody
|
||||
|
||||
key := tenantQueueKey(t.AccessKey, queueName)
|
||||
|
||||
models.SyncQueues.RLock()
|
||||
queue, exists := models.SyncQueues.Queues[key]
|
||||
if !exists {
|
||||
models.SyncQueues.RUnlock()
|
||||
queue := models.SyncQueues.GetRLocked(key)
|
||||
if queue == nil {
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
maxMsgSize := queue.MaximumMessageSize
|
||||
currentMsgCount := len(queue.Messages)
|
||||
queueIsFIFO := queue.IsFIFO
|
||||
models.SyncQueues.RUnlock()
|
||||
queue.RUnlockMu()
|
||||
|
||||
sendEntries := requestBody.Entries
|
||||
|
||||
@@ -104,14 +102,8 @@ func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody
|
||||
return utils.CreateErrorResponseV1("OverLimit", true)
|
||||
}
|
||||
|
||||
models.SyncQueues.Lock()
|
||||
// Перепроверка под Lock: очередь могла быть удалена между RLock-проверкой
|
||||
// выше и захватом Lock (иначе nil-pointer deref).
|
||||
queue = models.SyncQueues.Queues[key]
|
||||
if queue == nil {
|
||||
models.SyncQueues.Unlock()
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
queue.LockMu()
|
||||
defer queue.UnlockMu()
|
||||
newMsgs := make([]models.SqsMessage, 0, len(sendEntries))
|
||||
for _, sendEntry := range sendEntries {
|
||||
msg := models.SqsMessage{MessageBody: sendEntry.MessageBody}
|
||||
@@ -151,7 +143,6 @@ func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody
|
||||
persistence.SaveMessages(key, newMsgs)
|
||||
// Метаданные очереди (FIFO state, duplicates) — без Messages
|
||||
persistence.SaveQueue(key, queue)
|
||||
models.SyncQueues.Unlock()
|
||||
|
||||
respStruct := models.SendMessageBatchResponse{
|
||||
Xmlns: models.BaseXmlns,
|
||||
|
||||
@@ -45,13 +45,12 @@ func SetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo
|
||||
key := tenantQueueKey(t.AccessKey, queueName)
|
||||
|
||||
log.Infof("Set Queue Attributes: %s (tenant: %s)", queueName, t.ID)
|
||||
models.SyncQueues.Lock()
|
||||
defer models.SyncQueues.Unlock()
|
||||
queue, ok := models.SyncQueues.Queues[key]
|
||||
if !ok {
|
||||
queue := models.SyncQueues.GetLocked(key)
|
||||
if queue == nil {
|
||||
log.Warningf("Set Queue Attributes: %s, queue does not exist for tenant %s", queueName, t.ID)
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
defer queue.UnlockMu()
|
||||
// Список имён переданных атрибутов — для whitelist-валидации.
|
||||
// Query-протокол: имена видны в form (Attribute.N.Name).
|
||||
// JSON-протокол: структура уже разобрана из тела, список имён недоступен → nil
|
||||
|
||||
@@ -41,13 +41,11 @@ func TagQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
|
||||
key := tenantQueueKey(t.AccessKey, queueName)
|
||||
|
||||
models.SyncQueues.Lock()
|
||||
defer models.SyncQueues.Unlock()
|
||||
|
||||
queue, exists := models.SyncQueues.Queues[key]
|
||||
if !exists {
|
||||
queue := models.SyncQueues.GetLocked(key)
|
||||
if queue == nil {
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
defer queue.UnlockMu()
|
||||
|
||||
// Инициализируем Tags если nil (для старых очередей, созданных без Tags)
|
||||
if queue.Tags == nil {
|
||||
|
||||
@@ -41,13 +41,11 @@ func UntagQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
|
||||
key := tenantQueueKey(t.AccessKey, queueName)
|
||||
|
||||
models.SyncQueues.Lock()
|
||||
defer models.SyncQueues.Unlock()
|
||||
|
||||
queue, exists := models.SyncQueues.Queues[key]
|
||||
if !exists {
|
||||
queue := models.SyncQueues.GetLocked(key)
|
||||
if queue == nil {
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
defer queue.UnlockMu()
|
||||
|
||||
if queue.Tags != nil {
|
||||
for _, tagKey := range requestBody.TagKeys {
|
||||
|
||||
@@ -37,18 +37,25 @@ func StartGaugeUpdater(interval time.Duration, quit <-chan bool) {
|
||||
// updateGauges — пересчитывает sqs_queues_count и sqs_messages_count per tenant.
|
||||
// Формат ключа очереди: "{accessKey}:{queueName}" — берём часть до первого ":".
|
||||
func updateGauges() {
|
||||
// Собираем данные под RLock — не блокируем запись
|
||||
// Снимок указателей под коротким RLock, затем содержимое — под per-queue RLock
|
||||
models.SyncQueues.RLock()
|
||||
snapshot := make(map[string]*models.Queue, len(models.SyncQueues.Queues))
|
||||
for key, queue := range models.SyncQueues.Queues {
|
||||
snapshot[key] = queue
|
||||
}
|
||||
models.SyncQueues.RUnlock()
|
||||
|
||||
tenantQueues := make(map[string]int)
|
||||
tenantMessages := make(map[string]int)
|
||||
for key, queue := range models.SyncQueues.Queues {
|
||||
for key, queue := range snapshot {
|
||||
// Ключ формата "accessKey:queueName" — tenant определяется по accessKey
|
||||
parts := strings.SplitN(key, ":", 2)
|
||||
tenantKey := parts[0]
|
||||
tenantQueues[tenantKey]++
|
||||
queue.RLockMu()
|
||||
tenantMessages[tenantKey] += len(queue.Messages)
|
||||
queue.RUnlockMu()
|
||||
}
|
||||
models.SyncQueues.RUnlock()
|
||||
|
||||
// Сбрасываем старые значения и записываем новые
|
||||
QueuesGauge.Reset()
|
||||
|
||||
@@ -74,6 +74,31 @@ func (s *QueueStore) RemoveQueue(key string) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetLocked — возвращает очередь с захваченной per-queue write-блокировкой.
|
||||
// nil если очереди нет. Вызывающий ОБЯЗАН вызвать q.UnlockMu().
|
||||
func (s *QueueStore) GetLocked(key string) *Queue {
|
||||
s.RLock()
|
||||
q := s.Queues[key]
|
||||
s.RUnlock()
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
q.LockMu()
|
||||
return q
|
||||
}
|
||||
|
||||
// GetRLocked — то же, но read-блокировка. Вызывающий ОБЯЗАН вызвать q.RUnlockMu().
|
||||
func (s *QueueStore) GetRLocked(key string) *Queue {
|
||||
s.RLock()
|
||||
q := s.Queues[key]
|
||||
s.RUnlock()
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
q.RLockMu()
|
||||
return q
|
||||
}
|
||||
|
||||
// CountTenantQueues — число очередей тенанта (по accessKey) за O(1).
|
||||
// Вызывать под SyncQueues.RLock() или Lock().
|
||||
func (s *QueueStore) CountTenantQueues(accessKey string) int {
|
||||
|
||||
+58
-16
@@ -142,23 +142,65 @@ func asyncWrite(fn func()) {
|
||||
}()
|
||||
}
|
||||
|
||||
// marshalQueueMeta — сериализует очередь БЕЗ Messages (и без Messages в DLQ).
|
||||
// DeadLetterQueue сохраняется как ссылка (Name/URL/Arn), но без сообщений.
|
||||
// ВАЖНО: вызывать под SyncQueues.Lock() — временно nil'ит Messages.
|
||||
// queueMetaDTO — сериализуемая копия очереди БЕЗ Messages (и без сообщений DLQ).
|
||||
// Отдельный тип, т.к. models.Queue содержит sync.RWMutex и его нельзя копировать/
|
||||
// сериализовать целиком, а временное зануление Messages мутировало бы живую очередь.
|
||||
// Имена полей совпадают с models.Queue → json совместим при загрузке.
|
||||
type queueMetaDTO struct {
|
||||
Name string
|
||||
URL string
|
||||
Arn string
|
||||
VisibilityTimeout int
|
||||
ReceiveMessageWaitTimeSeconds int
|
||||
DelaySeconds int
|
||||
MaximumMessageSize int
|
||||
MessageRetentionPeriod int
|
||||
MaxReceiveCount int
|
||||
IsFIFO bool
|
||||
FIFOMessages map[string]int
|
||||
FIFOSequenceNumbers map[string]int
|
||||
EnableDuplicates bool
|
||||
Duplicates map[string]time.Time
|
||||
Tags map[string]string
|
||||
LastPurgeTime time.Time
|
||||
DeadLetterQueue *queueMetaDTO
|
||||
}
|
||||
|
||||
// queueToMetaDTO — строит снимок очереди без сообщений (и без сообщений DLQ),
|
||||
// НЕ мутируя исходную очередь. Рекурсия ограничена глубиной 1.
|
||||
func queueToMetaDTO(q *models.Queue) *queueMetaDTO {
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
dto := &queueMetaDTO{
|
||||
Name: q.Name,
|
||||
URL: q.URL,
|
||||
Arn: q.Arn,
|
||||
VisibilityTimeout: q.VisibilityTimeout,
|
||||
ReceiveMessageWaitTimeSeconds: q.ReceiveMessageWaitTimeSeconds,
|
||||
DelaySeconds: q.DelaySeconds,
|
||||
MaximumMessageSize: q.MaximumMessageSize,
|
||||
MessageRetentionPeriod: q.MessageRetentionPeriod,
|
||||
MaxReceiveCount: q.MaxReceiveCount,
|
||||
IsFIFO: q.IsFIFO,
|
||||
FIFOMessages: q.FIFOMessages,
|
||||
FIFOSequenceNumbers: q.FIFOSequenceNumbers,
|
||||
EnableDuplicates: q.EnableDuplicates,
|
||||
Duplicates: q.Duplicates,
|
||||
Tags: q.Tags,
|
||||
LastPurgeTime: q.LastPurgeTime,
|
||||
}
|
||||
if q.DeadLetterQueue != nil {
|
||||
dto.DeadLetterQueue = queueToMetaDTO(q.DeadLetterQueue)
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
// marshalQueueMeta — сериализует метаданные очереди БЕЗ сообщений,
|
||||
// НЕ мутируя исходную очередь (см. queueMetaDTO). Безопасно вызывать
|
||||
// под per-queue блокировкой только этой очереди.
|
||||
func marshalQueueMeta(queue *models.Queue) ([]byte, error) {
|
||||
savedMsgs := queue.Messages
|
||||
queue.Messages = nil
|
||||
var savedDLQMsgs []models.SqsMessage
|
||||
if queue.DeadLetterQueue != nil {
|
||||
savedDLQMsgs = queue.DeadLetterQueue.Messages
|
||||
queue.DeadLetterQueue.Messages = nil
|
||||
}
|
||||
data, err := json.Marshal(queue)
|
||||
queue.Messages = savedMsgs
|
||||
if queue.DeadLetterQueue != nil {
|
||||
queue.DeadLetterQueue.Messages = savedDLQMsgs
|
||||
}
|
||||
return data, err
|
||||
return json.Marshal(queueToMetaDTO(queue))
|
||||
}
|
||||
|
||||
// SaveQueue — сохраняет МЕТАДАННЫЕ очереди (без сообщений) в Redis асинхронно.
|
||||
|
||||
@@ -47,6 +47,20 @@ DLQ), а не построчная замена. Делать её «вслеп
|
||||
рискованно: ошибка = дедлоки/потеря данных на проде. Рекомендуется отдельный
|
||||
проход: сначала тесты на FIFO/DLQ/batch, затем атомарный перевод.
|
||||
|
||||
## СТАТУС: РЕАЛИЗОВАНО (2026-10-06)
|
||||
|
||||
Перевод выполнен атомарно. Что сделано:
|
||||
- `marshalQueueMeta` больше НЕ мутирует очередь (DTO-снапшот queueMetaDTO).
|
||||
- Все операции с содержимым очереди используют per-queue лок
|
||||
(`QueueStore.GetLocked/GetRLocked` + `queue.LockMu/RUnlockMu`) в gosqs, admin,
|
||||
metrics/gauge_updater, PeriodicTasks.
|
||||
- Глобальный `SyncQueues`-лок остался ТОЛЬКО для доступа к map и неизменяемых
|
||||
полей (create/delete/list/find/snapshot).
|
||||
- DLQ-переносы берут лок DLQ отдельно (порядок main→DLQ).
|
||||
- Устранён дедлок в create_queue (setQueueAttributesV1 вызывается ВНЕ глобального лока).
|
||||
- Добавлены конкурентные тесты (два тенанта / одна очередь), прогон `go test -race` — зелёный.
|
||||
|
||||
|
||||
## P1.2 (индексация сообщений) — та же природа
|
||||
|
||||
Индексация `Messages` (receipt→index) меняет структуру хранения, затронутую теми
|
||||
|
||||
Reference in New Issue
Block a user