Files
SQS-service/app/models/models.go
T
Repinoid 31840b9756 fix: DLQ-релинк после загрузки из Redis + безопасная эвикция retention + ParseForm
1. DLQ больше не сериализуется вложенным объектом (была detached-копия после
   загрузки + неограниченная рекурсия при цикле): в метаданных хранится только
   имя (dead_letter_queue_name), после LoadAllQueues указатель релинкуется на
   объект из map. Закрывает потерянные переносы в DLQ и чужой мьютекс.
2. Retention-эвикция: компакция НА МЕСТЕ (чтение value в i, запись в n<=i)
   вместо kept := Messages[:0] — устранён алиасинг на переиспользуемом срезе.
3. countBatchEntries: явный req.ParseForm() — корректный msg_count для батчей.
Тесты: +DLQ-перенос, +эвикция с сохранением порядка; go test -race зелёный.
2026-10-07 07:18:31 +03:00

166 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
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()
}
}