fix(telemetry): ts = время приёма (ReceivedAt), а не момент INSERT
Раньше ts проставлялся now() в БД при INSERT → при батчинге и параллельных записях порядок телеметрии (ORDER BY ts DESC) ломался. Теперь в ts идёт ReceivedAt из envelope (серверное время приёма, ставит bridge); битый/пустой received_at → zero → стор подставляет now(). Схема не меняется (используется существующая колонка ts). Обновлены callers: consumer/processor.go и legacy cmd/sqs-consumer.
This commit is contained in:
@@ -160,7 +160,11 @@ func processSQSTelemetry(ctx context.Context, body string, store *iotpg.IoTPostg
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := store.InsertTelemetry(ctx, envelope.Namespace, envelope.DeviceID, "", envelope.Payload); err != nil {
|
receivedAt, perr := time.Parse(time.RFC3339, envelope.ReceivedAt)
|
||||||
|
if perr != nil {
|
||||||
|
receivedAt = time.Time{}
|
||||||
|
}
|
||||||
|
if err := store.InsertTelemetry(ctx, envelope.Namespace, envelope.DeviceID, "", receivedAt, envelope.Payload); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.services.ngcloud.ru/Nail/IoT/internal/service/bridge"
|
"gitea.services.ngcloud.ru/Nail/IoT/internal/service/bridge"
|
||||||
"gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg"
|
"gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg"
|
||||||
@@ -29,7 +30,13 @@ func processTelemetry(ctx context.Context, body string, store *iotpg.IoTPostgres
|
|||||||
if err := store.EnsureTenantDB(ctx, envelope.Namespace); err != nil {
|
if err := store.EnsureTenantDB(ctx, envelope.Namespace); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := store.InsertTelemetry(ctx, envelope.Namespace, envelope.DeviceID, envelope.MessageID, envelope.Payload); err != nil {
|
// Время приёма сохраняем в 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ type IoTPostgresStore struct {
|
|||||||
type telemetryInsert struct {
|
type telemetryInsert struct {
|
||||||
deviceID string
|
deviceID string
|
||||||
messageID string
|
messageID string
|
||||||
|
ts time.Time
|
||||||
payload []byte
|
payload []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,23 +173,28 @@ func (s *IoTPostgresStore) flushTenant(ctx context.Context, ns string, rows []te
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// INSERT INTO iot_telemetry (device_id, payload, message_id) VALUES ($1,$2,$3),...
|
// INSERT INTO iot_telemetry (device_id, payload, message_id, ts) VALUES ($1,$2,$3,$4),...
|
||||||
// ON CONFLICT DO NOTHING — дедуп по (device_id, message_id) при at-least-once
|
// ts — ВРЕМЯ ПРИЁМА (bridge ReceivedAt), а не момент INSERT: иначе батчинг и
|
||||||
// повторе из SQS (P0-8). Пустой message_id вставляется как NULL (NULL в
|
// параллельные записи ломают порядок телеметрии (ORDER BY ts DESC).
|
||||||
// уникальном индексе не конфликтует → без ложного дедупа).
|
// Zero → now() (fallback для legacy-сообщений без received_at).
|
||||||
|
// ON CONFLICT DO NOTHING — дедуп по (device_id, message_id) (P0-8).
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("INSERT INTO iot_telemetry (device_id, payload, message_id) VALUES ")
|
sb.WriteString("INSERT INTO iot_telemetry (device_id, payload, message_id, ts) VALUES ")
|
||||||
args := make([]any, 0, len(rows)*3)
|
args := make([]any, 0, len(rows)*4)
|
||||||
for i, r := range rows {
|
for i, r := range rows {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
sb.WriteString(",")
|
sb.WriteString(",")
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf("($%d,$%d,$%d)", i*3+1, i*3+2, i*3+3))
|
sb.WriteString(fmt.Sprintf("($%d,$%d,$%d,$%d)", i*4+1, i*4+2, i*4+3, i*4+4))
|
||||||
var mid any
|
var mid any
|
||||||
if r.messageID != "" {
|
if r.messageID != "" {
|
||||||
mid = r.messageID
|
mid = r.messageID
|
||||||
}
|
}
|
||||||
args = append(args, r.deviceID, r.payload, mid)
|
ts := r.ts
|
||||||
|
if ts.IsZero() {
|
||||||
|
ts = time.Now().UTC()
|
||||||
|
}
|
||||||
|
args = append(args, r.deviceID, r.payload, mid, ts)
|
||||||
}
|
}
|
||||||
sb.WriteString(" ON CONFLICT DO NOTHING")
|
sb.WriteString(" ON CONFLICT DO NOTHING")
|
||||||
_, err = tenantDB.ExecContext(ctx, sb.String(), args...)
|
_, err = tenantDB.ExecContext(ctx, sb.String(), args...)
|
||||||
@@ -318,11 +324,13 @@ ON iot_telemetry (device_id, message_id);
|
|||||||
// возвращается вызывающему); неполные батчи дописывает фоновый flusher.
|
// возвращается вызывающему); неполные батчи дописывает фоновый flusher.
|
||||||
// messageID — уникальный id сообщения для дедупликации (может быть пустым →
|
// messageID — уникальный id сообщения для дедупликации (может быть пустым →
|
||||||
// тогда строка не участвует в дедупе).
|
// тогда строка не участвует в дедупе).
|
||||||
func (s *IoTPostgresStore) InsertTelemetry(ctx context.Context, namespace, deviceID, messageID string, payload json.RawMessage) error {
|
// receivedAt — время приёма сообщения (для колонки ts); zero → стор подставит now().
|
||||||
|
func (s *IoTPostgresStore) InsertTelemetry(ctx context.Context, namespace, deviceID, messageID string, receivedAt time.Time, payload json.RawMessage) error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.batches[namespace] = append(s.batches[namespace], telemetryInsert{
|
s.batches[namespace] = append(s.batches[namespace], telemetryInsert{
|
||||||
deviceID: deviceID,
|
deviceID: deviceID,
|
||||||
messageID: messageID,
|
messageID: messageID,
|
||||||
|
ts: receivedAt,
|
||||||
payload: append([]byte(nil), payload...),
|
payload: append([]byte(nil), payload...),
|
||||||
})
|
})
|
||||||
full := len(s.batches[namespace]) >= telemetryBatchSize
|
full := len(s.batches[namespace]) >= telemetryBatchSize
|
||||||
|
|||||||
Reference in New Issue
Block a user