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