- apiBase → window.location.origin (same-origin с API: консоль и /v1 раздаёт один
iot-service).
- mqttBase → wss://exqx.containerk8s.dev.nubes.ru/mqtt (как в landing.html).
- Убраны упоминания «оператор»/k8s в комментариях; MQTTX-инструкция — новый хост.
Убран ранний дроп пустого тела; теперь оно идёт через processTelemetry →
errInvalidEnvelope → DLQ (или дроп после maxReceiveCount, если DLQ недоступна).
bridge пишет, consumer парсит RFC3339Nano (субсекундная точность). Раньше RFC3339
(секунды) + non-FIFO очередь → показания в одну секунду могли встать не в том
порядке. Parse RFC3339Nano совместим со старыми RFC3339-сообщениями (доля секунды
опциональна).
- bridge: SetMaxReconnectInterval(30с) — дефолт ~10м слишком долог для телеметрии.
- logging middleware: /health,/healthz,/ready → Debug (не засорять лог).
- processor: 'telemetry buffered' вместо Info 'saved' (данные ещё буферизованы).
store.Create классифицирует 23505 по имени constraint: (namespace,device_id) →
ErrDeviceIDExists, иначе ErrAlreadyExists. Handler отдаёт разные 409-сообщения.
Помогает Terraform понять, какое поле конфликтует.
- handler CreateIoTDevice: пароль устройства хранится bcrypt-хэшем (hashPassword),
открытый пароль отдаётся один раз в 201.
- store.EnsureBridgeDevice: пароль бриджа тоже bcrypt-хэшем.
- emqx/emqx.conf: password_hash_algorithm name=bcrypt, salt_rounds=10 (подтверждено
по emqx/emqx: HOCON-ключ salt_rounds; Go bcrypt.DefaultCost=10).
- Добавлена зависимость golang.org/x/crypto v0.42.0.
iotDeviceUpdateRequest получил поле metadata; UpdateIoTDevice обновляет переданные
поля (enabled и/или metadata). Добавлен store.UpdateMetadata. Хотя бы одно поле
обязательно. Раньше metadata нельзя было изменить (только enabled).
- QueryTelemetry: ORDER BY ts DESC, id DESC (был без tiebreaker → нестабильная
пагинация при одинаковых ts).
- ListIoTDevices: limit дефолт 100, потолок 1000 (ошибка парсинга больше не даёт
безлимитный запрос).
- Логи 'batch dropped' → 'валидные строки — построчно' (после fallback батч НЕ
теряется целиком).
- consumer.Run получил onReady: /ready теперь false при ошибке ReceiveMessage
(раньше флаг consumer'а выставлялся до старта и не отражал SQS).
- main.go: wg.Wait заменён на waitWithTimeout (shutdownGrace=15с) — зависшую
подсистему не ждём бесконечно.
- iotpg.Close идемпотентен (sync.Once) — повторный вызов не паникует.
- Удалён internal/service/api/handler/mqtt_auth.go: методы MQTTAuth/MQTTAcl
нигде не подключены (мёртвый код; при подключении небезопасен — доверял
клиентскому clientid и разрешал subscribe на '#').
- admin.go: сравнение ADMIN_STATS_TOKEN через subtle.ConstantTimeCompare.
- replaceDSNDatabase: возвращает ОШИБКУ для key=value DSN (раньше молча отдавал
исходный DSN → телеметрия всех тенантов ушла бы в management-БД). Query
отделяется первым '?', поэтому sslrootcert=/path больше не ломает поиск БД.
- EnsureTenantDB: guard safeNamespaceRe (defense-in-depth, совпадает с API-валидацией);
небезопасный namespace → ErrInvalidNamespace → в consumer классифицируется как
постоянная ошибка (DLQ, не вечный ретрай).
Было: main выставлял ready.SetBridge(true) ПЕРЕД bridge.Run, поэтому «соединён,
но подписка не прошла» давал /ready=200 и молча терял телеметрию до реконнекта.
Теперь bridge сам управляет флагом (колбэк onSubscribed): true только после
успешного SUBSCRIBE, false при потере соединения/сбое подписки.
Было: при недоступной DLQ битое сообщение НЕ подтверждалось и крутилось на
основной очереди бесконечно. Теперь читается ApproximateReceiveCount; после
maxReceiveCount=5 битое сообщение подтверждается (дроп с громким логом), а не
зацикливается. При доступной DLQ — уходит туда на первом приёме.
os.Exit обходит defer-ы: на fatal-пути и при ошибке HTTP-сервера iotStore.Close()
(финальный flushAll последнего батча телеметрии) не выполнялся. Логика вынесена в
run() int, main = os.Exit(run()); все выходы через return — defer-ы отрабатывают.
- Добавлен ResolveQueueURLWithRetry (10 попыток × 2с backoff): временная ошибка
GetQueueUrl больше не роняет bridge/consumer без ретрая.
- main.go: при ПОСТОЯННОЙ ошибке bridge/consumer процесс завершается с кодом ≠ 0
(fatal-флаг + cancel), чтобы платформа перезапустила контейнер — раньше процесс
оставался жив, а /health отдавал 200.
- Диспатчер получил собственный ctx (не сигнальный): при shutdown воркеры
успевают отправить накопленное в SQS (раньше сразу возвращали «context
canceled» и дропали остаток). Флаш ограничен bridgeFlushTimeout=10с.
- enqueue/closeAndWait синхронизированы мьютексом (closed-флаг): поздний колбэк
paho больше не паникует на «send on closed channel».
- sendWithRetry больше не дропает после 3 попыток (~7с) при почти пустом буфере:
ретраит до ctx.Done() (shutdown) или успеха, backoff капится на 8с.
- Добавлен sendAttemptTimeout=10с на одну попытку — зависшее соединение SQS
не блокирует воркер навсегда.
- ResolveQueueURL для DLQ теперь ретраится 3 раза с backoff: временная ошибка
больше не отключает DLQ навсегда.
- Битый envelope подтверждается только если DLQ реально приняла его; при
недоступной DLQ/сбое sendToDLQ сообщение НЕ подтверждается (вернётся после
visibility timeout), а не дропается.
OnConnectHandler вызывал c.Disconnect(100) при таймауте/ошибке подписки. В paho
это ЯВНОЕ отключение — автореконнект после него не срабатывает, и bridge
«глохнет» (соединения нет, подписки нет, /ready=true).
Теперь: подписка ретраится 3 раза с паузой 2с; при сбое — ждём следующего
реконнекта (OnConnectHandler снова вызовется). Disconnect не вызывается.
processTelemetry теперь отбрасывает (в DLQ) envelope с пустым namespace,
device_id или payload ДО записи в БД — иначе пустой namespace создавал бы
tenant-БД «tenant_», а пустой payload давал NULL (23502)/битый JSONB и вечный
ретрай. Дополняет построчный fallback в flushTenant (🔴2).
Одна «ядовитая» строка (NULL/пустой payload, \u0000 в JSONB, битый JSON)
ронила весь multi-VALUES батч (до telemetryBatchSize валидных строк), который
классифицировался как permanent и выбрасывался целиком.
Добавлен flushRowsIndividually: при не-retryable ошибке батча вставляем строки
по одной (ON CONFLICT DO NOTHING) — валидные сохраняются, битая логируется.