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

6.3 KiB

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), что затрагивает сериализацию и все пути — крупная рискованная правка с маргинальной выгодой на текущих объёмах. Отложено до появления реальной нагрузки/лимитов.