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

1148 lines
88 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 110 ошибка;
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: 110, выход → 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-мин нагрузочный тест.
## Фиксы гонок map — под падал под нагрузкой (14.08.2026)
**Инцидент**: 30-мин прогон показал 4 сбоя /health; под перезапускался 2 раза
(Exit Code 2). Логи упавшего контейнера: `fatal error: concurrent map read and map write`,
горутина `ChangeMessageVisibilityV1`, `change_message_visibility.go:50` —
чтение `SyncQueues.Queues[key]` БЕЗ блокировки; писатель — DeleteQueueV1 (delete под Lock),
массово вызываемый тестом.
**Анализ Соннета** (подтверждён): 3 файла читают map до Lock; смежный риск nil-deref
(очередь удаляется между проверкой и Lock); receive_message.go имел два RLock с окном
nil-deref; рекомендован перенос проверки внутрь Lock + defer Unlock.
**Исправлено (v0.1.35, digest `sha256:7436efd8…`)**:
- `change_message_visibility.go`: проверка существования внутри Lock, defer Unlock.
- `change_message_visibility_batch.go`: то же.
- `delete_message_batch.go`: то же.
- `send_message.go`: nil-guard под Lock + повторная проверка OOM-лимита под Lock.
- `send_message_batch.go`: nil-guard под Lock.
- `receive_message.go`: один RLock вместо двух (nil-deref окно закрыто).
- `errors.go`: добавлен код ошибки `OverLimit` (раньше отсутствовал — zero SqsErrorType).
**Проверки**: go build/vet OK, models-тесты OK; короткий прогон 120с: неожиданных 2
(оба сетевые ReadTimeout), сервисных 0, health_fail 0; под restarts 0.
30-мин прогон запущен 18:52:18 +04 — в процессе.
## Сетевые сбои на ВМ и фикс DNS (14.08.2026)
**Факт**: 30-мин прогон после фиксов гонок: неожиданных 19 (17 ReadTimeout +
2 EndpointConnection), health_fail 8, НО под restarts 0 (гонки устранены).
Замер 30 × /health с ВМ: 4 сбоя `[Errno -3] Temporary failure in name resolution` —
DNS ВМ. Проверки ВМ: resolv.conf = один `8.8.8.8`; dig 20/20 ок, python 60/60 ок —
сбои периодические (потери UDP к резолверу).
**Фикс (по команде пользователя)**:
1. В `/etc/resolv.conf` ВМ добавлен резерв `nameserver 1.1.1.1` — сбои остались (2/60).
2. В `/etc/hosts` ВМ добавлено `185.247.187.151 sqs.containerk8s.dev.nubes.ru` —
резолв из файла, 100/100, 0–5 мс. Зависимость от DNS устранена.
**Финальный 30-мин прогон**: запущен 18:30:52 +03 (ВМ) — в процессе.
## Q&A Соннета — какие ещё тесты провести (14.08.2026)
**Вопрос (кратко)**: достаточен ли набор проверок; какие сценарии не покрыты; норма ли
ReadTimeout 0.04%; серверные таймауты; нужен ли CLI v2 в нагрузке; что ещё до релиза.
**Ответ Соннета (ключевое)**:
- Не покрыто, но критично: рестарт пода с in-flight сообщениями (asyncWrite до смерти пода
→ двойная доставка); unbounded goroutines при деградации Redis (OOM на 512Mi);
long-poll goroutine leak; FIFO-порядок под конкурентными receive; DLQ e2e; dedup-окно e2e.
- Восстановление очередей из Redis после рестарта — обязательный тест.
- ReadTimeout 0.042% — граница, снижать до <0.01%: проверить кластеризацию ошибок во времени
(lock contention vs gateway), keep-alive/RST на шлюзе, TCP retransmits пода.
- Серверные таймауты добавить: ReadHeaderTimeout 10s, WriteTimeout 35s (20с long-poll + буфер),
IdleTimeout 90s, MaxHeaderBytes 8192. ReadTimeout НЕ ставить (рвёт long-poll).
- AWS CLI v2 (query-mode JSON) в нагрузке — обязательно, другой путь сериализации.
- Память (RSS 2+ часа), billing-счётчики, JWT-истечение mid-request, лимит 50 очередей
(граница включительно), /metrics корректность после прогона.
- Приоритеты: P0 серверные таймауты + рестарт с in-flight; P1 Redis chaos + long-poll leak;
P2 CLI v2 нагрузка + FIFO dedup/DLQ e2e; P3 RSS.
## План диагностики САМОЙ платформы (HTTP контейнер), независимо от SQS (14.08.2026)
Пользователь: проверить, как HTTP-контейнер платформы сбоит сам по себе (аналогия:
managed flask/nodejs/lucee не пропускают большие файлы с локали). Отделяем транспорт от логики SQS.
A. Через /health (минимум логики): burst 500 параллельных; большие заголовки 8–16KB;
медленный запрос (1 байт/с); keep-alive 50 запросов на одном соединении; HTTP/1.0, HEAD, POST.
B. Через SQS API, смотрим транспорт: SendMessage 256KB (с ВМ И с локальной машины);
chunked-тело; Receive 10×256KB (~2.5MB ответ); long-poll 20с (локаль vs ВМ).
C. Известные аномалии: GET /ui через ingress висит ~150с (внутри пода мгновенно);
ReadTimeout-кластеры; max latency 52с.
D. TCP: retransmit-счётчики пода (/proc/net/snmp) до/после.
Итог: таблица «локаль vs ВМ vs внутри пода» — что режет платформа, а что сервис.
## РЕЗУЛЬТАТЫ диагностики платформы (tests/platform_probe.py, 14.08.2026)
**ГЛАВНАЯ НАХОДКА — большие POST-тела зависают на ~51с (защитный слой платформы):**
- send 248KB — 0.090.22с; 5664KB — то быстро, то виснет (порог плавает);
80KB/100KB — первый запрос 50–52с, следующие 0.03с.
- Воспроизводится и с ВМ, и с локальной машины → общий узел DDoS-Guard/WAF.
- 100KB с БИТОЙ SigV4 — тоже 50.4с (висит путь, не наш auth/сервис).
- /health параллельно зависшему запросу — 0.02–0.05с (сервис жив).
- TCP connect+TLS мгновенны всегда (в т.ч. после idle 60/120с).
- Большие ОТВЕТЫ не режутся: receive 10×256KB (2.5MB) — 0.26с.
- Long-poll 20с держится честно (20.01с) — платформа не режет до 20с.
**Прочие ограничения платформы:**
- Заголовки >16KB → 400 Bad Request (8KB проходит).
- Chunked Transfer-Encoding → 400.
- POST /health → 400; HEAD /health → 405 (наш mux); HTTP/1.0 → ок (апгрейд).
**Нагрузка на под**: burst 500 × /health: 500/500 ok, p50=2.54с, p95=3.43с
это CPU-лимит пода 500m, а не платформа (внутренние запросы мгновенны).
**Следствие**: клиенты с сообщениями >~64KB и read_timeout < 51с получают
ReadTimeout на первом запросе в окне (это объясняет кластеры ReadTimeout и
max latency 52с в нагрузочных тестах). Рекомендации: клиентам read_timeout ≥ 60с
и retries; вопрос про WAF-инспекцию больших POST — в поддержку платформы.
## УТОЧНЕНИЕ ПРИЧИНЫ 51с — это MTU, как в drhider (14.08.2026)
**Гипотеза WAF выше — НЕВЕРНА.** Найдено в `~/nubes/drhider`:
- `PROBLEM-AND-SOLUTION.md` «Проблема 1: 51-секундная задержка (MTU)»: POST из
внешней сети — пауза 51с; внутри кластера — мгновенно. Причина: underlay Nubes
MTU 1450, Cilium Geneve +50, MTU пода 1500 → cross-node пакет 1550 → фрагментация
→ дроп → retransmit ~51с. Решение: `cilium mtu: 1400` (1400+50=1450).
- `docs/about1500.md`: готовый текст обращения в поддержку Nubes.
- VIP drhider — тот же `185.247.187.151`, что у `sqs.containerk8s.dev.nubes.ru`.
**Проверено у нас (факты)**:
- Cilium кластера iot-naeel: `mtu: 1400` — внутренний участок УЖЕ исправлен;
под eth0 MTU 1400 ✓.
- ВМ: ens192 MTU **1500** → сегменты 1460 не влезают в underlay 1450 на участке
ВМ→шлюзы платформы (traceroute: 10.10.102.1 → 100.64.x.x → 10.200.x.x → 5.181.12.1).
- Локальная машина (интернет → DDoS-Guard): та же 51с.
- Большие ОТВЕТЫ (под→клиент, 1400-байтные сегменты) проходят быстро.
- Порог зависания плавает ~56–64KB: зависит от необходимости полных 1460-байтных
сегментов.
**РЕШЕНИЕ ПОЛЬЗОВАТЕЛЯ (14.08.2026)**:
- КОД НЕ МЕНЯТЬ (лимит размера остаётся 262144/AWS).
- kubectl — только для диагностики; прод — кластер облака БЕЗ нашего доступа
(править MTU можем только там через поддержку Nubes).
- ТЕСТЫ — без больших сообщений: valid-тела ≤48KB; сценарий «превышение лимита»
через очередь с MaximumMessageSize=1024 и телом 2KB (граничное условие
сохраняется, большое тело не шлётся).
- Ограничение платформы (51с на большие POST) остаётся для реальных клиентов —
адресовать в поддержку Nubes текстом из drhider/docs/about1500.md.
## Тесты без больших тел (14.08.2026)
**Правки тестов (код сервиса НЕ менялся)**:
- `load_test.py`: `rand_body` ≤48KB; сценарий «превышение лимита» переделан —
очередь с `MaximumMessageSize=1024` + тело 2KB (граничное условие MessageTooBig
сохраняется, большое тело по сети не шлётся, MTU-зависаний нет).
- `platform_probe.py`: B6/B8 — 32KB вместо 256KB.
- Коммит `96f55be`, запушен, синхронизирован на ВМ.
**Короткий прогон 120с**: expected 3691, unexpected 2 (оба сетевые ReadTimeout),
soft_warn=0, health_fail=0. Новый граничный сценарий работает (InvalidParameterValue 246).
**30-мин прогон**: запущен 19:56:34 +03 (ВМ) — в процессе.
**ИТОГИ 30-мин прогона (завершён 20:26:52 +03)**:
- Операций 105 810 (рекорд серии); корректных 12 433; ожидаемых отказов 93 362;
неожиданных **15 = все ReadTimeoutError (сеть, 0.014%)**; сервисных 0.
- Некорректное принято: 0. Сбоев /health: 0. Рестарты пода: 0 (157 мин работы).
- Латентность: min 0.004с, avg 0.045с, max 51.9с (один потерянный ответ + ретраи).
- MTU-зависаний нет (большие тела исключены из тестов).
- Все граничные сценарии работают: NonExistentQueue 24 896, InvalidParameterValue
(AWS) 18 672, MissingParameter 12 448, ReceiptHandleIsInvalid 6 226, Purge ×2,
batch 11/дубли/пустой — по 6 224, превышение лимита (очередь 1024) 6 224.
**ИТОГ СЕРИИ v0.1.35**: сервис под нагрузкой стабилен (0 падений, 0 сервисных
ошибок, 0 принятых недопустимых условий). Остаточные неожиданные — только
сетевые потери ответов (0.01–0.04%). Ограничение платформы (большие POST ~51с,
MTU) — в поддержку Nubes.
## СУРОВЫЙ ТЕСТ: 3 тенанта одновременно (14.08.2026)
**Подготовка**: токены 3 юзеров (`~/nubes/tokens_all.txt`); email из JWT;
детерминированные креды вычислены алгоритмом сервиса; логин `POST /ui/api/auth`
по всем трём токенам — 200, max_queues=50 (PingNubesAPI перебирает стенды
PROD/TEST/DEV — токен от любого стенда принимается). Мусор вычищен до старта.
**Прогон**: три `load_test.py` параллельно, 12 воркеров на один под (CPU 500m),
30 мин, запуск 21:18:21 +03, завершение ≈ 21:48 +03.
**Итоги по тенантам** (неожиданные — только ReadTimeout, сеть):
| Тенант | Операций | ok | Ожид. отказов | Неожиданных | soft_warn | /health | avg |
|---|---|---|---|---|---|---|---|
| tazetdinovn@gmail.com | 60 792 | 7 148 | 53 640 | 4 | 0 | 0 | 0.097с |
| tazet@narod.ru | 56 083 | 6 593 | 49 485 | 5 | 0 | 0 | 0.104с |
| ntazetdinov@nubes.ru | 51 716 | 6 075 | 45 632 | 9 | 0 | 0 | 0.107с |
Суммарно 168 591 операция (×1.6 одиночного прогона). Некорректное принято: 0 у всех.
Под: restarts 0 (4ч13м работы). После прогона очереди вычищены до 0/0/0.
В `load_test.py` добавлена авточистка очередей до и после прогона (коммит `77b8586`).
## Локализация 51с — финальные факты (15.08.2026)
**Топология входа** (kubectl, только чтение):
- Все домены (sqs/drhider/pythonk8s/…) — один `shturval-ingress-controller`
(nginx) в ns `ingress`, Service LoadBalancer externalIP **185.247.187.151**,
NodePort 443:32391; kube-vip (DaemonSet `shturval-vip-*`, hostNetwork) — по
поду на каждую из 4 нод.
- Наш сервис: ClusterIP 10.104.111.239:4100 → под 172.16.2.226 (нода bhbvs).
- Ingress-поды: control-plane + v8zq4, MTU 1400; наш под MTU 1400 — внутри кластера чисто.
**Решающие замеры с ВМ**:
- MSS от VIP = **1448** (платформа клампит, но недостаточно: сегменты 1448 →
IP-пакеты 1488 > underlay 1450 → дроп при DF).
- Большой POST (100KB) через VIP → ответ 403 через **51.08с** (воспроизведено сырым сокетом).
- Ноды кластера с ВМ НЕ доступны напрямую (`No route to host` на 10.10.102.4:32391) —
L2-обхода нет, ВМ→ноды идёт тем же платформенным маршрутом.
**ВЫВОД**: дроп — на платформенном участке «клиент → VIP» (underlay 1450,
MSS clamp 1448, PMTUD мёртв). Кластером (kubectl) НЕ чинится — чинится только:
1) поддержкой Nubes (clamp MSS ≤1400 на шлюзах / починить PMTUD); 2) обходным
прокси на ВМ с MTU 1400 на ens192 (вход на ВМ чист — факт drhider; выход ВМ→кластер
починится уменьшением MTU ВМ); 3) временным лимитом размера в сервисе.
## План Соннета — выполнение (15.08.2026)
**Backup**: ветка `backup-v0.1.35-2026-08-15` (запушена в Gitea), работа в `main`.
**P0.1 Серверные таймауты — СДЕЛАНО** (коммит `af0325e`, digest `sha256:4385597f…`):
`http.Server{ReadHeaderTimeout:10s, WriteTimeout:35s, IdleTimeout:90s, MaxHeaderBytes:8192}`,
`ReadTimeout` убран (не рвать большие/медленные тела и long-poll).
**P0.2 Рестарт с in-flight — СДЕЛАНО, PASS** (`tests/reboot_probe.py`):
10 очередей × 20 сообщений + 100 сообщений (50 in-flight) → `kubectl delete pod
--grace-period=0 --force` → после подъёма: /health ok; restore 10×20 OK;
main: получено 100, уникальных 100, пропущено 0, дублей 0.
**НАЙДЕН ДЕФЕКТ (критичный)**: после деплоя таймаутов под рестартовал — и у
тенанта «воскресли» 50 удалённых ранее очередей (лимит 50 → LimitExceeded).
Причина: `DeleteQueue` пишет удаление в Redis АСИНХРОННО (asyncWrite); при
SIGKILL/рестарте пода запись не успевает → очередь остаётся в Redis →
`LoadAllQueues` восстанавливает её. Удаления должны быть синхронными
(хотя бы DEL очереди) — НЕ ИСПРАВЛЕНО, задокументировано.
## P2.6 FIFO/DLQ e2e — результаты (15.08.2026)
`tests/fifo_dlq_probe.py`:
- **FIFO порядок**: PASS — 10 сообщений группы выданы строго m0..m9.
- **FIFO dedup** (дубль с тем же MessageDeduplicationId внутри окна): PASS —
в очереди одно сообщение («first»).
- **DLQ (RedrivePolicy)**: **FAIL — найден баг**: `setQueueAttributesV1` ищет DLQ
по ГОЛОМУ имени `models.SyncQueues.Queues[queueName]`, а ключи в map
tenant-scoped "{accessKey}:{queueName}" → DLQ не находится → CreateQueue
с RedrivePolicy всегда InvalidAttributeValue. RedrivePolicy не работает.
НЕ ИСПРАВЛЕНО, задокументировано.
**Истечение dedup-окна (5 мин) не проверено** — тест быстрый, только «внутри окна».
## ИСПРАВЛЕНИЯ дефектов (15.08.2026, коммит `7467ecc`, digest `sha256:b81f855a…`)
1. **Воскрешение удалённых очередей/сообщений** (critical): все УДАЛЕНИЯ из
Redis переведены в синхронный режим (DeleteQueue, DeleteMessagePersist,
DeleteMessagesPersist, PurgeMessagesPersist; таймауты 3–5с). Записи
(SaveMessage/SaveMessages/SaveQueue/SaveTenantRaw) остались асинхронными.
Причина дефекта: удаления шли через asyncWrite и терялись при SIGKILL/рестарте.
2. **RedrivePolicy (DLQ) не работал**: `setQueueAttributesV1` искал DLQ по
голому имени, а ключи очередей tenant-scoped → InvalidAttributeValue всегда.
Теперь DLQ ищется по `{tenantAccessKey}:{queueName}`; в вызовы передан t.AccessKey.
**Верификация после деплоя**:
- `fifo_dlq_probe.py`: FIFO порядок PASS, FIFO dedup PASS, **DLQ PASS**
(сообщение после 2 попыток ушло в DLQ, в основной пусто).
- Тест «воскрешение»: 3 очереди созданы и удалены → SIGKILL пода → после подъёма
удалённые НЕ вернулись (осталась только сирота от первого FAIL-прогона DLQ —
удалена вручную). Фикс работает.
## 2026-08-15 (продолжение) — план Соннета: P1.4, P2.5, P3.7, P3.8, фикс api_test
**Регрессия api_test после фиксов удалений (найдена и закрыта)**:
- После деплоя синхронных удалений api_test стал 21/22: FAIL на DeleteMessage
(ReceiptHandleIsInvalid), стабильно 2 прогона.
- Диагностика: тест делал receive → ChangeMessageVisibility(timeout=**1с**) → delete.
По AWS-семантике через 1с сообщение снова видимо и handle сбрасывается →
delete по старому handle → ReceiptHandleIsInvalid. Сетевые задержки >1с
(в т.ч. шум MTU-проблемы платформы) приводили к случайному FAIL.
- Замеры: CV=1с → run1 FAIL, runs 2-3 OK; CV=5с → **5/5 OK (fails=0/5)**.
Сервис корректен (AWS-семантика), тест использовал слишком агрессивный таймаут.
- Фикс: `tests/api_test.sh` — `--visibility-timeout 1` → `5` + комментарий.
- Прогон после фикса: **22/22**. Коммит bdc7336, запушен.
**P1.4 long-poll leak (200 клиентов)**:
- 200 одновременных receive с WaitTimeSeconds=20 (boto3, без ретраев), очередь
longpoll-leak-*, после — cleanup. Ошибок клиента: 0.
- Threads пода: 12 до → **13 после**. Утечки горутин/потоков НЕТ. PASS.
**P2.5 CLI v2 в нагрузке** (`tests/cli_load_test.py`, на ВМ):
- 50 циклов aws cli send/receive/delete = 150 операций, failed=0.
- Латентность: send p50=775мс p95=936мс; receive p50=761мс p95=826мс;
delete p50=761мс p95=839мс. PASS.
**P3.8 граница лимита очередей (DefaultTenantMaxQueues=50)**:
- Создание до отказа: ровно **50** создаются, **51-я → LimitExceeded**
(AWS.SimpleQueueService.LimitExceeded). После cleanup 0 очередей. PASS.
**P3.7 RSS 2+ часа (запущен фоновый мониторинг)**:
- nohup на ВМ: каждые 60с пишет VmRSS/Threads в `rss_monitor.log` (125 сэмплов).
- Стартовый сэмпл 08:16 +0300: VmRSS=16588 kB, Threads=13.
- Результат — после завершения мониторинга.
## 2026-08-15 (вечер) — финальная полная регрессия на ВМ + флап fifo_dlq_probe
**Запуск**: всё на ВМ (пользователь оффлайн), `tests/run_full_regression.sh`
в nohup → `full_regression.log`. Длительность 24с.
**Результаты**:
- api_test.sh: **22/22** (PASS=22 FAIL=0)
- sdk_test.py: **15/15** (PASS=15 FAIL=0)
- fifo_dlq_probe.py: **флап** — 3 прогона: PASS / FAIL / PASS (dlq: count=0).
**Разбор флапа dlq (сервис корректен, гонка в тесте)**:
- Механика сервиса: возврат видимости и перенос в DLQ делает тик PeriodicTasks
(период 1с) при истечении VisibilityTimeout. Перенос — при Retry >= MaxReceiveCount.
- Тест: VisibilityTimeout=1с, sleep 1.5с между receive, после 3-й попытки
СРАЗУ drain(dlq). При рассинхроне фазы тика (attempt 2 empty — тик не успел
вернуть сообщение за 1.5с) вторая доставка случается в attempt 3, и проверка
DLQ выполняется до тика переноса → dlq пуст → FAIL. Через ~2с сообщение
всё равно ушло бы в DLQ.
- Доказательство: `tests/dlq_deterministic_probe.py` — тот же сценарий, но с
ожиданием 2.5с после каждой доставки: **5/5 PASS**
(r1=1 r2=1 r3=0 dlq=1 main_left=0 во всех 5 прогонах).
- Вывод: перенос в DLQ работает стабильно; флап — исключительно тайминг теста.
**Предложенная правка** `tests/fifo_dlq_probe.py` (жду «делай»): после цикла
попыток — опрос DLQ до 10с (drain каждые 1с) вместо мгновенного drain.
**RSS-мониторинг P3.7**: продолжает писаться в `rss_monitor.log` (старт
08:16 +0300, VmRSS=16588 kB, Threads=13).
## 2026-08-15 — фикс fifo_dlq_probe применён, финальная регрессия зелёная
**Правка** (`tests/fifo_dlq_probe.py`, по команде «делай»): после цикла попыток
DLQ опрашивается до 10с (drain каждые 1с) вместо мгновенной проверки —
устранена гонка с тиком PeriodicTasks (период 1с).
**Проверка**: 5/5 PASS подряд, включая «тяжёлые» фазы (attempt 2 empty /
attempt 3 received), которые раньше флапали.
**Финальная полная регрессия на ВМ** (`full_regression2.log`, 26с):
- api_test.sh: **PASS=22 FAIL=0**
- sdk_test.py: **PASS=15 FAIL=0**
- fifo_dlq_probe.py: fifo_order PASS, fifo_dedup PASS, dlq PASS, exit=0
ВСЕ тесты зелёные. План Соннета выполнен: P0.1, P0.2, P1.4, P2.5, P2.6,
P3.8 — PASS; P3.7 (RSS 2ч) — мониторинг в процессе.
## 2026-08-15 — P3.7 (RSS 2ч) завершён: PASS
Мониторинг отработал полные 125 сэмплов (каждые 60с), 08:16 → 10:21 +0300:
- VmRSS: старт 16588 kB → финал **15648 kB** (не растёт, даже −940 kB после
стабилизации GC). Плато 15648 kB держится последние сэмплы.
- Threads: стабильно **13** на всём интервале.
- Утечки памяти/потоков НЕТ. План Соннета выполнен полностью.
## 2026-08-15 (день) — сравнение shared-SQS vs Yandex YMQ (очередь newsqs)
**Скрипт**: `tests/compare_ymq_vs_shared.py` (новый, локально + ВМ). N=50,
тело 512 байт, последовательно: send x50 → receive+delete x50. retries=0.
**Прогон 1** (ВМ):
- shared-SQS: send p50=6мс p95=17мс **max=12648мс**; receive p50=7мс; delete p50=8мс; 50/50, errs=0, total=13.9с.
- YMQ: send p50=62мс p95=76мс max=84мс; receive p50=60мс; delete p50=62мс; 50/50, errs=0, total=9.4с.
**Прогон 2** (ВМ, повтор):
- shared-SQS: send p50=8мс p95=16мс **max=25446мс**; receive p50=6мс; delete p50=7мс; 50/50, errs=0, total=26.6с.
- YMQ: send p50=62мс p95=69мс max=72мс; receive p50=60мс; delete p50=62мс; 50/50, errs=0, total=9.5с.
**Выводы (факты)**:
1. По p50 наш сервис в ~8–10 раз быстрее YMQ (6–8мс против 6062мс).
2. У shared-SQS в КАЖДОМ прогоне ровно один send зависает на 12.6с / 25.4с
(тело 512 байт — размер ни при чём; errs=0, ответ приходит). YMQ — без
выбросов, стабилен.
3. Гипотеза выброса: платформенные «паузы» сети к поду Nubes (ранее фиксировали
зависания ~51с на больших POST из-за MTU/MSS 1448) — но при 512 байтах
причина требует отдельного расследования (tcpdump/тайминги платформы).
## 2026-08-15 — ДЛИННОЕ сравнение с ЛОКАЛИ (30 мин), shared-SQS vs YMQ
**Скрипт**: `tests/long_compare_local.py` (ping-pong: пул K=10, раунды
попеременно shared→ymq→shared..., receive→delete→send; retries=0,
read_timeout=30с; лог `tests/long_compare.log`). Локаль (WSL), 12:2712:57.
**Итог (317 раундов):**
- shared-SQS: send n=302 p50=90мс p95=269мс max=2785мс; receive n=293 p50=89мс
p95=279мс max=2577мс; delete n=293 p50=90мс p95=103мс max=2462мс;
**errors=49 ReadTimeoutError (30с)**.
- YMQ: send n=327 p50=138мс p95=157мс max=355мс; receive n=316 p50=150мс
p95=421мс max=2473мс; delete n=316 p50=138мс p95=152мс max=2105мс;
errors=1 ProxyConnectionError.
**Выводы (факты):**
1. По p50 наш быстрее: 89–90мс против 138150мс (~1.51.7 раза).
2. НО у нас **49 запросов из 888 (5.5%) вообще не получили ответ за 30с**
(каждый таймаут — 30с простоя); у Яндекса 1 ошибка из 959.
3. Полезной работы за 30 мин: наш 888 операций, Яндекс 959 — таймауты съели
преимущество по скорости.
4. Тренд: таймауты равномерны весь тест (~1 в 37с) — системная проблема
сетевого пути локаль→шлюз Nubes, не код сервиса (с ВМ их нет).
5. Выбросы max (2.5–2.8с) есть у ОБОИХ: наш send max=2785мс, YMQ receive
max=2473мс — сетевые, не сервисные.
## 2026-08-15 — план сетевых экспериментов от DeepSeek V4 Flash (НЕ Соннет)
Пользователь по ошибке задал вопрос DeepSeek V4 Flash вместо Соннета.
План сохранён: `doc/thinking/nubes-network-bug-plan.md` с пометкой о слабом
агенте и поправками основного агента.
Оценка плана (основной агент): в целом сильный — верно выделяет ДВА явления
(MTU для больших + независимые потери для малых 512 байт), верная декомпозиция
по request_time/upstream_response_time nginx. Но: ошибка в команде cilium
monitor (через cilium-operator — неверно, надо cilium-под), echo-сервис
переоценён (проблема между интернетом и внутренней сетью — echo внутри неё),
tcpdump на WSL требует sudo/интерфейс, шаг 7 (матрица размеров) слишком
дорогой. Рекомендованный старт: nginx-логи → tcpdump → cilium monitor.
## 2026-08-15 — план v2 от DeepSeek V4 Pro (новый чат, НЕ Соннет) + сравнение
Сохранён: `doc/thinking/nubes-network-bug-plan-v2-deepseek-pro.md`.
Оценка: план Pro сильнее Flash. Плюсы: сырые TCP с критерием доставки по TCP
ACK (не HTTP), PMTU-зонд ping -M do с порогами 1372/1422/1460/1472, второй
клиент с другого ISP, kubectl port-forward как изоляция приложения, таблица
«что считается доказательством», верное наблюдение что 51с — прикладной таймер
а не TCP-backoff. Минусы: kube-proxy может отсутствовать (Cilium replacement),
debug-контейнеры на managed скорее всего запрещены (план сам оговаривает),
эхо-NodePort требует открыть NodePort на платформе (проверяемо).
Сравнение Flash vs Pro: Flash — декомпозиция по nginx-логам, но ошибка
cilium-operator, echo переоценён, нет PMTU/port-forward/второго ISP.
Pro — методологически полнее, грубых ошибок нет.
Рекомендованный синтез-порядок: nginx-логи → PMTU-зонд → сырые TCP →
port-forward → tcpdump клиента → второй ISP → cilium monitor → echo → тикет.
## 2026-08-15 — план v3 от СОННЕТА + сравнение трёх планов
Сохранён: `doc/thinking/nubes-network-bug-plan-v3-sonnet.md`.
Оценка (основной агент): план Соннета — лучший из трёх. Ключевые достоинства:
явное разделение явлений A (MTU/PMTUD, 51с = экспонента RTO 1-2-4-8-16) и
B (регулярность 1/37с = таймер gateway/kube-vip/conntrack), port-forward первым
kubectl-тестом, точный список размеров ping-зонда, корреляция по TCP seq.
Поправки: fallback если ICMP закрыт (mtr --tcp/TCP-порог), добавить второй
ISP и контрольный канал YMQ, port-forward идёт через API-сервер (исключает
приложение, но не доказывает шлюз), точный интервал таймаутов мерить новым
тестом с timestamp на каждый таймаут.
Сравнение: Flash — ошибки и недооценка; Pro — силён, но без явного разделения
явлений; Соннет — принять за основу с поправками.
## 2026-08-15 — план v4 от ОПУС 4.8 + ответы на его уточнения
Сохранён: `doc/thinking/nubes-network-bug-plan-v4-opus.md` (план + ответы).
Опус изучил репозиторий и три предыдущих плана; его план согласуется с
Соннетом (разделение A/B, port-forward первым, tcpdump по seq, cilium monitor
на поде). Ответы основного агента на уточнения Опуса:
1) второго ISP нет, полагаемся на контрольный канал YMQ (1 ошибка на 959
против 49 таймаутов Nubes — локальный интернет исключён);
2) debug-под с NET_RAW скорее всего запрещён, серверный tcpdump не получится —
nginx-логи + cilium monitor + echo А/Б;
3) echo деплоить только если фазы 0–2 не хватят (риск: NodePort на managed).
Поправка: port-forward идёт через API-сервер — доказывает невиновность
приложения, но не вину шлюза.
Все 4 плана сохранены в doc/thinking/: Flash, Pro, Sonnet, Opus.
## 2026-08-15 — финал плана (Опус) + сравнение ЧЕТЫРЁХ агентов
Финал дописан в `doc/thinking/nubes-network-bug-plan-v4-opus.md` (решения
зафиксированы: YMQ-канал вместо второго ISP; NET_RAW — одна попытка; echo
отложить; port-forward = только невиновность приложения).
Мнение основного агента о четырёх ответах на один промпт: условия не равны
(Соннет читал Flash/Pro, Опус читал все + репозиторий — накопленный контекст).
Вклады: Flash — первым увидел явление B; Pro — инструменты (PMTU-зонд, второй
ISP, port-forward, таблица доказательств); Sonnet — структура (A/B, RTO,
таймер 37с); Opus — интеграция и фиксация решений. Конвергенция на одной
цепочке = план устойчив. Итог — продукт всех участников. План готов к
исполнению: Фаза 0 → 1 → 2 → 3 → 4.
## 2026-08-15 — Фаза 0 старт: kubectl НЕДОСТУПЕН (токен истёк)
Шаги:
1. `kubectl -n ingress get pods/svc` с ВМ → "You must be logged in to the
server (server has asked for credentials)".
2. Диагностика: kubeconfig ~/.kube/config на ВМ от **Aug 14 12:29**, контекст
`tazetdinovn@gmail.com@iot-naeel`, авторизация — token (exec-plugin нет).
Токен истёк к 14:13 15.08 (срок жизни ~26 ч).
3. История shell ВМ: kubeconfig ранее обновлялся вручную:
`xclip -selection clipboard -o > ~/.kube/config` — т.е. пользователь копирует
новый kubeconfig из буфера (консоль Nubes) на ВМ.
Результат: Фаза 0 заблокирована — нужен новый kubeconfig от пользователя
(консоль Nubes → скопировать → на ВМ: xclip -o > ~/.kube/config или заменить
файл). После обновления продолжить: nginx access-log → kube-vip логи →
логи пода.
## 2026-08-15 — Фаза 0 ВЫПОЛНЕНА: nginx чист, kube-vip с ошибками leaderelection
Окно теста: 08:26:4709:00 UTC (30-мин тест 12:27–12:57 локального времени).
**nginx access-log (оба пода ingress, за окно):**
- 874 записи к нашему хосту: 862×status=200, 12×404 (боты).
- **max ingress_request_time = 0.068с** за весь тест.
- **status=499 = 0** (ни одного клиентского обрыва).
- Вывод: с точки зрения nginx ВСЕ запросы обработаны за миллисекунды и
успешно. 49 клиентских таймаутов по 30с nginx НЕ видит → ответы теряются
МЕЖДУ nginx и клиентом (обратный путь/шлюз), либо часть запросов не дошла
до nginx (записей 862 против 888 операций — но боты засоряют счёт, точный
сплит не выводим).
**kube-vip (новое подозрение):**
- Под shturval-vip-dp24p (нода control-plane) — **каждые ~1с ошибка**:
`leaderelection: error initially creating leader election record: namespaces
"0a42bef3-7ce1-42b7-aa80-4274c4d9568f" not found`.
- Под пытается создать запись лидера в НЕСУЩЕСТВУЮЩЕМ namespace — похоже на
настроечный баг платформы. Влияние на VIP неизвестно — нужны логи остальных
4 vip-подов (кто лидер, были ли флапы).
**Наш под:** 682 строки логов за окно, ошибок нет; 13 warnings «Receipt Handle
not found» только в 08:5808:59 (фаза cleanup — норма).
**Следующие шаги Фазы 0:** логи остальных kube-vip подов (лидерство/флапы) →
externalTrafficPolicy сервиса ingress → Фаза 1 (PMTU-зонд + новый тест с
timestamp таймаутов).
## 2026-08-15 — Фаза 0 добита: kube-vip provider ломается, vipHost на control-plane
- Сервис ingress: `kube-vip.io/vipHost: iot-naeel-control-plane-xb699` —
VIP (185.247.187.151) статически закреплён за control-plane нодой.
- externalTrafficPolicy: Cluster, type: LoadBalancer, nodePorts: 32391 (https),
30739 (http).
- Provider kube-vip: «failed to ensure load balancer: no address pools could be
found» с экспоненциальным backoff — в окне теста события в 08:20:56, 08:25:57,
08:30:57 UTC (потолок 5 мин). Прямой корреляции с таймаутами ~37с НЕТ.
- ВСЕ 4 vip-пода (3 worker + 1 control-plane) каждые ~1с бьются в leaderelection:
namespaces "0a42bef3-..." not found (namespace удалён, поды остались).
- Итог Фазы 0: nginx чист (max 68мс, 499=0) → потери между nginx и клиентом;
kube-vip сконфигурирован с ошибками (сломанный leader election + LB без
пулов) — кандидат в тикет, но прямой связи с 37с пока не доказано.
## 2026-08-15 — Фаза 1: 403 InvalidClientTokenId С ЛОКАЛИ (с ВМ работает)
Факты:
1. TCP-зонд MTU-порога (443, бинарный): до 20KB — всё мгновенно (403 от пода,
mss=1448 подтверждён). В диапазоне 20–200KB — кластеры зависаний на 20с
(«порог» ~82KB), но зависания кластерные и, вероятно, не от размера.
2. 60 чистых connect к 443 — 0 сбоев; 5-мин серия POST /health (512б, 538 проб)
**0 сбоев** → явление B не воспроизводится на /health, привязано к
SQS-трафику или к другим условиям.
3. Порт 32391 (NodePort) с локали НЕДОСТУПЕН (SYN дроп), реальный путь — 443.
4. **НОВОЕ**: с ~14:30 все AWS-запросы С ЛОКАЛИ дают 403 InvalidClientTokenId
(aws CLI и boto3, креды те же). С ВМ те же креды РАБОТАЮТ (list-queues OK).
/health с локали — 200. Значит: внешний путь к шлюзу теперь отвергает
токен, внутренний — принимает. Днём (30-мин тест 12:27–12:57) внешний путь
РАБОТАЛ → аутентификация внешнего пути сломалась/сбросилась между 13:00 и
14:30. Возможная связь: обновление kubeconfig пользователем ~14:10–14:20.
5. Фаза 1 (SQS-цикл с таймстемпами) ЗАБЛОКИРОВАНА этим 403.
## 2026-08-15 — ОПРОВЕРЖЕНИЕ: «403 InvalidClientTokenId с локали» = моя ошибка env
Разбор: после команд YMQ в терминале остались экспортированные
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY от Яндекса (YCAJE.../YCNbo...).
boto3-скрипт и aws CLI без явных кредов подхватывали ИХ → shared-SQS отвечал
InvalidClientTokenId. С ВМ я всегда экспортировал креды shared-SQS явно —
поэтому «с ВМ работало». Явления C НЕТ. Фейковая подпись проверена: с локали
запрос доходит до пода корректно (SignatureDoesNotMatch), auth работает.
Урок: в boto3 всегда передавать aws_access_key_id/aws_secret_access_key явно,
не полагаться на env терминала.
## 2026-08-15 — ПЕРИОД ТАЙМАУТОВ ИЗМЕРЕН: ровно ~32.9с + полная проверка
**SQS-цикл 10 мин (Фаза 1, локаль, явные креды shared):** 348 раундов
(receive→delete→send 512б), 19 ReadTimeout (30с). Интервалы между сбоями:
32.8, 32.8, 32.8, 32.9, 32.8, 32.9, 32.9, 32.9, 32.8, 32.9, 32.9, 32.7, 32.9,
32.9, 32.9, 32.8, 33.0, 32.8 — **все 18 интервалов в 32.7–33.0с**.
Вывод: это ТОЧНО таймер (равномерность до 0.1с), не случайные потери.
Кандидаты: 32.9с ≈ период какого-то шлюзового цикла (kube-vip announce,
conntrack GC, session timeout) — искать у платформы.
Примечание: ранее оценка «~37с» была по косвенным данным, точный период 32.9с.
**Полная проверка (по требованию):**
1. py_compile всех tests/*.py — OK. bash -n всех tests/*.sh — OK.
2. Креды YMQ (newsqs) — рабочие (list-queues OK). Креды shared — рабочие.
3. В secrets/ лежат ДВА файла с ОДИНАКОВЫМИ новыми кредами: yandex_ymq_newsqs.txt
и yandex_ymq_fork8s.txt (дубликат, 461 байт, 11:54/11:55). Старых кредов
YCAJEQDz нигде нет. Дубликат fork8s.txt подлежит удалению (по команде).
4. git status: не закоммичен только tests/tcp_mtu_probe.py (сейчас коммитится).
5. Эпизод «403 с локали» — подтверждённо моя ошибка env (креды Яндекса в
терминале), не платформа. Сейчас все команды идут с ЯВНЫМИ кредами.