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:
+42
-22
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user