Возврат до defer queue.UnlockMu() оставлял блокировку захваченной навсегда —
очередь «зависала»: все последующие Lock/RLock ждали вечно. Проявлялось при
LiveCount()==0 (все сообщения удалены/tombstone). Найдено стресс-тестом
(2000 сообщений × циклы): стек горутин показал массовое ожидание RLockMu.
- receive: defer до проверки пустоты.
- регрессионный тест TestReceiveEmptyQueue_DoesNotLeakLock (падает на дефекте,
проходит на фиксе).
- models.Queue: deadIdx/receiptIdx/uuidIdx/liveCount/deadCount + примитивы
(AppendMessage/RemoveIndex/Compact/CompactIfNeeded/Reset/EnsureIndexes/
IndexOfReceipt/IndexOfUUID/MarkInFlight/ClearInFlight/InFlightIndexes/LiveCount).
Удаление — tombstone за O(1), физическая чистка амортизированная.
- DeleteMessage/ChangeMessageVisibility(±Batch): поиск по индексу вместо скана
среза; удаление без сдвига остатка.
- DeleteMessageBatch: tombstone вместо полной пересборки среза.
- processQueueTick: visibility/DLQ по индексу in-flight (O(in-flight)),
retention — tombstone, в конце — CompactIfNeeded.
- send-пути: AppendMessage + LiveCount (OOM-лимит по живым).
- admin/metrics/persistence: учёт живых сообщений, индексы после загрузки из Redis.
- Redis-схема не менялась (сообщения и так в per-message HASH).
1. DLQ больше не сериализуется вложенным объектом (была detached-копия после
загрузки + неограниченная рекурсия при цикле): в метаданных хранится только
имя (dead_letter_queue_name), после LoadAllQueues указатель релинкуется на
объект из map. Закрывает потерянные переносы в DLQ и чужой мьютекс.
2. Retention-эвикция: компакция НА МЕСТЕ (чтение value в i, запись в n<=i)
вместо kept := Messages[:0] — устранён алиасинг на переиспользуемом срезе.
3. countBatchEntries: явный req.ParseForm() — корректный msg_count для батчей.
Тесты: +DLQ-перенос, +эвикция с сохранением порядка; go test -race зелёный.
Добавлены gosqs.BuildQueueForTenant (URL/ARN/дефолты/FIFO-детект как в CreateQueueV1)
и gosqs.BuildSqsMessage (UUID+MD5). admin.createTenantQueue и sendMessageToQueue
переведены на них: раньше UI создавал очередь без URL/ARN/IsFIFO и сообщение без MD5,
теперь поведение UI == SQS API + проверка размера при отправке.
Все операции с содержимым очереди (send/receive/delete/visibility/purge/attributes/
tags/admin/gauge/tick) переведены на per-queue RWMutex (QueueStore.GetLocked/
GetRLocked + queue.LockMu). Глобальный SyncQueues-лок остался только для map и
неизменяемых полей. DLQ-переносы берут лок DLQ отдельно (порядок main→DLQ).
marshalQueueMeta больше не мутирует очередь (DTO-снапшот). Устранён дедлок
create_queue (setQueueAttributesV1 вызывается вне глобального лока). Добавлены
конкурентные тесты; go test -race зелёный. Снимает боттлнек: синхронный Redis-I/O
больше не держит глобальный лок.
Добавлены поле mu (не сериализуется) и LockMu/RLockMu-аксессоры в models.Queue;
исправлено копирование Queue by value (numberOfHiddenMessagesInQueue).
Глобальный лок пока используется везде — поведение не менялось.
Задокументированы блокеры полного перехода (DLQ-указатель, мутирующий
marshalQueueMeta, релинк DLQ после загрузки, атомарность перевода).
Введён тип QueueStore с индексом accessKey→{queueName}. Мутации очередей идут через
AddQueue/RemoveQueue (create/delete/restore/config/seed/admin), подсчёт и список —
через CountTenantQueues/TenantQueueNames. Устранены O(total) скан-по-префиксу в
countTenantQueues, deleteTenant, listTenantQueues, createTenantQueue. API SyncQueues
(Queues, Lock) сохранён — остальной код не затронут.
RecordUsage теперь кладёт запись в буферизованный канал (1024), один воркер пишет
в PG — убран навал горутин, ждущих коннект пула (было 5). При переполнении буфера
запись дропается с логом (биллинг жертвуем ради стабильности). Для batch-операций
(SendMessageBatch/DeleteMessageBatch/ChangeMessageVisibilityBatch) msg_count теперь
реальное число записей, а не всегда 1.
Connect сохраняет параметры подключения и запускает фоновую проверку (ping каждые
30с). При обрыве Redis-клиент оставался не-nil и записи молча падали в лог; теперь
соединение восстанавливается автоматически через reconnect().
POST /admin/tenants теперь принимает опциональные access_key/secret_key. Если
задано любое из полей — оба обязательны (мин. длина 8/16). С произвольными
кредами tenantID генерируется случайно (CreateFixedAutoID). Без кредов —
поведение прежнее (store.Create).
/metrics отдаёт per-tenant данные (accessKey, число очередей и сообщений), что
является утечкой наружу на публичном endpoint. Теперь /metrics защищён тем же
Bearer admin-токеном (timing-safe сравнение). Скрепперу VM токен задаётся в конфиге.
bearerAuthMiddleware теперь сравнивает Authorization через subtle.ConstantTimeCompare
(с предварительной проверкой длины), а не через != — устраняет timing side-channel
при подборе admin-токена.
Удалены authenticateUIDemoToken, uiDemoToken/uiDemoTenantID/uiDemoEmail и их
константы (demo-ui-shared-sqs-ngcloud-2026) — публичный путь входа в UI мимо
JWT-валидации. seed.go теперь создаёт demo-тенанта со СЛУЧАЙНЫМИ кредами
(tenant.Create), без захардкоженного SecretKey в исходнике. Тест
TestJWTAuth_AllowsDemoToken заменён на TestJWTAuth_RejectsMissingToken.
processQueueTick теперь эвиктит сообщения старше retention (SentTime + retention < now):
удаляет их из очереди и из Redis (DeleteMessagesPersist), разблокируя FIFO-группы.
Retention == 0 означает отсутствие истечения. PeriodicTasks итерируется по (key, queue),
чтобы передать queueKey для удаления из Redis. Устраняет забивание очередей до OverLimit.
13 файлов *_test.go требовали отсутствующие в git пакеты app/fixtures и
app/test, и даже с ними падали бы: написаны под дофорковый GoAws, где не было
тенанта в контексте и tenant-scoped ключей {accessKey}:{queueName}.
Причина зафиксирована в doc/thinking/tests-inherited-broken-2026-10-06.md.
Тесты требуют тенанта из контекста и tenant-scoped ключей, а написаны под
дофорковый GoAws. Восстановление app/fixtures + app/test сделает их
компилируемыми, но падающими. Нужны новые тесты под мультитенантное поведение.
Раньше любой статус, кроме 401/403 (включая 5xx/429/редиректы), считался
подтверждением токена. Теперь валидность подтверждается только явным 2xx;
401/403 — отклонён, прочие статусы — ошибка стенда (пробуем следующий).
Добавлен persistence.SaveMessageSync (синхронный HSet, возвращает ошибку).
SendMessage теперь пишет сообщение в Redis ДО возврата клиенту; при ошибке Redis
сообщение откатывается из памяти и возвращается InternalError (новая ошибка в
models/errors.go). Устраняет окно потери подтверждённого сообщения при краше пода
между 200 OK и асинхронной записью.
CreateFromJWT больше не выводит ID/AccessKey/SecretKey из email через SHA256
с захардкоженной солью (это позволяло вычислить чужие креды по публичному email).
Креды генерируются случайно через crypto/rand. Удалены tenantIDFromEmail,
accessKeyFromEmail, secretKeyFromEmail, secretKeySalt и ветка legacy-пересчёта.
Идемпотентность по email сохранена (повторный логин возвращает тот же тенант).