# Журнал сессии — 2026-08-14 (продолжение миграции shared-sqs) **Рабочая папка:** /home/naeel/nubes/SQS-service --- ## Текущее состояние - Сервис ЗАДЕПЛОЕН и работает: `https://sqs.containerk8s.dev.nubes.ru` - `/health` → `OK` (200) - Образ: Docker Hub `naeel/shared-sqs:latest` - Репа: Gitea `gitea.services.ngcloud.ru/Nail/SQS-service` --- ## Что сделано сегодня 1. Добавлен инфо-блок «Информация о сервисе» на страницу входа `/ui` (`app/ui/index.html`). Коммит `032ca66`. Образ пересобран, запушен (`digest 4bb4f20d`). 2. **Проблема:** вход в UI с реальным JWT → `Ошибка: token rejected by cloud API (HTTP 403)`. Причина: контейнер валидировал JWT на `deck-api-test.ngcloud.ru/api/v1` (default в `app/admin/admin.go`), а production JWT туда не подходит. 3. **Поиск правильного endpoint** (в `~/tf_provider`, по указанию пользователя): - `deck-api*.ngcloud.ru` — **ЛЕГАСИ** (пользователь явно запретил этот формат) - Правильный: **API Gateway** `lk-api-gateway.ngcloud.ru` 4. Найдено в `~/tf_provider/TOOLS/config/prod/profile.env`: ``` NUBES_API_ENDPOINT="https://lk-api-gateway.ngcloud.ru/api/v1/svc" ``` (test: `...-test...`, dev: `...-dev...`) Также default в `TOOLS/yaml-generator/internal/config/config.go`: `getenvDefault("NUBES_API_ENDPOINT", "https://lk-api-gateway.ngcloud.ru/api/v1/svc")` 5. **Тест токенов против gateway** (токены из `/home/naeel/nubes/tokens_all.txt`): - БЕЗ User-Agent → **все 403** (DDoS-Guard блокирует) - С `User-Agent: Mozilla/5.0`: | Токен | gateway-PROD | gateway-TEST | |---|---|---| | tazetdinovn@gmail.com (TEST) | 401 | **200** | | tazet@narod.ru (PROD) | **200** | 401 | | ntazetdinov@nubes.ru (PROD) | **200** | 401 | --- ## Выводы (факты) 1. Правильный `NUBES_ENDPOINT` = **`https://lk-api-gateway.ngcloud.ru/api/v1/svc`** 2. **КРИТИЧНО:** наш код `PingNubesAPI` (`app/auth/jwt.go`) **НЕ шлёт User-Agent** → gateway возвращает 403 → токен всегда отклоняется. Нужна правка кода: добавить `User-Agent: Mozilla/5.0`. --- ## Предстоящие правки (СОГЛАСОВАНЫ? — нет, ждут «делай») 1. `Dockerfile`: `ENV NUBES_ENDPOINT=https://lk-api-gateway.ngcloud.ru/api/v1/svc` 2. `app/auth/jwt.go`: в `PingNubesAPI` добавить `User-Agent: Mozilla/5.0` 3. `app/admin/admin.go`: сменить default endpoint на gateway PROD После правок: пересборка → push Docker Hub → redeploy → проверка входа в UI. --- ## Правила, зафиксированные в этой сессии - Код менять ТОЛЬКО после согласования с пользователем. - deck-api*.ngcloud.ru — ЛЕГАСИ, НЕ использовать (только lk-api-gateway). - ngcloud за DDoS-Guard: curl-запросы всегда с `User-Agent: Mozilla/5.0`. - Всё документировать в HISTORY. --- ## v0.1.26 — вывод версии на экран + фикс схемы тегов (14.08.2026, утро) **Проблема**: пользователь заметил, что версии не повышаются — всё время пушился только `latest`, под мог кэшировать `latest` и redeploy подтягивал старый образ. Плюс запрос: выводить версию на экран. **Что сделано**: 1. `app/models/constants.go`: `var Version = "dev"` (переопределяется через ldflags). 2. `app/router/router.go`: публичный `/health` теперь отдаёт JSON `{"status":"ok","version":"v0.1.26"}`. 3. `app/admin/admin.go`: `adminHealthDetail` дополнен полем `version` (в /admin/health и /ui/api/health). 4. `app/ui/index.html`: - блок «Информация о сервисе» на странице входа: строка «Версия» (fetch публичного /health); - образ в инфо-блоке теперь `naeel/shared-sqs:v0.1.26`; - дашборд: карточка «Версия» (health.version). 5. `Makefile`: `VERSION=v0.1.26`, `LDFLAGS=-X shared-sqs/app/models.Version=$(VERSION)`, `docker-build` тегает `$(VERSION)` + `latest`; исправлена цель `build` (`./app/cmd/` вместо одного файла goaws.go). 6. `Dockerfile`: `ARG VERSION=dev`, ldflags при сборке. **Проверки**: make build OK; `/health` → `{"status":"ok","version":"v0.1.26"}`; go test router/admin/models — ok. **Git**: коммит `3a105de` «v0.1.26: …», пуш в Gitea (`166baa9..3a105de`). **Образ**: `naeel/shared-sqs:v0.1.26` собран и запушен на Docker Hub, digest `sha256:b4d1dccda75a67e1a5db3887576e1112993d6485504e683d0fa82a2374bca99b`, `latest` указывает на него же. **Следующий шаг**: в deck указать путь образа `naeel/shared-sqs:v0.1.26` и сделать redeploy — тогда под гарантированно стянет новый образ (код с перебором стендов NUBES_ENDPOINT + версия на экране). --- ## РАЗВЯЗКА: образ обновлён через kubectl, вход в UI работает (14.08.2026, утро) **Как выяснилось**: - Наш deck-контейнер — это deployment `containerk8s` в namespace `f1ffb134-7d16-45bd-8bef-69f6ec8ab33c` (ns = UUID инстанса), образ `naeel/shared-sqs:latest`, pullPolicy=Always. - Старый апрельский Helm-релиз `shared-sqs` (ns `shared-sqs`, v0.1.24, 122 дня) — ЛЕГАСИ, не имел отношения к живому сервису. УДАЛЁН по команде пользователя: `helm uninstall shared-sqs` + `kubectl delete namespace shared-sqs` (кластер подтвердил NotFound). - Причина «redeploy не помогает»: под с pullPolicy=Always получал из кэша платформенного зеркала `docker.io` (в kube-system всё на mirror.k8s.ngcloud.ru) старый digest `be2108fd…`, хотя в Docker Hub `latest` = `b4d1dccd…` (v0.1.26). Кэш зеркала по тегу `latest` не протухал из-за частых пушей. **Что сделано**: 1. `kubectl -n f1ffb134-… set image deployment/containerk8s app=naeel/shared-sqs:v0.1.26` (внимание: контейнер в поде называется `app`, а не `containerk8s`). Уникальный тег обошёл кэш зеркала → под `containerk8s-cc5849c-8rx6l` с digest `b4d1dccd…`. 2. Проверка: `GET /health` → `{"status":"ok","version":"v0.1.26"}`. 3. Вход в UI с JWT `tazetdinovn@gmail.com` — УСПЕШНО: тенант auto-provisioned (`SSAK-0f00384dc3cdc9ca18a50e3a10`), на дашборде карточка «Версия: v0.1.26». **Наблюдения по gateway (важно на будущее)**: - TEST-токен: с рабочей станции (python urllib) TEST-стенд = 200; с ВМ (curl) = 401; из пода (wget) = 401. При этом Go-клиент сервиса (crypto/tls) проходит и получает 200 — DDoS-Guard/шлюз различает клиентов по TLS-фингерпринту/IP. Тесты curl/wget из пода НЕ показательны для Go-клиента сервиса. - Перебор стендов PROD→TEST→DEV в `PingNubesAPI` работает как задумано. **Правила этой сессии** (дополнено): - Deck-контейнер «Простой HTTP контейнер» = deployment `containerk8s`, контейнер `app`, ns = UUID инстанса. Правки образа через kubectl НЕ откатываются (ownerReferences нет). - `latest` в этом кластере кэшируется зеркалом → обновлять только уникальным тегом через `kubectl set image`. --- ## v0.1.27 — страница-описание сервиса на корне `/` (14.08.2026) **Задача**: на `https://sqs.containerk8s.dev.nubes.ru/` разместить описание сервиса и список команд в дизайне Nubes (`~/nubes/design/nubes-design-system.md`). **Что сделано**: 1. `app/ui/info.html` — публичная страница: топбар с лого, hero с бейджем «тестирование», карточка «Подключение» (endpoint, SigV4, регион us-east-1, /ui, /health, /metrics, Redis), таблица всех 17 Actions, примеры AWS CLI. Версия подставляется через `{{VERSION}}`. 2. `app/ui/static/{logo, favicon}.svg` — SVG-файлы из design-системы (лого ТОЛЬКО файлом, не inline). 3. `app/ui/embed.go`: `InfoHandler(version)`, `StaticHandler()`; embed расширен (`info.html static`). 4. `app/router/router.go`: `GET /` без Action → info-страница; POST/GET с Action → SQS API как раньше. Маршрут `/static` — публично. SQS API не задет (проверено: POST Action=ListQueues → 403 без сигнатуры, как было). **Инцидент rsync (важно!)**: rsync с ДВУМЯ источниками (`app/` + `Makefile`) в dest `~/terra/SQS-service/` высыпал содержимое `app/` в КОРЕНЬ репо на ВМ (`~/terra/SQS-service/ui/`, `router/` и т.д.), а настоящий `app/` остался старым → первый образ v0.1.27 (digest e69807fe) собрался из СТАРОГО кода (v0.1.26). Исправлено: повторный rsync с ОДНИМ источником `app/` → `~/terra/SQS-service/app/`, пересборка. **Итог**: правильный образ v0.1.27, digest `sha256:51c94894…`. В кластер поставлен ПО DIGEST (`set image app=naeel/shared-sqs@sha256:51c94894…`) — digest обходит кэш зеркала по тегу. Под `containerk8s-7985cd4985-gwkc6`, Ready. Проверки на проде: `/health` → v0.1.27, `GET /` → 200 text/html (страница), `/static/logo.svg` → 200, SQS POST → 403 (норма без сигнатуры). **Незакрытый хвост**: на ВМ в корне `~/terra/SQS-service/` остался мусор от ошибочного rsync (топ-уровневые `ui/`, `router/` и др. с новым содержимым). На сборку не влияет (Dockerfile берёт `./app/`). Удаление — по команде пользователя. ## Зачистка мусора на ВМ (14.08.2026) По команде пользователя удалены untracked топ-уровневые каталоги в `~/terra/SQS-service/`, оставшиеся от ошибочного rsync (дубли содержимого `app/`): `admin auth billing conf gosqs interfaces metrics models persistence router tenant ui utils`. `app/` не тронут (проверено: rootHandler на месте, app/ui/static и info.html целы). Оставлено: app, cmd, doc, Dockerfile, go.mod, go.sum, HISTORY, Makefile, README.md, secrets. --- ## Архитектурный разбор (Claude Sonnet) — переход идентичности на email (14.08.2026) **Запрос к Sonnet**: файлы только `app/auth/jwt.go`, `app/tenant/tenant_store.go`, `app/admin/admin.go`, `app/gosqs/tenant_helpers.go`, `app/persistence/redis.go`, `app/cmd/goaws.go`, `app/auth/auth_middleware.go`, `app/ui/index.html`. **Ответ Sonnet (выводы)**: 1. Сломается: `TenantIDFromSub()` в jwt.go; вызов в `jwtAuth` (admin.go ~236); `access_key/secret_key` в ответе jwtAuth; `GetBySub` в `jwtMiddleware` (~288); `bySub` → `byEmail` в TenantStore (NewTenantStore, CreateFromJWT, Delete, LoadTenant, GetBySub). 2. Пересчёт легаси при 0 очередях безопасен, но ОБЯЗАТЕЛЬНО: удалить старый тенант из Redis (`ssq:tenants` по старому ID) и сохранить новый — иначе после рестарта старые рандомные ключи оживут. Весь пересчёт — под `s.mu.Lock()`. 3. Детерминированные функции — в jwt.go; byEmail-индекс — в tenant_store.go; LoadTenant индексирует по Email только при `Email != ""` (защита от коллизии `byEmail[""]` с демо-тенантом CreateFixed). 4. Дыра — orphan AccessKey в byAccessKey: при пересчёте удалять из byAccessKey ДО добавления нового. 5. Критичное неучтённое: (a) email может быть пустым → проверка в jwtAuth (400), не в ParseJWTClaims; (b) QueueUrl содержит TenantID — при смене ID URL меняется, у клиентов захардкоженные URL сломаются (сейчас очередей нет — не критично); (c) демо-тенант без Email не должен попадать в byEmail. **Решение (принято)**: все пункты включаем в v0.1.28. Токен — только аутентификация; email из JWT детерминированно задаёт TenantID/AccessKey/SecretKey; рандома нет; ключи на экране не светятся — только по кнопке в модалке с подсказкой; `POST /ui/api/auth` перестаёт отдавать ключи; новый `GET /ui/api/credentials` под JWT-сессией. --- ## v0.1.28 — email-идентичность, детерминированные креды, credentials-эндпоинт (14.08.2026) **Что сделано**: 1. `app/tenant/tenant_store.go`: индекс `bySub` → `byEmail`; `GetByEmail` вместо `GetBySub`; `CreateFromJWT(sub, email, maxQueues)` — ID/AccessKey/SecretKey детерминированы из email (SHA256, соль `shared-sqs:secret-key:v1` для секрета). Рандом остался только для manual-тенантов. Легаси-тенант (старые ключи/ID) при входе полностью пересчитывается: старые индексы и Redis-запись удаляются под мьютексом. 2. `app/auth/jwt.go`: `TenantIDFromSub` (sless) УДАЛЁН — связей с внешними сервисами больше нет. 3. `app/admin/admin.go`: `jwtAuth` требует email (400 без него), НЕ возвращает access/secret key; новый `GET /ui/api/credentials` отдаёт ключи под JWT-сессией; jwtMiddleware по email. 4. `app/ui/index.html`: Access Key убран из таблицы и карточки тенанта; кнопки «Credentials» (дашборд + тенант) открывают модалку с ключами и подсказкой (зачем ключи, export-команды, пример aws cli). Логин-страница: образ v0.1.28. 5. `Makefile`: VERSION=v0.1.28. **Проверки**: build OK, vet OK, тесты admin/models OK. Локально через мок-gateway: auth без ключей ✓, credentials отдаёт ключи ✓, без токена 401 ✓. Хеши сверены с python: tenant_id `t-fec713a719ad0b33`, AK `SSAK-fec713a719ad0b33c91fa54a`, SK `e898ed410a…`. **Деплой**: образ v0.1.28 digest `sha256:c6b63f4c…`, поставлен по digest (`set image … app=naeel/shared-sqs@sha256:c6b63f4c…`), под `containerk8s-86df69bfb-drv9x` Ready. `/health` на проде → `{"status":"ok","version":"v0.1.28"}`. **⚠ ПРОБЛЕМА (не наш код)**: TEST-gateway теперь отдаёт 401 на запросы с датацентровых IP (кластер iot-naeel И ВМ 5.172.178.213) — с рабочей станции тот же токен даёт 200. Ранее (~09:00) под успешно валидировался. Похоже на rate-limit/бан DDoS-Guard по egress-IP платформы после сегодняшних частых запросов. Вход в UI будет 403, пока шлюз не отпустит. SQS API, /health, /, /metrics не затронуты. --- ## v0.1.29 — светлый Nubes-дизайн консоли, читаемая модалка Credentials (14.08.2026) **Проблема (жалоба юзера)**: консоль была на тёмной палитре старого iot-дизайна (чёрный фон, мелкий шрифт), модалка Credentials нечитаемая. Требование: Nubes-дизайн, читаемо. **Что сделано** (`app/ui/index.html`): 1. Палитра → светлая design-system Nubes: bg #fafafa, поверхности #ffffff, border #d1d5db, primary #2563eb, muted #6b7280, карточки белые с radius 12 + тень. 2. Логотип без invert-фильтра (тёмный лого на белом), шрифт system-ui 14px, заголовки 20-22px, бейджи/кнопки в светлых тонах, textarea/input белые. 3. Модалка Credentials переделана: читаемый блок подсказки — зачем ключи + ДВА способа (env-export И файл ~/.aws/credentials) + пример с --profile sqs. Ширина модалки 560px, скролл. **Деплой**: v0.1.29 digest `sha256:d1ccb53d…`, `set image` по digest, под `containerk8s-75f766b9d7-82wl7`. Проверки: /health → v0.1.29; /ui отдаёт светлую тему (#fafafa/#ffffff/#2563eb, тёмных цветов нет). **Остаётся открытым**: gateway 401 с датацентровых IP (см. раздел v0.1.28) — вход в UI заработает, когда шлюз отпустит egress платформы. --- ## v0.1.30 — локальные логотип и favicon в консоли (14.08.2026) В консоли /ui были внешние ассеты `terra.k8c.ru/.../logo.svg` и `favicon.png` — не грузились (битый лого в шапке, нет фавикона). Заменены на локальные вшитые `/static/logo.svg` и `/static/favicon.svg` (embed в образ, раздаются с корня роутером). Деплой: digest `sha256:deab2979…`, под готов. Проверки: /health → v0.1.30, /ui отдаёт 2× static/logo.svg + 1× static/favicon.svg, оба файла отвечают 200. --- ## v0.1.31 — админский UI /admin + юзерский UI без слова «тенант» (14.08.2026) **Сделано**: 1. Новый `app/ui/admin.html` — админская консоль: вход по admin-токену, таблица тенантов (имя, ID, Access Key, лимит, статус, создан), создание тенанта с показом ключей в модалке, удаление с подтверждением. Светлый Nubes-дизайн, /static/logo.svg + favicon, версия через {{VERSION}}. 2. `app/ui/embed.go`: embed admin.html + `AdminHandler(version)`. 3. `app/router/router.go`: GET /admin и /admin/ → админская страница (зарегистрированы ДО admin subrouter, чтобы PathPrefix("/admin") с bearer-auth их не перехватил). 4. Юзерский UI: из видимого текста убрано слово «тенант» — «Мой аккаунт», «Аккаунт не найден», убрана карточка-счётчик «Тенанты», «креды вашего аккаунта», «Создать аккаунт», ссылка на /admin в инфо-блоке логина. **Проверки**: build OK, vet OK, тесты admin OK. Локально: /admin → 200 (страница), /admin/tenants без токена/с неверным токеном → 401, с верным → []. Прод: /health → v0.1.31, /admin → 200 (SQS Admin, v0.1.31), /ui содержит «аккаунт» 3× и не содержит «Тенанты». **Инциденты по пути**: - Пуш на Docker Hub спотыкался об IPv6 (network unreachable) и 502 — добил ретраями, digest v0.1.31 = `sha256:34c3619d…`. - Токен kubeconfig на ВМ истёк в 09:14 MSK (24ч) — юзер обновил, деплой добит: `set image …@sha256:34c3619d…`, под `containerk8s-768668b9d4-stcgv` Ready. --- ## v0.1.32 — главная с пошаговым стартом, примеры на /examples (14.08.2026) **Жалобы юзера**: главная не объясняет с первого раза, как войти/где креды/что делать; примеры засоряли главную мелким шрифтом; в консоли лишняя хлебная крошка и таблица из одной строки. **Сделано**: 1. `app/ui/info.html`: блок «Как начать — 3 шага» (1. Открыть консоль → кнопка, 2. Получить ключи через Credentials, 3. Подключить клиент с быстрым примером + ссылка на /examples). Таблица команд и примеры убраны с главной, вместо них — карточка-ссылка на /examples. Слово «тенант» убрано из текста главной. 2. Новый `app/ui/examples.html` (GET /examples): крупный шрифт 15px, команды 14px, пошагово: ключи (env И файл ~/.aws/credentials) → создать очередь → отправить → получить/удалить, плюс таблица всех 17 Actions. 3. `app/ui/embed.go`: ExamplesHandler; `app/router/router.go`: маршрут /examples. 4. `app/ui/index.html`: enterApp сразу открывает аккаунт (showTenant первого тенанта), дашборд с таблицей — только запасной вариант; хлебные крошки «Мой аккаунт › email» удалены. **Проверки**: локально root → «Как начать/Открыть консоль», /examples → 200 (Шаг 1..4, таблица). Прод: /health → v0.1.32, / → 200, /examples → 200. digest `sha256:4834b999…`. --- ## v0.1.33 — строгий нейтральный стиль текстов (14.08.2026) Жалоба: фамильярный тон на главной («Войди», «Получи», «Подключи свой клиент»). Заменено на нейтральный официальный стиль: - «Порядок действий — 3 шага»: «Вход в консоль», «Получение ключей доступа», «Подключение клиента». - Примеры (/examples): «Шаг 1. Указание ключей доступа», «Шаг 2. Создание очереди», «Шаг 3. Отправка сообщения», «Шаг 4. Получение и удаление сообщения»; формулировки без повелительных форм. Деплой: v0.1.33 digest `sha256:196283c1…`, проверено: /health → v0.1.33, корень отдаёт новые заголовки. --- ## ✅ Полный цикл подтверждён: email-креды работают на проде (14.08.2026) Юзер залогинился (gateway отпустил egress платформы), легаси-тенант пересчитан, в модалке Credentials показаны детерминированные ключи: - Access Key: `SSAK-fec713a719ad0b33c91fa54a` - Secret Key: `e898ed410a20ff166f51a52ba39a2954fb87e2da200b1648c3975772bfe9e2b4` Совпадают с расчётным SHA256(email) — email-идентичность v0.1.28 работает. Smoke-тест: `aws sqs list-queues --endpoint-url https://sqs.containerk8s.dev.nubes.ru` с этими ключами → 200, `{"QueueUrls": []}`. SigV4-аутентификация на проде подтверждена. --- ## Q&A с Claude Sonnet — кэш валидации JWT (14.08.2026) **Запрос**: файлы только `app/auth/jwt.go`, `app/admin/admin.go`; задача — кэш успешной валидации токена (TTL 5 мин) из-за нестабильного шлюза Nubes. **Ответ Sonnet**: 1. Кэш — в auth (новый `app/auth/cache.go`): `PingNubesAPICached`, `WarmTokenCache`, `StartTokenCacheCleanup`. В admin.go: jwtMiddleware → PingNubesAPICached; jwtAuth → свежая проверка + WarmTokenCache; NewHandler → cleanup. 2. Ключ `hex(sha256(token))`; `map[string]tokenCacheEntry{validUntil}` + `sync.RWMutex` (не sync.Map — нужна итерация при очистке); уборщик — ticker 5 мин. 3. `ErrTokenRejected` (sentinel, `%w`) — отличать 401/403 от сетевых ошибок; сигнатура PingNubesAPI не меняется. 4. Таблица: HIT → nil; MISS+2xx → записать; MISS+401/403 → удалить, вернуть ошибку; MISS+сетевая → ошибка; протухший+сетевая → опция grace +2 мин (принято). 5. Гонки: параллельные MISS безвредны (singleflight избыточен); запись в кэш до проверки ctx; рестарт пода сбрасывает кэш (норма); exp проверяется локально ДО кэша/шлюза. **Решение (принято)**: всё выше + grace-продление 2 мин при сетевой ошибке для ранее валидного токена. TTL 5 мин; отозванный токен умирает максимум через 5 мин; изоляция тенантов не затрагивается. --- ## v0.1.34 — кэш валидации JWT (14.08.2026) **Проблема**: jwtMiddleware валидировал токен через Nubes-шлюз на КАЖДЫЙ запрос консоли; шлюз нестабильно отвечает с IP платформы (то 200, то 401) → консоль периодически падала. **Реализовано** (по плану Sonnet): 1. `app/auth/cache.go`: кэш успешной валидации в памяти: `map[hex(sha256(token))]{validUntil}` + `sync.RWMutex`, TTL 5 мин, grace +2 мин при сетевой ошибке для ранее валидного токена, фоновая очистка (ticker 5 мин, sync.Once). 2. `app/auth/jwt.go`: sentinel `ErrTokenRejected` (`%w`) для 401/403 — отличие от сетевых ошибок. 3. `app/admin/admin.go`: `jwtAuth` — всегда свежая проверка + `WarmTokenCache` после успеха; `jwtMiddleware` — `PingNubesAPICached`; `NewHandler` — старт очистки. **Проверено локально (мок-шлюз)**: логин с живым моком → 200; мок убит → логин 403 (свежо, правильно), CREDS → 200 по кэшу (шлюз не нужен), неизвестный токен → 403. Vet/тесты OK. **Smoke на проде**: 11 PASS, 1 FAIL — только логин (шлюз режет IP платформы; по дизайну логин всегда свежо). SQS-цикл create→send→receive→delete→delete-queue — PASS. **Особенность платформы**: GET /ui через ingress не закрывает соединение (~150с, curl exit 28), внутри пода мгновенно. В smoke /ui проверяется range-запросом (206, 0.2с). Браузеру не мешает (HTML приходит сразу). **Деплой**: v0.1.34 digest `sha256:17f9bbdb…`, под Ready, /health → v0.1.34. --- ## Программная проверка SQS API — CLI и SDK, локально и с ВМ (14.08.2026) Добавлены тесты: - `tests/api_test.sh` — aws cli, 22 проверки (полный цикл + FIFO + теги + purge). - `tests/sdk_test.py` — boto3 (как реальное приложение), 15 проверок, таймауты на уровне SDK. Результаты: - Локально, CLI: 22/22 PASS; SDK (boto3 1.43.71): 15/15 PASS. - С ВМ, CLI: 22/22 PASS; SDK (boto3 1.34.46): 15/15 PASS. Единственный разовый FAIL (DeleteMessage после ChangeMessageVisibility) при первом прогоне не воспроизвёлся — повторный прогон и ручная проверка: exit=0. Случайный сбой сети. --- ## Нагрузочный тест 30 мин (tests/load_test.py) — результаты и находки (14.08.2026) **Прогон**: 1800с, 4 воркера, против прода. Итог: 776 операций. - Корректных: 75; ожидаемых отказов: 0; НЕОЖИДАННЫХ отказов: 653 (коды: 400×549, 404×44, ReadTimeoutError×60); - Некорректные условия приняты без ошибок: 47; сбоев /health: 1 (разовый таймаут); под НЕ рестартовал. - Латентность: min 0.08s, avg 2.63s, max 54.4s. Вердикт: ЕСТЬ ПРОБЛЕМЫ. **Критично — потери сообщений**: send возвращает успех, receive не отдаёт сообщение. Воспроизведено БЕЗ нагрузки: 3 из 25 циклов (и 2 из 25 под нагрузкой). Зависания receive >10с. **Недостатки обработки ошибок (пробы)**: на некорректные запросы сервис отдаёт сырые HTTP 400/404 без AWS-XML (SDK получает Code=None) или молча принимает недопустимое: - SetQueueAttributes: VisibilityTimeout=43201 принят; неверное имя атрибута принято; нечисловое значение → сырой 400. - SendMessage: пустое тело → сырой 400; 300КБ → сырой 400; несуществующая очередь → сырой 400. - Batch: 11 записей / дубли Id / пустой список → сырой 400. - DeleteMessage с битым ReceiptHandle → сырой 404. - ReceiveMessage MaxNumberOfMessages 11/0/-1 — принято (клэмпится). - Повторный PurgeQueue (<60с) — принят; DeleteQueue несуществующей — принят. - FIFO SendMessage без MessageGroupId — принят. **Мой предварительный анализ кода** (для Sonnet): - models.go IsReadyForReceipt(): showAt = SentTime + randomLatency(конфиг, сейчас 0/0) + DelaySecs. - gosqs.go PeriodicTasks (1с): сброс видимости, dedup, DLQ; LockGroup держится до UnlockGroup. - send_message.go: добавление в SyncQueues.Queues[key].Messages + SaveMessage(Redis); память — единственный источник для receive (Redis не читается) — при 2+ подах/редиплое сообщения, принятые одним подом, невидимы другому. - receive_message.go: skip сообщений с ReceiptHandle != "" и не готовых; long poll 100ms тик. - ReadTimeout 60× и max latency 54с — подозрение на платформенный ingress (аналог /ui 152с). --- ## Q&A с Claude Sonnet — разбор дефектов по итогам нагрузочного теста (14.08.2026) **Ответ Sonnet (по файлам из промпта)**: Q1 Потери сообщений: TOCTOU в long-poll (receive_message.go:82-99) — два потока видят сообщение, один забирает, второй возвращает пусто; включается при WaitTimeSeconds>0 (клиент или атрибут очереди). Вторично для FIFO: LockGroup (models.go:82-89) пересоздаёт FIFOMessages map целиком — теряются локи групп. Q2 Receive >10с: (а) атрибут очереди ReceiveMessageWaitTimeSeconds без гарда (receive_message.go:62-66); (б) SaveMessage/SaveQueue внутри глобального Lock (send_message.go:97-103) — медленный Redis блокирует всех; (в) PeriodicTasks держит Lock на весь обход всех очередей (gosqs.go:18-58). Q3 Сырые 400/404: actionHandler пишет "Bad Request" текстом (router.go:199-201); encodeResponse для JSON-протокола не пишет тело при GetResult()==nil (router.go:110-113). Q4 Валидации: VisibilityTimeout clamp→bounds-check (queue_attributes/set_queue_attributes.go:29); whitelist имён атрибутов (IgnoreUnknownKeys в utils.go:29 пропускает всё); MaxNumberOfMessages clamp→ошибка (receive_message.go:41); PurgeQueue повторный<60с→PurgeQueueInProgress (LastPurgeTime в Queue); FIFO без MessageGroupId→MissingParameter (send_message.go:46-49); actionHandler→AWS-XML GeneralError. Q5 Сервис сам может держать запрос >25с: Redis-записи под Lock + PeriodicTasks с полным обходом. Q6 Дополнительно: SetQueueAttributes обнуляет не переданные атрибуты (zero-value >= 0); unprotected map read (receive_message.go:51) — data race; NumberOfReceives не инкрементируется; DeleteQueue всегда успешен; NextSequenceNumber пересоздаёт map (models.go:62-70). **ПЛАН v0.1.35 (принят)**: 1. TOCTOU: выборка сообщений — единая критическая секция под Lock. 2. Redis-сетевые вызовы — вне глобального Lock (снапшот под локом, запись после). 3. PeriodicTasks: сузить критическую секцию. 4. RLock для Queues[key] в receive_message.go:51. 5. SetQueueAttributes: применять только явно переданные атрибуты. 6. LockGroup/NextSequenceNumber: не пересоздавать map (вставлять ключ). 7. actionHandler: AWS-XML GeneralError вместо текста. 8. encodeResponse (JSON): тело ошибки при GetResult()==nil; проверить CreateErrorResponseV1. 9. Валидации: VisibilityTimeout bounds; whitelist атрибутов; MaxNumberOfMessages 1–10 ошибка; PurgeQueueInProgress (LastPurgeTime); FIFO MissingParameter; SendMessage пустое тело/размер — AWS-коды. 10. NumberOfReceives инкремент при выдаче. После: 25-цикловая проверка целостности, smoke, повторный 30-мин нагрузочный тест. --- ## v0.1.35 — реализация фиксов по итогам нагрузочного теста (14.08.2026) **Изменённые файлы** (правки с подробными комментариями каждой функции и логики): 1. `app/router/router.go`: - `resolveProtocol`: заголовок `x-amzn-query-mode: true` (AWS CLI v2/boto3) → принудительно Query-протокол (XML-ответы). Раньше query-mode запросы получали JSON-ошибки → botocore Code=None («сырые 400/404»). - `extractAction`: Action берётся сначала из `X-Amz-Target` (JSON и query-mode клиенты), затем из form `Action` — для query-mode раньше Action не находился. - `encodeResponse` (JSON): ошибки — в формате AWS-JSON `{"__type":"com.amazonaws.sqs#","message":"..."}` (раньше голый ErrorResult). - `actionHandler`: неизвестный Action → GeneralError в формате протокола клиента (раньше текст "Bad Request"). Убран импорт `io`. 2. `app/gosqs/receive_message.go`: - TOCTOU устранён: проверка готовности И пометка in-flight — под одним Lock (`receiveMessagesUnderLock`). Раньше два параллельных receive видели одно сообщение, второй получал пусто → «потери». - MaxNumberOfMessages: 1–10, выход → InvalidParameterValue (был clamp). - Проверка существования очереди — под RLock (была гонка чтения map). - `msg.NumberOfReceives++` при выдаче (счётчик доставок). 3. `app/gosqs/send_message.go`: - FIFO без MessageGroupId → MissingParameter (было: принималось). - Док-схема функции; Redis-записи асинхронны, под Lock только marshal. 4. `app/gosqs/gosqs.go`: - `PeriodicTasks`: снапшот указателей под RLock + обработка каждой очереди под отдельным Lock (раньше глобальный Lock на весь обход всех очередей). 5. `app/models/models.go`: - `LockGroup`/`NextSequenceNumber`: map больше НЕ пересоздаётся (терялись блокировки/счётчики остальных групп). - Новое поле `Queue.LastPurgeTime`. 6. `app/gosqs/queue_attributes.go` + `set_queue_attributes.go` + `create_queue.go`: - Применяются только явно переданные атрибуты (!= 0) — раньше zero-value обнулял непереданные атрибуты. - Bounds-check → InvalidParameterValue (был clamp). - Whitelist имён → InvalidAttributeName (Query-протокол). 7. `app/gosqs/purge_queue.go`: повторный purge < 60 с → PurgeQueueInProgress. 8. `app/gosqs/delete_queue.go`: удаление несуществующей очереди → QueueNotFound. 9. `app/models/errors.go`: добавлены `InvalidAttributeName`, `PurgeQueueInProgress`. **Проверки**: go build OK (v0.1.35), go vet чистый (кроме известной некомпиляции тестов gosqs — пакет fixtures), go test app/models OK. **Ход деплоя и эмпирические уточнения**: - Первый вариант фикса resolveProtocol («query-mode → XML») оказался НЕВЕРНЫМ: эмпирика показала, что AWS CLI v2 в query-mode парсит именно JSON-ответы (list-queues на XML вернул None, на JSON — список). Откачено: протокол ответа по Content-Type, как было; дефект «Code=None» закрыт правильным форматом JSON-ошибок {"__type":"com.amazonaws.sqs#Code","message":"..."} в encodeResponse и GeneralError в actionHandler. - Whitelist имён атрибутов для JSON-протокола реализован через QueueAttributes.UnmarshalJSON (единый список models.AttrNameWhitelist); в whitelist включены FifoQueue/ContentBasedDeduplication (иначе CLI v2 не мог создать FIFO — регрессия была поймана api_test). - Прод v0.1.35, digest `sha256:7d1c82e9…` (по digest). **Проверки против прода (v0.1.35)**: - Пробы ошибок: пустое тело → MissingParameter; max=11 → InvalidParameterValue; VisibilityTimeout=43201 → InvalidParameterValue; FooBar=1 → InvalidParameterValue; повторный purge → PurgeQueueInProgress; delete несуществующей → NonExistentQueue; FIFO без MessageGroupId → MissingParameter. Все с AWS-кодами (Code=None устранён). - api_test.sh (CLI v2): 22/22 PASS. sdk_test.py (boto3): 15/15 PASS. - Целостность 25 циклов: с ВМ 25/25 PASS (lost=0, mismatch=0, errors=0). С локальной машины периодически ReceiptHandleIsInvalid — анализ логов пода показал: это retry-дубли (первый delete успешен, ответ теряется на пути клиент→шлюз; сообщение фактически удалено и подбирается следующими циклами). Инфраструктурная потеря ответов (DDoS-Guard/Nubes gateway), не дефект сервиса. - smoke.sh: 11/11 PASS. - 30-мин нагрузочный тест с ВМ — в процессе. ## Лимит очередей 50 + очистка (14.08.2026) **Нагрузочный тест (1-й прогон v0.1.35, с ВМ)**: 217440 операций, но 214683 «неожиданных» = LimitExceeded. Причина: у JWT-тенанта лимит 10 очередей (CreateFromJWT хардкод 10), а 9 мусорных очередей от прежних тестов заняли почти весь лимит — 4 воркера теста упирались в лимит на create-queue. Некорректное принято: 0. /health сбоев: 0. max latency 77.6с = ReadTimeoutError (25с × 3 ретрая бото3) — потери ответов на пути, не сервис. **Изменения (по команде пользователя)**: - `app/tenant/tenant_store.go`: константа `DefaultTenantMaxQueues = 50`; `CreateFromJWT` теперь обновляет `MaxQueues` существующего тенанта до актуального значения при КАЖДОМ логине + персистит в Redis (иначе после рестарта пода из Redis восстановился бы старый лимит). - `app/admin/admin.go`: `CreateFromJWT(..., tenant.DefaultTenantMaxQueues)`. - Деплой v0.1.35 digest `sha256:72f11c67…`. - Удалены все мусорные очереди тенанта (11 штук: probe-*, err-test*, integr-*, vt-check-*, load-*, int-*): осталось 0. **Осталось**: логин в консоль для применения лимита 50 к существующему тенанту, затем повторный 30-мин нагрузочный тест.