Пункт 4 ревью: убран дублирующий блок перестройки индексов в Compact (ensureIndexes на входе + один RebuildIndexes на выходе). Поведение то же, логика чище. Дополнен HISTORY разделом о фиксах код-ревью.
447 lines
20 KiB
Go
447 lines
20 KiB
Go
package models
|
||
|
||
import (
|
||
"strconv"
|
||
"sync"
|
||
"time"
|
||
|
||
log "github.com/sirupsen/logrus"
|
||
)
|
||
|
||
type MessageStructure string
|
||
type Protocol string
|
||
|
||
type MessageAttribute struct {
|
||
BinaryListValues []string `json:"BinaryListValues,omitempty" xml:"BinaryListValues,omitempty"` // currently unsupported by AWS
|
||
BinaryValue string `json:"BinaryValue,omitempty" xml:"BinaryValue,omitempty"`
|
||
DataType string `json:"DataType,omitempty" xml:"DataType,omitempty"`
|
||
StringListValues []string `json:"StringListValues,omitempty" xml:"StringListValues,omitempty"` // currently unsupported by AWS
|
||
StringValue string `json:"StringValue,omitempty" xml:"StringValue,omitempty"`
|
||
}
|
||
|
||
type SqsMessage struct {
|
||
MessageBody string
|
||
Uuid string
|
||
MD5OfMessageAttributes string
|
||
MD5OfMessageBody string
|
||
ReceiptHandle string
|
||
ReceiptTime time.Time
|
||
VisibilityTimeout time.Time
|
||
NumberOfReceives int
|
||
Retry int
|
||
MessageAttributes map[string]MessageAttribute
|
||
GroupID string
|
||
DeduplicationID string
|
||
SentTime time.Time
|
||
DelaySecs int
|
||
}
|
||
|
||
func (m *SqsMessage) IsReadyForReceipt() bool {
|
||
randomLatency, err := generateRandomLatency()
|
||
if err != nil {
|
||
log.Error(err)
|
||
return true
|
||
}
|
||
showAt := m.SentTime.Add(randomLatency).Add(time.Duration(m.DelaySecs) * time.Second)
|
||
return showAt.Before(time.Now())
|
||
}
|
||
|
||
type Queue struct {
|
||
// mu — per-queue блокировка содержимого очереди (Messages, Duplicates, FIFO-
|
||
// состояние, LastPurgeTime). Заменяет удержание глобального SyncQueues.Lock
|
||
// на время операций с одной очередью. sync.RWMutex сериализации не подлежит.
|
||
mu sync.RWMutex `json:"-"`
|
||
|
||
Name string
|
||
URL string
|
||
Arn string
|
||
VisibilityTimeout int // seconds
|
||
ReceiveMessageWaitTimeSeconds int
|
||
DelaySeconds int
|
||
MaximumMessageSize int
|
||
MessageRetentionPeriod int // seconds (реализовано в processQueueTick)
|
||
Messages []SqsMessage
|
||
// ─── Индексация сообщений (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
|
||
// LastPurgeTime — время последнего успешного PurgeQueue.
|
||
// Используется для запрета повторного purge ранее чем через 60 секунд
|
||
// (требование AWS: PurgeQueueInProgress). Нулевое значение = purge ещё не выполнялся.
|
||
LastPurgeTime time.Time
|
||
}
|
||
|
||
// 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() }
|
||
|
||
// NextSequenceNumber — возвращает следующий порядковый номер сообщения FIFO-группы.
|
||
//
|
||
// Логическая схема: каждый вызов инкрементирует счётчик группы и возвращает
|
||
// его строковое представление (порядковые номера в FIFO должны расти монотонно).
|
||
//
|
||
// ВАЖНО (фикс): раньше при ПЕРВОМ обращении к группе map пересоздавалась целиком
|
||
// (q.FIFOSequenceNumbers = map[string]int{groupId: 0}) — счётчики ВСЕХ остальных
|
||
// групп терялись, и порядковые номера начинали дублироваться. Теперь map только
|
||
// инициализируется один раз (если nil), а счётчик конкретной группы инкрементируется.
|
||
func (q *Queue) NextSequenceNumber(groupId string) string {
|
||
if q.FIFOSequenceNumbers == nil {
|
||
q.FIFOSequenceNumbers = make(map[string]int)
|
||
}
|
||
q.FIFOSequenceNumbers[groupId]++
|
||
return strconv.Itoa(q.FIFOSequenceNumbers[groupId])
|
||
}
|
||
|
||
// IsLocked — проверяет, заблокирована ли FIFO-группа.
|
||
// Группа блокируется при выдаче её сообщения в ReceiveMessage и остаётся
|
||
// заблокированной, пока сообщение in-flight (до DeleteMessage или таймаута
|
||
// видимости) — это гарантирует порядок обработки внутри группы.
|
||
func (q *Queue) IsLocked(groupId string) bool {
|
||
_, ok := q.FIFOMessages[groupId]
|
||
return ok
|
||
}
|
||
|
||
// LockGroup — блокирует FIFO-группу (помечает как занятую).
|
||
//
|
||
// ВАЖНО (фикс): раньше при ПЕРВОМ обращении map пересоздавалась целиком
|
||
// (q.FIFOMessages = map[string]int{groupId: 0}) — блокировки ВСЕХ остальных
|
||
// групп терялись, и их сообщения могли выдаваться одновременно. Теперь map
|
||
// только инициализируется один раз, а для группы проставляется ключ.
|
||
func (q *Queue) LockGroup(groupId string) {
|
||
if q.FIFOMessages == nil {
|
||
q.FIFOMessages = make(map[string]int)
|
||
}
|
||
q.FIFOMessages[groupId] = 0
|
||
}
|
||
|
||
// UnlockGroup — снимает блокировку FIFO-группы.
|
||
// Вызывается после удаления сообщения или при истечении таймаута видимости
|
||
// (см. PeriodicTasks → processQueueTick).
|
||
func (q *Queue) UnlockGroup(groupId string) {
|
||
if _, ok := q.FIFOMessages[groupId]; ok {
|
||
delete(q.FIFOMessages, groupId)
|
||
}
|
||
}
|
||
|
||
// IsDuplicate — проверяет, было ли сообщение с таким deduplicationId уже
|
||
// добавлено в очередь в пределах deduplication-окна.
|
||
//
|
||
// Логическая схема: дубликаты отслеживаются ТОЛЬКО для FIFO-очередей с
|
||
// включённой дупликацией (EnableDuplicates) и непустым deduplicationId.
|
||
// Для остальных случаев дубликаты не отслеживаются (AWS-семантика).
|
||
func (q *Queue) IsDuplicate(deduplicationId string) bool {
|
||
if !q.EnableDuplicates || !q.IsFIFO || deduplicationId == "" {
|
||
return false
|
||
}
|
||
|
||
_, ok := q.Duplicates[deduplicationId]
|
||
|
||
return ok
|
||
}
|
||
|
||
// InitDuplicatation — регистрирует deduplicationId с текущим временем.
|
||
// Эта отметка — начало deduplication-окна; просроченные записи удаляются
|
||
// фоновой задачей PeriodicTasks (deduplication period expired).
|
||
// Для не-FIFO очередей или пустого deduplicationId — no-op.
|
||
func (q *Queue) InitDuplicatation(deduplicationId string) {
|
||
if !q.EnableDuplicates || !q.IsFIFO || deduplicationId == "" {
|
||
return
|
||
}
|
||
|
||
if _, ok := q.Duplicates[deduplicationId]; !ok {
|
||
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-формат). ВАЖНО (инвариант): оценка «в порядке» проверяет лишь
|
||
// согласованность чисел и не перестраивает уже корректные map-индексы — поэтому
|
||
// мутировать Messages ВНЕ методов (AppendMessage/RemoveIndex/Compact/Reset),
|
||
// например менять ReceiptHandle на месте, НЕЛЬЗЯ.
|
||
// Вызывать под 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() {
|
||
// ensureIndexes гарантирует len(deadIdx)==len(Messages) (иначе слоты разъедутся)
|
||
// и корректность deadCount. При согласованном состоянии — две дешёвые проверки.
|
||
q.ensureIndexes()
|
||
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 заново посчитает liveCount/deadCount и заполнит receipt/uuid).
|
||
q.deadIdx = q.deadIdx[: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)
|
||
}
|
||
}
|
||
|
||
// ResetVisibilityAndTryDLQ — сбрасывает видимость сообщения по индексу и, если
|
||
// исчерпан MaxReceiveCount при настроенной DLQ, переносит его в DLQ и удаляет
|
||
// из исходной очереди (tombstone). Возвращает false, если сообщение на самом
|
||
// деле перенесено в DLQ (удалено из очереди).
|
||
//
|
||
// Единый путь для одиночной и батчевой ChangeMessageVisibility: раньше логика
|
||
// DLQ была только в одиночной версии, а батч лишь инкрементировал Retry — из-за
|
||
// этого retry-счётчик и DLQ-перенос могли расходиться.
|
||
// Вызывать под write-блокировкой q. Перенос в DLQ выполняется под её блокировкой.
|
||
// Возвращает: msgUuid, movedToDLQ.
|
||
func (q *Queue) ResetVisibilityAndTryDLQ(i int, receiptHandle string) (msgUuid string, movedToDLQ bool) {
|
||
q.ensureIndexes()
|
||
q.ClearInFlight(i, receiptHandle)
|
||
msg := &q.Messages[i]
|
||
msg.ReceiptTime = time.Now().UTC()
|
||
msg.ReceiptHandle = ""
|
||
msg.VisibilityTimeout = time.Now().Add(time.Duration(q.VisibilityTimeout) * time.Second)
|
||
msg.Retry++
|
||
msgUuid = msg.Uuid
|
||
if q.MaxReceiveCount > 0 && q.DeadLetterQueue != nil && msg.Retry >= q.MaxReceiveCount {
|
||
dlqMsg := *msg
|
||
q.DeadLetterQueue.LockMu()
|
||
q.DeadLetterQueue.AppendMessage(dlqMsg)
|
||
q.DeadLetterQueue.UnlockMu()
|
||
q.RemoveIndex(i)
|
||
return msgUuid, true
|
||
}
|
||
return msgUuid, false
|
||
}
|
||
|
||
// InFlightIndexes — снимок индексов in-flight сообщений (для visibility-тика).
|
||
// Возвращает срез индексов в Messages. Вызывать под write-блокировкой.
|
||
//
|
||
// ВАЖНО: индексы — снимок на момент вызова. Некоторые могут стать неактуальными
|
||
// (handle переехал/сообщение удалено), поэтому потребитель ОБЯЗАН перепроверять
|
||
// каждое сообщение по индексу (живость: в границах среза, не tombstone, handle
|
||
// не пуст) — как это делает processQueueTick.
|
||
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
|
||
}
|