kvdb+sqldb: use the same tx retry helper

This commit is contained in:
Andras Banki-Horvath committed 2024-04-11 15:04:02 +02:00
1 parent 976f5d2760
commit 43f4b14c28
6 files changed
+437 -255

No files matched your search

+45 -110
View File
@@ -8,12 +8,12 @@ import (
"errors"
"fmt"
"io"
"math"
"math/rand"
"strings"
"sync"
"time"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/lightningnetwork/lnd/sqldb"
)
const (
@@ -24,18 +24,7 @@ const (
// DefaultNumTxRetries is the default number of times we'll retry a
// transaction if it fails with an error that permits transaction
// repetition.
DefaultNumTxRetries = 10
// DefaultInitialRetryDelay is the default initial delay between
// retries. This will be used to generate a random delay between -50%
// and +50% of this value, so 20 to 60 milliseconds. The retry will be
// doubled after each attempt until we reach DefaultMaxRetryDelay. We
// start with a random value to avoid multiple goroutines that are
// created at the same time to effectively retry at the same time.
DefaultInitialRetryDelay = time.Millisecond * 50
// DefaultMaxRetryDelay is the default maximum delay between retries.
DefaultMaxRetryDelay = time.Second * 5
DefaultNumTxRetries = 50
)
// Config holds a set of configuration options of a sql database connection.
@@ -227,112 +216,43 @@ func (db *db) Update(f func(tx walletdb.ReadWriteTx) error,
return db.executeTransaction(f, reset, false)
}
// randRetryDelay returns a random retry delay between -50% and +50% of the
// configured delay that is doubled for each attempt and capped at a max value.
func randRetryDelay(initialRetryDelay, maxRetryDelay time.Duration,
attempt int) time.Duration {
halfDelay := initialRetryDelay / 2
randDelay := rand.Int63n(int64(initialRetryDelay)) //nolint:gosec
// 50% plus 0%-100% gives us the range of 50%-150%.
initialDelay := halfDelay + time.Duration(randDelay)
// If this is the first attempt, we just return the initial delay.
if attempt == 0 {
return initialDelay
}
// For each subsequent delay, we double the initial delay. This still
// gives us a somewhat random delay, but it still increases with each
// attempt. If we double something n times, that's the same as
// multiplying the value with 2^n. We limit the power to 32 to avoid
// overflows.
factor := time.Duration(math.Pow(2, math.Min(float64(attempt), 32)))
actualDelay := initialDelay * factor
// Cap the delay at the maximum configured value.
if actualDelay > maxRetryDelay {
return maxRetryDelay
}
return actualDelay
}
// executeTransaction creates a new read-only or read-write transaction and
// executes the given function within it.
func (db *db) executeTransaction(f func(tx walletdb.ReadWriteTx) error,
reset func(), readOnly bool) error {
// waitBeforeRetry is a helper function that will wait for a random
// interval before exiting to retry the db transaction. If false is
// returned, then this means that daemon is shutting down so we
// should abort the retries.
waitBeforeRetry := func(attemptNumber int) bool {
retryDelay := randRetryDelay(
DefaultInitialRetryDelay, DefaultMaxRetryDelay,
attemptNumber,
)
log.Debugf("Retrying transaction due to tx serialization "+
"error, attempt_number=%v, delay=%v", attemptNumber,
retryDelay)
select {
// Before we try again, we'll wait with a random backoff based
// on the retry delay.
case <-time.After(retryDelay):
return true
// If the daemon is shutting down, then we'll exit early.
case <-db.ctx.Done():
return false
}
makeTx := func() (sqldb.Tx, error) {
return newReadWriteTx(db, readOnly)
}
for i := 0; i < DefaultNumTxRetries; i++ {
execTxBody := func(tx sqldb.Tx) error {
kvTx, ok := tx.(*readWriteTx)
if !ok {
return fmt.Errorf("expected *readWriteTx, got %T", tx)
}
reset()
tx, err := newReadWriteTx(db, readOnly)
if err != nil {
dbErr := MapSQLError(err)
if IsSerializationError(dbErr) {
// Nothing to roll back here, since we didn't
// even get a transaction yet.
if waitBeforeRetry(i) {
continue
}
}
return dbErr
}
err = catchPanic(func() error { return f(tx) })
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Errorf("Error rolling back tx: %v",
rollbackErr)
}
return err
}
dbErr := tx.Commit()
if IsSerializationError(dbErr) {
_ = tx.Rollback()
if waitBeforeRetry(i) {
continue
}
}
return dbErr
return catchPanic(func() error { return f(kvTx) })
}
// If we get to this point, then we weren't able to successfully commit
// a tx given the max number of retries.
return ErrRetriesExceeded
onBackoff := func(retry int, delay time.Duration) {
log.Tracef("Retrying transaction due to tx serialization "+
"error, attempt_number=%v, delay=%v", retry, delay)
}
rollbackTx := func(tx sqldb.Tx) error {
kvTx, ok := tx.(*readWriteTx)
if !ok {
return fmt.Errorf("expected *readWriteTx, got %T", tx)
}
return attemptRollback(kvTx)
}
return sqldb.ExecuteSQLTransactionWithRetry(
db.ctx, makeTx, rollbackTx, execTxBody, onBackoff,
DefaultNumTxRetries,
)
}
// PrintStats returns all collected stats pretty printed into a string.
@@ -367,3 +287,18 @@ func (db *db) Close() error {
return dbConns.Close(db.cfg.Dsn)
}
// attemptRollback attempts to roll back the transaction, and if it fails, it
// will return the error. If the transaction was already closed, it will return
// nil.
func attemptRollback(tx *readWriteTx) error {
rollbackErr := tx.Rollback()
if rollbackErr != nil &&
!errors.Is(rollbackErr, walletdb.ErrTxClosed) &&
!strings.Contains(rollbackErr.Error(), "conn closed") {
return fmt.Errorf("error rolling back tx: %w", rollbackErr)
}
return nil
}