P1.1 groundwork: per-queue mutex в Queue + анализ блокеров

Добавлены поле mu (не сериализуется) и LockMu/RLockMu-аксессоры в models.Queue;
исправлено копирование Queue by value (numberOfHiddenMessagesInQueue).
Глобальный лок пока используется везде — поведение не менялось.
Задокументированы блокеры полного перехода (DLQ-указатель, мутирующий
marshalQueueMeta, релинк DLQ после загрузки, атомарность перевода).
This commit is contained in:
Repinoid
2026-10-06 18:39:26 +03:00
parent 3a52960d5b
commit ed17c6cbd0
4 changed files with 70 additions and 3 deletions
@@ -0,0 +1,54 @@
# 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, затем атомарный перевод.
## P1.2 (индексация сообщений) — та же природа
Индексация `Messages` (receipt→index) меняет структуру хранения, затронутую теми
же путями (receive/delete/visibility/tick/сериализация), и требует покрытия тех
же путей. Рекомендуется вместе с P1.1.