Files
IoT/internal/service/consumer/processor.go
T
Repinoid faba28f824 fix(iot): replaceDSNDatabase (смешение тенантов) + guard namespace (Этап 4)
- replaceDSNDatabase: возвращает ОШИБКУ для key=value DSN (раньше молча отдавал
  исходный DSN → телеметрия всех тенантов ушла бы в management-БД). Query
  отделяется первым '?', поэтому sslrootcert=/path больше не ломает поиск БД.
- EnsureTenantDB: guard safeNamespaceRe (defense-in-depth, совпадает с API-валидацией);
  небезопасный namespace → ErrInvalidNamespace → в consumer классифицируется как
  постоянная ошибка (DLQ, не вечный ретрай).
2026-10-10 19:13:35 +03:00

65 lines
3.0 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 {
if errors.Is(err, iotpg.ErrInvalidNamespace) {
// Небезопасный namespace — постоянная ошибка → в DLQ (не ретраить).
log.Warn("consumer: invalid namespace, routing to DLQ", "namespace", envelope.Namespace)
return errInvalidEnvelope
}
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
}