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