SQLStore: Refactor query retries to use exponential backoff (#58559)

This commit is contained in:
Sofia Papagiannaki
2022-11-24 13:12:50 +02:00
committed by GitHub
parent a2f1d2e102
commit f3da48bd50
2 changed files with 42 additions and 24 deletions
+23 -8
View File
@@ -9,10 +9,13 @@ import (
"xorm.io/xorm" "xorm.io/xorm"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/grafana/grafana/pkg/util/retryer"
"github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
) )
var sessionLogger = log.New("sqlstore.session") var sessionLogger = log.New("sqlstore.session")
var ErrMaximumRetriesReached = errutil.NewBase(errutil.StatusInternal, "sqlstore.max-retries-reached")
type DBSession struct { type DBSession struct {
*xorm.Session *xorm.Session
@@ -68,23 +71,34 @@ func (ss *SQLStore) WithDbSession(ctx context.Context, callback DBTransactionFun
func (ss *SQLStore) WithNewDbSession(ctx context.Context, callback DBTransactionFunc) error { func (ss *SQLStore) WithNewDbSession(ctx context.Context, callback DBTransactionFunc) error {
sess := &DBSession{Session: ss.engine.NewSession(), transactionOpen: false} sess := &DBSession{Session: ss.engine.NewSession(), transactionOpen: false}
defer sess.Close() defer sess.Close()
return ss.withRetry(ctx, callback, 0)(sess) retry := 0
return retryer.Retry(ss.retryOnLocks(ctx, callback, sess, retry), ss.dbCfg.QueryRetries, time.Millisecond*time.Duration(10), time.Second)
} }
func (ss *SQLStore) withRetry(ctx context.Context, callback DBTransactionFunc, retry int) DBTransactionFunc { func (ss *SQLStore) retryOnLocks(ctx context.Context, callback DBTransactionFunc, sess *DBSession, retry int) func() (retryer.RetrySignal, error) {
return func(sess *DBSession) error { return func() (retryer.RetrySignal, error) {
retry++
err := callback(sess) err := callback(sess)
ctxLogger := tsclogger.FromContext(ctx) ctxLogger := tsclogger.FromContext(ctx)
var sqlError sqlite3.Error var sqlError sqlite3.Error
if errors.As(err, &sqlError) && retry < ss.dbCfg.QueryRetries && (sqlError.Code == sqlite3.ErrLocked || sqlError.Code == sqlite3.ErrBusy) { if errors.As(err, &sqlError) && (sqlError.Code == sqlite3.ErrLocked || sqlError.Code == sqlite3.ErrBusy) {
time.Sleep(time.Millisecond * time.Duration(10))
ctxLogger.Info("Database locked, sleeping then retrying", "error", err, "retry", retry, "code", sqlError.Code) ctxLogger.Info("Database locked, sleeping then retrying", "error", err, "retry", retry, "code", sqlError.Code)
return ss.withRetry(ctx, callback, retry+1)(sess) // retryer immediately returns the error (if there is one) without checking the response
// therefore we only have to send it if we have reached the maximum retries
if retry == ss.dbCfg.QueryRetries {
return retryer.FuncError, ErrMaximumRetriesReached.Errorf("retry %d: %w", retry, err)
}
return retryer.FuncFailure, nil
} }
return err if err != nil {
return retryer.FuncError, err
}
return retryer.FuncComplete, nil
} }
} }
@@ -96,7 +110,8 @@ func (ss *SQLStore) withDbSession(ctx context.Context, engine *xorm.Engine, call
if isNew { if isNew {
defer sess.Close() defer sess.Close()
} }
return ss.withRetry(ctx, callback, 0)(sess) retry := 0
return retryer.Retry(ss.retryOnLocks(ctx, callback, sess, retry), ss.dbCfg.QueryRetries, time.Millisecond*time.Duration(10), time.Second)
} }
func (sess *DBSession) InsertId(bean interface{}) (int64, error) { func (sess *DBSession) InsertId(bean interface{}) (int64, error) {
+19 -16
View File
@@ -15,48 +15,51 @@ func TestRetryingOnFailures(t *testing.T) {
store.dbCfg.QueryRetries = 5 store.dbCfg.QueryRetries = 5
funcToTest := map[string]func(ctx context.Context, callback DBTransactionFunc) error{ funcToTest := map[string]func(ctx context.Context, callback DBTransactionFunc) error{
"WithDbSession()": store.WithDbSession, "WithDbSession": store.WithDbSession,
"WithNewDbSession()": store.WithNewDbSession, "WithNewDbSession": store.WithNewDbSession,
} }
for name, f := range funcToTest { for name, f := range funcToTest {
i := 0
t.Run(fmt.Sprintf("%s should return the error immediately if it's other than sqlite3.ErrLocked or sqlite3.ErrBusy", name), func(t *testing.T) { t.Run(fmt.Sprintf("%s should return the error immediately if it's other than sqlite3.ErrLocked or sqlite3.ErrBusy", name), func(t *testing.T) {
err := f(context.Background(), func(sess *DBSession) error { i := 0
callback := func(sess *DBSession) error {
i++ i++
return errors.New("some error") return errors.New("some error")
}) }
err := f(context.Background(), callback)
require.Error(t, err) require.Error(t, err)
require.Equal(t, i, 1) require.Equal(t, 1, i)
}) })
i = 0
t.Run(fmt.Sprintf("%s should return the sqlite3.Error if all retries have failed", name), func(t *testing.T) { t.Run(fmt.Sprintf("%s should return the sqlite3.Error if all retries have failed", name), func(t *testing.T) {
err := f(context.Background(), func(sess *DBSession) error { i := 0
callback := func(sess *DBSession) error {
i++ i++
return sqlite3.Error{Code: sqlite3.ErrBusy} return sqlite3.Error{Code: sqlite3.ErrBusy}
}) }
err := f(context.Background(), callback)
require.Error(t, err) require.Error(t, err)
var driverErr sqlite3.Error var driverErr sqlite3.Error
require.ErrorAs(t, err, &driverErr) require.ErrorAs(t, err, &driverErr)
require.Equal(t, i, store.dbCfg.QueryRetries+1) require.Equal(t, store.dbCfg.QueryRetries, i)
}) })
i = 0
t.Run(fmt.Sprintf("%s should not return the error if successive retries succeed", name), func(t *testing.T) { t.Run(fmt.Sprintf("%s should not return the error if successive retries succeed", name), func(t *testing.T) {
err := f(context.Background(), func(sess *DBSession) error { i := 0
callback := func(sess *DBSession) error {
i++
var err error var err error
switch { switch {
case i >= store.dbCfg.QueryRetries: case store.dbCfg.QueryRetries == i:
err = nil err = nil
default: default:
err = sqlite3.Error{Code: sqlite3.ErrBusy} err = sqlite3.Error{Code: sqlite3.ErrBusy}
} }
i++
return err return err
}) }
err := f(context.Background(), callback)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, i, store.dbCfg.QueryRetries+1) require.Equal(t, store.dbCfg.QueryRetries, i)
}) })
} }