350 lines
20 KiB
Markdown
350 lines
20 KiB
Markdown
# 2026-04-09 — Thinking Log
|
||
|
||
**Агент:** GitHub Copilot (Claude Opus 4)
|
||
|
||
---
|
||
|
||
## Анализ H2 file lock — корневая причина
|
||
|
||
### Симптом
|
||
При каждом `kubectl rollout restart` ElasticMQ стартует, но `SendMessage` зависает навсегда.
|
||
В логах: `The file is locked: /data/elasticmq.mv.db [2.2.224/7]`, затем `dead letters` и `AskTimeoutException` на SendMessage.
|
||
|
||
### Ошибочная гипотеза (v0.1.10)
|
||
Предположил что JVM не освобождает JDBC-level lock при crash → добавил `FILE_LOCK=NO` в JDBC URI.
|
||
**Это было НЕПРАВИЛЬНО.** `FILE_LOCK=NO` отключает только JDBC soft-lock. H2 MVStore использует `java.nio.FileChannel.lock()` — это OS-level file lock, не зависящий от JDBC параметров.
|
||
|
||
### Почему "работало" после каждой чистки
|
||
После `rm /data/elasticmq.mv.db` + restart — файл создаётся заново, lock отсутствует. Но при следующем rollout restart проблема возвращается.
|
||
|
||
### Корневая причина (найдена 2026-04-09)
|
||
|
||
**Deployment strategy: `RollingUpdate` + PVC: `ReadWriteOnce`**
|
||
|
||
Цепочка событий при `kubectl rollout restart`:
|
||
1. Kubernetes добавляет аннотацию `restartedAt` → меняется template → начинается rollout
|
||
2. Стратегия `RollingUpdate` (maxSurge=25%, maxUnavailable=25%) → для replicas=1:
|
||
- maxSurge=1 (ceil 0.25) → Kubernetes поднимает НОВЫЙ pod
|
||
- maxUnavailable=0 (floor 0.25) → старый pod ЕЩЁ ЖИВА
|
||
3. PVC `ReadWriteOnce` — допускает mount с нескольких pod на ОДНОЙ НОДЕ (это не ReadWriteOncePod)
|
||
4. Оба pod монтируют один PVC → оба пытаются открыть `/data/elasticmq.mv.db`
|
||
5. Старый ElasticMQ держит `FileChannel.lock()` → новый ElasticMQ получает `MVStoreException: The file is locked`
|
||
6. Persistence actor (SqlQueuePersistenceActor) в новом pod падает → dead letters
|
||
7. Старый pod убивается (readinessProbe eventual fail) → lock освобождается — но поздно
|
||
8. SQS REST server работает (port 9324 слушает), но WRITE-операции (SendMessage) зависают — actor мёртв
|
||
|
||
### Решение — 3 изменения в ensureDeployment
|
||
|
||
1. **Strategy: Recreate** (вместо RollingUpdate)
|
||
- Kubernetes СНАЧАЛА убивает старый pod, ПОТОМ поднимает новый
|
||
- Два pod НИКОГДА не работают одновременно → lock невозможен
|
||
- Downtime ~25-30 секунд (JVM startup) — допустимо для мультитенант SQS
|
||
|
||
2. **preStop hook: sleep 3**
|
||
- При SIGTERM JVM начинает shutdown
|
||
- `sleep 3` даёт H2 время на `fsync` + `FileChannel.close()`
|
||
- Без preStop: Kubernetes может убить pod раньше чем H2 закончит flush
|
||
|
||
3. **livenessProbe timeoutSeconds: 1 → 3**
|
||
- JVM стартует за 20-23 секунды
|
||
- initialDelaySeconds=5 + failureThreshold=5 × period=10 = 55 сек запас — хватает для старта
|
||
- НО: `timeoutSeconds=1` — если GC pause > 1 сек → liveness fail → unnecessary restart → CrashLoopBackOff
|
||
- Поднимаем до 3 секунд. GC pause > 3 сек — это уже реальная проблема которую стоит рестартить
|
||
|
||
## Дополнительные обнаруженные проблемы
|
||
|
||
### /_next/ и /queues/ ingress — глобальные (архитектурная)
|
||
Пути `/_next/` и `/queues/` на хосте `sqs.kube5s.ru` общие. При двух тенантах с `enableUI=true` — конфликт ingress.
|
||
**Решение отложено** — пока один тенант с UI. При мультитенант UI → нужен отдельный хост per tenant.
|
||
|
||
### imagePullPolicy: Always на UI
|
||
`softwaremill/elasticmq-ui:latest` + `Always` → upstream может сломать при обновлении.
|
||
**Пока оставляем** — будем пинить версию когда стабилизируем.
|
||
|
||
### memory limit 512Mi vs Xmx 384m
|
||
`-Xmx384m` + JVM overhead ~150 МБ = ~534 МБ > limit 512 Mi. OOMKill возможен при нагрузке.
|
||
**Пока оставляем** — в idle не стреляет. Учтём при нагрузочном тестировании.
|
||
|
||
## Самоанализ ошибки
|
||
|
||
Почему неправильно решил в v0.1.10:
|
||
- Увидел `The file is locked` → сразу искал H2-настройки → нашёл `FILE_LOCK=NO`
|
||
- НЕ проверил deployment strategy (RollingUpdate — default в Kubernetes)
|
||
- НЕ проверил ReadWriteOnce behavior (допускает multi-pod на одной ноде)
|
||
- НЕ проверил что происходит при rollout (два pod одновременно)
|
||
- Лечил симптом (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 тесты. Ждём указания пользователя.
|
||
|
||
---
|
||
|
||
## Агент: GitHub Copilot (Claude Opus 4.6) — SQS Console UI
|
||
|
||
### Задача
|
||
Создание веб-интерфейса для shared-sqs по образцу IoT Console (Nubes branding).
|
||
|
||
### Анализ
|
||
- Изучил HTML/CSS IoT Console (`iot.kube5s.ru/console`) — 1330 строк, vanilla SPA
|
||
- Извлёк палитру Nubes: `#001C34` navy, `#001120` bg, `#1a7fd4` accent, `#e2ecf6` text
|
||
- Изучил admin API: GET /admin/health, GET/POST /admin/tenants, GET/DELETE /admin/tenants/{id}
|
||
- Изучил структуры: `TenantStore`, `SyncQueues.Queues`, `Queue`, `SqsMessage`
|
||
|
||
### Реализация
|
||
|
||
1. **API endpoint** — `GET /admin/tenants/{id}/queues` в `app/admin/admin.go`:
|
||
- Ищет очереди по префиксу `AccessKey:` в `SyncQueues.Queues`
|
||
- Возвращает JSON: name, messages, not_visible, visibility_timeout, max_message_size, retention_period
|
||
- Thread-safe (RLock/RUnlock)
|
||
|
||
2. **Embedded UI** — `app/ui/embed.go` + `app/ui/index.html`:
|
||
- `go:embed index.html` → `http.FileServer(http.FS(content))`
|
||
- SPA ~400 строк HTML + CSS + JS, vanilla, без фреймворков
|
||
- Логин по admin bearer token (проверка через `/admin/health`)
|
||
- Dashboard: stats grid (tenants, queues, messages, status) + таблица тенантов
|
||
- Tenant detail: breadcrumb, stats, таблица очередей с live-данными
|
||
- CRUD тенантов: создание (модалка), удаление (confirm), показ credentials
|
||
- Auto-refresh каждые 10с, sessionStorage для авто-логина
|
||
- XSS-защита через `esc()` (textContent → innerHTML)
|
||
- Responsive: mobile-friendly grid
|
||
|
||
3. **Route** — `/ui` в `app/router/router.go`:
|
||
- `r.PathPrefix("/ui").Handler(http.StripPrefix("/ui", ui.Handler()))`
|
||
- До SQS subrouter (не перехватывается auth middleware)
|
||
|
||
### Деплой
|
||
- Docker образ `naeel/shared-sqs:v0.1.4` — собран, запушен
|
||
- Deployment обновлён, rollout successful
|
||
- UI доступен: `https://qu.kube5s.ru/ui/`
|
||
- API проверен: health (1 tenant, 5 queues), tenant queues endpoint работает
|
||
|
||
### Коммит
|
||
- `12b3bb9` — feat(shared-sqs): add SQS Console UI (v0.1.4)
|
||
- Pushed to `sqs-operator`
|
||
|
||
---
|
||
# GitHub Copilot (Claude Sonnet 4.6) — продолжение сессии 2026-04-09 (вечер)
|
||
|
||
## Контекст
|
||
|
||
Продолжение с конца предыдущего сеанса. Была реализована база admins API.
|
||
Нужно было:
|
||
1. Исправить `models.NewUUID()` → найти правильную функцию генерации UUID
|
||
2. Написать frontend для peek/send/purge/queue CRUD
|
||
3. Собрать + задеплоить v0.1.6
|
||
|
||
## Анализ + ход мыслей
|
||
|
||
### UUID
|
||
Сделал `grep` по Uuid в gosqs пакете → нашёл `uuid.NewString()` из `github.com/google/uuid`.
|
||
Исправил admin.go: добавил import + заменил `models.NewUUID()` → `uuid.NewString()`.
|
||
Сборка прошла чисто.
|
||
|
||
### Frontend (index.html)
|
||
Прочитал весь текущий HTML/JS. Отметил:
|
||
- CSS уже есть: `.card`, `.btn`, `.badge`, `.modal`, `.hidden`
|
||
- JS helpers: `esc()`, `api()`, `fmtDate()`, `fmtBytes()`, `fmtDuration()`
|
||
- Шаблонные literals со вложенностью (outer: `el.innerHTML = \`...\``, inner: `.map(q => \`...\`)`)
|
||
|
||
Решения:
|
||
- **IDs для expandable rows**: `msgs-${q.name}`, `qicon-${q.name}` — SQS-имена только `[a-zA-Z0-9_-]`, безопасно
|
||
- **msgCache**: хранить весь объект сообщения в Map по id — чтобы не передавать body через onclick attrs (безопаснее, нет проблем с кавычками)
|
||
- **State для модалок**: `_sendState`, `_createQueueState` — сохраняем перед открытием модалки
|
||
- **template literal nesting**: `${esc(tenant.id)}` в inner template работает т.к. tenant из closure
|
||
|
||
Добавлено в HTML:
|
||
- 3 новые модалки: `#modal-send`, `#modal-msg-detail`, `#modal-queue-create`
|
||
- CSS: `.msg-expand`, `.msg-expand-inner`, `.queue-name-link`
|
||
- Toolbar очередей: + кнопка "+ Очередь"
|
||
- Каждая строка очереди: клик на имя → toggle сообщений; кнопки 📨 🗑 ✕
|
||
- Скрытая expandable строка с `#msgs-inner-{name}`
|
||
- JS: `toggleMessages`, `loadMessages`, `renderQueueMessages`, `openMsgDetail`, `closeMsgDetail`, `openSendModal`, `closeSendModal`, `sendMessage`, `purgeQueueConfirm`, `openCreateQueueModal`, `closeQueueModal`, `createQueue`, `deleteQueueConfirm`, `copyTextarea`
|
||
|
||
Синтаксис проверен через `node -e "new Function(script)"` → OK.
|
||
|
||
### Деплой
|
||
- Docker build v0.1.6 на VM → успешно
|
||
- `docker push naeel/shared-sqs:v0.1.6` → успешно
|
||
- `kubectl set image` → **ОШИБКА**: JWT токен в kubeconfig на VM истёк в 16:01 UTC, текущее время 16:07 UTC
|
||
- Решение: обновил `deployment.yaml` с новым тегом → пользователь задеплоит после обновления токена
|
||
|
||
### Коммит
|
||
- `610c604` feat(shared-sqs): queue CRUD + message peek/send/purge in UI (v0.1.6)
|
||
- Pushed to branch `shared-sqs`
|
||
|
||
## Итог
|
||
|
||
Всё реализовано. Осталось только задеплоить — нужен свежий K8s токен (текущий истёк).
|
||
Команда деплоя:
|
||
```
|
||
kubectl -n shared-sqs set image deployment/shared-sqs shared-sqs=naeel/shared-sqs:v0.1.6
|
||
# или
|
||
kubectl apply -f shared-sqs/deployments/k8s/deployment.yaml
|
||
```
|