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

149 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Журнал сессии — 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`.