diff --git a/doc/thinking/2026-04-09.md b/doc/thinking/2026-04-09.md index b9c77fc..e834ada 100644 --- a/doc/thinking/2026-04-09.md +++ b/doc/thinking/2026-04-09.md @@ -75,3 +75,170 @@ - Лечил симптом (lock message) вместо причины (concurrent access) **Вывод:** при любой ошибке связанной с persistence/lock/state — ПЕРВЫМ делом проверять: кто ещё имеет доступ к файлу? Сколько pod одновременно работают? Какая стратегия деплоя? + +--- + +## Анализ shared-sqs — форк GoAWS для multi-tenant SQS + +**Агент:** GitHub Copilot (Claude Opus 4) +**Время:** 2026-04-09, вечер + +### Контекст +Пользователь решил делать shared multi-tenant SQS сервис (вариант А — форк GoAWS). +Нужен детальный план для другого агента (Sonnet). + +### Исследование GoAWS + +Скачал и проанализировал исходники: +- **router.go** — gorilla/mux, единый `actionHandler` диспатчит по Action name из routingTableV1 +- **globals.go** — `SyncQueues` = один map[string]*Queue с RWMutex. Это ВЕСЬ state. +- **models.go** — Queue struct: Name, URL, ARN, Messages []SqsMessage, VisibilityTimeout и т.д. +- **create_queue.go** — создаёт очередь, ключ в map = queueName, URL = `http://host:port/accountID/queueName` +- **send_message.go** — извлекает queueName из последнего сегмента URL, ищет в SyncQueues +- **gosqs.go** — PeriodicTasks каждую секунду: visibility timeout reset, DLQ routing, dedup cleanup +- **configuration.go** — Environment struct с Host, Port, Region, AccountID (глобальная, ОДНА на всех) + +### Ключевое наблюдение + +GoAWS УЖЕ имеет `/{account}/{queueName}` маршрут в роутере. AccountID используется в URL/ARN. +Это значит: tenantID = accountID — естественное отображение. +Queue URL для тенанта: `http://host:port/{tenantID}/{queueName}` — совпадает с route pattern. + +### Архитектурное решение: tenant isolation + +Ключ в SyncQueues: `{accessKey}:{queueName}` (вместо просто `{queueName}`) +Почему AccessKey: уже есть в auth context, уникален, не надо лишний lookup по TenantID→AccessKey. + +### Идентифицированные ловушки (17 штук) + +Задокументировал в PLAN.md все точки где могут быть баги: +1. crypto/rand vs math/rand для генерации ключей +2. Удаление из двух индексов при delete tenant +3. Парсинг AWS Signature V4 Authorization header +4. Long polling таймаут (до 20 сек) +5. Игнорирование X-Amz-Security-Token +6. URL parsing — последний сегмент = queueName (НЕ tenantID) +7. getQueueFromPath() — совместимость с новым URL форматом +8. FIFO queue naming (.fifo суффикс) +9. DLQ ARN parsing для tenant-scoped lookup +10. QueueUrl в ответах ОБЯЗАН содержать tenantID +11. DELETE tenant → удалить все очереди (memory leak) +12. Admin API — отдельная auth (bearer token, не tenant credentials) +13. Graceful shutdown (GoAWS не обрабатывает signals) +14. Deployment strategy: Recreate (урок из ERR-SQS-06) +15. .gitignore для бинарника +16. go mod tidy после удаления SNS +17. Import paths: goaws → shared-sqs + +### Результат + +Создан `shared-sqs/PLAN.md` — ~400 строк, 9 этапов, 17 ловушек, примеры кода. +Достаточно подробный для Sonnet чтобы реализовать без дополнительных вопросов. + +--- + +## Этап 1 — Клон GoAWS + чистка (GitHub Copilot / Claude Sonnet 4.6) + +**Время:** 2026-04-09 + +### Анализ перед началом + +Директория `shared-sqs/` содержит только `PLAN.md` и `doc/`. Нужно: +1. Клонировать GoAWS upstream в `_upstream/`, скопировать `app/`, `go.mod`, `go.sum`, `Dockerfile` +2. Сменить module на `shared-sqs`, Go 1.22 +3. Заменить все import paths `github.com/Admiral-Piett/goaws/app/` → `shared-sqs/app/` +4. Удалить `gosns/` и все SNS-связанные места +5. Удалить тестовые директории + +### Риски +- GoAWS может иметь скрытые зависимости на SNS внутри SQS-handlers (например, DeadLetter → SNS upsert) +- `router.go` содержит SNS-actions в routingTableV1 — нужно аккуратно вычистить +- После удаления SNS могут остаться неиспользуемые imports — нужен `go mod tidy` + +### Стратегия +Все команды — по SSH. Файлы go модифицировать через `sed` (замена import paths). +После удаления SNS — `go build`, смотреть на ошибки, фиксить одну за одной. + +### Результат Этапа 1 +- GoAWS склонирован, module = `shared-sqs`, Go 1.22 +- Все import paths заменены (sed по всем .go) +- gosns/, mocks/, fixtures/, servertest/, test/ — удалены +- SNS-код убран из: models.go, configuration.go, globals.go, helpers.go, requests.go, responses.go, router.go, conf/config.go +- Нюанс: responses.go был обрезан слишком агрессивно — DeleteMessageBatch типы потерялись. Добавил их вручную. +- go build OK, /health → "OK" +- Коммит: f4352a1 + +--- + +## Этап 2 — TenantStore (GitHub Copilot / Claude Sonnet 4.6) + +### Анализ +Нужно создать `app/tenant/tenant_store.go`: +- Tenant struct: ID, Name, AccessKey, SecretKey, MaxQueues, CreatedAt, Active +- TenantStore: два индекса (byID + byAccessKey), RWMutex +- Ловушка #1: crypto/rand для ключей +- Ловушка #2: Delete из ОБОИХ индексов +- AccessKey формат: `SSAK-{12 random bytes hex}` = SSAK- + 24 hex chars +- SecretKey: 32 bytes → 64 hex chars + +--- + +## Сессия 2 (продолжение) — Claude Sonnet 4.6 + +### Продолжение с момента прерывания + +Предыдущая сессия была прервана на Этапе 4 (все 13 handlers переписаны, but go build not yet run). + +**Немедленное действие:** go build → чисто. Этап 4 пройден. + +### Этап 4 — результат +- go build: OK +- Бинарник: 10MB +- Коммит: 08053ca "shared-sqs: Этап 4 — изоляция очередей по тенанту" + +### Этап 5 — Admin API + +**Решение:** Создан `app/admin/admin.go` с handlers: +- `POST /admin/tenants` — create, возвращает secret_key ОДИН РАЗ +- `GET /admin/tenants` — список без secret_key +- `GET /admin/tenants/{id}` — деталь без secret_key +- `DELETE /admin/tenants/{id}` — удаляет тенанта + все его очереди из SyncQueues (Trap #11) +- `GET /admin/health` — детальный health с counts + +**Trap #12 реализован:** Bearer token auth в bearerAuthMiddleware, полностью изолирован от tenant auth. + +**Trap #11 реализован:** DELETE итерирует SyncQueues, удаляет все ключи с prefix `{accessKey}:`. Без этого — memory leak. + +### Этап 6 — Entry Point + +**Обновлён goaws.go:** +- Флаги: --config, --admin-token, --port, --debug, --loglevel +- Admin token: flag > env SHARED_SQS_ADMIN_TOKEN > `log.Fatal` (Trap #13) +- TenantStore инициализируется при старте +- `router.New(tenantStore, adminToken)` — передача зависимостей +- HTTP сервер с таймаутами (WriteTimeout = 35s > max WaitTimeSeconds 20s для long polling) +- Graceful shutdown: SIGTERM/SIGINT → close(quit) → srv.Shutdown(10s) + +**Trap #13 реализован:** SIGTERM → quit channel → PeriodicTasks останавливается корректно. + +### Этап 7 — Dockerfile + K8s + +**Dockerfile:** multi-stage (golang:1.22-alpine → alpine:3.19), CGO_ENABLED=0 +**K8s manifests:** +- namespace.yaml, deployment.yaml, service.yaml, secret.yaml +- `strategy: Recreate` — НЕ RollingUpdate (Trap #14: in-memory state, split brain risk) + +### Этап 8 — Makefile + +Таргеты: build, docker-build, docker-push, test, run, clean. +**Фикс:** Makefile через heredoc потерял табы → пересоздан через Python с \t. + +### Итоговое состояние + +go build → OK (все этапы 1-8) +Коммиты: +- 08053ca — Этап 4 +- 0736832 — Этапы 5+6 +- 2c9a2b2 — Этапы 7+8 + +Остался Этап 9 — bash тесты. Ждём указания пользователя.