89 lines
7.0 KiB
Markdown
89 lines
7.0 KiB
Markdown
# 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.
|
||
|