fix(iot): сериализовать flush (гонка с фоновым flusher'ом — потеря данных)

🔴1 ревью: flushAll и синхронная запись полного батча в InsertTelemetry писали
параллельно с фоновым flusher'ом (каждые 200мс). Flush() из consumer видел
пустой буфер (фоновый flusher уже забрал строки) и возвращал nil → consumer
подтверждал SQS-сообщение, пока INSERT ещё в полёте/упал.

Добавлен flushMu: сериализует grab буфера + INSERT между фоновым flusher'ом,
Flush() и полным батчом InsertTelemetry. Теперь Flush дожидается in-flight
записи и возвращает реальную ошибку.
This commit is contained in:
Repinoid
2026-10-10 18:52:43 +03:00
parent 8335afb617
commit 7407de5fd3
+19 -1
View File
@@ -43,6 +43,11 @@ type IoTPostgresStore struct {
// батчинг вставок (фикс MEDIUM ревью 2026-08-16: 1000 msg/s = 1000 INSERT)
mu sync.Mutex
// flushMu сериализует фактическую запись (grab буфера + INSERT) между
// фоновым flusher'ом, Flush() из consumer и синхронной записью полного
// батча в InsertTelemetry. Без него Flush видел бы пустой буфер и возвращал
// nil, пока INSERT ещё в полёте → подтверждение SQS до записи (потеря).
flushMu sync.Mutex
batches map[string][]telemetryInsert
stopFlush chan struct{}
flushWG sync.WaitGroup
@@ -125,6 +130,12 @@ func (s *IoTPostgresStore) startFlusher() {
// в буфер и будут повторены. Иначе фоновый flusher мог бы молча потерять
// телеметрию, а consumer, увидев пустой буфер в Flush, подтвердил бы SQS-сообщение.
func (s *IoTPostgresStore) flushAll(ctx context.Context) error {
// flushMu: см. поле. Сериализует запись целиком, чтобы Flush() из consumer
// дождался in-flight INSERT фонового flusher'а и вернул РЕАЛЬНУЮ ошибку
// (или nil только если всё записано), а не «буфер пуст, пока идёт запись».
s.flushMu.Lock()
defer s.flushMu.Unlock()
s.mu.Lock()
if len(s.batches) == 0 {
s.mu.Unlock()
@@ -342,7 +353,11 @@ func (s *IoTPostgresStore) InsertTelemetry(ctx context.Context, namespace, devic
s.mu.Unlock()
if full {
if err := s.flushTenant(ctx, namespace, rows); err != nil {
// Синхронная запись полного батча — под тем же flushMu, что и фоновый
// flusher и Flush: чтобы не писать параллельно (см. поле flushMu).
s.flushMu.Lock()
err := s.flushTenant(ctx, namespace, rows)
if err != nil {
if isRetryableFlushErr(err) {
// Не теряем строки: возвращаем в буфер (сеть/PG временно лежит).
s.mu.Lock()
@@ -351,6 +366,9 @@ func (s *IoTPostgresStore) InsertTelemetry(ctx context.Context, namespace, devic
} else {
s.log.Error("iotpg: permanent error, batch dropped", "namespace", namespace, "rows", len(rows), "err", err)
}
}
s.flushMu.Unlock()
if err != nil {
return err
}
}