P0-7: подтверждение из SQS только после успешного flush телеметрии в БД

InsertTelemetry лишь буферизует и возвращает nil до фактической записи. Consumer
удалял сообщение сразу после processTelemetry == nil — краш/flush-ошибка в этом
окне = безвозвратная потеря. Теперь: копим receipt handles, вызываем store.Flush,
и только при успехе DeleteMessage; при ошибке flush — не удаляем (сообщения
вернутся после visibility timeout). Добавлен iotpg.Flush().
This commit is contained in:
Repinoid
2026-10-07 07:23:25 +03:00
parent a4e786d03b
commit 3175605931
2 changed files with 34 additions and 8 deletions
+26 -8
View File
@@ -54,18 +54,24 @@ func Run(ctx context.Context, cfg *config.Config, sqsClient *sqs.Client, store *
} }
processBackoff := time.Second processBackoff := time.Second
// Копим подтверждения, но НЕ удаляем из SQS сразу: InsertTelemetry лишь
// буферизует и возвращает nil до фактической записи. Удаляем только после
// успешного Flush — иначе краш/flush-ошибка = потеря подтверждённых
// сообщений (P0-7).
type pendingAck struct {
handle *string
id string
}
var pending []pendingAck
for _, msg := range resp.Messages { for _, msg := range resp.Messages {
if msg.Body == nil || *msg.Body == "" { if msg.Body == nil || *msg.Body == "" {
log.Warn("consumer: empty message body, deleting", log.Warn("consumer: empty message body", "message_id", aws.ToString(msg.MessageId))
"message_id", aws.ToString(msg.MessageId)) pending = append(pending, pendingAck{msg.ReceiptHandle, aws.ToString(msg.MessageId)})
if err := deleteMessage(ctx, sqsClient, queueURL, msg.ReceiptHandle, log); err != nil {
log.Error("consumer: DeleteMessage failed", "err", err, "message_id", aws.ToString(msg.MessageId))
}
continue continue
} }
if err := processTelemetry(ctx, *msg.Body, store, log); err != nil { if err := processTelemetry(ctx, *msg.Body, store, log); err != nil {
log.Error("consumer: process telemetry", "err", err, "message_id", aws.ToString(msg.MessageId)) log.Error("consumer: process telemetry", "err", err, "message_id", aws.ToString(msg.MessageId))
// Сообщение вернётся в очередь после visibility timeout. // Сообщение НЕ подтверждаем — вернётся после visibility timeout.
// Backoff с jitter — не долбить упавший PG в цикле // Backoff с jitter — не долбить упавший PG в цикле
// (фикс HIGH из ревью 2026-08-16: лавина ретраев). // (фикс HIGH из ревью 2026-08-16: лавина ретраев).
if !sleepCtx(ctx, processBackoff) { if !sleepCtx(ctx, processBackoff) {
@@ -78,8 +84,20 @@ func Run(ctx context.Context, cfg *config.Config, sqsClient *sqs.Client, store *
continue continue
} }
processBackoff = time.Second processBackoff = time.Second
if err := deleteMessage(ctx, sqsClient, queueURL, msg.ReceiptHandle, log); err != nil { pending = append(pending, pendingAck{msg.ReceiptHandle, aws.ToString(msg.MessageId)})
log.Error("consumer: DeleteMessage failed", "err", err, "message_id", aws.ToString(msg.MessageId)) }
if len(pending) == 0 {
continue
}
// Сначала гарантируем запись в БД, только потом удаляем из SQS.
if err := store.Flush(ctx); err != nil {
log.Error("consumer: flush failed — сообщения НЕ удалены, вернутся после visibility timeout", "err", err)
continue
}
for _, p := range pending {
if err := deleteMessage(ctx, sqsClient, queueURL, p.handle, log); err != nil {
log.Error("consumer: DeleteMessage failed", "err", err, "message_id", p.id)
} }
} }
} }
@@ -141,6 +141,14 @@ func (s *IoTPostgresStore) flushAll(reportErr bool) error {
return firstErr return firstErr
} }
// Flush — принудительно синхронно пишет все накопленные батчи телеметрии в БД и
// возвращает первую ошибку. Нужен consumer'у, чтобы подтверждать (удалять из SQS)
// ТОЛЬКО фактически записанные данные: без этого краш/flush-ошибка после
// InsertTelemetry (которая лишь буферизует и возвращает nil) = потеря сообщений.
func (s *IoTPostgresStore) Flush(ctx context.Context) error {
return s.flushAll(true)
}
// flushTenant — INSERT батча строк одного тенанта (multi-VALUES). // flushTenant — INSERT батча строк одного тенанта (multi-VALUES).
func (s *IoTPostgresStore) flushTenant(ns string, rows []telemetryInsert) error { func (s *IoTPostgresStore) flushTenant(ns string, rows []telemetryInsert) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)