Что происходит при ошибке в транзакции
Что проверяют
Знаете ли вы, что после ошибки транзакция не закрывается сама, и умеете ли гарантированно её завершить в Go, где нет ни исключений, ни автоматического отката при выходе из блока.
Как отвечать
Разделить два уровня. На уровне базы ошибка внутри транзакции в PostgreSQL переводит её в оборванное состояние: все последующие запросы отвечают ошибкой, пока не придёт откат, а частично применённые изменения не видны никому и пропадают. Если нужно пережить ошибку одного запроса, ставят точку сохранения через SAVEPOINT и откатываются к ней.
На уровне Go важнее другое: транзакция держит соединение из пула, и незакрытая транзакция это утечка, которая кончается зависанием всех запросов. Рабочая идиома:
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() // после успешного Commit вернёт sql.ErrTxDone, это нормально
if _, err := tx.ExecContext(ctx, q, arg); err != nil {
return err
}
return tx.Commit()Ошибку Commit тоже надо проверять: именно там вылезают отложенные проверки ограничений и конфликты сериализации. Отказ по сериализации это не баг, а штатный сигнал повторить транзакцию целиком, с выдержкой и ограничением числа попыток.
Где спотыкаются
Вызывают Rollback только в ветке с ошибкой и забывают про ранние выходы из функции. Одна пропущенная ветка, и соединение не возвращается в пул. defer сразу после начала транзакции закрывает все выходы разом.
Второе: смешивают db.Exec и tx.Exec в одном сценарии. Запрос через db уходит по другому соединению, вне транзакции: он не увидит незакоммиченных изменений, а после отката его собственный эффект останется в базе.
Третье: считают откат обязанностью базы при обрыве связи. Сервер действительно закроет транзакцию, когда заметит разрыв, но до этого момента блокировки живут. Поэтому транзакции передают context с таймаутом и не растягивают их на сетевые вызовы к чужим сервисам.