Files
SQS-service/doc/thinking/p1.1-per-queue-lock-analysis-2026-10-06.md
T

89 lines
7.0 KiB
Markdown
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.
# P1.1 (per-queue mutex) — анализ и состояние (2026-10-06)
## Зачем нужен
Сейчас все операции с очередями держат ОДИН глобальный `models.SyncQueues.RWMutex`.
Проблемы:
1. Любая операция блокирует весь сервис. При этом часть операций делает
**синхронный Redis-I/O прямо под глобальным локом** (таймаут до 3–5с):
`SaveMessageSync` (send), `DeleteMessagePersist` (delete), `DeleteMessagesPersist`
(retention-tick), `PurgeMessagesPersist` (purge), `DeleteQueue`. При деградации
Redis весь сервис встаёт на секунды.
2. `receiveMessagesUnderLock` держит глобальный лок на время O(N)-скана сообщений.
## Введённый groundwork (безопасно, поведение не меняется)
- `models.Queue` получил поле `mu sync.RWMutex` (не сериализуется) + методы
`LockMu/UnlockMu/RLockMu/RUnlockMu`.
- Исправлено копирование `Queue` by value (`numberOfHiddenMessagesInQueue`) —
иначе `go vet` ругался на copylocks.
- Глобальный лок пока используется везде (как раньше) — **поведение корректно**.
## Блокеры полного перехода (почему это не механическая замена)
1. **DLQ — общий указатель.** `Queue.DeadLetterQueue *Queue`. Перенос сообщения в
DLQ мутирует `DeadLetterQueue.Messages` из-под лока ИСХОДНОЙ очереди
(`gosqs.go:130`, `change_message_visibility.go:76`). При per-queue локах нужен
порядок взятия (сначала исходная, потом DLQ) + гарантия отсутствия цикла.
2. **`marshalQueueMeta` мутирует ЧУЖУЮ очередь.** `persistence/redis.go:146-160`
временно зануляет `queue.Messages` И `queue.DeadLetterQueue.Messages` ради
сериализации. Под глобальным локом это безопасно; под per-queue — гонка с
операциями над DLQ. Нужен НЕмутирующий снапшот (DTO без `Messages`),
но нельзя копировать `Queue` целиком (копируется мьютекс) → нужен отдельный
тип-снапшот.
3. **DLQ-указатель после загрузки из Redis не релинкуется** на объект из map
(загружается как вложенная копия) — предсуществующая проблема, которую
P1.1 обнажит (переносы в DLQ пойдут в detached-копию).
4. **Объём — атомарный.** Переводить на per-queue локи надо СРАЗУ все операции
с содержимым очереди (send/receive/delete/visibility/purge/attributes/tags/
admin/gauge-tick) — частичный перевод создаёт рассинхрон (операции перестанут
взаимоисключаться). Это ~20+ мест в 8 файлах.
5. **Нет покрытия** для FIFO/batch/DLQ/visibility — самых сложных путей.
## Вывод
P1.1 — это дизайнерская задача (порядок локов + неммутирующий снапшот + релинк
DLQ), а не построчная замена. Делать её «вслепую» без покрытия FIFO/batch/DLQ
рискованно: ошибка = дедлоки/потеря данных на проде. Рекомендуется отдельный
проход: сначала тесты на FIFO/DLQ/batch, затем атомарный перевод.
## СТАТУС: РЕАЛИЗОВАНО (2026-10-06)
Перевод выполнен атомарно. Что сделано:
- `marshalQueueMeta` больше НЕ мутирует очередь (DTO-снапшот queueMetaDTO).
- Все операции с содержимым очереди используют per-queue лок
(`QueueStore.GetLocked/GetRLocked` + `queue.LockMu/RUnlockMu`) в gosqs, admin,
metrics/gauge_updater, PeriodicTasks.
- Глобальный `SyncQueues`-лок остался ТОЛЬКО для доступа к map и неизменяемых
полей (create/delete/list/find/snapshot).
- DLQ-переносы берут лок DLQ отдельно (порядок main→DLQ).
- Устранён дедлок в create_queue (setQueueAttributesV1 вызывается ВНЕ глобального лока).
- Добавлены конкурентные тесты (два тенанта / одна очередь), прогон `go test -race` — зелёный.
## P1.2 (индексация сообщений) — та же природа
Индексация `Messages` (receipt→index) меняет структуру хранения, затронутую теми
же путями (receive/delete/visibility/tick/сериализация), и требует покрытия тех
же путей. Рекомендуется вместе с P1.1.
## P1.2 — СТАТУС: ОТЛОЖЕНО (обоснование, 2026-10-06)
После P1.1 главный боттлнек (глобальный лок на синхронный Redis-I/O и O(N)-скан
по ВСЕМ очередям) устранён: теперь O(N)-сканы ограничены ОДНОЙ очередью и не
блокируют остальные. Индекс `receipt→index` поверх срезa `[]SqsMessage` НЕ даёт
O(1): удаление из середины среза сдвигает все последующие индексы (O(N) на
переиндексацию). Реальный выигрыш требует смены контейнера (map+order-list), что
затрагивает сериализацию и все пути — крупная рискованная правка с маргинальной
выгодой на текущих объёмах. Отложено до появления реальной нагрузки/лимитов.
## P1.2 — СДЕЛАНО (2026-10-07)
Возврат к пункту по прямому указанию. Решение — tombstone + receipt/uuid-индексы
+ амортизированная компакция (удаление НЕ сдвигает срез → индексы стабильны,
O(1)-удаление без смены контейнера и без изменения Redis-схемы).
Дизайн и результаты: `p1.2-message-index-design-2026-10-07.md`.
По ходу найден и исправлен дефект утечки per-queue блокировки в receive
(возврат до `defer UnlockMu`) — см. там же, раздел 6.