- 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) — повторный вызов не паникует.
- replaceDSNDatabase: возвращает ОШИБКУ для key=value DSN (раньше молча отдавал
исходный DSN → телеметрия всех тенантов ушла бы в management-БД). Query
отделяется первым '?', поэтому sslrootcert=/path больше не ломает поиск БД.
- EnsureTenantDB: guard safeNamespaceRe (defense-in-depth, совпадает с API-валидацией);
небезопасный namespace → ErrInvalidNamespace → в consumer классифицируется как
постоянная ошибка (DLQ, не вечный ретрай).
Одна «ядовитая» строка (NULL/пустой payload, \u0000 в JSONB, битый JSON)
ронила весь multi-VALUES батч (до telemetryBatchSize валидных строк), который
классифицировался как permanent и выбрасывался целиком.
Добавлен flushRowsIndividually: при не-retryable ошибке батча вставляем строки
по одной (ON CONFLICT DO NOTHING) — валидные сохраняются, битая логируется.
🔴1 ревью: flushAll и синхронная запись полного батча в InsertTelemetry писали
параллельно с фоновым flusher'ом (каждые 200мс). Flush() из consumer видел
пустой буфер (фоновый flusher уже забрал строки) и возвращал nil → consumer
подтверждал SQS-сообщение, пока INSERT ещё в полёте/упал.
Добавлен flushMu: сериализует grab буфера + INSERT между фоновым flusher'ом,
Flush() и полным батчом InsertTelemetry. Теперь Flush дожидается in-flight
записи и возвращает реальную ошибку.
Раньше ts проставлялся now() в БД при INSERT → при батчинге и параллельных
записях порядок телеметрии (ORDER BY ts DESC) ломался. Теперь в ts идёт
ReceivedAt из envelope (серверное время приёма, ставит bridge); битый/пустой
received_at → zero → стор подставляет now(). Схема не меняется (используется
существующая колонка ts). Обновлены callers: consumer/processor.go и legacy
cmd/sqs-consumer.
isRetryableFlushErr: '08' (connection) и 57P01/02/03 (shutdown/crash) — retryable
(возврат в буфер); '22' (data exception) и '23' (integrity violation) — permanent
(батч выбрасывается с логом, НЕ возвращается в буфер). Иначе один невалидный
payload зацикливал flush namespace навсегда (вечные 200мс-ретраи). Относится к
обоим путям: flushAll и InsertTelemetry(full).
flushAll и InsertTelemetry (путь заполнения батча) вынимали строки из буфера и
подменяли буфер ДО записи: при ошибке flushTenant строки исчезали. Хуже —
фоновый flusher мог забрать строки, которые consumer ждал в Flush, тогда Flush
видел пустой буфер, возвращал nil, и consumer подтверждал SQS-сообщение с
незаписанными данными (потеря). Теперь при ошибке строки возвращаются в буфер
в обоих путях. ctx проброшен в flushAll/flushTenant. Consumer хранит receipt
handle значением (string), не указателем SDK.
Envelope получил MessageID (UUID); таблица iot_telemetry — столбец message_id +
UNIQUE(device_id,message_id); батч-INSERT ON CONFLICT DO NOTHING. Дубли при
at-least-once повторе из SQS больше не вставляются. Пустой message_id → NULL
(без ложного дедупа).
InsertTelemetry лишь буферизует и возвращает nil до фактической записи. Consumer
удалял сообщение сразу после processTelemetry == nil — краш/flush-ошибка в этом
окне = безвозвратная потеря. Теперь: копим receipt handles, вызываем store.Flush,
и только при успехе DeleteMessage; при ошибке flush — не удаляем (сообщения
вернутся после visibility timeout). Добавлен iotpg.Flush().
- MQTTAuth: replaced Get(Name=deviceID) with List+filter by Spec.DeviceID
(K8s object name != deviceID — caused deny for all devices)
- EnsureTenantDB: added GRANT role TO CURRENT_USER before CREATE DATABASE OWNER
(PG15+ requires SET ROLE privileges for target owner)
- Image: naeel/iot-operator:v0.2.5
- E2E test passed: device create → MQTT publish → SQS → Postgres → REST API