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

7.5 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 ничего не записалось).