Files
IoT/internal/service/consumer/processor.go
T
Repinoid 85bcf26220 chore(iot): MaxReconnectInterval 30с + Debug для health-проб и телеметрии (ревью)
- bridge: SetMaxReconnectInterval(30с) — дефолт ~10м слишком долог для телеметрии.
- logging middleware: /health,/healthz,/ready → Debug (не засорять лог).
- processor: 'telemetry buffered' вместо Info 'saved' (данные ещё буферизованы).
2026-10-10 19:24:12 +03:00

67 lines
3.2 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
}
// Debug: данные ещё только буферизованы (не записаны), Info на каждое
// сообщение засорял бы лог (ревью 2026-10-10).
log.Debug("consumer: telemetry buffered",
"namespace", envelope.Namespace,
"device", envelope.DeviceID,
)
return nil
}