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
+42 -22
View File
@@ -445,15 +445,23 @@ func (h *Handler) listTenantQueues(w http.ResponseWriter, r *http.Request) {
RetentionPeriod int `json:"retention_period"`
}
queues := make([]queueInfo, 0)
type nameQueue struct {
name string
q *models.Queue
}
models.SyncQueues.RLock()
pairs := make([]nameQueue, 0)
for _, name := range models.SyncQueues.TenantQueueNames(t.AccessKey) {
q := models.SyncQueues.Queues[prefix+name]
if q == nil {
continue
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{