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:
@@ -43,6 +43,11 @@ type IoTPostgresStore struct {
|
|||||||
|
|
||||||
// батчинг вставок (фикс MEDIUM ревью 2026-08-16: 1000 msg/s = 1000 INSERT)
|
// батчинг вставок (фикс MEDIUM ревью 2026-08-16: 1000 msg/s = 1000 INSERT)
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
// flushMu сериализует фактическую запись (grab буфера + INSERT) между
|
||||||
|
// фоновым flusher'ом, Flush() из consumer и синхронной записью полного
|
||||||
|
// батча в InsertTelemetry. Без него Flush видел бы пустой буфер и возвращал
|
||||||
|
// nil, пока INSERT ещё в полёте → подтверждение SQS до записи (потеря).
|
||||||
|
flushMu sync.Mutex
|
||||||
batches map[string][]telemetryInsert
|
batches map[string][]telemetryInsert
|
||||||
stopFlush chan struct{}
|
stopFlush chan struct{}
|
||||||
flushWG sync.WaitGroup
|
flushWG sync.WaitGroup
|
||||||
@@ -125,6 +130,12 @@ func (s *IoTPostgresStore) startFlusher() {
|
|||||||
// в буфер и будут повторены. Иначе фоновый flusher мог бы молча потерять
|
// в буфер и будут повторены. Иначе фоновый flusher мог бы молча потерять
|
||||||
// телеметрию, а consumer, увидев пустой буфер в Flush, подтвердил бы SQS-сообщение.
|
// телеметрию, а consumer, увидев пустой буфер в Flush, подтвердил бы SQS-сообщение.
|
||||||
func (s *IoTPostgresStore) flushAll(ctx context.Context) error {
|
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()
|
s.mu.Lock()
|
||||||
if len(s.batches) == 0 {
|
if len(s.batches) == 0 {
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
@@ -342,7 +353,11 @@ func (s *IoTPostgresStore) InsertTelemetry(ctx context.Context, namespace, devic
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
if full {
|
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) {
|
if isRetryableFlushErr(err) {
|
||||||
// Не теряем строки: возвращаем в буфер (сеть/PG временно лежит).
|
// Не теряем строки: возвращаем в буфер (сеть/PG временно лежит).
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@@ -351,6 +366,9 @@ func (s *IoTPostgresStore) InsertTelemetry(ctx context.Context, namespace, devic
|
|||||||
} else {
|
} else {
|
||||||
s.log.Error("iotpg: permanent error, batch dropped", "namespace", namespace, "rows", len(rows), "err", err)
|
s.log.Error("iotpg: permanent error, batch dropped", "namespace", namespace, "rows", len(rows), "err", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
s.flushMu.Unlock()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user