6.4 KiB
P1.2 — индексация сообщений: дизайн и решение (2026-10-07)
Статус: ЗАКРЫВАЕМ отсрочку от 2026-10-06. Задача — сделать DeleteMessage и visibility-перерасчёт НЕ зависящими от полного размера очереди. Источник:
plan-for-flash-2026-10-05.md(P1.2),p1.1-per-queue-lock-analysis-2026-10-06.md.
1. Постановка
Queue.Messages []SqsMessage — единственное хранилище сообщений. Пути:
| Путь | Сейчас |
|---|---|
ReceiveMessage (receiveMessagesUnderLock) |
скан всего среза (нужен — ищем готовые) |
| DeleteMessage | скан среза по ReceiptHandle, затем сдвиг append(Messages[:i], Messages[i+1:]...) |
| DeleteMessageBatch | полная пересборка среза |
| ChangeMessageVisibility(±Batch) | скан по ReceiptHandle |
processQueueTick (видимость + DLQ) |
скан всего среза |
| Retention-эвикция | скан всего среза (компакция на месте) |
numberOfHiddenMessagesInQueue, admin/health/peek/metrics |
скан |
2. Почему «просто индекс receipt→index» недостаточно
Индекс поверх среза не даёт O(1) на удалении: физическое удаление из середины среза сдвигает все последующие элементы и ломает все сохранённые индексы (O(N) на переиндексацию). Именно это было причиной отсрочки 2026-10-06.
3. Решение: tombstone + индексы + амортизированная компакция
Ключевая идея: удаление не двигает элементы. Слот помечается «мёртвым»
(tombstone) за O(1); физическая чистка (компакция) выполняется редко —
раз в секунду в processQueueTick и по порогу при удалении.
Это сохраняет порядок сообщений (важно для FIFO и порядка выдачи), не меняет Redis-схему (сообщения и так лежат отдельными HASH-полями) и делает индексы стабильными между компакциями.
3.1. Новые поля models.Queue (все json:"-", в Redis не попадают)
Messages []SqsMessage // физическое хранилище; слот может быть tombstone
deadIdx []bool // параллельный Messages: true = слот удалён
receiptIdx map[string]int // ReceiptHandle → индекс (только in-flight)
uuidIdx map[string]int // Uuid → индекс (только живые)
liveCount int // число живых сообщений
deadCount int // число tombstone-слотов
Схема Redis не меняется: queueMetaDTO сообщения не содержит, а сами сообщения
хранятся отдельными полями ssq:msg:{key} → UUID. Tombstone-сообщения из Redis
удаляются сразу (как и сейчас, HDel синхронно), поэтому «воскрешения» после
рестарта не будет.
3.2. API Queue (примитивы, мутировать Messages напрямую запрещено)
AppendMessage(m SqsMessage) int— добавить (append) + проставить индексы;RemoveIndex(i int)— tombstone за O(1) + снять с индексов;IndexOfReceipt(rh string) (int, bool)/IndexOfUUID(u string) (int, bool);MarkInFlight(i int, rh string)/ClearInFlight(i int, rh string);IsDead(i int) bool,LiveCount() int,InFlightIndexes() []int;Compact()— физическое удаление tombstone + перестройка индексов (O(N));Reset()— очистка (purge);RebuildIndexes()— восстановление индексов (загрузка из Redis, ленивая защита).
Индексы строятся лениво (ensureIndexes), поэтому Queue, собранные литералом
в тестах/загрузчике, работают без изменений вызовов.
3.3. Что становится O(1) / O(in-flight)
- DeleteMessage →
IndexOfReceipt+RemoveIndex— O(1) (+ редкая компакция). - ChangeMessageVisibility(±Batch) →
IndexOfReceipt— O(1) на запись. - visibility-перерасчёт в тике → итерация ТОЛЬКО по
receiptIdx(in-flight), а не по всем сообщениям — O(in-flight). - DLQ-перенос в тике — внутри той же итерации по in-flight.
3.4. Что осознанно остаётся O(N)
- receive — нужно найти подходящие сообщения; полный O(1) требует ready-heap и в объём P1.2 не входит (критерий плана — delete + visibility).
- retention-эвикция — раз в секунду, скан (отдельный пункт P1.3, уже сделан).
Компакция порогом: при deadCount > 1024 && deadCount > liveCount — прямо в
RemoveIndex (амортизированно O(1) на удаление).
4. Затронутые файлы
app/models/models.go, app/models/helpers.go, app/gosqs/{receive_message, delete_message, delete_message_batch, change_message_visibility, change_message_visibility_batch, gosqs, send_message, send_message_batch, get_queue_attributes}.go, app/admin/admin.go, app/metrics/gauge_updater.go,
app/persistence/redis.go, app/gosqs/tenant_flow_test.go.
5. Критерий готовности
DeleteMessage/ChangeMessageVisibilityне сканируют срез.- visibility-перерасчёт в тике идёт по числу in-flight, не по размеру очереди.
- Все существующие тесты зелёные; добавлены тесты на tombstone/компакцию.
- Redis-схема не изменилась; рестарт не «воскрешает» удалённые сообщения.