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:
Repinoid
2026-10-06 18:58:16 +03:00
parent ed17c6cbd0
commit fd8ffc329d
21 changed files with 252 additions and 181 deletions
+58 -16
View File
@@ -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 асинхронно.