aboutsummaryrefslogtreecommitdiffstats
path: root/models/db
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2022-11-13 04:18:50 +0800
committerGitHub <noreply@github.com>2022-11-12 21:18:50 +0100
commit34283a74e85278fed2c9b70d6f8749dc6a4001ca (patch)
treef8fa43399cb723d55efc34dacf7947faf0ed5acf /models/db
parenta0a425a13ba587829a831aaecd8469d39d372111 (diff)
downloadgitea-34283a74e85278fed2c9b70d6f8749dc6a4001ca.tar.gz
gitea-34283a74e85278fed2c9b70d6f8749dc6a4001ca.zip
Allow detect whether it's in a database transaction for a context.Context (#21756)
Fix #19513 This PR introduce a new db method `InTransaction(context.Context)`, and also builtin check on `db.TxContext` and `db.WithTx`. There is also a new method `db.AutoTx` has been introduced but could be used by other PRs. `WithTx` will always open a new transaction, if a transaction exist in context, return an error. `AutoTx` will try to open a new transaction if no transaction exist in context. That means it will always enter a transaction if there is no error. Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: 6543 <6543@obermui.de>
Diffstat (limited to 'models/db')
-rw-r--r--models/db/context.go54
-rw-r--r--models/db/context_test.go33
-rw-r--r--models/db/error.go3
-rw-r--r--models/db/index_test.go8
4 files changed, 87 insertions, 11 deletions
diff --git a/models/db/context.go b/models/db/context.go
index e90780e4e9..8c81469ebc 100644
--- a/models/db/context.go
+++ b/models/db/context.go
@@ -8,6 +8,7 @@ import (
"context"
"database/sql"
+ "xorm.io/xorm"
"xorm.io/xorm/schemas"
)
@@ -86,7 +87,11 @@ type Committer interface {
}
// TxContext represents a transaction Context
-func TxContext() (*Context, Committer, error) {
+func TxContext(parentCtx context.Context) (*Context, Committer, error) {
+ if InTransaction(parentCtx) {
+ return nil, nil, ErrAlreadyInTransaction
+ }
+
sess := x.NewSession()
if err := sess.Begin(); err != nil {
sess.Close()
@@ -97,14 +102,24 @@ func TxContext() (*Context, Committer, error) {
}
// WithTx represents executing database operations on a transaction
-// you can optionally change the context to a parent one
-func WithTx(f func(ctx context.Context) error, stdCtx ...context.Context) error {
- parentCtx := DefaultContext
- if len(stdCtx) != 0 && stdCtx[0] != nil {
- // TODO: make sure parent context has no open session
- parentCtx = stdCtx[0]
+// 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,
+// 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))
}
+ return txWithNoCheck(parentCtx, f)
+}
+func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
@@ -180,3 +195,28 @@ func EstimateCount(ctx context.Context, bean interface{}) (int64, error) {
}
return rows, err
}
+
+// 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()
+ }
+ }
+ if e == nil {
+ return false
+ }
+
+ switch t := e.(type) {
+ case *xorm.Engine:
+ return false
+ case *xorm.Session:
+ return t.IsInTx()
+ default:
+ return false
+ }
+}
diff --git a/models/db/context_test.go b/models/db/context_test.go
new file mode 100644
index 0000000000..f00e7834ed
--- /dev/null
+++ b/models/db/context_test.go
@@ -0,0 +1,33 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package db_test
+
+import (
+ "context"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestInTransaction(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ assert.False(t, db.InTransaction(db.DefaultContext))
+ assert.NoError(t, db.WithTx(db.DefaultContext, func(ctx context.Context) error {
+ assert.True(t, db.InTransaction(ctx))
+ return nil
+ }))
+
+ ctx, committer, err := db.TxContext(db.DefaultContext)
+ 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.True(t, db.InTransaction(ctx))
+ return nil
+ }))
+}
diff --git a/models/db/error.go b/models/db/error.go
index 9577fa55db..85cba5c4e9 100644
--- a/models/db/error.go
+++ b/models/db/error.go
@@ -5,11 +5,14 @@
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
diff --git a/models/db/index_test.go b/models/db/index_test.go
index 1ea30e2b60..cf777b9d38 100644
--- a/models/db/index_test.go
+++ b/models/db/index_test.go
@@ -59,7 +59,7 @@ func TestSyncMaxResourceIndex(t *testing.T) {
assert.EqualValues(t, 62, maxIndex)
// commit transaction
- err = db.WithTx(func(ctx context.Context) error {
+ err = db.WithTx(db.DefaultContext, func(ctx context.Context) error {
err = db.SyncMaxResourceIndex(ctx, "test_index", 10, 73)
assert.NoError(t, err)
maxIndex, err = getCurrentResourceIndex(ctx, "test_index", 10)
@@ -73,7 +73,7 @@ func TestSyncMaxResourceIndex(t *testing.T) {
assert.EqualValues(t, 73, maxIndex)
// rollback transaction
- err = db.WithTx(func(ctx context.Context) error {
+ err = db.WithTx(db.DefaultContext, func(ctx context.Context) error {
err = db.SyncMaxResourceIndex(ctx, "test_index", 10, 84)
maxIndex, err = getCurrentResourceIndex(ctx, "test_index", 10)
assert.NoError(t, err)
@@ -102,7 +102,7 @@ func TestGetNextResourceIndex(t *testing.T) {
assert.EqualValues(t, 2, maxIndex)
// commit transaction
- err = db.WithTx(func(ctx context.Context) error {
+ err = db.WithTx(db.DefaultContext, func(ctx context.Context) error {
maxIndex, err = db.GetNextResourceIndex(ctx, "test_index", 20)
assert.NoError(t, err)
assert.EqualValues(t, 3, maxIndex)
@@ -114,7 +114,7 @@ func TestGetNextResourceIndex(t *testing.T) {
assert.EqualValues(t, 3, maxIndex)
// rollback transaction
- err = db.WithTx(func(ctx context.Context) error {
+ err = db.WithTx(db.DefaultContext, func(ctx context.Context) error {
maxIndex, err = db.GetNextResourceIndex(ctx, "test_index", 20)
assert.NoError(t, err)
assert.EqualValues(t, 4, maxIndex)