Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always reuse transaction #22362

Merged
merged 5 commits into from
Jan 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion models/activities/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func CountNotifications(ctx context.Context, opts *FindNotificationOptions) (int

// CreateRepoTransferNotification creates notification for the user a repository was transferred to
func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
return db.AutoTx(ctx, func(ctx context.Context) error {
return db.WithTx(ctx, func(ctx context.Context) error {
var notify []*Notification

if newOwner.IsOrganization() {
Expand Down
71 changes: 41 additions & 30 deletions models/db/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,22 @@ type Engined interface {

// GetEngine will get a db Engine from this context or return an Engine restricted to this context
func GetEngine(ctx context.Context) Engine {
if e := getEngine(ctx); e != nil {
return e
}
return x.Context(ctx)
}

// getEngine will get a db Engine from this context or return nil
func getEngine(ctx context.Context) Engine {
if engined, ok := ctx.(Engined); ok {
return engined.Engine()
}
enginedInterface := ctx.Value(enginedContextKey)
if enginedInterface != nil {
return enginedInterface.(Engined).Engine()
}
return x.Context(ctx)
return nil
}

// Committer represents an interface to Commit or Close the Context
Expand All @@ -87,10 +95,22 @@ type Committer interface {
Close() error
}

// TxContext represents a transaction Context
// halfCommitter is a wrapper of Committer.
// It can be closed early, but can't be committed early, it is useful for reusing a transaction.
type halfCommitter struct {
Committer
}

func (*halfCommitter) Commit() error {
// do nothing
return nil
}

// TxContext represents a transaction Context,
// it will reuse the existing transaction in the parent context or create a new one.
func TxContext(parentCtx context.Context) (*Context, Committer, error) {
if InTransaction(parentCtx) {
return nil, nil, ErrAlreadyInTransaction
if sess, ok := inTransaction(parentCtx); ok {
return newContext(parentCtx, sess, true), &halfCommitter{Committer: sess}, nil
Comment on lines +98 to +113
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not sure if this is the definitive answer.
I'll accept it for now, but I think it would be better if Commit would still be possible with the behavior that it opens a new transaction afterward.

}

sess := x.NewSession()
Expand All @@ -102,20 +122,11 @@ func TxContext(parentCtx context.Context) (*Context, Committer, error) {
return newContext(DefaultContext, sess, true), sess, nil
}

// WithTx represents executing database operations on a transaction
// This function will always open a new transaction, if a transaction exist in parentCtx return an error.
func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error {
if InTransaction(parentCtx) {
return ErrAlreadyInTransaction
}
return txWithNoCheck(parentCtx, f)
}

// AutoTx represents executing database operations on a transaction, if the transaction exist,
// WithTx represents executing database operations on a transaction, if the transaction exist,
// this function will reuse it otherwise will create a new one and close it when finished.
func AutoTx(parentCtx context.Context, f func(ctx context.Context) error) error {
if InTransaction(parentCtx) {
return f(newContext(parentCtx, GetEngine(parentCtx), true))
func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error {
if sess, ok := inTransaction(parentCtx); ok {
return f(newContext(parentCtx, sess, true))
}
return txWithNoCheck(parentCtx, f)
}
Expand Down Expand Up @@ -202,25 +213,25 @@ func EstimateCount(ctx context.Context, bean interface{}) (int64, error) {

// InTransaction returns true if the engine is in a transaction otherwise return false
func InTransaction(ctx context.Context) bool {
var e Engine
if engined, ok := ctx.(Engined); ok {
e = engined.Engine()
} else {
enginedInterface := ctx.Value(enginedContextKey)
if enginedInterface != nil {
e = enginedInterface.(Engined).Engine()
}
}
_, ok := inTransaction(ctx)
return ok
}

func inTransaction(ctx context.Context) (*xorm.Session, bool) {
e := getEngine(ctx)
if e == nil {
return false
return nil, false
}

switch t := e.(type) {
case *xorm.Engine:
return false
return nil, false
case *xorm.Session:
return t.IsInTx()
if t.IsInTx() {
return t, true
}
return nil, false
default:
return false
return nil, false
}
}
56 changes: 55 additions & 1 deletion models/db/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,62 @@ func TestInTransaction(t *testing.T) {
assert.NoError(t, err)
defer committer.Close()
assert.True(t, db.InTransaction(ctx))
assert.Error(t, db.WithTx(ctx, func(ctx context.Context) error {
assert.NoError(t, db.WithTx(ctx, func(ctx context.Context) error {
assert.True(t, db.InTransaction(ctx))
return nil
}))
}

func TestTxContext(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

{ // create new transaction
ctx, committer, err := db.TxContext(db.DefaultContext)
assert.NoError(t, err)
assert.True(t, db.InTransaction(ctx))
assert.NoError(t, committer.Commit())
}

{ // reuse the transaction created by TxContext and commit it
ctx, committer, err := db.TxContext(db.DefaultContext)
engine := db.GetEngine(ctx)
assert.NoError(t, err)
assert.True(t, db.InTransaction(ctx))
{
ctx, committer, err := db.TxContext(ctx)
assert.NoError(t, err)
assert.True(t, db.InTransaction(ctx))
assert.Equal(t, engine, db.GetEngine(ctx))
assert.NoError(t, committer.Commit())
}
assert.NoError(t, committer.Commit())
}

{ // reuse the transaction created by TxContext and close it
ctx, committer, err := db.TxContext(db.DefaultContext)
engine := db.GetEngine(ctx)
assert.NoError(t, err)
assert.True(t, db.InTransaction(ctx))
{
ctx, committer, err := db.TxContext(ctx)
assert.NoError(t, err)
assert.True(t, db.InTransaction(ctx))
assert.Equal(t, engine, db.GetEngine(ctx))
assert.NoError(t, committer.Close())
}
assert.NoError(t, committer.Close())
}

{ // reuse the transaction created by WithTx
assert.NoError(t, db.WithTx(db.DefaultContext, func(ctx context.Context) error {
assert.True(t, db.InTransaction(ctx))
{
ctx, committer, err := db.TxContext(ctx)
assert.NoError(t, err)
assert.True(t, db.InTransaction(ctx))
assert.NoError(t, committer.Commit())
}
return nil
}))
}
}
3 changes: 0 additions & 3 deletions models/db/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
package db

import (
"errors"
"fmt"

"code.gitea.io/gitea/modules/util"
)

var ErrAlreadyInTransaction = errors.New("database connection has already been in a transaction")

// ErrCancelled represents an error due to context cancellation
type ErrCancelled struct {
Message string
Expand Down
2 changes: 1 addition & 1 deletion models/issues/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -2365,7 +2365,7 @@ func CountOrphanedIssues(ctx context.Context) (int64, error) {
// DeleteOrphanedIssues delete issues without a repo
func DeleteOrphanedIssues(ctx context.Context) error {
var attachmentPaths []string
err := db.AutoTx(ctx, func(ctx context.Context) error {
err := db.WithTx(ctx, func(ctx context.Context) error {
var ids []int64

if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
Expand Down
2 changes: 1 addition & 1 deletion models/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ func changeProjectStatus(ctx context.Context, p *Project, isClosed bool) error {
// DeleteProjectByID deletes a project from a repository. if it's not in a database
// transaction, it will start a new database transaction
func DeleteProjectByID(ctx context.Context, id int64) error {
return db.AutoTx(ctx, func(ctx context.Context) error {
return db.WithTx(ctx, func(ctx context.Context) error {
p, err := GetProjectByID(ctx, id)
if err != nil {
if IsErrProjectNotExist(err) {
Expand Down
2 changes: 1 addition & 1 deletion models/repo/collaboration.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid in
return nil
}

return db.AutoTx(ctx, func(ctx context.Context) error {
return db.WithTx(ctx, func(ctx context.Context) error {
e := db.GetEngine(ctx)

collaboration := &Collaboration{
Expand Down
2 changes: 1 addition & 1 deletion models/repo_transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func TestRepositoryReadyForTransfer(status repo_model.RepositoryStatus) error {
// CreatePendingRepositoryTransfer transfer a repo from one owner to a new one.
// it marks the repository transfer as "pending"
func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repoID int64, teams []*organization.Team) error {
return db.AutoTx(ctx, func(ctx context.Context) error {
return db.WithTx(ctx, func(ctx context.Context) error {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion modules/notification/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ func (ns *notificationService) NotifyPullReviewRequest(ctx context.Context, doer
}

func (ns *notificationService) NotifyRepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) {
err := db.AutoTx(ctx, func(ctx context.Context) error {
err := db.WithTx(ctx, func(ctx context.Context) error {
return activities_model.CreateRepoTransferNotification(ctx, doer, newOwner, repo)
})
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion modules/repository/collaborator.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
)

func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error {
return db.AutoTx(ctx, func(ctx context.Context) error {
return db.WithTx(ctx, func(ctx context.Context) error {
collaboration := &repo_model.Collaboration{
RepoID: repo.ID,
UserID: u.ID,
Expand Down
2 changes: 1 addition & 1 deletion services/issue/comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_mode

// DeleteComment deletes the comment
func DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) error {
err := db.AutoTx(ctx, func(ctx context.Context) error {
err := db.WithTx(ctx, func(ctx context.Context) error {
return issues_model.DeleteComment(ctx, comment)
})
if err != nil {
Expand Down