Skip to content

Commit a357143

Browse files
Fix halfCommitter and WithTx (#22366)
Related to #22362. I overlooked that there's always `committer.Close()`, like: ```go ctx, committer, err := db.TxContext(db.DefaultContext) if err != nil { return nil } defer committer.Close() // ... if err != nil { return nil } // ... return committer.Commit() ``` So the `Close` of `halfCommitter` should ignore `commit and close`, it's not a rollback. See: [Why `halfCommitter` and `WithTx` should rollback IMMEDIATELY or commit LATER](#22366 (comment)). Co-authored-by: techknowlogick <[email protected]>
1 parent 99a675f commit a357143

File tree

2 files changed

+124
-5
lines changed

2 files changed

+124
-5
lines changed

models/db/context.go

+22-5
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,31 @@ type Committer interface {
9898
// halfCommitter is a wrapper of Committer.
9999
// It can be closed early, but can't be committed early, it is useful for reusing a transaction.
100100
type halfCommitter struct {
101-
Committer
101+
committer Committer
102+
committed bool
102103
}
103104

104-
func (*halfCommitter) Commit() error {
105-
// do nothing
105+
func (c *halfCommitter) Commit() error {
106+
c.committed = true
107+
// should do nothing, and the parent committer will commit later
106108
return nil
107109
}
108110

111+
func (c *halfCommitter) Close() error {
112+
if c.committed {
113+
// it's "commit and close", should do nothing, and the parent committer will commit later
114+
return nil
115+
}
116+
117+
// it's "rollback and close", let the parent committer rollback right now
118+
return c.committer.Close()
119+
}
120+
109121
// TxContext represents a transaction Context,
110122
// it will reuse the existing transaction in the parent context or create a new one.
111123
func TxContext(parentCtx context.Context) (*Context, Committer, error) {
112124
if sess, ok := inTransaction(parentCtx); ok {
113-
return newContext(parentCtx, sess, true), &halfCommitter{Committer: sess}, nil
125+
return newContext(parentCtx, sess, true), &halfCommitter{committer: sess}, nil
114126
}
115127

116128
sess := x.NewSession()
@@ -126,7 +138,12 @@ func TxContext(parentCtx context.Context) (*Context, Committer, error) {
126138
// this function will reuse it otherwise will create a new one and close it when finished.
127139
func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error {
128140
if sess, ok := inTransaction(parentCtx); ok {
129-
return f(newContext(parentCtx, sess, true))
141+
err := f(newContext(parentCtx, sess, true))
142+
if err != nil {
143+
// rollback immediately, in case the caller ignores returned error and tries to commit the transaction.
144+
_ = sess.Close()
145+
}
146+
return err
130147
}
131148
return txWithNoCheck(parentCtx, f)
132149
}

models/db/context_committer_test.go

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package db // it's not db_test, because this file is for testing the private type halfCommitter
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
type MockCommitter struct {
14+
wants []string
15+
gots []string
16+
}
17+
18+
func NewMockCommitter(wants ...string) *MockCommitter {
19+
return &MockCommitter{
20+
wants: wants,
21+
}
22+
}
23+
24+
func (c *MockCommitter) Commit() error {
25+
c.gots = append(c.gots, "commit")
26+
return nil
27+
}
28+
29+
func (c *MockCommitter) Close() error {
30+
c.gots = append(c.gots, "close")
31+
return nil
32+
}
33+
34+
func (c *MockCommitter) Assert(t *testing.T) {
35+
assert.Equal(t, c.wants, c.gots, "want operations %v, but got %v", c.wants, c.gots)
36+
}
37+
38+
func Test_halfCommitter(t *testing.T) {
39+
/*
40+
Do something like:
41+
42+
ctx, committer, err := db.TxContext(db.DefaultContext)
43+
if err != nil {
44+
return nil
45+
}
46+
defer committer.Close()
47+
48+
// ...
49+
50+
if err != nil {
51+
return nil
52+
}
53+
54+
// ...
55+
56+
return committer.Commit()
57+
*/
58+
59+
testWithCommitter := func(committer Committer, f func(committer Committer) error) {
60+
if err := f(&halfCommitter{committer: committer}); err == nil {
61+
committer.Commit()
62+
}
63+
committer.Close()
64+
}
65+
66+
t.Run("commit and close", func(t *testing.T) {
67+
mockCommitter := NewMockCommitter("commit", "close")
68+
69+
testWithCommitter(mockCommitter, func(committer Committer) error {
70+
defer committer.Close()
71+
return committer.Commit()
72+
})
73+
74+
mockCommitter.Assert(t)
75+
})
76+
77+
t.Run("rollback and close", func(t *testing.T) {
78+
mockCommitter := NewMockCommitter("close", "close")
79+
80+
testWithCommitter(mockCommitter, func(committer Committer) error {
81+
defer committer.Close()
82+
if true {
83+
return fmt.Errorf("error")
84+
}
85+
return committer.Commit()
86+
})
87+
88+
mockCommitter.Assert(t)
89+
})
90+
91+
t.Run("close and commit", func(t *testing.T) {
92+
mockCommitter := NewMockCommitter("close", "close")
93+
94+
testWithCommitter(mockCommitter, func(committer Committer) error {
95+
committer.Close()
96+
committer.Commit()
97+
return fmt.Errorf("error")
98+
})
99+
100+
mockCommitter.Assert(t)
101+
})
102+
}

0 commit comments

Comments
 (0)