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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user