processQueueTick теперь эвиктит сообщения старше retention (SentTime + retention < now): удаляет их из очереди и из Redis (DeleteMessagesPersist), разблокируя FIFO-группы. Retention == 0 означает отсутствие истечения. PeriodicTasks итерируется по (key, queue), чтобы передать queueKey для удаления из Redis. Устраняет забивание очередей до OverLimit.
159 lines
6.4 KiB
Go
159 lines
6.4 KiB
Go
package gosqs
|
|
|
|
import (
|
|
"net/url"
|
|
"time"
|
|
|
|
"shared-sqs/app/models"
|
|
"shared-sqs/app/persistence"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
func init() {
|
|
models.SyncQueues.Queues = make(map[string]*models.Queue)
|
|
}
|
|
|
|
// PeriodicTasks — фоновая обработка очередей, выполняется раз в `d` (1 секунда).
|
|
//
|
|
// Задачи на каждый такт (для КАЖДОЙ очереди, см. processQueueTick):
|
|
// 1. Очистка просроченных записей deduplication-окна (FIFO).
|
|
// 2. Возврат сообщений в видимое состояние по истечении VisibilityTimeout.
|
|
// 3. Перенос сообщений в DeadLetterQueue при достижении MaxReceiveCount.
|
|
//
|
|
// ВАЖНО (фикс производительности): раньше глобальный SyncQueues.Lock удерживался
|
|
// на время обхода ВСЕХ очередей — при большом числе очередей HTTP-обработчики
|
|
// блокировались на секунды (наблюдались ReadTimeout и latency до 54 секунд).
|
|
// Теперь блокировка захватывается ПО ОДНОЙ очереди: между обработкой двух
|
|
// очередей другие горутины могут вклиниться.
|
|
func PeriodicTasks(d time.Duration, quit chan bool) {
|
|
ticker := time.NewTicker(d)
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
// Снапшот (key, указатель) очередей под коротким RLock.
|
|
// Указатели остаются валидными после снятия RLock: очереди из map
|
|
// не удаляются во время итерации, а объекты живут в куче.
|
|
models.SyncQueues.RLock()
|
|
type qEntry struct {
|
|
key string
|
|
queue *models.Queue
|
|
}
|
|
queues := make([]qEntry, 0, len(models.SyncQueues.Queues))
|
|
for key, queue := range models.SyncQueues.Queues {
|
|
queues = append(queues, qEntry{key, queue})
|
|
}
|
|
models.SyncQueues.RUnlock()
|
|
|
|
// Каждую очередь обрабатываем под ОТДЕЛЬНЫМ Lock — блокировка одной
|
|
// очереди не мешает обработке остальных и HTTP-запросам.
|
|
for _, qi := range queues {
|
|
models.SyncQueues.Lock()
|
|
processQueueTick(qi.key, qi.queue)
|
|
models.SyncQueues.Unlock()
|
|
}
|
|
case <-quit:
|
|
ticker.Stop()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// processQueueTick — обработка ОДНОЙ очереди за один такт.
|
|
//
|
|
// ВЫЗЫВАТЬ ТОЛЬКО под удержанием SyncQueues.Lock.
|
|
//
|
|
// Логическая схема:
|
|
// 0. Эвикция сообщений по MessageRetentionPeriod (старше retention удаляются).
|
|
// 1. Deduplication-окно (FIFO): удаляем записи старше DeduplicationPeriod.
|
|
// 2. Проходим по сообщениям очереди:
|
|
// - пустой ReceiptHandle (не in-flight) — пропускаем;
|
|
// - VisibilityTimeout ещё не истёк — пропускаем;
|
|
// - иначе: возвращаем видимость (сброс ReceiptHandle, разблокировка
|
|
// FIFO-группы, обновление ReceiptTime), инкрементируем Retry;
|
|
// - если Retry >= MaxReceiveCount и настроена DLQ — переносим сообщение
|
|
// в DLQ и удаляем из исходной очереди.
|
|
func processQueueTick(key string, queue *models.Queue) {
|
|
// 0. Эвикция просроченных сообщений по MessageRetentionPeriod.
|
|
// Retention == 0 означает «без истечения» (не эвиктируем).
|
|
if queue.MessageRetentionPeriod > 0 {
|
|
cutoff := time.Now().Add(-time.Duration(queue.MessageRetentionPeriod) * time.Second)
|
|
expired := make([]string, 0) // uuid просроченных сообщений для удаления из Redis
|
|
kept := queue.Messages[: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)
|
|
}
|
|
continue
|
|
}
|
|
kept = append(kept, *msg)
|
|
}
|
|
if len(expired) > 0 {
|
|
queue.Messages = kept
|
|
log.Infof("Queue [%s]: evicted %d messages by MessageRetentionPeriod", queue.Name, len(expired))
|
|
// Удаляем просроченные из Redis, чтобы не воскресли после рестарта.
|
|
persistence.DeleteMessagesPersist(key, expired)
|
|
}
|
|
}
|
|
|
|
// 1. Просроченные записи deduplication-окна.
|
|
for dedupId, startTime := range queue.Duplicates {
|
|
if time.Now().After(startTime.Add(models.DeduplicationPeriod)) {
|
|
log.Debugf("deduplication period for message with deduplicationId [%s] expired", dedupId)
|
|
delete(queue.Duplicates, dedupId)
|
|
}
|
|
}
|
|
|
|
// 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 // сообщение не в полёте
|
|
}
|
|
if msg.VisibilityTimeout.After(time.Now()) {
|
|
continue // таймаут видимости ещё действует
|
|
}
|
|
|
|
log.Debugf("Making message visible again %s", msg.ReceiptHandle)
|
|
queue.UnlockGroup(msg.GroupID)
|
|
msg.ReceiptHandle = ""
|
|
msg.ReceiptTime = time.Now().UTC()
|
|
msg.Retry++
|
|
|
|
// 3. Перенос в DLQ при превышении MaxReceiveCount.
|
|
if queue.MaxReceiveCount > 0 &&
|
|
queue.DeadLetterQueue != nil &&
|
|
msg.Retry >= queue.MaxReceiveCount {
|
|
queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, *msg)
|
|
queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...)
|
|
i-- // элемент на позиции i удалён — повторно проверяем новый элемент на ней
|
|
}
|
|
}
|
|
}
|
|
|
|
func numberOfHiddenMessagesInQueue(queue models.Queue) int {
|
|
num := 0
|
|
for _, m := range queue.Messages {
|
|
if m.ReceiptHandle != "" || m.DelaySecs > 0 && time.Now().Before(m.SentTime.Add(time.Duration(m.DelaySecs)*time.Second)) {
|
|
num++
|
|
}
|
|
}
|
|
return num
|
|
}
|
|
|
|
func getQueueFromPath(formVal string, theUrl string) string {
|
|
if formVal != "" {
|
|
return formVal
|
|
}
|
|
u, err := url.Parse(theUrl)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return u.Path
|
|
}
|