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:
@@ -54,18 +54,24 @@ func Run(ctx context.Context, cfg *config.Config, sqsClient *sqs.Client, store *
|
||||
}
|
||||
|
||||
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 {
|
||||
if msg.Body == nil || *msg.Body == "" {
|
||||
log.Warn("consumer: empty message body, deleting",
|
||||
"message_id", 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))
|
||||
}
|
||||
log.Warn("consumer: empty message body", "message_id", aws.ToString(msg.MessageId))
|
||||
pending = append(pending, pendingAck{msg.ReceiptHandle, aws.ToString(msg.MessageId)})
|
||||
continue
|
||||
}
|
||||
if err := processTelemetry(ctx, *msg.Body, store, log); err != nil {
|
||||
log.Error("consumer: process telemetry", "err", err, "message_id", aws.ToString(msg.MessageId))
|
||||
// Сообщение вернётся в очередь после visibility timeout.
|
||||
// Сообщение НЕ подтверждаем — вернётся после visibility timeout.
|
||||
// Backoff с jitter — не долбить упавший PG в цикле
|
||||
// (фикс HIGH из ревью 2026-08-16: лавина ретраев).
|
||||
if !sleepCtx(ctx, processBackoff) {
|
||||
@@ -78,8 +84,20 @@ func Run(ctx context.Context, cfg *config.Config, sqsClient *sqs.Client, store *
|
||||
continue
|
||||
}
|
||||
processBackoff = time.Second
|
||||
if err := deleteMessage(ctx, sqsClient, queueURL, msg.ReceiptHandle, log); err != nil {
|
||||
log.Error("consumer: DeleteMessage failed", "err", err, "message_id", aws.ToString(msg.MessageId))
|
||||
pending = append(pending, pendingAck{msg.ReceiptHandle, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user