# Код-ревью shared-sqs (Opus, 2026-10-10) > Ответ Opus на промпт «сделай код-ревью и посоветуй по архитектуре». > Дословно, без правок. Все файлы из списка прочитаны. Строки взяты из листингов, часть пунктов опирается на прочитанное ранее. Пункты, которые стоит перепроверить перед правкой, помечены «проверить». Теги [Ф1–Ф6] указывают на ваши фокусные области. ## 🔴 Критично: потеря данных, падение процесса, обход безопасности 1. **[Ф1] Fatal `concurrent map iteration and map write` при повторном CreateQueue.** `create_queue.go:84` вызывает `SaveQueue` для существующей очереди под глобальным локом, но без `queue.mu`. `SaveQueue` маршалит `Duplicates`, `FIFO*` и `Tags`, а `send_message` пишет в них под `queue.mu`. Это не panic, а fatal runtime, который `recover` не ловит и который убивает весь процесс. Идемпотентный CreateQueue — частый вызов SDK. Тот же класс ошибки: `admin.go:498-535` и `seed.go`. 2. **[Ф1] `list_queue_tags.go:49-60` отдаёт в ответ ссылку на `queue.Tags`.** `RUnlock` срабатывает по `defer` при return, а XML-маршалинг идёт позже, в router. Параллельный `TagQueue` даёт тот же fatal. Нужна копия мапы. 3. **[Ф1] Инверсия порядка локов и дедлок через ожидающего писателя.** - `queue.mu` → `SyncQueues.RLock`: `set_queue_attributes.go:48,63` → `queue_attributes.go:78`. - `SyncQueues.RLock` → `queue.RLock`: скан в `admin.go:435-486` (подтверждено), `gauge_updater` (проверить, держит ли глобальный RLock). - Любой писатель в `SyncQueues` (`create_queue.go:73`, `delete_queue.go:45`, admin) блокирует новых читателей Go RWMutex. Получается цикл, и сервис встаёт целиком. - Отдельно: `models.go:410` — ABBA и self-дедлок на DLQ (цикл A→B→A или DLQ=self не отсекается в `queue_attributes.go:85`). - `conf/config.go:126-134` — `return` без `Unlock` и дедлок на старте. 4. **[Ф1, Ф3] Redis-I/O под глобальным локом.** `delete_queue.go:45-56` выполняет синхронный `DeleteQueue` под `SyncQueues.Lock`. Комментарии «асинхронно» (`:24,55`) устарели. Redis-пауза 3–5 с останавливает все тенанты. То же в `admin.go:498` и `gosqs.go:107`. 5. **[Ф3] Подтверждённое сообщение теряется при сбое Redis.** - `send_message.go:157` и `send_message_batch.go:133-139,153-158`: при ошибке `SaveMessage(s)Sync` откатывается только `RemoveIndex`. - `Duplicates` и `FIFOSequenceNumbers` не откатываются, `InitDuplicatation` уже сработал. - Ретрай клиента с тем же dedupId получает 200 без сохранения. - Таймаут Redis неоднозначен: запись могла пройти, и после рестарта появится фантом. 6. **[Ф3] Receive, visibility-return и DLQ-перенос не персистятся.** - Затронуты `gosqs.go:124-154`, `change_message_visibility*.go`, `receive_message.go`. - DLQ-копия не пишется (`models.go:409-413`), а `change_message_visibility.go:72-81` делает только Delete в источнике. - После рестарта сообщение воскресает в источнике и пропадает из DLQ. - Порядок при загрузке теряется: `redis.go:504-513` итерирует HGETALL без сортировки, FIFO ломается. 7. **[Ф1, Ф2] Зависание FIFO-групп навсегда.** - `ResetVisibilityAndTryDLQ` (`models.go:389-417`) не делает `UnlockGroup`, путь из `change_message_visibility.go:64` и batch-версии. - `purge_queue.go:63-64` сбрасывает `Duplicates`, но не `FIFOMessages`. - После рестарта `FIFOMessages` из Redis (`redis.go:163,448`) не согласован с in-flight, а тик снимает лок только по `receiptIdx`. 8. **DeleteMessageBatch с пустым `ReceiptHandle` удаляет все неполученные сообщения.** - `delete_message_batch.go:79-97` сопоставляет handle сканом. У невыданных сообщений handle `""` и совпадает с записью `""`. - Query-парсер (`requests.go:568`) отсекает пустой handle, JSON-протокол — нет. - Одиночный delete безопасен: индекс пропускает `""`. 9. **[Ф4] HA — это не HA.** - `leader.go:106-126`: ошибка Refresh не демотирует лидера, lease-проверки нет. - `goaws.go:171-191`: пассив выполняет `PeriodicTasks` и пишет в Redis. Handlers лидерство не проверяют, защита — только `/ready`. - При повышении пассива состояние из Redis не перечитывается: он работает со снимком своего старта. - Без Redis `TryAcquireLock` и `RefreshLock` возвращают true (`redis.go:666-715`), каждый инстанс становится «лидером». - Нет fencing-токена. - `redis.go:61-85`: при неудачном Connect клиент остаётся nil навсегда, reconnect не запускается. 10. **[Ф3, Ф4] Старт с пустым состоянием.** `goaws.go:103-106,125-134` при недоступном Redis или ошибке `LoadAllQueues` продолжает работу. Следом `SaveQueue` перезапишет Redis пустотой. Нужен fail-fast. 11. **[Ф6] Тело подменяется при валидной подписи.** `auth_middleware.go:166-169,234-243` берёт `X-Amz-Content-Sha256` из заголовка и не сверяет с реальным хешем тела. 12. **[Ф6] JWT без проверки подписи.** - `jwt.go`: идентичность берётся из непроверенного payload, `Exp==0` пропускается. - Валидность подтверждает GET на любой из трёх шлюзов, включая dev и test. - `tenant_store.go:213-291`: тенант находится по email из такого токена, возможен захват чужого тенанта. - `cache.go:68-75`: при ошибке сети grace продлевает отозванный токен. 13. **[Ф3] Удалённый тенант воскресает.** `redis.go:594-609` `DeleteTenant` не чистит `ssq:tenant_ak`, а read-through в auth (`:63-74`) его возвращает. Найденный тенант кэшируется навсегда. 14. **[Ф5] OOM-потолок на уровне процесса.** - Лимит только по числу сообщений: 120k × 256 КБ = ~30 ГБ на одну очередь (`validation.go:43-44`). - Лимитов по байтам или памяти нет ни на очередь, ни на тенанта, ни глобально. - Размер атрибутов не входит в проверку (`MaxMessageAttributeSize` объявлен и не используется). - Один тенант валит процесс всем. 15. **[Ф3] `redis.go:33,116-133`: `Client` глобален, `reconnect()` подменяет его без синхронизации.** Это data race, старый клиент не закрывается. go-redis и так переподключается сам, `reconnect()` не нужен. ## 🟡 Существенно **Гонки и конкурентность** - **[Ф1]** `globals.go:77-100`: `GetLocked` отпускает глобальный лок до `LockMu`, и после взятия лока нет проверки, что очередь ещё в map. - Запись в удалённую очередь возвращает 200, а сообщение пропало. - `SaveMessagesSync` пишет `ssq:msg:{key}` уже после `DeleteQueue`, это осиротевшие ключи в Redis. - Аналог: `send_message_batch.go:46-53` → `:105`. - **[Ф3]** `redis.go:215-236`: seq-guard проверяется до HSET (TOCTOU). `:394-416`: проверка seq сразу после инкремента — тавтология, запущенная async-`SaveQueue` воскрешает метаданные после `HDel`. Карты seq растут вечно (`:44-46`). - **[Ф3]** `purge_queue.go:68`: синхронный Redis под `queue.mu`, ошибка не возвращается, память очищена, а Redis нет. Ранее запущенные async `SaveMessage` могут записать сообщение после DEL. **Tombstone и индексы** - **[Ф2]** `models.go:201-208`: `ensureIndexes` тавтологичен. Проверка `liveCount == len-deadCount` не ловит рассинхрон map-индексов. - **[Ф2]** `models.go:262-283`: `RemoveIndex` не обнуляет слот, тело сообщения живёт до компакции. Порог 1024 слота или половина — до 256 МБ мусора на очередь. - **[Ф2]** `RebuildIndexes` на старте разреженный, после загрузки `deadCount=0`: компакции нет, но и мусора нет. Это нормально, если порядок восстановлен сортировкой (см. 🔴6). - `models.go:113-141`: `FIFOSequenceNumbers` и `FIFOMessages` растут без чистки. - `models.go:172-180`: запись в nil-map при `Duplicates==nil`. **Семантика SQS** - Dedup: - `delete_message.go:59` и `delete_message_batch.go:93` делают `delete(Duplicates)` при удалении, окно 5 мин нарушено. - `requests.go:422-423`: `ContentBasedDeduplication` принимается и игнорируется, FIFO без dedupId не дедуплицируется. - Дубль возвращает новый MessageId. - Receive и счётчики: - `Retry++` на таймауте видимости (`gosqs.go:142`): `MaxReceiveCount` считается по таймаутам. - `receive_message.go:203,247`: `ApproximateReceiveCount` завышен на 1. - `ReceiptTime` перезаписывается, `FirstReceiveTimestamp` неверен. - `models.go:405`: `ChangeMessageVisibility(0)` скрывает сообщение ещё на `q.VisibilityTimeout`, а должно быть видно сразу (проверить по receive). - `change_message_visibility_batch.go:72`: нет проверки отрицательных значений. - `queue_attributes.go:41-70`: 0 означает «не передан», и поэтому нельзя вернуть `DelaySeconds`, `ReceiveMessageWaitTimeSeconds` или `VisibilityTimeout` в 0. - `requests.go:35-37,30`: невалидные значения и пустая строка молча игнорируются вместо ошибки. - `queue_attributes.go:73-87`: - нет проверки self-ссылки, цикла, совпадения FIFO-типа и диапазона `maxReceiveCount`; - `DeadLetterQueueName` здесь не выставляется (проверить, заполняет ли его `SaveQueue`, иначе DLQ теряется после рестарта); - `delete_queue.go:53` не отвязывает DLQ-ссылки других очередей. - `create_queue.go:73-85`: повторный CreateQueue с другими атрибутами возвращает 200 вместо `QueueNameExists`. - `create_queue.go:66`, `set_queue_attributes.go:59`: точное сравнение Content-Type. Для `json-1.1` или с `charset` whitelist обходится. - Batch: - `send_message_batch.go:87-103` валидирует пакет целиком, вместо per-entry `Failed`. - Нет суммарного лимита 256 КБ. - Per-message `DelaySeconds` не применяется (`:118-127`, проверить паритет с single). - `delete_message_batch.go:65-72`: дубль handle теряет Id из ответа. - `delete_message_batch.go:79` — O(N) скан вместо `IndexOfReceipt`, при том что `change_message_visibility_batch.go:84` индекс использует. - Код ошибки `"1"`, порядок ответа недетерминирован. - Query-парсеры (`requests.go:235,568,602`) молча обрезают пакет на первой пустой записи. - `get_queue_attributes.go:89-100`: - `Approximate…` включает in-flight и delayed; - timestamp-заглушки `"0000000000"`; - нет `FifoQueue`, `ContentBasedDeduplication`, `Policy`, `…Delayed`, что ломает Terraform и мониторинг. - `list_queues.go:34-43`: скан всех очередей сервиса под глобальным RLock при готовом tenant-индексе. Пагинации нет (`MaxResults` и `NextToken` игнорируются), порядок случайный. - `tag_queue.go:56-60`: нет лимитов на теги (AWS: 50 шт., ключ 128, значение 256). Теги сериализуются на каждый `SaveQueue`, это DoS-вектор. **Производительность [Ф5]** - `receive_message.go:164-216`: два полных скана под write-локом. Long-poll опрашивает каждые 100 мс с эксклюзивным локом на каждого поллера. - `SaveQueue` на каждый send, delete и CMV (`send_message.go:159`, `delete_message*.go`, `change_message_visibility_batch.go:117`): O(Duplicates + FIFO + Tags) под локом. - `gosqs.go:50-54`: один горутин-тик на все очереди последовательно, с синхронным Redis (`:107`). - `redis.go:422-543`: - `LoadAllQueues` грузит всё последовательно, общий таймаут 30 с; - ошибка очереди → `continue`, очередь грузится пустой; - время старта и RAM пропорциональны полному состоянию. **HA и жизненный цикл [Ф4]** - `router.go:214-228`: `/ready` не учитывает Redis, загрузку состояния и shutdown. `ReadyFunc` — глобальная переменная. - `goaws.go:248-268`: - `billing.Close()` идёт до дренажа HTTP; - async Redis-записи не дренируются; - не-ready на время дренажа нет; - `log.Fatalf` в `:245` не отпускает лок; - `leader.go:63-76`: `Stop` освобождает лок до дренажа. - `goaws.go:204-211`: нет `ReadTimeout` (медленное тело), лимитов соединений и rate-limit. Тело читается целиком до проверки подписи. **Безопасность [Ф6]** - SigV4: - `auth_middleware.go:296-326` не требует подписи `host`, `x-amz-date`, `x-amz-target`; - `:158` clock skew 15 мин (в AWS — 5); - `:215` presigned не проверяет дату из будущего; - `:290` `canonicalURI` зависит от нормализации nginx и заголовка Host; - превышение лимита тела даёт 403 `SignatureDoesNotMatch`, а не 413 (`router.go:124-133`); - rate-limit по неудачным попыткам нет, ошибки раскрывают причину (`:81`). - `auth_middleware.go:63-74`: каждый запрос с неизвестным accessKey ходит в Redis (HGET до 3 с) без негативного кэша. `jwt.go:90-133` — до трёх последовательных запросов по 10 с на токен, без singleflight. - `admin.go`: - статичный Bearer без rate-limit; - `:223` `jwtMiddleware` пропускает путь по `HasSuffix("/auth")`; - `:263-267` IDOR-защита только если в `mux.Vars` есть id; - `:409-420` `deleteTenant`: in-flight handlers пишут в удалённые очереди; - `:194,207-212` токен и `SecretKey` отдаются в теле ответа; - `:640-669` `sendMessageToQueue` без лимита `MaxMessagesForQueue` и с async-сохранением. - `tenant_store.go`: секреты в Redis открытым текстом. Нет проверки уникальности email (`:58-108`), `MaxQueues` перезаписывается при каждом логине (`:240`), `LoadTenant` затирает свежие правки (`:194-202`). - `router.go:72-74`: маршруты `/{account}` и `/{account}/{queueName}` перекрывают `/admin/*` и `/health` для POST. Bypass в auth по этим путям fail-open, но nil-проверка tenant присутствует во всех 17 просмотренных handlers. - `goaws.go:148-150`: при `SEED_DEMO` креды демо-тенанта пишутся в лог. **Прочее** - `router.go:250-277`: `countBatchEntries` работает только с form, для JSON-протокола возвращает 0 и биллинг батчей занижен. `:297` `msgBytes` — размер запроса, а не ответа. - `billing.go`: - канал 1024, переполнение дропает записи; - `db.Exec` без таймаута; - `Close()` обнуляет `db` без синхронизации с воркером (data race); - DSN собирается через Sprintf. - `metrics`: - label tenant: accessKey в gauge и `tenant.ID` в counters, accessKey утекает в метрики; - `Reset()` gauge даёт окно пустых значений; - label не чистится при удалении тенанта. - `conf/config.go`: - `:111` yaml-очереди кладутся без префикса `accessKey:` и обходят tenant-изоляцию; - `:189-196` при пустой RedrivePolicy `queues[deadLetterQueueName]` ищется без tenant-префикса. - `seed.go`: ARN собирается с `t.ID` вместо `AccountID`, очереди без FIFO/DLQ-полей, `SaveMessages` асинхронный. ## 🟢 Мелочи - Расхождение дефолтов: - `build.go:19-32` (admin) задаёт `VisibilityTimeout`, `MaxSize`, `Retention` напрямую; - `CreateQueueV1` берёт их из `NewCreateQueueRequest` по yaml; - при нулях в yaml пути разойдутся. - Мёртвый код и дубли: - `validation.go`: `ClampInt`, `ValidateMessageAttributes`, `MaxRequestBodySize=1MB` (router — 3 МБ), `MaxTenantsDefault` дублирует `MaxTenantsGlobal`; - `AvailableQueueAttributes` и `AttrNameWhitelist` — два списка; - `DeduplicationPeriod` — `var`; - `send_message.go:73` — мёртвая ветка `mux.Vars`. - `BaseResponseMetadata.RequestId` — нулевой UUID во всех ответах: корреляция невозможна. - `models/helpers.go`: `rand.Intn(max-min)` паникует при `max