mirror of https://github.com/grafana/grafana
Merge pull request #12203 from bergquist/bus_multi_dispatch
bus: support multiple dispatch in one transactionpull/11773/merge
commit
d6f4313c2f
@ -0,0 +1,71 @@ |
||||
package sqlstore |
||||
|
||||
import ( |
||||
"context" |
||||
"reflect" |
||||
|
||||
"github.com/go-xorm/xorm" |
||||
) |
||||
|
||||
type DBSession struct { |
||||
*xorm.Session |
||||
events []interface{} |
||||
} |
||||
|
||||
type dbTransactionFunc func(sess *DBSession) error |
||||
|
||||
func (sess *DBSession) publishAfterCommit(msg interface{}) { |
||||
sess.events = append(sess.events, msg) |
||||
} |
||||
|
||||
func newSession() *DBSession { |
||||
return &DBSession{Session: x.NewSession()} |
||||
} |
||||
|
||||
func startSession(ctx context.Context, engine *xorm.Engine, beginTran bool) (*DBSession, error) { |
||||
value := ctx.Value(ContextSessionName) |
||||
var sess *DBSession |
||||
sess, ok := value.(*DBSession) |
||||
|
||||
if !ok { |
||||
newSess := &DBSession{Session: engine.NewSession()} |
||||
if beginTran { |
||||
err := newSess.Begin() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
return newSess, nil |
||||
} |
||||
|
||||
return sess, nil |
||||
} |
||||
|
||||
func withDbSession(ctx context.Context, callback dbTransactionFunc) error { |
||||
sess, err := startSession(ctx, x, false) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return callback(sess) |
||||
} |
||||
|
||||
func (sess *DBSession) InsertId(bean interface{}) (int64, error) { |
||||
table := sess.DB().Mapper.Obj2Table(getTypeName(bean)) |
||||
|
||||
dialect.PreInsertId(table, sess.Session) |
||||
|
||||
id, err := sess.Session.InsertOne(bean) |
||||
|
||||
dialect.PostInsertId(table, sess.Session) |
||||
|
||||
return id, err |
||||
} |
||||
|
||||
func getTypeName(bean interface{}) (res string) { |
||||
t := reflect.TypeOf(bean) |
||||
for t.Kind() == reflect.Ptr { |
||||
t = t.Elem() |
||||
} |
||||
return t.Name() |
||||
} |
@ -1,90 +0,0 @@ |
||||
package sqlstore |
||||
|
||||
import ( |
||||
"reflect" |
||||
"time" |
||||
|
||||
"github.com/go-xorm/xorm" |
||||
"github.com/grafana/grafana/pkg/bus" |
||||
"github.com/grafana/grafana/pkg/log" |
||||
sqlite3 "github.com/mattn/go-sqlite3" |
||||
) |
||||
|
||||
type DBSession struct { |
||||
*xorm.Session |
||||
events []interface{} |
||||
} |
||||
|
||||
type dbTransactionFunc func(sess *DBSession) error |
||||
|
||||
func (sess *DBSession) publishAfterCommit(msg interface{}) { |
||||
sess.events = append(sess.events, msg) |
||||
} |
||||
|
||||
func newSession() *DBSession { |
||||
return &DBSession{Session: x.NewSession()} |
||||
} |
||||
|
||||
func inTransaction(callback dbTransactionFunc) error { |
||||
return inTransactionWithRetry(callback, 0) |
||||
} |
||||
|
||||
func inTransactionWithRetry(callback dbTransactionFunc, retry int) error { |
||||
var err error |
||||
|
||||
sess := newSession() |
||||
defer sess.Close() |
||||
|
||||
if err = sess.Begin(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = callback(sess) |
||||
|
||||
// special handling of database locked errors for sqlite, then we can retry 3 times
|
||||
if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 { |
||||
if sqlError.Code == sqlite3.ErrLocked { |
||||
sess.Rollback() |
||||
time.Sleep(time.Millisecond * time.Duration(10)) |
||||
sqlog.Info("Database table locked, sleeping then retrying", "retry", retry) |
||||
return inTransactionWithRetry(callback, retry+1) |
||||
} |
||||
} |
||||
|
||||
if err != nil { |
||||
sess.Rollback() |
||||
return err |
||||
} else if err = sess.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if len(sess.events) > 0 { |
||||
for _, e := range sess.events { |
||||
if err = bus.Publish(e); err != nil { |
||||
log.Error(3, "Failed to publish event after commit", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (sess *DBSession) InsertId(bean interface{}) (int64, error) { |
||||
table := sess.DB().Mapper.Obj2Table(getTypeName(bean)) |
||||
|
||||
dialect.PreInsertId(table, sess.Session) |
||||
|
||||
id, err := sess.Session.InsertOne(bean) |
||||
|
||||
dialect.PostInsertId(table, sess.Session) |
||||
|
||||
return id, err |
||||
} |
||||
|
||||
func getTypeName(bean interface{}) (res string) { |
||||
t := reflect.TypeOf(bean) |
||||
for t.Kind() == reflect.Ptr { |
||||
t = t.Elem() |
||||
} |
||||
return t.Name() |
||||
} |
@ -0,0 +1,106 @@ |
||||
package sqlstore |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana/pkg/bus" |
||||
"github.com/grafana/grafana/pkg/log" |
||||
sqlite3 "github.com/mattn/go-sqlite3" |
||||
) |
||||
|
||||
func (ss *SqlStore) InTransaction(ctx context.Context, fn func(ctx context.Context) error) error { |
||||
return ss.inTransactionWithRetry(ctx, fn, 0) |
||||
} |
||||
|
||||
func (ss *SqlStore) inTransactionWithRetry(ctx context.Context, fn func(ctx context.Context) error, retry int) error { |
||||
sess, err := startSession(ctx, ss.engine, true) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer sess.Close() |
||||
|
||||
withValue := context.WithValue(ctx, ContextSessionName, sess) |
||||
|
||||
err = fn(withValue) |
||||
|
||||
// special handling of database locked errors for sqlite, then we can retry 3 times
|
||||
if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 { |
||||
if sqlError.Code == sqlite3.ErrLocked { |
||||
sess.Rollback() |
||||
time.Sleep(time.Millisecond * time.Duration(10)) |
||||
ss.log.Info("Database table locked, sleeping then retrying", "retry", retry) |
||||
return ss.inTransactionWithRetry(ctx, fn, retry+1) |
||||
} |
||||
} |
||||
|
||||
if err != nil { |
||||
sess.Rollback() |
||||
return err |
||||
} |
||||
|
||||
if err = sess.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if len(sess.events) > 0 { |
||||
for _, e := range sess.events { |
||||
if err = bus.Publish(e); err != nil { |
||||
ss.log.Error("Failed to publish event after commit", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func inTransactionWithRetry(callback dbTransactionFunc, retry int) error { |
||||
return inTransactionWithRetryCtx(context.Background(), callback, retry) |
||||
} |
||||
|
||||
func inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc, retry int) error { |
||||
sess, err := startSession(ctx, x, true) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer sess.Close() |
||||
|
||||
err = callback(sess) |
||||
|
||||
// special handling of database locked errors for sqlite, then we can retry 3 times
|
||||
if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 { |
||||
if sqlError.Code == sqlite3.ErrLocked { |
||||
sess.Rollback() |
||||
time.Sleep(time.Millisecond * time.Duration(10)) |
||||
sqlog.Info("Database table locked, sleeping then retrying", "retry", retry) |
||||
return inTransactionWithRetry(callback, retry+1) |
||||
} |
||||
} |
||||
|
||||
if err != nil { |
||||
sess.Rollback() |
||||
return err |
||||
} else if err = sess.Commit(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if len(sess.events) > 0 { |
||||
for _, e := range sess.events { |
||||
if err = bus.Publish(e); err != nil { |
||||
log.Error(3, "Failed to publish event after commit", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func inTransaction(callback dbTransactionFunc) error { |
||||
return inTransactionWithRetry(callback, 0) |
||||
} |
||||
|
||||
func inTransactionCtx(ctx context.Context, callback dbTransactionFunc) error { |
||||
return inTransactionWithRetryCtx(ctx, callback, 0) |
||||
} |
@ -0,0 +1,60 @@ |
||||
package sqlstore |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/models" |
||||
|
||||
. "github.com/smartystreets/goconvey/convey" |
||||
) |
||||
|
||||
type testQuery struct { |
||||
result bool |
||||
} |
||||
|
||||
var ProvokedError = errors.New("testing error.") |
||||
|
||||
func TestTransaction(t *testing.T) { |
||||
ss := InitTestDB(t) |
||||
|
||||
Convey("InTransaction asdf asdf", t, func() { |
||||
cmd := &models.AddApiKeyCommand{Key: "secret-key", Name: "key", OrgId: 1} |
||||
|
||||
err := AddApiKey(cmd) |
||||
So(err, ShouldBeNil) |
||||
|
||||
deleteApiKeyCmd := &models.DeleteApiKeyCommand{Id: cmd.Result.Id, OrgId: 1} |
||||
|
||||
Convey("can update key", func() { |
||||
err := ss.InTransaction(context.Background(), func(ctx context.Context) error { |
||||
return DeleteApiKeyCtx(ctx, deleteApiKeyCmd) |
||||
}) |
||||
|
||||
So(err, ShouldBeNil) |
||||
|
||||
query := &models.GetApiKeyByIdQuery{ApiKeyId: cmd.Result.Id} |
||||
err = GetApiKeyById(query) |
||||
So(err, ShouldEqual, models.ErrInvalidApiKey) |
||||
}) |
||||
|
||||
Convey("wont update if one handler fails", func() { |
||||
err := ss.InTransaction(context.Background(), func(ctx context.Context) error { |
||||
err := DeleteApiKeyCtx(ctx, deleteApiKeyCmd) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return ProvokedError |
||||
}) |
||||
|
||||
So(err, ShouldEqual, ProvokedError) |
||||
|
||||
query := &models.GetApiKeyByIdQuery{ApiKeyId: cmd.Result.Id} |
||||
err = GetApiKeyById(query) |
||||
So(err, ShouldBeNil) |
||||
So(query.Result.Id, ShouldEqual, cmd.Result.Id) |
||||
}) |
||||
}) |
||||
} |
Loading…
Reference in new issue