12 KiB
Журнал сессии — 2026-10-11 (IoT: внешнее код-ревью и фиксы)
Запись по требованию «документировать всё, включая собственные ошибки» (приказ 2026-09-24). Предыдущие:
HISTORY/2026-10-07-session-log.md,HISTORY/2026-10-10-session-log.md. Основание: внешнее ревью Claude Sonnet по промпту../CLAUDE-REVIEW-PROMPT.md(40 пунктов на два сервиса; SQS-часть — вSQS-service/HISTORY/2026-10-11-session-log.md).
1. Задача
Разобрать находки ревью по монолиту iot-service и исправить их.
2. Что исправлено
🔴 Критичные
- 🔴8/🔴9 — poison-строка в
flushAll(internal/storage/iotpg/iot_telemetry_store.go:166,177). При постоянной (не retryable) ошибкеflushAllвозвращал ошибку → consumer (consumer.go:159) не удалял батч из SQS → бесконечный redelivery всего батча. Теперь ошибку ставит только retryable-ветка; постоянная ошибка означает «валидные строки уже вставлены построчно, битые отброшены» →Flushвозвращает nil, батч подтверждается. Коммит1fe543f.
🟡 Важные
| Пункт | Файл | Суть | Коммит |
|---|---|---|---|
| 🟡13 | bridge/sender.go |
Ретраил бесконечно любые ошибки SQS; permanent (4xx/размер/нет очереди) занимали воркеры → классификация isPermanentSQSErr (smithy APIError) |
657850d |
| 🟡14 | bridge/handler.go |
MessageID = uuid на каждый колбэк → повторная QoS1-доставка не дедуплицировалась. Теперь SHA256(ns|device|topic|payload) |
657850d |
| 🟡15 | bridge/bridge.go |
После 3 неудач подписки ждали следующего реконнекта (которого не будет при живом соединении) → бесконечный ретрай подписки | 657850d |
| (new) | bridge/handler.go |
Defense-in-depth: валидация формата ns/deviceID из топика (барьер — EMQX authz) |
657850d |
| 🟡17 | consumer/consumer.go |
receiveCount проверялся только для битого envelope → применяем maxReceiveCount ко всем ошибкам |
5114320 |
| 🟡18 | cmd/iot-service/main.go |
run() выходил до завершения server.Shutdown → deferred Close срабатывали до дренажа in-flight. Добавлен shutdownDone |
5114320 |
| 🟡19 | api/handler/devices.go |
Ошибка GetByName после успешного INSERT давала 500 (пароль терялся, повтор → 409) → отдаём 201 с in-memory объектом |
5114320 |
| 🟡11 | iotpg/iot_telemetry_store.go |
getTenantDB/QueryTelemetry не проверяли safeNamespaceRe (a_b ↔ a-b → одна БД) → валидация на всех путях |
f41a9fe |
Осознанно НЕ менялось — РЕШЕНИЯ (2026-10-11)
- 🟡16 (QoS1 ack): решение — оставить async-ack + метрика дропов. Синхронный
ack (подтверждать только после отправки в SQS) блокировал бы MQTT-колбэк при
недоступности SQS — это уже чинили ранее (потеря ВСЕХ сообщений). Throughput
важнее; дроп при переполнении фиксируется счётчиком
dropped. - 🟡12 (
AUTH_TEST_MODE): решение — оставить как есть. Уже fail-closed: при отсутствииJWT_HMAC_SECRETиAUTH_TEST_MODE=falsemiddleware отказывает в обслуживании (500), а test-mode срабатывает только без секрета.
3. Прочее (перед ревью, в этой же сессии)
193509c— консоль: namespace =sub(убралиsless-{SHA256}, расходившийся с middleware) и удалили несуществующийPOST /v1/namespaces/{ns}/ensure.
4. Верификация
go build ./...,go vet ./...,go test -race ./...— зелёные.- Push:
54fe1f5..f41a9feвorigin/master.
5. Деплой на Nubes (Terraform) — образ EMQX, 2026-10-11
terraform apply упал на nubes_http.iot_emqx через ~70 с (стадия validation,
22.0 с): платформа требует РОВНО ОДИН EXPOSE в образе, иначе
«Не указан applicationUsePort…». У emqx/emqx:5.5.1 — 7 EXPOSE
(18083/1883/4370/5369/8083/8084/8883). Параметра applicationUsePort у
провайдера nubes 3.1.0 нет (проверено: generated/prod/resources_yaml/98_http.yaml
generated/prod/docs-rendered/http.md+ исходники провайдера — совпадений нет), поэтому лечится только пересборкой образа.
Что сделано (образ nail/iot-emqx:v0.2.5):
emqx/Dockerfile— multi-stage:FROM emqx/emqx:5.5.1 AS base→FROM scratch→COPY --from=base / /. EXPOSE/ENV/VOLUME наследуемого образа НЕ переносятся вscratch, поэтому остаётся единственныйEXPOSE 8083. ENVLANG/LC_ALL/PATHобъявлены заново.USER emqx+WORKDIR /opt/emqx- штатные
ENTRYPOINT/CMDбазы.
- ⛔ Попытка
FROM debian:bookworm-slim+COPY --from=base / /НЕ работает:cannot copy to non-directory: /bin— в bookworm/binсимлинк (usrmerge), в emqx-образе (Debian 11)/bin— реальный каталог.
- штатные
emqx/emqx.conf— удалёнsalt_rounds = 10изpassword_hash_algorithm: схема 5.5.1 его НЕ принимает (unknown_fields, unknown => "salt_rounds"→call_hocon_failed, контейнер не стартует). Роль поля нулевая: EMQX только сверяет bcrypt-хеш из БД, cost берётся из самого хеша.
Верификация образа (локально):
docker inspect … --format '{{json .Config.ExposedPorts}}'→{"8083/tcp":{}};- запуск контейнера →
Listener tcp:default on 0.0.0.0:1883 started,Listener ws:default on 0.0.0.0:8083 started,EMQX 5.5.1 is running now!, статусrunning. Ошибкиeconnrefusedк127.0.0.1:5432в тесте ожидаемы (PG рядом нет). - Реестр Gitea: теги
latest,v0.2.4,v0.2.5. terraform.tfvars/terraform.tfvars.example:iot_emqx_version = "v0.2.5".
⚠️ Блокер деплоя: инстанс iot-emqx (3bdaef8e-e880-4419-870a-bd0158826751)
остался в состоянии not created после неудачного apply и держит имя. По
документации платформы такие инстансы через API не удаляются — удалять вручную
в ЛК (deck). terraform state list пуст (в state ничего не записалось).
6. Второй terraform apply — падение iot-service и РАЗБОР через кластер
Инстанс-блокер был удалён в ЛК владельцем (3bdaef8e… → 404). Повторный apply:
EMQX создался и работает, nubes_http.iot_service упал через ~10 мин:
Приложение не запустилось. Производится полный откат установки. Ошибка: jlib.k8s [correctReplicaActive] | Под(ы) не работают: 'containerk8s' (Deployment),
затем таймаут клиента 600 с. Namespace iot-service откатан и удалён.
Разбор (kubeconfig ~/.kube/config → кластер iot-naeel, контекст
tazetdinovn@gmail.com@iot-naeel):
- Причина падения — PG, а не образ. Эфемерный диагностический под
(
kubectl run iot-diag-bad, env — как в tfvars, удалён после проверки):Приложение падает наlevel=ERROR msg="devices store" err="store: init schema: pq: must be owner of table iot_devices (42501)" exit=1 reason=ErrorCREATE UNIQUE INDEX IF NOT EXISTS(internal/service/store/open.go:64- SQL на
:29): таблица уже существует и принадлежит ДРУГОЙ роли.
- SQL на
- Владелец. В БД
iotdbинстанса509145c3…таблицыiot_devices,iot_devices_pkey,idx_iot_devices_ns_device,tenant_credentialsпринадлежали ролиiot_service(создана 2026-08-16, пароль утерян — нет ни в vault платформы, ни вIoT/pg.key). Рольiotserviceиз tfvars — не владелец и без прав:has_table_privilege('iotservice','iot_devices','SELECT')=false. Данные:iot_devices26 строк (в т.ч.__bridge/iot-bridge),tenant_credentials31. - Вторая ошибка (сработала бы следующей).
MQTT_HOSTизlocal.emqx_internal_host=iot-emqx.<uid>.svc.cluster.local— NXDOMAIN. Service в namespace платформы называетсяcontainerk8s:containerk8s.<uid>.svc.cluster.local→ 10.98.42.138 ✅ (провереноgetent hostsизнутри пода EMQX; тот же формат —HISTORY/2026-08-16-session-log.md:824).
Исправление (2026-10-11)
- PG (через
psqlв подеpostgresqlk8s-0, рольpostgres):BEGIN; ALTER TABLE public.iot_devices OWNER TO iotservice; ALTER TABLE public.tenant_credentials OWNER TO iotservice; ALTER INDEX public.iot_devices_pkey OWNER TO iotservice; ALTER INDEX public.idx_iot_devices_ns_device OWNER TO iotservice; COMMIT;Пароли не менялись, БД не создавались, env EMQX не менялся. deploy/terraform/resources.tf—emqx_internal_host→containerk8s.<instanceUid>(былоresource_name).
Перепроверка после правки (обязательна)
- Владельцы в PG: все 4 объекта →
iotservice✅ - DDL приложения под
iotserviceпо TCP (sslmode=require), ровно тот же SQL:CREATE TABLE … / CREATE UNIQUE INDEX IF NOT EXISTS …→exit=0, без 42501 ✅ - Данные целы:
iot_devices26 строк,tenant_credentials31 ✅ - Диагностический под
iot-diag-okсMQTT_HOST=containerk8s.<uid>:devices store ready,bridge device ensured,telemetry store ready,bridge: MQTT connected,bridge: subscribed filter=+/telemetry/+;/health=200,/ready=200{"status":"ready"}✅ terraform validateSuccess,terraform plan= 1 to add, 0 to change, 0 to destroy (EMQX не меняется — его env правки не требует);terraform console local.emqx_internal_host=containerk8s.0d09ee82-c8f9-458f-88a3-89571e5676ed.svc.cluster.local✅- Диагностические поды удалены, в ns EMQX остались только
containerk8sиvmagent.