Files
SQS-service/doc/thinking/opus-report-analysis-2026-10-10.md
T

91 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Критический разбор код-ревью Opus (2026-10-10)
> Дополняет `opus-report-2026-10-10.md` (дословный отчёт Opus).
> Здесь — проверка ключевых пунктов по коду + что подтверждено/отклонено + решения, которые нужны.
## 1. Вердикт
Отчёт сильный по конкуренции и durability. Главный дефект отчёта — смешивает три категории
без разделения: **(а)** реальные баги, **(б)** осознанный дизайн (JWT trusted-perimeter),
**(в)** отклонения от семантики AWS (в основном косметика). Часть 🔴 надо понизить.
## 2. Подтверждено по коду (критично, чинится без владельца)
### 2.1. 🔴1 — фатал `concurrent map iteration and map write` в CreateQueue
- `create_queue.go:73-84`: ветка «очередь уже существует» вызывает `SaveQueue(key, queue)`
под `SyncQueues.Lock`, **без** `queue.mu`.
- `SaveQueue` → `json.Marshal` (`redis.go:208,220`) итерирует `Duplicates`, `FIFOSequenceNumbers`,
`FIFOMessages`, `Tags`.
- Мутаторы этих map (send/tag/CMV) держат только `queue.mu` (`GetLocked` берёт глобальный RLock
на миг и отпускает — `globals.go:77-88`), поэтому глобальный Lock их не блокирует.
- Условие: идемпотентный `CreateQueue` существующей FIFO-очереди (обычный вызов SDK при старте)
+ параллельный `SendMessage`. Вероятность растёт с размером map. Необратимый краш процесса
(`recover` не ловит fatal).
- Шапка `redis.go:211-213` («вызывать под `SyncQueues.Lock`») описывает неверный контракт.
### 2.2. 🔴5 — неполный откат dedup при сбое Redis
- `send_message.go:151-160`: при ошибке `SaveMessageSync` откатывается только `RemoveIndex`,
а `InitDuplicatation` выполняется всё равно → ретрай с тем же dedupId даёт 200, но сообщение
не сохранено. Аналог в `send_message_batch.go`.
- `FIFOSequenceNumbers` (инкремент в `models.go:117`) тоже не откатывается.
### 2.3. 🔴8 — пустой `ReceiptHandle` в DeleteMessageBatch (protocol-gated)
- Срабатывает **только** при `Content-Type: application/x-amz-json-1.0`: тело декодируется
напрямую (`utils.go:35-46`), валидация `receiptHandle == ""` (`requests.go:568`, Query-форма)
на этом пути не вызывается.
- `delete_message_batch.go:51-57` проверяет только уникальность `Id`, handle не валидируется.
- `deleteMessageMap[""]` совпадает с невыданными сообщениями (у них `ReceiptHandle == ""`),
цикл `:79-98` удаляет все такие сообщения очереди.
- Одиночный `DeleteMessage` безопасен: пустой handle не кладётся в `receiptIdx`
(`models.go:233,251,373`).
### 2.4. Дополнительно подтверждено (не тестировалось, статический разбор)
- Инверсия порядка локов (`queue.mu` ↔ `SyncQueues.RLock`) и Redis-I/O под глобальным локом
(`delete_queue.go:45-56`).
- Fail-fast отсутствует: старт с недоступным Redis продолжает работу и следом `SaveQueue`
перезаписывает Redis пустотой (`goaws.go:103-106,125-134`).
## 3. Решение владельца: JWT
`jwt.go:7-11` прямо документирует trusted-perimeter: «Подпись JWT НЕ проверяется — аналогично
sless», валидация через `PingNubesAPI` (GET на 3 шлюза, приём по 2xx).
**Ключевой факт, который не проверен:** проверяет ли шлюз подпись и отдаёт 200 только для
валидных токенов? От этого зависит классификация:
- шлюз проверяет подпись → «доверяем шлюзу», не уязвимость;
- нет → 🔴, чинить первым делом.
Независимо от ответа чинятся три вещи:
1. `jwt.go:90-92` — три шлюза (prod/test/dev): токен тестового стенда принимается как prod.
Оставить prod-шлюз.
2. `cache.go:68-75` — grace при сетевой ошибке продлевается повторно → отозванный токен живёт
бесконечно. Нужен потолок суммарного grace (например, 2 мин от последней успешной проверки).
3. `tenant_store.go:213-291` — идентичность по email; привязать к стабильному `sub`/`iss`,
иначе смена email на IdP отдаёт чужой тенант.
## 4. Решение владельца: лимит по байтам (OOM, 🔴14)
Сейчас лимит только по числу сообщений (`validation.go:43-44`), по байтам/памяти — нет.
Нужны входные данные владельца: RAM ВМ и целевое число тенантов.
Структура (рекомендация):
- **Cap на тенанта** (суммарные байты тел+атрибутов по всем очередям) — изоляция.
- **Cap на очередь** — производный (доля тенантского лимита).
- **Глобальный предохранитель** — отказ send при RSS > порога (напр. 70% RAM), единственная
защита от суммы тенантов.
Реализация — счётчик байт в `Queue` (append/delete/purge/compact/restore) + тенантский
аккумулятор → требует `QueueStore`-интерфейса (план A).
Минимальный шаг без решения по квотам: снизить `MaxMessagesPerQueue` + глобальный
предохранитель по RSS.
## 5. Порядок исправления (после решений владельца)
1. 🔴1 — убрать `SaveQueue` без `queue.mu` в `create_queue.go:84` (и `admin.go`/`seed.go`).
2. 🔴8 — валидация `ReceiptHandle != ""` в `delete_message_batch.go`.
3. 🔴5 — полный откат dedup/FIFO-seq при сбое `SaveMessage(s)Sync`.
4. Lock order + I/O под глобальным локом.
5. Fail-fast на старте без Redis.
6. JWT (после решения п.3) + лимит по байтам (после п.4).