Files
IoT/internal/service/consumer/processor.go
T
Repinoid 2cca7cb856 fix(iot): семантическая валидация envelope (пустые поля → DLQ)
processTelemetry теперь отбрасывает (в DLQ) envelope с пустым namespace,
device_id или payload ДО записи в БД — иначе пустой namespace создавал бы
tenant-БД «tenant_», а пустой payload давал NULL (23502)/битый JSONB и вечный
ретрай. Дополняет построчный fallback в flushTenant (🔴2).
2026-10-10 18:53:59 +03:00

60 lines
2.7 KiB
Go
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.
// processor.go — разбор envelope телеметрии и запись в per-tenant PostgreSQL.
package consumer
import (
"context"
"encoding/json"
"errors"
"log/slog"
"time"
"gitea.services.ngcloud.ru/Nail/IoT/internal/service/bridge"
"gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg"
)
// errInvalidEnvelope — битый/неразборчивый envelope: повторная обработка
// бессмысленна, сообщение следует отправить в DLQ, а не ретраить бесконечно.
var errInvalidEnvelope = errors.New("invalid telemetry envelope")
// processTelemetry десериализует envelope и пишет в tenant DB.
// Битый JSON — возвращает errInvalidEnvelope (consumer шлёт в DLQ),
// а НЕ молчаливый nil: иначе мусор подтверждается и теряется безвозвратно.
func processTelemetry(ctx context.Context, body string, store *iotpg.IoTPostgresStore, log *slog.Logger) error {
var envelope bridge.TelemetryEnvelope
if err := json.Unmarshal([]byte(body), &envelope); err != nil {
log.Warn("consumer: invalid envelope, routing to DLQ", "err", err)
return errInvalidEnvelope
}
// Семантическая валидация envelope ДО записи в БД: пустые namespace/device_id
// или пустой payload — битое сообщение, ретраем не исправится. Пустой
// namespace → EnsureTenantDB создал бы БД «tenant_», пустой payload → NULL
// (23502) или битый JSONB. Такое шлём в DLQ (errInvalidEnvelope), а не
// бесконечно ретраим (ревью 2026-10-10, 🔴2).
if envelope.Namespace == "" || envelope.DeviceID == "" || len(envelope.Payload) == 0 {
log.Warn("consumer: invalid envelope (empty namespace/device_id/payload), routing to DLQ",
"namespace", envelope.Namespace, "device", envelope.DeviceID)
return errInvalidEnvelope
}
// EnsureTenantDB идемпотентен, кэшируется после первого вызова.
if err := store.EnsureTenantDB(ctx, envelope.Namespace); err != nil {
return err
}
// Время приёма сохраняем в ts (порядок телеметрии). Битый/пустой received_at
// → zero, стор подставит now().
receivedAt, perr := time.Parse(time.RFC3339, envelope.ReceivedAt)
if perr != nil {
receivedAt = time.Time{}
}
if err := store.InsertTelemetry(ctx, envelope.Namespace, envelope.DeviceID, envelope.MessageID, receivedAt, envelope.Payload); err != nil {
return err
}
log.Info("consumer: telemetry saved",
"namespace", envelope.Namespace,
"device", envelope.DeviceID,
)
return nil
}