149 lines
12 KiB
Markdown
149 lines
12 KiB
Markdown
# Журнал сессии — 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`.
|