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:
+257
-11
@@ -61,18 +61,33 @@ type Queue struct {
|
||||
MaximumMessageSize int
|
||||
MessageRetentionPeriod int // seconds (реализовано в processQueueTick)
|
||||
Messages []SqsMessage
|
||||
DeadLetterQueue *Queue
|
||||
// ─── Индексация сообщений (P1.2) ────────────────────────────────────────
|
||||
// deadIdx — параллельный Messages слайс tombstone-флагов: true = слот
|
||||
// логически удалён. Удаление НЕ сдвигает элементы (O(1)), поэтому индексы
|
||||
// (receiptIdx/uuidIdx) остаются валидными; физическая чистка — Compact.
|
||||
// Менять Messages напрямую НЕЛЬЗЯ: только через AppendMessage/RemoveIndex/
|
||||
// Compact/Reset (см. ниже). Прямая замена допустима лишь для полной очистки
|
||||
// и загрузки из Redis (с последующим RebuildIndexes).
|
||||
deadIdx []bool `json:"-"`
|
||||
// receiptIdx — ReceiptHandle → индекс в Messages (только in-flight сообщения).
|
||||
receiptIdx map[string]int `json:"-"`
|
||||
// uuidIdx — Uuid → индекс в Messages (только живые сообщения).
|
||||
uuidIdx map[string]int `json:"-"`
|
||||
// liveCount — число живых сообщений; deadCount — число tombstone-слотов.
|
||||
liveCount int `json:"-"`
|
||||
deadCount int `json:"-"`
|
||||
DeadLetterQueue *Queue
|
||||
// DeadLetterQueueName — имя DLQ. В Redis-метаданных хранится ТОЛЬКО имя
|
||||
// (а не вложенный объект): после загрузки указатель DeadLetterQueue
|
||||
// релинкуется на объект из общей map (см. persistence.LoadAllQueues).
|
||||
DeadLetterQueueName string `json:"dead_letter_queue_name,omitempty"`
|
||||
MaxReceiveCount int
|
||||
IsFIFO bool
|
||||
FIFOMessages map[string]int
|
||||
FIFOSequenceNumbers map[string]int
|
||||
EnableDuplicates bool
|
||||
Duplicates map[string]time.Time
|
||||
Tags map[string]string
|
||||
IsFIFO bool
|
||||
FIFOMessages map[string]int
|
||||
FIFOSequenceNumbers map[string]int
|
||||
EnableDuplicates bool
|
||||
Duplicates map[string]time.Time
|
||||
Tags map[string]string
|
||||
// LastPurgeTime — время последнего успешного PurgeQueue.
|
||||
// Используется для запрета повторного purge ранее чем через 60 секунд
|
||||
// (требование AWS: PurgeQueueInProgress). Нулевое значение = purge ещё не выполнялся.
|
||||
@@ -81,10 +96,10 @@ type Queue struct {
|
||||
|
||||
// LockMu/UnlockMu/RLockMu/RUnlockMu — доступ к per-queue блокировке извне пакета models
|
||||
// (поле mu неэкспортируемо).
|
||||
func (q *Queue) LockMu() { q.mu.Lock() }
|
||||
func (q *Queue) UnlockMu() { q.mu.Unlock() }
|
||||
func (q *Queue) RLockMu() { q.mu.RLock() }
|
||||
func (q *Queue) RUnlockMu() { q.mu.RUnlock() }
|
||||
func (q *Queue) LockMu() { q.mu.Lock() }
|
||||
func (q *Queue) UnlockMu() { q.mu.Unlock() }
|
||||
func (q *Queue) RLockMu() { q.mu.RLock() }
|
||||
func (q *Queue) RUnlockMu() { q.mu.RUnlock() }
|
||||
|
||||
// NextSequenceNumber — возвращает следующий порядковый номер сообщения FIFO-группы.
|
||||
//
|
||||
@@ -163,3 +178,234 @@ func (q *Queue) InitDuplicatation(deduplicationId string) {
|
||||
q.Duplicates[deduplicationId] = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── P1.2: индексация сообщений (tombstone + индексы) ──────────────────────
|
||||
//
|
||||
// Все методы ниже мутируют состояние очереди → вызывать ТОЛЬКО под
|
||||
// per-queue write-блокировкой (queue.LockMu). Читающие методы (LiveCount,
|
||||
// IsDead) безопасны под RLock.
|
||||
|
||||
// compactDeadThreshold — минимальное число tombstone-слотов, после которого
|
||||
// компакция при удалении разрешена. Защищает от O(N)-чистки на мелких очередях:
|
||||
// при удалении слот подрезается с хвоста, остальная чистка — в processQueueTick.
|
||||
const compactDeadThreshold = 1024
|
||||
|
||||
// ensureIndexes — ленивая инициализация/починка индексов.
|
||||
// При корректном состоянии — две дешёвые проверки; O(N) — если Messages
|
||||
// изменили в обход методов (загрузка из Redis, тесты, старый JSON-формат).
|
||||
// Вызывать под write-блокировкой.
|
||||
func (q *Queue) ensureIndexes() {
|
||||
if len(q.deadIdx) == len(q.Messages) &&
|
||||
q.receiptIdx != nil && q.uuidIdx != nil &&
|
||||
q.liveCount == len(q.Messages)-q.deadCount {
|
||||
return
|
||||
}
|
||||
q.RebuildIndexes()
|
||||
}
|
||||
|
||||
// RebuildIndexes — перестраивает deadIdx/receiptIdx/uuidIdx/liveCount/deadCount
|
||||
// по текущему Messages. Существующие tombstone СОХРАНЯЮТСЯ по позициям (если
|
||||
// deadIdx длиннее Messages — обрезается, короче — добивается false).
|
||||
// Вызывать под write-блокировкой.
|
||||
func (q *Queue) RebuildIndexes() {
|
||||
if len(q.deadIdx) > len(q.Messages) {
|
||||
q.deadIdx = q.deadIdx[:len(q.Messages)]
|
||||
}
|
||||
for len(q.deadIdx) < len(q.Messages) {
|
||||
q.deadIdx = append(q.deadIdx, false)
|
||||
}
|
||||
q.receiptIdx = make(map[string]int, len(q.Messages))
|
||||
q.uuidIdx = make(map[string]int, len(q.Messages))
|
||||
q.liveCount = 0
|
||||
q.deadCount = 0
|
||||
for i := range q.Messages {
|
||||
if q.deadIdx[i] {
|
||||
q.deadCount++
|
||||
continue
|
||||
}
|
||||
q.liveCount++
|
||||
if u := q.Messages[i].Uuid; u != "" {
|
||||
q.uuidIdx[u] = i
|
||||
}
|
||||
if rh := q.Messages[i].ReceiptHandle; rh != "" {
|
||||
q.receiptIdx[rh] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AppendMessage — добавляет сообщение в конец очереди, поддерживая индексы.
|
||||
// Возвращает индекс добавленного сообщения. Вызывать под write-блокировкой.
|
||||
func (q *Queue) AppendMessage(m SqsMessage) int {
|
||||
q.ensureIndexes()
|
||||
q.Messages = append(q.Messages, m)
|
||||
q.deadIdx = append(q.deadIdx, false)
|
||||
idx := len(q.Messages) - 1
|
||||
q.liveCount++
|
||||
if m.Uuid != "" {
|
||||
q.uuidIdx[m.Uuid] = idx
|
||||
}
|
||||
if m.ReceiptHandle != "" {
|
||||
q.receiptIdx[m.ReceiptHandle] = idx
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
// RemoveIndex — логически удаляет сообщение по индексу (tombstone) за O(1).
|
||||
// ВАЖНО: длина q.Messages НЕ меняется (слот остаётся tombstone) — поэтому
|
||||
// индексы, взятые до вызова, остаются валидными, а итерации по срезу
|
||||
// безопасны. Физическая чистка — Compact/CompactIfNeeded (в processQueueTick).
|
||||
// Вызывать под write-блокировкой.
|
||||
func (q *Queue) RemoveIndex(i int) {
|
||||
q.ensureIndexes()
|
||||
if i < 0 || i >= len(q.Messages) || q.deadIdx[i] {
|
||||
return
|
||||
}
|
||||
msg := &q.Messages[i]
|
||||
q.deadIdx[i] = true
|
||||
q.deadCount++
|
||||
if q.liveCount > 0 {
|
||||
q.liveCount--
|
||||
}
|
||||
if msg.Uuid != "" {
|
||||
if idx, ok := q.uuidIdx[msg.Uuid]; ok && idx == i {
|
||||
delete(q.uuidIdx, msg.Uuid)
|
||||
}
|
||||
}
|
||||
if rh := msg.ReceiptHandle; rh != "" {
|
||||
if idx, ok := q.receiptIdx[rh]; ok && idx == i {
|
||||
delete(q.receiptIdx, rh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CompactIfNeeded — компакция при заметной доле tombstone-слотов.
|
||||
// Амортизированно: копия среза делается не на каждое удаление, а когда число
|
||||
// «мёртвых» слотов становится заметным (≥ 1024 или ≥ половины среза).
|
||||
// Вызывать под write-блокировкой ВНЕ итераций по Messages (меняет индексы).
|
||||
func (q *Queue) CompactIfNeeded() {
|
||||
if q.deadCount == 0 {
|
||||
return
|
||||
}
|
||||
if q.deadCount >= compactDeadThreshold || q.deadCount*2 >= len(q.Messages) {
|
||||
q.Compact()
|
||||
}
|
||||
}
|
||||
|
||||
// Compact — физически удаляет tombstone-слоты, сохраняя порядок живых, и
|
||||
// перестраивает индексы. O(N). Вызывать под write-блокировкой.
|
||||
func (q *Queue) Compact() {
|
||||
if q.deadCount == 0 {
|
||||
return
|
||||
}
|
||||
if len(q.deadIdx) != len(q.Messages) {
|
||||
q.RebuildIndexes()
|
||||
if q.deadCount == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
n := 0
|
||||
for i := range q.Messages {
|
||||
if q.deadIdx[i] {
|
||||
continue
|
||||
}
|
||||
if n != i {
|
||||
q.Messages[n] = q.Messages[i]
|
||||
}
|
||||
n++
|
||||
}
|
||||
// Обнуляем «хвост», чтобы не держать ссылки на удалённые сообщения (GC).
|
||||
tail := q.Messages[n:]
|
||||
for i := range tail {
|
||||
tail[i] = SqsMessage{}
|
||||
}
|
||||
q.Messages = q.Messages[:n]
|
||||
// После чистки tombstone нет — сбрасываем флаги, чтобы RebuildIndexes
|
||||
// построил индексы только по живым (иначе старые deadIdx-флаги «съедут»).
|
||||
q.deadIdx = q.deadIdx[:0]
|
||||
q.deadCount = 0
|
||||
q.RebuildIndexes()
|
||||
}
|
||||
|
||||
// Reset — полностью очищает сообщения и индексы (purge).
|
||||
// Вызывать под write-блокировкой.
|
||||
func (q *Queue) Reset() {
|
||||
q.Messages = nil
|
||||
q.deadIdx = nil
|
||||
q.receiptIdx = make(map[string]int)
|
||||
q.uuidIdx = make(map[string]int)
|
||||
q.liveCount = 0
|
||||
q.deadCount = 0
|
||||
}
|
||||
|
||||
// EnsureIndexes — экспортируемая обёртка над ensureIndexes: строит индексы,
|
||||
// если очередь была собрана в обход методов (литералом, из Redis, тестами).
|
||||
// Вызывать под write-блокировкой.
|
||||
func (q *Queue) EnsureIndexes() {
|
||||
q.ensureIndexes()
|
||||
}
|
||||
|
||||
// IndexOfReceipt — индекс сообщения по ReceiptHandle (только in-flight) за O(1).
|
||||
// Вызывать под write-блокировкой (ensureIndexes может перестроить индексы).
|
||||
func (q *Queue) IndexOfReceipt(receiptHandle string) (int, bool) {
|
||||
q.ensureIndexes()
|
||||
i, ok := q.receiptIdx[receiptHandle]
|
||||
if !ok || i < 0 || i >= len(q.Messages) || q.deadIdx[i] {
|
||||
return 0, false
|
||||
}
|
||||
return i, true
|
||||
}
|
||||
|
||||
// IndexOfUUID — индекс живого сообщения по Uuid за O(1).
|
||||
// Вызывать под write-блокировкой.
|
||||
func (q *Queue) IndexOfUUID(uuid string) (int, bool) {
|
||||
q.ensureIndexes()
|
||||
i, ok := q.uuidIdx[uuid]
|
||||
if !ok || i < 0 || i >= len(q.Messages) || q.deadIdx[i] {
|
||||
return 0, false
|
||||
}
|
||||
return i, true
|
||||
}
|
||||
|
||||
// MarkInFlight — регистрирует ReceiptHandle → индекс (для tick по in-flight).
|
||||
// Вызывать под write-блокировкой.
|
||||
func (q *Queue) MarkInFlight(i int, receiptHandle string) {
|
||||
q.ensureIndexes()
|
||||
if receiptHandle != "" {
|
||||
q.receiptIdx[receiptHandle] = i
|
||||
}
|
||||
}
|
||||
|
||||
// ClearInFlight — снимает регистрацию ReceiptHandle (возврат видимости).
|
||||
// Вызывать под write-блокировкой.
|
||||
func (q *Queue) ClearInFlight(i int, receiptHandle string) {
|
||||
if receiptHandle == "" {
|
||||
return
|
||||
}
|
||||
if idx, ok := q.receiptIdx[receiptHandle]; ok && idx == i {
|
||||
delete(q.receiptIdx, receiptHandle)
|
||||
}
|
||||
}
|
||||
|
||||
// InFlightIndexes — снимок индексов in-flight сообщений (для visibility-тика).
|
||||
// Возвращает срез индексов в Messages. Вызывать под write-блокировкой.
|
||||
func (q *Queue) InFlightIndexes() []int {
|
||||
idx := make([]int, 0, len(q.receiptIdx))
|
||||
for _, i := range q.receiptIdx {
|
||||
idx = append(idx, i)
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
// IsDead — tombstone-ли слот. Безопасно под RLock (не мутирует состояние).
|
||||
func (q *Queue) IsDead(i int) bool {
|
||||
if i < 0 || i >= len(q.deadIdx) {
|
||||
return false
|
||||
}
|
||||
return q.deadIdx[i]
|
||||
}
|
||||
|
||||
// LiveCount — число живых (не tombstone) сообщений.
|
||||
// Безопасно под RLock. Эквивалент прежнего len(queue.Messages).
|
||||
func (q *Queue) LiveCount() int {
|
||||
return len(q.Messages) - q.deadCount
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user