# 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.