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:
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user