20 KiB
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:
- Kubernetes добавляет аннотацию
restartedAt→ меняется template → начинается rollout - Стратегия
RollingUpdate(maxSurge=25%, maxUnavailable=25%) → для replicas=1:- maxSurge=1 (ceil 0.25) → Kubernetes поднимает НОВЫЙ pod
- maxUnavailable=0 (floor 0.25) → старый pod ЕЩЁ ЖИВА
- PVC
ReadWriteOnce— допускает mount с нескольких pod на ОДНОЙ НОДЕ (это не ReadWriteOncePod) - Оба pod монтируют один PVC → оба пытаются открыть
/data/elasticmq.mv.db - Старый ElasticMQ держит
FileChannel.lock()→ новый ElasticMQ получаетMVStoreException: The file is locked - Persistence actor (SqlQueuePersistenceActor) в новом pod падает → dead letters
- Старый pod убивается (readinessProbe eventual fail) → lock освобождается — но поздно
- SQS REST server работает (port 9324 слушает), но WRITE-операции (SendMessage) зависают — actor мёртв
Решение — 3 изменения в ensureDeployment
-
Strategy: Recreate (вместо RollingUpdate)
- Kubernetes СНАЧАЛА убивает старый pod, ПОТОМ поднимает новый
- Два pod НИКОГДА не работают одновременно → lock невозможен
- Downtime ~25-30 секунд (JVM startup) — допустимо для мультитенант SQS
-
preStop hook: sleep 3
- При SIGTERM JVM начинает shutdown
sleep 3даёт H2 время наfsync+FileChannel.close()- Без preStop: Kubernetes может убить pod раньше чем H2 закончит flush
-
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 все точки где могут быть баги:
- crypto/rand vs math/rand для генерации ключей
- Удаление из двух индексов при delete tenant
- Парсинг AWS Signature V4 Authorization header
- Long polling таймаут (до 20 сек)
- Игнорирование X-Amz-Security-Token
- URL parsing — последний сегмент = queueName (НЕ tenantID)
- getQueueFromPath() — совместимость с новым URL форматом
- FIFO queue naming (.fifo суффикс)
- DLQ ARN parsing для tenant-scoped lookup
- QueueUrl в ответах ОБЯЗАН содержать tenantID
- DELETE tenant → удалить все очереди (memory leak)
- Admin API — отдельная auth (bearer token, не tenant credentials)
- Graceful shutdown (GoAWS не обрабатывает signals)
- Deployment strategy: Recreate (урок из ERR-SQS-06)
- .gitignore для бинарника
- go mod tidy после удаления SNS
- 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/. Нужно:
- Клонировать GoAWS upstream в
_upstream/, скопироватьapp/,go.mod,go.sum,Dockerfile - Сменить module на
shared-sqs, Go 1.22 - Заменить все import paths
github.com/Admiral-Piett/goaws/app/→shared-sqs/app/ - Удалить
gosns/и все SNS-связанные места - Удалить тестовые директории
Риски
- 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_keyGET /admin/tenants/{id}— деталь без secret_keyDELETE /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) Коммиты:
Остался Этап 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:
#001C34navy,#001120bg,#1a7fd4accent,#e2ecf6text - Изучил admin API: GET /admin/health, GET/POST /admin/tenants, GET/DELETE /admin/tenants/{id}
- Изучил структуры:
TenantStore,SyncQueues.Queues,Queue,SqsMessage
Реализация
-
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)
- Ищет очереди по префиксу
-
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
-
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. Нужно было:
- Исправить
models.NewUUID()→ найти правильную функцию генерации UUID - Написать frontend для peek/send/purge/queue CRUD
- Собрать + задеплоить 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с новым тегом → пользователь задеплоит после обновления токена
Коммит
610c604feat(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