From c4da02c8147e131b932bfb8782554ba854bdd3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Fri, 14 Aug 2026 18:48:35 +0400 Subject: [PATCH] =?UTF-8?q?v0.1.35:=20=D1=83=D1=81=D1=82=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B3=D0=BE=D0=BD=D0=BA=D0=B8=20map=20(Ch?= =?UTF-8?q?angeVisibility/DeleteBatch)=20=D0=B8=20nil-deref=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=20Lock=20+=20OverLimit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/gosqs/change_message_visibility.go | 28 +++++++++++-------- app/gosqs/change_message_visibility_batch.go | 14 ++++++---- app/gosqs/delete_message_batch.go | 23 ++++++++------- app/gosqs/receive_message.go | 17 +++++------ app/gosqs/send_message.go | 28 ++++++++++++++----- app/gosqs/send_message_batch.go | 6 ++++ app/models/errors.go | 2 ++ tests/__pycache__/load_test.cpython-312.pyc | Bin 0 -> 23926 bytes 8 files changed, 74 insertions(+), 44 deletions(-) create mode 100644 tests/__pycache__/load_test.cpython-312.pyc diff --git a/app/gosqs/change_message_visibility.go b/app/gosqs/change_message_visibility.go index c2dbb3c..9836875 100644 --- a/app/gosqs/change_message_visibility.go +++ b/app/gosqs/change_message_visibility.go @@ -47,19 +47,26 @@ func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractRespo return utils.CreateErrorResponseV1("InvalidParameterValue", true) } - if _, ok := models.SyncQueues.Queues[key]; !ok { + // ВСЯ работа с очередью — под одним 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 { return utils.CreateErrorResponseV1("QueueNotFound", true) } - models.SyncQueues.Lock() messageFound := false var changedMsgUuid string var msgRemoved bool - for i := 0; i < len(models.SyncQueues.Queues[key].Messages); i++ { - queue := models.SyncQueues.Queues[key] + for i := 0; i < len(queue.Messages); i++ { msgs := queue.Messages if msgs[i].ReceiptHandle == receiptHandle { - timeout := models.SyncQueues.Queues[key].VisibilityTimeout + timeout := queue.VisibilityTimeout if visibilityTimeout == 0 { msgs[i].ReceiptTime = time.Now().UTC() msgs[i].ReceiptHandle = "" @@ -83,23 +90,22 @@ func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractRespo break } } - // Персистим изменения в Redis под Lock + // Персистим изменения в Redis под Lock (сетевые записи асинхронны). if messageFound { if msgRemoved { // Сообщение удалено (перемещено в DLQ) — удаляем из Redis persistence.DeleteMessagePersist(key, changedMsgUuid) } else { // Сообщение обновлено — перезаписываем в Redis - for i := range models.SyncQueues.Queues[key].Messages { - if models.SyncQueues.Queues[key].Messages[i].Uuid == changedMsgUuid { - persistence.SaveMessage(key, &models.SyncQueues.Queues[key].Messages[i]) + for i := range queue.Messages { + if queue.Messages[i].Uuid == changedMsgUuid { + persistence.SaveMessage(key, &queue.Messages[i]) break } } } - persistence.SaveQueue(key, models.SyncQueues.Queues[key]) + persistence.SaveQueue(key, queue) } - models.SyncQueues.Unlock() if !messageFound { return utils.CreateErrorResponseV1("MessageNotInFlight", true) } diff --git a/app/gosqs/change_message_visibility_batch.go b/app/gosqs/change_message_visibility_batch.go index 182963c..ad86ad1 100644 --- a/app/gosqs/change_message_visibility_batch.go +++ b/app/gosqs/change_message_visibility_batch.go @@ -42,10 +42,6 @@ func ChangeMessageVisibilityBatchV1(req *http.Request) (int, interfaces.Abstract key := tenantQueueKey(t.AccessKey, queueName) - if _, ok := models.SyncQueues.Queues[key]; !ok { - return utils.CreateErrorResponseV1("QueueNotFound", true) - } - if len(requestBody.Entries) == 0 { return utils.CreateErrorResponseV1("EmptyBatchRequest", true) } @@ -66,6 +62,13 @@ func ChangeMessageVisibilityBatchV1(req *http.Request) (int, interfaces.Abstract models.SyncQueues.Lock() defer models.SyncQueues.Unlock() + // Проверка существования очереди ВНУТРИ Lock: раньше была БЕЗ блокировки + // (чтение map) — concurrent map read/write при параллельном DeleteQueue. + queue, exists := models.SyncQueues.Queues[key] + if !exists { + return utils.CreateErrorResponseV1("QueueNotFound", true) + } + successEntries := make([]models.ChangeMessageVisibilityBatchResultEntry, 0) failedEntries := make([]models.BatchResultErrorEntry, 0) @@ -81,7 +84,6 @@ func ChangeMessageVisibilityBatchV1(req *http.Request) (int, interfaces.Abstract } messageFound := false - queue := models.SyncQueues.Queues[key] for i := 0; i < len(queue.Messages); i++ { if queue.Messages[i].ReceiptHandle == entry.ReceiptHandle { if entry.VisibilityTimeout == 0 { @@ -114,7 +116,7 @@ func ChangeMessageVisibilityBatchV1(req *http.Request) (int, interfaces.Abstract // Персистим изменения в Redis под Lock if len(successEntries) > 0 { - persistence.SaveQueue(key, models.SyncQueues.Queues[key]) + persistence.SaveQueue(key, queue) } respStruct := models.ChangeMessageVisibilityBatchResponse{ diff --git a/app/gosqs/delete_message_batch.go b/app/gosqs/delete_message_batch.go index dcaa50e..40d36b8 100644 --- a/app/gosqs/delete_message_batch.go +++ b/app/gosqs/delete_message_batch.go @@ -40,10 +40,6 @@ func DeleteMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBo key := tenantQueueKey(t.AccessKey, queueName) - if _, ok := models.SyncQueues.Queues[key]; !ok { - return utils.CreateErrorResponseV1("QueueNotFound", true) - } - if len(requestBody.Entries) == 0 { return utils.CreateErrorResponseV1("EmptyBatchRequest", true) } @@ -63,6 +59,13 @@ func DeleteMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBo models.SyncQueues.Lock() defer models.SyncQueues.Unlock() + // Проверка существования очереди ВНУТРИ Lock: раньше была БЕЗ блокировки + // (чтение map) — concurrent map read/write при параллельном DeleteQueue. + queue, exists := models.SyncQueues.Queues[key] + if !exists { + return utils.CreateErrorResponseV1("QueueNotFound", true) + } + deleteMessageMap := make(map[string]*deleteEntry) for _, entry := range requestBody.Entries { deleteMessageMap[entry.ReceiptHandle] = &deleteEntry{ @@ -74,13 +77,13 @@ func DeleteMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBo deletedEntries := make([]models.DeleteMessageBatchResultEntry, 0) deletedUuids := make([]string, 0) - remainingMessages := make([]models.SqsMessage, 0, len(models.SyncQueues.Queues[key].Messages)) + remainingMessages := make([]models.SqsMessage, 0, len(queue.Messages)) - for _, message := range models.SyncQueues.Queues[key].Messages { + for _, message := range queue.Messages { if de, found := deleteMessageMap[message.ReceiptHandle]; found { log.Debugf("FIFO Queue %s unlocking group %s:", queueName, message.GroupID) - models.SyncQueues.Queues[key].UnlockGroup(message.GroupID) - delete(models.SyncQueues.Queues[key].Duplicates, message.DeduplicationID) + queue.UnlockGroup(message.GroupID) + delete(queue.Duplicates, message.DeduplicationID) de.Deleted = true deletedEntries = append(deletedEntries, models.DeleteMessageBatchResultEntry{Id: de.Id}) deletedUuids = append(deletedUuids, message.Uuid) @@ -89,11 +92,11 @@ func DeleteMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBo } } - models.SyncQueues.Queues[key].Messages = remainingMessages + queue.Messages = remainingMessages // Удаляем сообщения из Redis отдельно — O(batch_size) persistence.DeleteMessagesPersist(key, deletedUuids) // Метаданные очереди (duplicates, FIFO state) - persistence.SaveQueue(key, models.SyncQueues.Queues[key]) + persistence.SaveQueue(key, queue) notFoundEntries := make([]models.BatchResultErrorEntry, 0) for _, de := range deleteMessageMap { diff --git a/app/gosqs/receive_message.go b/app/gosqs/receive_message.go index 799c1a8..ff071a0 100644 --- a/app/gosqs/receive_message.go +++ b/app/gosqs/receive_message.go @@ -83,23 +83,20 @@ func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) key := tenantQueueKey(t.AccessKey, queueName) - // Проверка существования очереди — под RLock (защита от параллельной записи - // в map; раньше map читалась без блокировки — гонка данных). + // ОДИН RLock на проверку существования И чтение атрибута очереди: + // раньше было два отдельных RLock — между ними очередь могла быть удалена + // (nil-deref при чтении ReceiveMessageWaitTimeSeconds). models.SyncQueues.RLock() - _, queueExists := models.SyncQueues.Queues[key] - models.SyncQueues.RUnlock() + queue, queueExists := models.SyncQueues.Queues[key] if !queueExists { + models.SyncQueues.RUnlock() return utils.CreateErrorResponseV1("QueueNotFound", true) } - - // --- Шаг 5: WaitTimeSeconds (long polling) --- - // 0 (не задан) → атрибут очереди ReceiveMessageWaitTimeSeconds (default 0). waitTimeSeconds := requestBody.WaitTimeSeconds if waitTimeSeconds == 0 { - models.SyncQueues.RLock() - waitTimeSeconds = models.SyncQueues.Queues[key].ReceiveMessageWaitTimeSeconds - models.SyncQueues.RUnlock() + waitTimeSeconds = queue.ReceiveMessageWaitTimeSeconds } + models.SyncQueues.RUnlock() if waitTimeSeconds < 0 || waitTimeSeconds > MaxReceiveMessageWaitTimeSeconds { return utils.CreateErrorResponseV1("InvalidParameterValue", true) } diff --git a/app/gosqs/send_message.go b/app/gosqs/send_message.go index a21b4af..f764845 100644 --- a/app/gosqs/send_message.go +++ b/app/gosqs/send_message.go @@ -129,22 +129,36 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { msg.DelaySecs = delaySecs models.SyncQueues.Lock() - fifoSeqNumber := "" - if models.SyncQueues.Queues[key].IsFIFO { - fifoSeqNumber = models.SyncQueues.Queues[key].NextSequenceNumber(messageGroupID) + // Перепроверка под 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-лимит). + if len(queue.Messages) >= MaxMessagesForQueue(queue.IsFIFO) { + models.SyncQueues.Unlock() + return utils.CreateErrorResponseV1("OverLimit", true) } - if !models.SyncQueues.Queues[key].IsDuplicate(messageDeduplicationID) { - models.SyncQueues.Queues[key].Messages = append(models.SyncQueues.Queues[key].Messages, msg) + fifoSeqNumber := "" + if queue.IsFIFO { + fifoSeqNumber = queue.NextSequenceNumber(messageGroupID) + } + + if !queue.IsDuplicate(messageDeduplicationID) { + queue.Messages = append(queue.Messages, msg) // Персистим одно сообщение отдельно — O(msg_size) вместо O(N*msg_size) persistence.SaveMessage(key, &msg) } else { log.Debugf("Duplicate message deduplicationId [%s] in queue [%s]", messageDeduplicationID, queueName) } - models.SyncQueues.Queues[key].InitDuplicatation(messageDeduplicationID) + queue.InitDuplicatation(messageDeduplicationID) // Сохраняем метаданные очереди (FIFO state, duplicates) — без Messages - persistence.SaveQueue(key, models.SyncQueues.Queues[key]) + persistence.SaveQueue(key, queue) models.SyncQueues.Unlock() // Логируем только метаданные — тело сообщения не логируется (perf + security) log.Infof("Queue: %s, MessageId: %s, Size: %d bytes", queueName, msg.Uuid, len(messageBody)) diff --git a/app/gosqs/send_message_batch.go b/app/gosqs/send_message_batch.go index 5cf24b4..9a46896 100644 --- a/app/gosqs/send_message_batch.go +++ b/app/gosqs/send_message_batch.go @@ -105,7 +105,13 @@ func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody } 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) + } newMsgs := make([]models.SqsMessage, 0, len(sendEntries)) for _, sendEntry := range sendEntries { msg := models.SqsMessage{MessageBody: sendEntry.MessageBody} diff --git a/app/models/errors.go b/app/models/errors.go index c36eebd..d52b344 100644 --- a/app/models/errors.go +++ b/app/models/errors.go @@ -28,6 +28,8 @@ func init() { "InvalidAttributeName": {HttpError: http.StatusBadRequest, Type: "InvalidAttributeName", Code: "AWS.SimpleQueueService.InvalidAttributeName", Message: "Unknown Attribute."}, // PurgeQueueInProgress — повторный PurgeQueue на очереди ранее чем через 60 секунд "PurgeQueueInProgress": {HttpError: http.StatusForbidden, Type: "Sender", Code: "AWS.SimpleQueueService.PurgeQueueInProgress", Message: "Only one PurgeQueue operation on a queue is allowed every 60 seconds."}, + // OverLimit — превышен лимит сообщений в очереди (OOM-защита) + "OverLimit": {HttpError: http.StatusBadRequest, Type: "OverLimit", Code: "OverLimit", Message: "The queue contains the maximum number of messages."}, } } diff --git a/tests/__pycache__/load_test.cpython-312.pyc b/tests/__pycache__/load_test.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd9c3eaaf197a8ec4c2a7af53a4dc29d1b360e11 GIT binary patch literal 23926 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 aJQK@RwLJUaM=Y!*Qfv<4!v6y)wh?Ur literal 0 HcmV?d00001