18 KiB
План доработок shared-sqs — для DeepSeek Flash (2026-10-05)
Исполнитель: кодер (DeepSeek Flash). Источник правил: память + отчёт Opus (
opus-report-2026-10-05.md) + мой анализ (my-analysis-of-opus-report-2026-10-05.md).⛔ ВАЖНО: прод НЕ используется, никто сервис не юзает. Обратная совместимость и миграции НЕ нужны. Всё делается корректно «на будущее». Кодер должен работать в репозитории
/home/naeel/nubes/SQS_und_IoT/SQS-service.
Правила работы (обязательные для кодера)
- Коммитить почаще — после каждой логически завершённой правки, отдельным
коммитом с понятным сообщением вида
sqs: <P0.1> .... Не копить правки. - Один трек-пункт = один/несколько коммитов. Не смешивать P0 и P2 в одном коммите.
- После каждой правки —
go build ./...иgo test ./...(зелёные до/после). - Не трогать файлы вне перечисленных ниже. Не менять публичные HTTP-контракты SQS API без явного указания (совместимость с AWS SDK должна сохраняться).
- Правила платформы (сетевые таймауты/MSS) в этот план НЕ входят — это не код.
ЧАСТЬ 1 — P0: блокеры продакшна (безопасность + durability)
P0.1 — Убрать детерминированный SecretKey (случайные креды)
Проблема: tenant_store.go выводит AccessKey/SecretKey из email через SHA256
с захардкоженной солью. Любой, кто знает email и соль (соль в исходнике), вычисляет
чужие креды и получает доступ к очередям через SQS-эндпоинт (JWT не нужен — только SigV4).
Файлы: app/tenant/tenant_store.go.
Что сделать:
- Удалить
tenantIDFromEmail,accessKeyFromEmail,secretKeyFromEmail, константуsecretKeySalt, и полностью переписатьCreateFromJWT:tenantID—generateTenantID()(случайный, уже есть);accessKey—generateAccessKey()(случайный, уже есть);secretKey—generateSecretKey()(случайный, уже есть).
- Сохранять связь
email → tenantв индексеbyEmail(как сейчас), НО ключи больше не зависят от email. - При повторном логине с тем же email — возвращать существующего тенанта
(идемпотентность по
byEmailсохранить), НЕ пересоздавать и НЕ пересчитывать ключи. - Убрать логику «legacy-пересчёта» (ветку удаления старой записи): с нуля её не нужно — если тенант с email найден, просто вернуть его как есть.
- Случайные ключи и так генерируются через
crypto/rand(generateAccessKey,generateSecretKey) — их уже существует, использовать их.
Критерий готовности: после логина новый тенант получает AccessKey/SecretKey,
не совпадающие с SHA256(email); повторный логин с тем же email возвращает те же
креды (из Redis/индекса), а не пересоздаёт.
P0.2 — Усилить доверие к JWT (не доверять неподписанному payload)
Проблема: jwt.go не проверяет подпись; PingNubesAPI принимает токен, если
хоть один стенд вернул не-401/403. Идентичность берётся из незаверенного claim email.
Файлы: app/auth/jwt.go, app/auth/cache.go.
Что сделать (минимально безопасно, без внешнего JWKS):
PingNubesAPI: принимать токен ТОЛЬКО при явном HTTP 2xx от доверенного эндпоинта (сейчас логика «любой ответ, кроме 401/403 → OK»). 401/403 →ErrTokenRejected; прочие коды → тоже считаем ошибкой (не принимать).- Порядок стендов оставить как есть (prod/test/dev), но успех — только по 2xx.
- Дополнительно: если в ответе валидирующего API доступна расшифровка токена
(email/sub), использовать её вместо доверия к payload. Если API возвращает только
статус — оставить claim
emailкак есть, но после P0.1 это уже не даёт чужих кредов. cache.go— оставить TTL 5м/grace 2м (риск отозванного токена приемлем и задокументирован).
Критерий готовности: токен, на который все стенды отвечают 500/503/иным не-401-кодом, больше НЕ принимается; принимается только явный 2xx.
P0.3 — Durability SendMessage (синхронный персист до 200)
Проблема: SendMessage отвечает 200, а Redis-запись уходит асинхронно в горутину
(redis.go asyncWrite). Краш пода в этом окне = потеря подтверждённого сообщения.
Файлы: app/persistence/redis.go, app/gosqs/send_message.go.
Что сделать:
- Ввести синхронный вариант записи сообщения (по аналогии с уже существующим
синхронным
DeleteMessagePersist):SaveMessageSync(key, msg)— выполняетHSetс коротким таймаутом и ждёт результат. - В
SendMessageV1: после добавления вqueue.MessagesвызыватьSaveMessageSyncДО возврата 200. При ошибке Redis — вернуть клиенту ошибку (InternalError) и откатить append сообщения (не оставлять сообщение в памяти без персиста), либо залогировать и вернуть ошибку — выбрать стратегию и зафиксировать её в комментарии. - Оставить
SaveMessage(async) для некритичных случаев, но SendMessage должен персистить синхронно.
Критерий готовности: краш пода сразу после 200 на SendMessage НЕ приводит к потере сообщения — оно уже в Redis.
ЧАСТЬ 2 — P1: архитектурная зрелость
P1.1 — Per-queue мьютекс вместо глобального
Проблема: один SyncQueues.RWMutex на весь сервис — любой send/receive/tick
берёт единственный Lock; long-poll переобходит его каждые 100 мс; O(N)-скан под локом.
Файлы: app/models/globals.go, app/models/models.go, все gosqs-хендлеры,
app/gosqs/gosqs.go (PeriodicTasks/processQueueTick), app/persistence/redis.go.
Что сделать:
- Добавить в
Queueполеmu sync.RWMutex(per-queue блокировка). SyncQueuesоставить какmap[string]*Queueпод отдельным короткимsync.RWMutexТОЛЬКО для доступа к самой map (вставка/удаление/чтение указателя), а состояние ОЧЕРЕДИ защищать её собственнымmu.- Перевести все операции на схему: короткий
SyncQueues.RLock→ взять указательqueue→SyncQueues.RUnlock→queue.mu.Lock()на время работы с содержимым. - В
processQueueTickснять глобальный Lock с обхода: пройти по снапшоту очередей, каждую блокировать еёmuотдельно. - Обновить
persistence-вызовы: они должны вызываться подqueue.mu, как сейчас под глобальным (см. комментарии «ВЫЗЫВАТЬ под Lock»).
Критерий готовности: нет ни одной операции, блокирующей все очереди сервиса на время O(N)-работы; тесты на конкурентный доступ проходят.
P1.2 — Индексация сообщений (убрать O(N) по слайсу)
Проблема: Messages []SqsMessage — receive/delete/visibility — O(N); при лимите
~120k сообщений и текущих локах это основной источник latency.
Файлы: app/models/models.go, app/gosqs/receive_message.go,
app/gosqs/delete_message.go, app/gosqs/gosqs.go.
Что сделать (инкрементально, без ломки схемы):
- Добавить в
QueueиндексreceiptIndex map[string]int(ReceiptHandle → индекс вMessages) и/илиreadyIndex []int; поддерживать его при каждом receive/delete/таймауте. - DeleteMessage: по ReceiptHandle — O(1) вместо скана слайса.
- Visibility-tick: по
receiptIndexопределять истёкшие, а не сканировать весь слайс. - Если полный рефакторинг со слайса на map+heap сложен — сначала версию с индексом,
сохранив слайс, чтобы не ломать Redis-сериализацию (
marshalMessage).
Критерий готовности: DeleteMessage и visibility-перерасчёт не зависят от полного размера очереди.
P1.3 — Реализовать MessageRetentionPeriod
Проблема: MessageRetentionPeriod помечен // TODO - not used; сообщения не
истекают, растут до лимита, потом OverLimit.
Файлы: app/models/models.go, app/gosqs/gosqs.go (processQueueTick),
app/persistence/redis.go.
Что сделать:
- В
processQueueTickдобавить эвикцию сообщений старше retention: еслиSentTime + MessageRetentionPeriod < now— удалить сообщение и его Redis-запись (HDel). - Учитывать retention при
IsReadyForReceipt/выдаче: не выдавать просроченные. - Дефолт retention уже задан (345600 = 4 дня в
config.goapplyEnvironmentDefaults).
Критерий готовности: сообщение автоматически исчезает из очереди и Redis по
истечении retention, очереди не забиваются до OverLimit.
P1.4 — Индекс тенант → его очереди
Проблема: countTenantQueues, listTenantQueues, createTenantQueue (лимит),
deleteTenant, gauge-updater — O(total) скан-по-префиксу по всем очередям.
Файлы: app/gosqs/tenant_helpers.go, app/admin/admin.go.
Что сделать:
- Вести индекс
map[accessKey]map[queueName]struct{}(или вTenantStore) для O(1) подсчёта/списка очередей тенанта. countTenantQueues→len(index[accessKey]).- Обновлять индекс в
CreateQueue/DeleteQueue/PurgeQueue/DeleteTenantи при восстановлении из Redis на старте.
Критерий готовности: лимиты/списки очередей не сканируют все очереди сервиса.
P1.5 — Убрать demo-bypass из прод-сборки
Проблема: хардкод demo-кредов (seed.go) и публичного UI-bypass-токена
(authenticateUIDemoToken в admin.go).
Файлы: app/cmd/seed.go, app/admin/admin.go.
Что сделать:
- Убрать
authenticateUIDemoTokenи фиксированный UI-токен; UI-доступ — только через реальную JWT-валидацию. - Демо-тенант (
SHARED_SQS_SEED_DEMO) оставить за env-флагом, но креды для демо генерировать черезCreateFixedтолько приSHARED_SQS_SEED_DEMO=true(как сейчас), НЕ хранить захардкоженныйdemo-secret-key-shared-sqs-ngcloud-2026в коде — генерировать случайным образом и выводить в лог при старте.
Критерий готовности: без env-флага SHARED_SQS_SEED_DEMO и без явно заданного
admin UI-токена в прод-конфиге бэкдор-путей нет.
ЧАСТЬ 3 — P2: надёжность, корректность, чистка
P2.1 — BYOC Admin API
Файлы: app/admin/admin.go, app/tenant/tenant_store.go.
- В
createTenantRequestдобавить опц. поляaccess_key,secret_key. - Если оба заданы →
CreateFixed(валидация мин. длины: access_key ≥8, secret_key ≥16). - Если одно без другого → 400. Экспортировать/использовать
generateTenantID. - Детали в
doc/byoc-credentials.md(раздел с TODO).
P2.2 — Чинить сиротство очередей (после отказа от legacy-пересчёта в P0.1 — уже неактуально)
После P0.1 ветка «legacy-пересчёта» удалена — сиротства больше не возникает.
Действие: убедиться, что после P0.1 в CreateFromJWT не осталось пути, где
тенант меняет AccessKey без переноса очередей. Если креды теперь неизменны на весь
жизненный цикл тенанта — пункт закрыт.
P2.3 — Constant-time сравнение admin-токена
Файлы: app/admin/admin.go.
- Заменить
authHeader != expectedнаsubtle.ConstantTimeCompare.
P2.4 — Единый путь создания/отправки (admin ≠ gosqs)
Файлы: app/admin/admin.go.
createTenantQueue/sendMessageToQueueдолжны переиспользовать логику gosqs (создание через общий код, FIFO-детект, проверка размера, MD5, дедуп), а не дублировать с иными дефолтами.
P2.5 — Биллинг: воркер/батч вместо горутины на операцию; корректный msg_count
Файлы: app/billing/billing.go, app/router/router.go (actionHandler).
- Заменить
go func(){ Insert }на буфер+воркер (батч-инсерты). RecordUsageпринимать корректныйmsg_count(для SendMessageBatch — число сообщений в батче), а не всегда 1.
P2.6 — Защитить/урезать /metrics
Файлы: app/router/router.go.
- Убрать per-tenant данные из публичных метрик ИЛИ закрыть
/metricsаутентификацией. - Минимально: убрать лейбл
tenantс идентификаторами из публичного/metrics, оставить агрегаты без идентификации тенантов.
P2.7 — Чистка легаси
Файлы: app/conf/goaws.yaml, app/conf/config.go, app/models/*.
- Убрать SNS Topics/Subscriptions из
goaws.yaml. - Убрать мёртвый путь загрузки yaml-очередей без tenant-префикса в
config.go. - Убрать SNS-поля из моделей.
- Не трогать
deployments/**(вне scope кода; при желании — отдельно).
P2.8 — Redis reconnect/health
Файлы: app/persistence/redis.go.
- После обрыва соединения — попытка переподключения / пометка нездоровья, а не
молчаливые ошибки в лог. Мин.: пересоздание
Clientпри повторном ping-фейле.
Порядок выполнения (коммитить почаще, по пунктам)
- P0.1 → коммит
- P0.3 → коммит (durability, пока не зависит от P1.1)
- P0.2 → коммит
- P1.1 → коммит (самый крупный, тестировать пристально)
- P1.2 → коммит
- P1.3 → коммит
- P1.4 → коммит
- P1.5 → коммит
- P2.1 … P2.8 → по одному коммиту каждый
После каждого коммита — go build ./... && go test ./....