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:
+44
-24
@@ -74,28 +74,34 @@ func PeriodicTasks(d time.Duration, quit chan bool) {
|
||||
// - если Retry >= MaxReceiveCount и настроена DLQ — переносим сообщение
|
||||
// в DLQ и удаляем из исходной очереди.
|
||||
func processQueueTick(key string, queue *models.Queue) {
|
||||
// Ленивая починка индексов (P1.2): очередь могла быть собрана литералом или
|
||||
// загружена из Redis — тогда индексы строятся здесь один раз.
|
||||
queue.EnsureIndexes()
|
||||
|
||||
// 0. Эвикция просроченных сообщений по MessageRetentionPeriod.
|
||||
// Retention == 0 означает «без истечения» (не эвиктируем).
|
||||
// Удаление — tombstone за O(1); физическая чистка — CompactIfNeeded ниже.
|
||||
if queue.MessageRetentionPeriod > 0 {
|
||||
cutoff := time.Now().Add(-time.Duration(queue.MessageRetentionPeriod) * time.Second)
|
||||
expired := make([]string, 0) // uuid просроченных сообщений для удаления из Redis
|
||||
// Компакция НА МЕСТЕ: читаем значение в i, пишем в n (n <= i) — без
|
||||
// алиасинга на переиспользуемом срезе и без лишней аллокации.
|
||||
n := 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)
|
||||
}
|
||||
if queue.IsDead(i) {
|
||||
continue
|
||||
}
|
||||
queue.Messages[n] = msg
|
||||
n++
|
||||
msg := queue.Messages[i]
|
||||
if !msg.SentTime.Before(cutoff) {
|
||||
continue
|
||||
}
|
||||
expired = append(expired, msg.Uuid)
|
||||
if msg.GroupID != "" {
|
||||
queue.UnlockGroup(msg.GroupID)
|
||||
}
|
||||
if msg.ReceiptHandle != "" {
|
||||
queue.ClearInFlight(i, msg.ReceiptHandle)
|
||||
}
|
||||
queue.RemoveIndex(i)
|
||||
}
|
||||
if len(expired) > 0 {
|
||||
queue.Messages = queue.Messages[:n]
|
||||
log.Infof("Queue [%s]: evicted %d messages by MessageRetentionPeriod", queue.Name, len(expired))
|
||||
// Удаляем просроченные из Redis, чтобы не воскресли после рестарта.
|
||||
persistence.DeleteMessagesPersist(key, expired)
|
||||
@@ -111,18 +117,25 @@ func processQueueTick(key string, queue *models.Queue) {
|
||||
}
|
||||
|
||||
// 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 // сообщение не в полёте
|
||||
// Итерируем ТОЛЬКО по in-flight (receiptIdx), а не по всему срезу — стоимость
|
||||
// пропорциональна числу сообщений «в полёте», а не размеру очереди (P1.2).
|
||||
log.Debugf("Queue [%s] live [%d]", queue.Name, queue.LiveCount())
|
||||
now := time.Now()
|
||||
for _, i := range queue.InFlightIndexes() {
|
||||
if i < 0 || i >= len(queue.Messages) || queue.IsDead(i) {
|
||||
continue
|
||||
}
|
||||
if msg.VisibilityTimeout.After(time.Now()) {
|
||||
msg := &queue.Messages[i]
|
||||
rh := msg.ReceiptHandle
|
||||
if rh == "" {
|
||||
continue // уже возвращено в видимость
|
||||
}
|
||||
if msg.VisibilityTimeout.After(now) {
|
||||
continue // таймаут видимости ещё действует
|
||||
}
|
||||
|
||||
log.Debugf("Making message visible again %s", msg.ReceiptHandle)
|
||||
log.Debugf("Making message visible again %s", rh)
|
||||
queue.ClearInFlight(i, rh)
|
||||
queue.UnlockGroup(msg.GroupID)
|
||||
msg.ReceiptHandle = ""
|
||||
msg.ReceiptTime = time.Now().UTC()
|
||||
@@ -132,18 +145,25 @@ func processQueueTick(key string, queue *models.Queue) {
|
||||
if queue.MaxReceiveCount > 0 &&
|
||||
queue.DeadLetterQueue != nil &&
|
||||
msg.Retry >= queue.MaxReceiveCount {
|
||||
dlqMsg := *msg
|
||||
queue.DeadLetterQueue.LockMu()
|
||||
queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, *msg)
|
||||
queue.DeadLetterQueue.AppendMessage(dlqMsg)
|
||||
queue.DeadLetterQueue.UnlockMu()
|
||||
queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...)
|
||||
i-- // элемент на позиции i удалён — повторно проверяем новый элемент на ней
|
||||
queue.RemoveIndex(i)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Компакция tombstone-слотов (амортизированно): срез не растёт бесконечно.
|
||||
queue.CompactIfNeeded()
|
||||
}
|
||||
|
||||
func numberOfHiddenMessagesInQueue(queue *models.Queue) int {
|
||||
num := 0
|
||||
for _, m := range queue.Messages {
|
||||
for i := range queue.Messages {
|
||||
if queue.IsDead(i) {
|
||||
continue
|
||||
}
|
||||
m := &queue.Messages[i]
|
||||
if m.ReceiptHandle != "" || m.DelaySecs > 0 && time.Now().Before(m.SentTime.Add(time.Duration(m.DelaySecs)*time.Second)) {
|
||||
num++
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user