Files
IoT/HISTORY/2026-10-11-session-log.md
T

12 KiB
Raw Blame History

Журнал сессии — 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=false middleware отказывает в обслуживании (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):

  1. emqx/Dockerfile — multi-stage: FROM emqx/emqx:5.5.1 AS base → FROM scratch → COPY --from=base / /. EXPOSE/ENV/VOLUME наследуемого образа НЕ переносятся в scratch, поэтому остаётся единственный EXPOSE 8083. ENV LANG/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 — реальный каталог.
  2. 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):

  1. Причина падения — 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=Error
    
    Приложение падает на CREATE UNIQUE INDEX IF NOT EXISTS (internal/service/store/open.go:64
    • SQL на :29): таблица уже существует и принадлежит ДРУГОЙ роли.
  2. Владелец. В БД 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_devices 26 строк (в т.ч. __bridge/iot-bridge), tenant_credentials 31.
  3. Вторая ошибка (сработала бы следующей). 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_devices 26 строк, tenant_credentials 31 ✅
  • Диагностический под 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 validate Success, 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.

Правка дефолтов конфига (2026-10-11, после разбора)

  • internal/service/config/defaults.go + Dockerfile: дефолт SQS_ENDPOINT был http://containerk8s.f1ffb134-…:4100 (устаревший namespace — shared-sqs давно на ВМ 213) → заменён на https://sqs.sinefo.ru.
  • DefaultMQTTHost был "emqx" (не резолвится: EMQX в другом namespace) → пусто, MQTT_HOST стал обязательным: config.Load добавляет его в список missing (MQTT_HOST (или MQTT_BROKER_URL)), а не строит кривой ws://:8083/mqtt.
  • В нашем деплое оба значения всё равно задаются Terraform'ом (resources.tf) — правка убирает ловушку для деплоя без tfvars. go build/vet/test зелёные.