shared-sqs: doc thinking — итог сессии 2 (этапы 4-8)

This commit is contained in:
Naeel
2026-04-09 13:17:42 +03:00
parent 2c9a2b2ebf
commit 65dea8aa6b
+167
View File
@@ -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 тесты. Ждём указания пользователя.