P1.2: индексация сообщений — tombstone-удаление O(1), receipt/uuid-индексы, tick по in-flight
- models.Queue: deadIdx/receiptIdx/uuidIdx/liveCount/deadCount + примитивы (AppendMessage/RemoveIndex/Compact/CompactIfNeeded/Reset/EnsureIndexes/ IndexOfReceipt/IndexOfUUID/MarkInFlight/ClearInFlight/InFlightIndexes/LiveCount). Удаление — tombstone за O(1), физическая чистка амортизированная. - DeleteMessage/ChangeMessageVisibility(±Batch): поиск по индексу вместо скана среза; удаление без сдвига остатка. - DeleteMessageBatch: tombstone вместо полной пересборки среза. - processQueueTick: visibility/DLQ по индексу in-flight (O(in-flight)), retention — tombstone, в конце — CompactIfNeeded. - send-пути: AppendMessage + LiveCount (OOM-лимит по живым). - admin/metrics/persistence: учёт живых сообщений, индексы после загрузки из Redis. - Redis-схема не менялась (сообщения и так в per-message HASH).
This commit is contained in:
@@ -56,48 +56,43 @@ func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractRespo
|
||||
messageFound := false
|
||||
var changedMsgUuid string
|
||||
var msgRemoved bool
|
||||
for i := 0; i < len(queue.Messages); i++ {
|
||||
msgs := queue.Messages
|
||||
if msgs[i].ReceiptHandle == receiptHandle {
|
||||
timeout := queue.VisibilityTimeout
|
||||
if visibilityTimeout == 0 {
|
||||
msgs[i].ReceiptTime = time.Now().UTC()
|
||||
msgs[i].ReceiptHandle = ""
|
||||
msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(timeout) * time.Second)
|
||||
msgs[i].Retry++
|
||||
if queue.MaxReceiveCount > 0 &&
|
||||
queue.DeadLetterQueue != nil &&
|
||||
msgs[i].Retry >= queue.MaxReceiveCount {
|
||||
changedMsgUuid = msgs[i].Uuid
|
||||
queue.DeadLetterQueue.LockMu()
|
||||
queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, msgs[i])
|
||||
queue.DeadLetterQueue.UnlockMu()
|
||||
queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...)
|
||||
msgRemoved = true
|
||||
} else {
|
||||
changedMsgUuid = msgs[i].Uuid
|
||||
}
|
||||
} else {
|
||||
msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(visibilityTimeout) * time.Second)
|
||||
changedMsgUuid = msgs[i].Uuid
|
||||
// O(1)-поиск по индексу ReceiptHandle (P1.2) вместо скана всего среза.
|
||||
if i, ok := queue.IndexOfReceipt(receiptHandle); ok {
|
||||
msg := &queue.Messages[i]
|
||||
timeout := queue.VisibilityTimeout
|
||||
if visibilityTimeout == 0 {
|
||||
// Возврат видимости: сброс handle + снятие с индекса in-flight.
|
||||
queue.ClearInFlight(i, receiptHandle)
|
||||
msg.ReceiptTime = time.Now().UTC()
|
||||
msg.ReceiptHandle = ""
|
||||
msg.VisibilityTimeout = time.Now().Add(time.Duration(timeout) * time.Second)
|
||||
msg.Retry++
|
||||
changedMsgUuid = msg.Uuid
|
||||
if queue.MaxReceiveCount > 0 &&
|
||||
queue.DeadLetterQueue != nil &&
|
||||
msg.Retry >= queue.MaxReceiveCount {
|
||||
// Перенос в DLQ + удаление из исходной очереди (tombstone, O(1)).
|
||||
dlqMsg := *msg
|
||||
queue.DeadLetterQueue.LockMu()
|
||||
queue.DeadLetterQueue.AppendMessage(dlqMsg)
|
||||
queue.DeadLetterQueue.UnlockMu()
|
||||
queue.RemoveIndex(i)
|
||||
msgRemoved = true
|
||||
}
|
||||
messageFound = true
|
||||
break
|
||||
} else {
|
||||
msg.VisibilityTimeout = time.Now().Add(time.Duration(visibilityTimeout) * time.Second)
|
||||
changedMsgUuid = msg.Uuid
|
||||
}
|
||||
messageFound = true
|
||||
}
|
||||
// Персистим изменения в Redis под Lock (сетевые записи асинхронны).
|
||||
if messageFound {
|
||||
if msgRemoved {
|
||||
// Сообщение удалено (перемещено в DLQ) — удаляем из Redis
|
||||
persistence.DeleteMessagePersist(key, changedMsgUuid)
|
||||
} else {
|
||||
} else if i, ok := queue.IndexOfUUID(changedMsgUuid); ok {
|
||||
// Сообщение обновлено — перезаписываем в Redis
|
||||
for i := range queue.Messages {
|
||||
if queue.Messages[i].Uuid == changedMsgUuid {
|
||||
persistence.SaveMessage(key, &queue.Messages[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
persistence.SaveMessage(key, &queue.Messages[i])
|
||||
}
|
||||
persistence.SaveQueue(key, queue)
|
||||
}
|
||||
|
||||
@@ -80,22 +80,22 @@ func ChangeMessageVisibilityBatchV1(req *http.Request) (int, interfaces.Abstract
|
||||
}
|
||||
|
||||
messageFound := false
|
||||
for i := 0; i < len(queue.Messages); i++ {
|
||||
if queue.Messages[i].ReceiptHandle == entry.ReceiptHandle {
|
||||
if entry.VisibilityTimeout == 0 {
|
||||
// Сброс: сообщение снова видимо, счётчик retry++
|
||||
queue.Messages[i].ReceiptTime = time.Now().UTC()
|
||||
queue.Messages[i].ReceiptHandle = ""
|
||||
queue.Messages[i].VisibilityTimeout = time.Now().Add(time.Duration(queue.VisibilityTimeout) * time.Second)
|
||||
queue.Messages[i].Retry++
|
||||
} else {
|
||||
queue.Messages[i].VisibilityTimeout = time.Now().Add(time.Duration(entry.VisibilityTimeout) * time.Second)
|
||||
}
|
||||
// Персистим изменённое сообщение отдельно
|
||||
persistence.SaveMessage(key, &queue.Messages[i])
|
||||
messageFound = true
|
||||
break
|
||||
// O(1)-поиск по индексу ReceiptHandle (P1.2) вместо скана всего среза.
|
||||
if i, ok := queue.IndexOfReceipt(entry.ReceiptHandle); ok {
|
||||
msg := &queue.Messages[i]
|
||||
if entry.VisibilityTimeout == 0 {
|
||||
// Сброс: сообщение снова видимо, счётчик retry++
|
||||
queue.ClearInFlight(i, entry.ReceiptHandle)
|
||||
msg.ReceiptTime = time.Now().UTC()
|
||||
msg.ReceiptHandle = ""
|
||||
msg.VisibilityTimeout = time.Now().Add(time.Duration(queue.VisibilityTimeout) * time.Second)
|
||||
msg.Retry++
|
||||
} else {
|
||||
msg.VisibilityTimeout = time.Now().Add(time.Duration(entry.VisibilityTimeout) * time.Second)
|
||||
}
|
||||
// Персистим изменённое сообщение отдельно
|
||||
persistence.SaveMessage(key, msg)
|
||||
messageFound = true
|
||||
}
|
||||
|
||||
if messageFound {
|
||||
|
||||
+16
-15
@@ -49,22 +49,23 @@ func DeleteMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
}
|
||||
defer queue.UnlockMu()
|
||||
|
||||
for i, msg := range queue.Messages {
|
||||
if msg.ReceiptHandle == receiptHandle {
|
||||
msgUuid := msg.Uuid
|
||||
queue.UnlockGroup(msg.GroupID)
|
||||
queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...)
|
||||
delete(queue.Duplicates, msg.DeduplicationID)
|
||||
// Удаляем одно сообщение из Redis — O(1)
|
||||
persistence.DeleteMessagePersist(key, msgUuid)
|
||||
// Метаданные очереди (duplicates, FIFO state)
|
||||
persistence.SaveQueue(key, queue)
|
||||
respStruct := models.DeleteMessageResponse{
|
||||
Xmlns: models.BaseXmlns,
|
||||
Metadata: models.BaseResponseMetadata,
|
||||
}
|
||||
return 200, &respStruct
|
||||
// O(1)-поиск сообщения по индексу ReceiptHandle → индекс в Messages (P1.2).
|
||||
// Раньше — линейный скан всего среза + сдвиг остатка (O(N) на большое тело).
|
||||
if i, ok := queue.IndexOfReceipt(receiptHandle); ok {
|
||||
msg := queue.Messages[i] // копия ДО мутации: RemoveIndex может сдвинуть срез
|
||||
queue.ClearInFlight(i, receiptHandle)
|
||||
queue.UnlockGroup(msg.GroupID)
|
||||
queue.RemoveIndex(i) // tombstone за O(1) + подрезка хвоста/амортизированная компакция
|
||||
delete(queue.Duplicates, msg.DeduplicationID)
|
||||
// Удаляем одно сообщение из Redis — O(1)
|
||||
persistence.DeleteMessagePersist(key, msg.Uuid)
|
||||
// Метаданные очереди (duplicates, FIFO state)
|
||||
persistence.SaveQueue(key, queue)
|
||||
respStruct := models.DeleteMessageResponse{
|
||||
Xmlns: models.BaseXmlns,
|
||||
Metadata: models.BaseResponseMetadata,
|
||||
}
|
||||
return 200, &respStruct
|
||||
}
|
||||
log.Warning("Receipt Handle not found")
|
||||
|
||||
|
||||
@@ -73,22 +73,29 @@ func DeleteMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBo
|
||||
|
||||
deletedEntries := make([]models.DeleteMessageBatchResultEntry, 0)
|
||||
deletedUuids := make([]string, 0)
|
||||
remainingMessages := make([]models.SqsMessage, 0, len(queue.Messages))
|
||||
|
||||
for _, message := range queue.Messages {
|
||||
if de, found := deleteMessageMap[message.ReceiptHandle]; found {
|
||||
log.Debugf("FIFO Queue %s unlocking group %s:", queueName, message.GroupID)
|
||||
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)
|
||||
} else {
|
||||
remainingMessages = append(remainingMessages, message)
|
||||
// Идём по индексам, не пересобирая срез (P1.2): удаление = tombstone за O(1),
|
||||
// индексы не сдвигаются, поэтому итерация остаётся корректной.
|
||||
for i := range queue.Messages {
|
||||
if queue.IsDead(i) {
|
||||
continue
|
||||
}
|
||||
message := &queue.Messages[i]
|
||||
de, found := deleteMessageMap[message.ReceiptHandle]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
log.Debugf("FIFO Queue %s unlocking group %s:", queueName, message.GroupID)
|
||||
// Читаем поля ДО RemoveIndex: компакция может перезаписать слот.
|
||||
groupID, dedupID, uuid := message.GroupID, message.DeduplicationID, message.Uuid
|
||||
queue.ClearInFlight(i, message.ReceiptHandle)
|
||||
queue.UnlockGroup(groupID)
|
||||
delete(queue.Duplicates, dedupID)
|
||||
de.Deleted = true
|
||||
deletedEntries = append(deletedEntries, models.DeleteMessageBatchResultEntry{Id: de.Id})
|
||||
deletedUuids = append(deletedUuids, uuid)
|
||||
queue.RemoveIndex(i)
|
||||
}
|
||||
|
||||
queue.Messages = remainingMessages
|
||||
// Удаляем сообщения из Redis отдельно — O(batch_size)
|
||||
persistence.DeleteMessagesPersist(key, deletedUuids)
|
||||
// Метаданные очереди (duplicates, FIFO state)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// app/gosqs/delete_middle_test.go
|
||||
// Тест P1.2: удаление сообщения из СЕРЕДИНЫ очереди не ломает остальные —
|
||||
// индексы receipt/uuid корректны (tombstone), удалённое не возвращается,
|
||||
// оставшиеся находятся по своим ReceiptHandle.
|
||||
package gosqs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"shared-sqs/app/models"
|
||||
)
|
||||
|
||||
func TestDeleteMessageV1_MiddleKeepsOthersReceivable(t *testing.T) {
|
||||
resetTestEnv(t)
|
||||
tnt := testTenant("SSAK-aaa")
|
||||
injectRequest(t, func(v interface{}) { v.(*models.CreateQueueRequest).QueueName = "q" })
|
||||
if code, _ := CreateQueueV1(reqWithTenant("/", tnt)); code != http.StatusOK {
|
||||
t.Fatal("create failed")
|
||||
}
|
||||
q := models.SyncQueues.Queues["SSAK-aaa:q"]
|
||||
q.MaximumMessageSize = 262144
|
||||
|
||||
// Три сообщения.
|
||||
for _, body := range []string{"m1", "m2", "m3"} {
|
||||
b := body
|
||||
injectRequest(t, func(v interface{}) {
|
||||
r := v.(*models.SendMessageRequest)
|
||||
r.QueueUrl = "http://x/tenant/q"
|
||||
r.MessageBody = b
|
||||
})
|
||||
if code, _ := SendMessageV1(reqWithTenant("/", tnt)); code != http.StatusOK {
|
||||
t.Fatal("send failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем все три разом.
|
||||
injectRequest(t, func(v interface{}) {
|
||||
r := v.(*models.ReceiveMessageRequest)
|
||||
r.QueueUrl = "http://x/tenant/q"
|
||||
r.MaxNumberOfMessages = 10
|
||||
})
|
||||
_, resp := ReceiveMessageV1(reqWithTenant("/", tnt))
|
||||
rr := resp.(models.ReceiveMessageResponse)
|
||||
if len(rr.Result.Messages) != 3 {
|
||||
t.Fatalf("receive вернул %d сообщений, want 3", len(rr.Result.Messages))
|
||||
}
|
||||
|
||||
// Удаляем СРЕДНЕЕ.
|
||||
middle := rr.Result.Messages[1]
|
||||
injectRequest(t, func(v interface{}) {
|
||||
r := v.(*models.DeleteMessageRequest)
|
||||
r.QueueUrl = "http://x/tenant/q"
|
||||
r.ReceiptHandle = middle.ReceiptHandle
|
||||
})
|
||||
if code, _ := DeleteMessageV1(reqWithTenant("/", tnt)); code != http.StatusOK {
|
||||
t.Fatal("delete failed")
|
||||
}
|
||||
if q.LiveCount() != 2 {
|
||||
t.Fatalf("LiveCount = %d, want 2", q.LiveCount())
|
||||
}
|
||||
if _, ok := q.IndexOfUUID(middle.MessageId); ok {
|
||||
t.Fatal("удалённое сообщение осталось в uuid-индексе")
|
||||
}
|
||||
// Оставшиеся по-прежнему in-flight и находятся по своим handle (индексы не «съехали»).
|
||||
for _, m := range []*models.ResultMessage{rr.Result.Messages[0], rr.Result.Messages[2]} {
|
||||
if _, ok := q.IndexOfReceipt(m.ReceiptHandle); !ok {
|
||||
t.Fatalf("handle оставшегося сообщения потерян: %s", m.ReceiptHandle)
|
||||
}
|
||||
}
|
||||
|
||||
// Возвращаем видимость оставшимся и получаем их снова — удалённого быть не должно.
|
||||
for _, m := range []*models.ResultMessage{rr.Result.Messages[0], rr.Result.Messages[2]} {
|
||||
handle := m.ReceiptHandle
|
||||
injectRequest(t, func(v interface{}) {
|
||||
r := v.(*models.ChangeMessageVisibilityRequest)
|
||||
r.QueueUrl = "http://x/tenant/q"
|
||||
r.ReceiptHandle = handle
|
||||
r.VisibilityTimeout = 0
|
||||
})
|
||||
if code, _ := ChangeMessageVisibilityV1(reqWithTenant("/", tnt)); code != http.StatusOK {
|
||||
t.Fatal("change visibility failed")
|
||||
}
|
||||
}
|
||||
|
||||
injectRequest(t, func(v interface{}) {
|
||||
r := v.(*models.ReceiveMessageRequest)
|
||||
r.QueueUrl = "http://x/tenant/q"
|
||||
r.MaxNumberOfMessages = 10
|
||||
})
|
||||
_, resp2 := ReceiveMessageV1(reqWithTenant("/", tnt))
|
||||
rr2 := resp2.(models.ReceiveMessageResponse)
|
||||
if len(rr2.Result.Messages) != 2 {
|
||||
t.Fatalf("повторный receive вернул %d, want 2", len(rr2.Result.Messages))
|
||||
}
|
||||
for _, m := range rr2.Result.Messages {
|
||||
if m.MessageId == middle.MessageId {
|
||||
t.Fatal("удалённое сообщение вернулось в receive")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func GetQueueAttributesV1(req *http.Request) (int, interfaces.AbstractResponseBo
|
||||
queueAttributes = append(queueAttributes, models.Attribute{Name: "VisibilityTimeout", Value: strconv.Itoa(queue.VisibilityTimeout)})
|
||||
}
|
||||
if shouldInclude("ApproximateNumberOfMessages") {
|
||||
queueAttributes = append(queueAttributes, models.Attribute{Name: "ApproximateNumberOfMessages", Value: strconv.Itoa(len(queue.Messages))})
|
||||
queueAttributes = append(queueAttributes, models.Attribute{Name: "ApproximateNumberOfMessages", Value: strconv.Itoa(queue.LiveCount())})
|
||||
}
|
||||
if shouldInclude("ApproximateNumberOfMessagesNotVisible") {
|
||||
queueAttributes = append(queueAttributes, models.Attribute{Name: "ApproximateNumberOfMessagesNotVisible", Value: strconv.Itoa(numberOfHiddenMessagesInQueue(queue))})
|
||||
|
||||
+44
-24
@@ -74,28 +74,34 @@ func PeriodicTasks(d time.Duration, quit chan bool) {
|
||||
// - если Retry >= MaxReceiveCount и настроена DLQ — переносим сообщение
|
||||
// в DLQ и удаляем из исходной очереди.
|
||||
func processQueueTick(key string, queue *models.Queue) {
|
||||
// Ленивая починка индексов (P1.2): очередь могла быть собрана литералом или
|
||||
// загружена из Redis — тогда индексы строятся здесь один раз.
|
||||
queue.EnsureIndexes()
|
||||
|
||||
// 0. Эвикция просроченных сообщений по MessageRetentionPeriod.
|
||||
// Retention == 0 означает «без истечения» (не эвиктируем).
|
||||
// Удаление — tombstone за O(1); физическая чистка — CompactIfNeeded ниже.
|
||||
if queue.MessageRetentionPeriod > 0 {
|
||||
cutoff := time.Now().Add(-time.Duration(queue.MessageRetentionPeriod) * time.Second)
|
||||
expired := make([]string, 0) // uuid просроченных сообщений для удаления из Redis
|
||||
// Компакция НА МЕСТЕ: читаем значение в i, пишем в n (n <= i) — без
|
||||
// алиасинга на переиспользуемом срезе и без лишней аллокации.
|
||||
n := 0
|
||||
for i := range queue.Messages {
|
||||
msg := queue.Messages[i]
|
||||
if msg.SentTime.Before(cutoff) {
|
||||
expired = append(expired, msg.Uuid)
|
||||
if msg.GroupID != "" {
|
||||
queue.UnlockGroup(msg.GroupID)
|
||||
}
|
||||
if queue.IsDead(i) {
|
||||
continue
|
||||
}
|
||||
queue.Messages[n] = msg
|
||||
n++
|
||||
msg := queue.Messages[i]
|
||||
if !msg.SentTime.Before(cutoff) {
|
||||
continue
|
||||
}
|
||||
expired = append(expired, msg.Uuid)
|
||||
if msg.GroupID != "" {
|
||||
queue.UnlockGroup(msg.GroupID)
|
||||
}
|
||||
if msg.ReceiptHandle != "" {
|
||||
queue.ClearInFlight(i, msg.ReceiptHandle)
|
||||
}
|
||||
queue.RemoveIndex(i)
|
||||
}
|
||||
if len(expired) > 0 {
|
||||
queue.Messages = queue.Messages[:n]
|
||||
log.Infof("Queue [%s]: evicted %d messages by MessageRetentionPeriod", queue.Name, len(expired))
|
||||
// Удаляем просроченные из Redis, чтобы не воскресли после рестарта.
|
||||
persistence.DeleteMessagesPersist(key, expired)
|
||||
@@ -111,18 +117,25 @@ func processQueueTick(key string, queue *models.Queue) {
|
||||
}
|
||||
|
||||
// 2. Возврат видимости истёкших сообщений + перенос в DLQ.
|
||||
log.Debugf("Queue [%s] length [%d]", queue.Name, len(queue.Messages))
|
||||
for i := 0; i < len(queue.Messages); i++ {
|
||||
msg := &queue.Messages[i]
|
||||
|
||||
if msg.ReceiptHandle == "" {
|
||||
continue // сообщение не в полёте
|
||||
// Итерируем ТОЛЬКО по in-flight (receiptIdx), а не по всему срезу — стоимость
|
||||
// пропорциональна числу сообщений «в полёте», а не размеру очереди (P1.2).
|
||||
log.Debugf("Queue [%s] live [%d]", queue.Name, queue.LiveCount())
|
||||
now := time.Now()
|
||||
for _, i := range queue.InFlightIndexes() {
|
||||
if i < 0 || i >= len(queue.Messages) || queue.IsDead(i) {
|
||||
continue
|
||||
}
|
||||
if msg.VisibilityTimeout.After(time.Now()) {
|
||||
msg := &queue.Messages[i]
|
||||
rh := msg.ReceiptHandle
|
||||
if rh == "" {
|
||||
continue // уже возвращено в видимость
|
||||
}
|
||||
if msg.VisibilityTimeout.After(now) {
|
||||
continue // таймаут видимости ещё действует
|
||||
}
|
||||
|
||||
log.Debugf("Making message visible again %s", msg.ReceiptHandle)
|
||||
log.Debugf("Making message visible again %s", rh)
|
||||
queue.ClearInFlight(i, rh)
|
||||
queue.UnlockGroup(msg.GroupID)
|
||||
msg.ReceiptHandle = ""
|
||||
msg.ReceiptTime = time.Now().UTC()
|
||||
@@ -132,18 +145,25 @@ func processQueueTick(key string, queue *models.Queue) {
|
||||
if queue.MaxReceiveCount > 0 &&
|
||||
queue.DeadLetterQueue != nil &&
|
||||
msg.Retry >= queue.MaxReceiveCount {
|
||||
dlqMsg := *msg
|
||||
queue.DeadLetterQueue.LockMu()
|
||||
queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, *msg)
|
||||
queue.DeadLetterQueue.AppendMessage(dlqMsg)
|
||||
queue.DeadLetterQueue.UnlockMu()
|
||||
queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...)
|
||||
i-- // элемент на позиции i удалён — повторно проверяем новый элемент на ней
|
||||
queue.RemoveIndex(i)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Компакция tombstone-слотов (амортизированно): срез не растёт бесконечно.
|
||||
queue.CompactIfNeeded()
|
||||
}
|
||||
|
||||
func numberOfHiddenMessagesInQueue(queue *models.Queue) int {
|
||||
num := 0
|
||||
for _, m := range queue.Messages {
|
||||
for i := range queue.Messages {
|
||||
if queue.IsDead(i) {
|
||||
continue
|
||||
}
|
||||
m := &queue.Messages[i]
|
||||
if m.ReceiptHandle != "" || m.DelaySecs > 0 && time.Now().Before(m.SentTime.Add(time.Duration(m.DelaySecs)*time.Second)) {
|
||||
num++
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func PurgeQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
}
|
||||
|
||||
log.Infof("Purging Queue: %s (tenant: %s)", queueName, t.ID)
|
||||
queue.Messages = nil
|
||||
queue.Reset()
|
||||
queue.Duplicates = make(map[string]time.Time)
|
||||
// Фиксируем время purge — защита от повторного вызова в течение 60 секунд.
|
||||
queue.LastPurgeTime = time.Now()
|
||||
|
||||
@@ -154,7 +154,7 @@ func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody)
|
||||
// - для FIFO-очереди блокируется группа (LockGroup) до DeleteMessage/таймаута.
|
||||
func receiveMessagesUnderLock(key string, visibilityTimeout, maxNumberOfMessages int) ([]*models.ResultMessage, bool) {
|
||||
queue := models.SyncQueues.GetLocked(key)
|
||||
if queue == nil || len(queue.Messages) == 0 {
|
||||
if queue == nil || queue.LiveCount() == 0 {
|
||||
return nil, false
|
||||
}
|
||||
defer queue.UnlockMu()
|
||||
@@ -169,6 +169,9 @@ func receiveMessagesUnderLock(key string, visibilityTimeout, maxNumberOfMessages
|
||||
if numMsg >= maxNumberOfMessages {
|
||||
break
|
||||
}
|
||||
if queue.IsDead(i) {
|
||||
continue // tombstone: сообщение логически удалено (P1.2)
|
||||
}
|
||||
msg := &queue.Messages[i]
|
||||
if msg.ReceiptHandle != "" {
|
||||
continue // сообщение уже в полёте у другого получателя
|
||||
@@ -192,6 +195,9 @@ func receiveMessagesUnderLock(key string, visibilityTimeout, maxNumberOfMessages
|
||||
msg.VisibilityTimeout = time.Now().Add(time.Duration(queue.VisibilityTimeout) * time.Second)
|
||||
}
|
||||
msg.NumberOfReceives++
|
||||
// Регистрируем handle→index: по нему DeleteMessage/ChangeMessageVisibility
|
||||
// работают за O(1), а visibility-тик итерирует только in-flight (P1.2).
|
||||
queue.MarkInFlight(i, msg.ReceiptHandle)
|
||||
|
||||
messages = append(messages, buildResultMessage(msg))
|
||||
numMsg++
|
||||
@@ -246,6 +252,9 @@ func buildResultMessage(m *models.SqsMessage) *models.ResultMessage {
|
||||
// читает общее состояние очереди.
|
||||
func queueHasReceivableMessages(queue *models.Queue) bool {
|
||||
for i := range queue.Messages {
|
||||
if queue.IsDead(i) {
|
||||
continue // tombstone (P1.2)
|
||||
}
|
||||
msg := &queue.Messages[i]
|
||||
if msg.ReceiptHandle != "" {
|
||||
continue
|
||||
|
||||
@@ -90,7 +90,7 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
defer queue.UnlockMu()
|
||||
|
||||
maxMessageSize := queue.MaximumMessageSize
|
||||
currentMsgCount := len(queue.Messages)
|
||||
currentMsgCount := queue.LiveCount()
|
||||
queueIsFIFO := queue.IsFIFO
|
||||
delaySecs := queue.DelaySeconds
|
||||
|
||||
@@ -129,7 +129,7 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
|
||||
// Повторная проверка лимита под per-queue Lock (между снимком выше и текущим
|
||||
// моментом другие горутины могли добавить сообщения — мягкий OOM-лимит).
|
||||
if len(queue.Messages) >= MaxMessagesForQueue(queue.IsFIFO) {
|
||||
if queue.LiveCount() >= MaxMessagesForQueue(queue.IsFIFO) {
|
||||
return utils.CreateErrorResponseV1("OverLimit", true)
|
||||
}
|
||||
|
||||
@@ -140,14 +140,14 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
||||
|
||||
persistErr := false
|
||||
if !queue.IsDuplicate(messageDeduplicationID) {
|
||||
queue.Messages = append(queue.Messages, msg)
|
||||
idx := queue.AppendMessage(msg)
|
||||
// Персистим сообщение СИНХРОННО до ответа клиенту: краш пода в окне
|
||||
// между 200 OK и async-записью приводил бы к потере подтверждённого
|
||||
// сообщения. При ошибке — откатываем append (не оставляем в памяти
|
||||
// сообщение, которого нет в Redis).
|
||||
if err := persistence.SaveMessageSync(key, &msg); err != nil {
|
||||
log.Errorf("SendMessage: не удалось персистить сообщение %s в очередь %s: %v", msg.Uuid, key, err)
|
||||
queue.Messages = queue.Messages[:len(queue.Messages)-1]
|
||||
queue.RemoveIndex(idx)
|
||||
persistErr = true
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -48,7 +48,7 @@ func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody
|
||||
return utils.CreateErrorResponseV1("QueueNotFound", true)
|
||||
}
|
||||
maxMsgSize := queue.MaximumMessageSize
|
||||
currentMsgCount := len(queue.Messages)
|
||||
currentMsgCount := queue.LiveCount()
|
||||
queueIsFIFO := queue.IsFIFO
|
||||
queue.RUnlockMu()
|
||||
|
||||
@@ -122,7 +122,7 @@ func SendMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBody
|
||||
fifoSeqNumber = queue.NextSequenceNumber(sendEntry.MessageGroupId)
|
||||
}
|
||||
if !queue.IsDuplicate(sendEntry.MessageDeduplicationId) {
|
||||
queue.Messages = append(queue.Messages, msg)
|
||||
queue.AppendMessage(msg)
|
||||
} else {
|
||||
log.Debugf("Duplicate deduplicationId [%s] in queue [%s]", sendEntry.MessageDeduplicationId, queueName)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user