Files
SQS-service/HISTORY/2026-08-14-session-log.md
T

522 lines
39 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.
# Журнал сессии — 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#<Code>","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.