Skip to content

Commit 71e7422

Browse files
committed
fix
1 parent d70af38 commit 71e7422

File tree

10 files changed

+118
-69
lines changed

10 files changed

+118
-69
lines changed

models/db/context.go

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44
package db
55

66
import (
7+
"bytes"
78
"context"
89
"database/sql"
10+
"errors"
11+
"runtime"
12+
13+
"code.gitea.io/gitea/modules/setting"
914

1015
"xorm.io/builder"
1116
"xorm.io/xorm"
@@ -15,62 +20,49 @@ import (
1520
// will be overwritten by Init with HammerContext
1621
var DefaultContext context.Context
1722

18-
// contextKey is a value for use with context.WithValue.
19-
type contextKey struct {
20-
name string
21-
}
23+
type engineContextKeyType struct{}
2224

23-
// enginedContextKey is a context key. It is used with context.Value() to get the current Engined for the context
24-
var (
25-
enginedContextKey = &contextKey{"engined"}
26-
_ Engined = &Context{}
27-
)
25+
var engineContextKey = engineContextKeyType{}
2826

2927
// Context represents a db context
3028
type Context struct {
3129
context.Context
32-
e Engine
33-
transaction bool
34-
}
35-
36-
func newContext(ctx context.Context, e Engine, transaction bool) *Context {
37-
return &Context{
38-
Context: ctx,
39-
e: e,
40-
transaction: transaction,
41-
}
42-
}
43-
44-
// InTransaction if context is in a transaction
45-
func (ctx *Context) InTransaction() bool {
46-
return ctx.transaction
30+
engine Engine
4731
}
4832

49-
// Engine returns db engine
50-
func (ctx *Context) Engine() Engine {
51-
return ctx.e
33+
func newContext(ctx context.Context, e Engine) *Context {
34+
return &Context{Context: ctx, engine: e}
5235
}
5336

5437
// Value shadows Value for context.Context but allows us to get ourselves and an Engined object
5538
func (ctx *Context) Value(key any) any {
56-
if key == enginedContextKey {
39+
if key == engineContextKey {
5740
return ctx
5841
}
5942
return ctx.Context.Value(key)
6043
}
6144

6245
// WithContext returns this engine tied to this context
6346
func (ctx *Context) WithContext(other context.Context) *Context {
64-
return newContext(ctx, ctx.e.Context(other), ctx.transaction)
47+
return newContext(ctx, ctx.engine.Context(other))
6548
}
6649

67-
// Engined structs provide an Engine
68-
type Engined interface {
69-
Engine() Engine
50+
func contextSafetyCheck() {
51+
if setting.IsProd {
52+
return
53+
}
54+
buf := make([]byte, 1024*100)
55+
runtime.Stack(buf, false)
56+
println(string(buf))
57+
if bytes.Contains(buf, []byte("models/db.Iterate[")) ||
58+
bytes.Contains(buf, []byte("xorm.io/xorm.(*Session).Iterate(")) {
59+
panic(errors.New("using context in an iterator would cause a corrupted results"))
60+
}
7061
}
7162

7263
// GetEngine will get a db Engine from this context or return an Engine restricted to this context
7364
func GetEngine(ctx context.Context) Engine {
65+
contextSafetyCheck()
7466
if e := getEngine(ctx); e != nil {
7567
return e
7668
}
@@ -79,12 +71,11 @@ func GetEngine(ctx context.Context) Engine {
7971

8072
// getEngine will get a db Engine from this context or return nil
8173
func getEngine(ctx context.Context) Engine {
82-
if engined, ok := ctx.(Engined); ok {
83-
return engined.Engine()
74+
if engined, ok := ctx.(*Context); ok {
75+
return engined.engine
8476
}
85-
enginedInterface := ctx.Value(enginedContextKey)
86-
if enginedInterface != nil {
87-
return enginedInterface.(Engined).Engine()
77+
if engined, ok := ctx.Value(engineContextKey).(*Context); ok {
78+
return engined.engine
8879
}
8980
return nil
9081
}
@@ -132,23 +123,23 @@ func (c *halfCommitter) Close() error {
132123
// d. It doesn't mean rollback is forbidden, but always do it only when there is an error, and you do want to rollback.
133124
func TxContext(parentCtx context.Context) (*Context, Committer, error) {
134125
if sess, ok := inTransaction(parentCtx); ok {
135-
return newContext(parentCtx, sess, true), &halfCommitter{committer: sess}, nil
126+
return newContext(parentCtx, sess), &halfCommitter{committer: sess}, nil
136127
}
137128

138129
sess := x.NewSession()
139130
if err := sess.Begin(); err != nil {
140-
sess.Close()
131+
_ = sess.Close()
141132
return nil, nil, err
142133
}
143134

144-
return newContext(DefaultContext, sess, true), sess, nil
135+
return newContext(DefaultContext, sess), sess, nil
145136
}
146137

147138
// WithTx represents executing database operations on a transaction, if the transaction exist,
148139
// this function will reuse it otherwise will create a new one and close it when finished.
149140
func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error {
150141
if sess, ok := inTransaction(parentCtx); ok {
151-
err := f(newContext(parentCtx, sess, true))
142+
err := f(newContext(parentCtx, sess))
152143
if err != nil {
153144
// rollback immediately, in case the caller ignores returned error and tries to commit the transaction.
154145
_ = sess.Close()
@@ -165,7 +156,7 @@ func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error)
165156
return err
166157
}
167158

168-
if err := f(newContext(parentCtx, sess, true)); err != nil {
159+
if err := f(newContext(parentCtx, sess)); err != nil {
169160
return err
170161
}
171162

models/db/context_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
"code.gitea.io/gitea/models/db"
1111
"code.gitea.io/gitea/models/unittest"
12+
"code.gitea.io/gitea/modules/setting"
13+
"code.gitea.io/gitea/modules/test"
1214

1315
"github.com/stretchr/testify/assert"
1416
)
@@ -84,3 +86,25 @@ func TestTxContext(t *testing.T) {
8486
}))
8587
}
8688
}
89+
90+
func TestContextSafety(t *testing.T) {
91+
defer test.MockVariableValue(&setting.IsProd, false)()
92+
type TestModel struct {
93+
ID int64
94+
}
95+
assert.NoError(t, unittest.GetXORMEngine().Sync(&TestModel{}))
96+
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &TestModel{}))
97+
assert.NoError(t, db.Insert(db.DefaultContext, &TestModel{ID: 1}))
98+
assert.PanicsWithError(t, "using context in an iterator would cause a corrupted results", func() {
99+
_ = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, m *TestModel) error {
100+
_ = db.GetEngine(ctx)
101+
return nil
102+
})
103+
})
104+
assert.PanicsWithError(t, "using context in an iterator would cause a corrupted results", func() {
105+
_ = unittest.GetXORMEngine().Iterate(&TestModel{}, func(i int, bean any) error {
106+
_ = db.GetEngine(db.DefaultContext)
107+
return nil
108+
})
109+
})
110+
}

models/db/engine.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,7 @@ func InitEngine(ctx context.Context) error {
161161
// SetDefaultEngine sets the default engine for db
162162
func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
163163
x = eng
164-
DefaultContext = &Context{
165-
Context: ctx,
166-
e: x,
167-
}
164+
DefaultContext = &Context{Context: ctx, engine: x}
168165
}
169166

170167
// UnsetDefaultEngine closes and unsets the default engine

models/db/install/db.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
)
1212

1313
func getXORMEngine() *xorm.Engine {
14-
return db.DefaultContext.(*db.Context).Engine().(*xorm.Engine)
14+
return db.GetEngine(db.DefaultContext).(*xorm.Engine)
1515
}
1616

1717
// CheckDatabaseConnection checks the database connection

models/db/iterate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"xorm.io/builder"
1212
)
1313

14-
// Iterate iterate all the Bean object
14+
// Iterate iterates all the Bean object
1515
func Iterate[Bean any](ctx context.Context, cond builder.Cond, f func(ctx context.Context, bean *Bean) error) error {
1616
var start int
1717
batchSize := setting.Database.IterateBufferSize

models/packages/debian/search.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,26 +75,27 @@ func ExistPackages(ctx context.Context, opts *PackageSearchOptions) (bool, error
7575
}
7676

7777
// SearchPackages gets the packages matching the search options
78-
func SearchPackages(ctx context.Context, opts *PackageSearchOptions, iter func(*packages.PackageFileDescriptor)) error {
79-
return db.GetEngine(ctx).
78+
func SearchPackages(ctx context.Context, opts *PackageSearchOptions) ([]*packages.PackageFileDescriptor, error) {
79+
var pkgFiles []*packages.PackageFile
80+
err := db.GetEngine(ctx).
8081
Table("package_file").
8182
Select("package_file.*").
8283
Join("INNER", "package_version", "package_version.id = package_file.version_id").
8384
Join("INNER", "package", "package.id = package_version.package_id").
8485
Where(opts.toCond()).
85-
Asc("package.lower_name", "package_version.created_unix").
86-
Iterate(new(packages.PackageFile), func(_ int, bean any) error {
87-
pf := bean.(*packages.PackageFile)
88-
89-
pfd, err := packages.GetPackageFileDescriptor(ctx, pf)
90-
if err != nil {
91-
return err
92-
}
93-
94-
iter(pfd)
95-
96-
return nil
97-
})
86+
Asc("package.lower_name", "package_version.created_unix").Find(&pkgFiles)
87+
if err != nil {
88+
return nil, err
89+
}
90+
pfds := make([]*packages.PackageFileDescriptor, 0, len(pkgFiles))
91+
for _, pf := range pkgFiles {
92+
pfd, err := packages.GetPackageFileDescriptor(ctx, pf)
93+
if err != nil {
94+
return nil, err
95+
}
96+
pfds = append(pfds, pfd)
97+
}
98+
return pfds, nil
9899
}
99100

100101
// GetDistributions gets all available distributions

models/unittest/fixtures.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) {
2525
if len(engine) == 1 {
2626
return engine[0]
2727
}
28-
return db.DefaultContext.(*db.Context).Engine().(*xorm.Engine)
28+
return db.GetEngine(db.DefaultContext).(*xorm.Engine)
2929
}
3030

3131
// InitFixtures initialize test fixtures for a test database

services/packages/cleanup/cleanup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
rpm_service "code.gitea.io/gitea/services/packages/rpm"
2323
)
2424

25-
// Task method to execute cleanup rules and cleanup expired package data
25+
// CleanupTask executes cleanup rules and cleanup expired package data
2626
func CleanupTask(ctx context.Context, olderThan time.Duration) error {
2727
if err := ExecuteCleanupRules(ctx); err != nil {
2828
return err

services/packages/debian/repository.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,11 @@ func buildPackagesIndices(ctx context.Context, ownerID int64, repoVersion *packa
206206
w := io.MultiWriter(packagesContent, gzw, xzw)
207207

208208
addSeparator := false
209-
if err := debian_model.SearchPackages(ctx, opts, func(pfd *packages_model.PackageFileDescriptor) {
209+
pfds, err := debian_model.SearchPackages(ctx, opts)
210+
if err != nil {
211+
return err
212+
}
213+
for _, pfd := range pfds {
210214
if addSeparator {
211215
fmt.Fprintln(w)
212216
}
@@ -220,10 +224,7 @@ func buildPackagesIndices(ctx context.Context, ownerID int64, repoVersion *packa
220224
fmt.Fprintf(w, "SHA1: %s\n", pfd.Blob.HashSHA1)
221225
fmt.Fprintf(w, "SHA256: %s\n", pfd.Blob.HashSHA256)
222226
fmt.Fprintf(w, "SHA512: %s\n", pfd.Blob.HashSHA512)
223-
}); err != nil {
224-
return err
225227
}
226-
227228
gzw.Close()
228229
xzw.Close()
229230

tests/integration/api_packages_debian_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"io"
1212
"net/http"
13+
"strconv"
1314
"strings"
1415
"testing"
1516

@@ -19,6 +20,7 @@ import (
1920
user_model "code.gitea.io/gitea/models/user"
2021
"code.gitea.io/gitea/modules/base"
2122
debian_module "code.gitea.io/gitea/modules/packages/debian"
23+
packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
2224
"code.gitea.io/gitea/tests"
2325

2426
"github.com/blakesmith/ar"
@@ -263,4 +265,37 @@ func TestPackageDebian(t *testing.T) {
263265
assert.Contains(t, body, "Components: "+strings.Join(components, " ")+"\n")
264266
assert.Contains(t, body, "Architectures: "+architectures[1]+"\n")
265267
})
268+
269+
t.Run("Cleanup", func(t *testing.T) {
270+
defer tests.PrintCurrentTest(t)()
271+
272+
rule := &packages.PackageCleanupRule{
273+
Enabled: true,
274+
RemovePattern: `.*`,
275+
MatchFullName: true,
276+
OwnerID: user.ID,
277+
Type: packages.TypeDebian,
278+
}
279+
280+
_, err := packages.InsertCleanupRule(db.DefaultContext, rule)
281+
assert.NoError(t, err)
282+
283+
// when there are a lot of packages (> 50 or 100), when the code use "Iterate" to get all packages, it cause bugs
284+
// because "Iterate" keeps a dangling SQL session but the callback function still uses the same session to execute statements.
285+
// The "Iterate" problem has been checked by TestContextSafety now, so here we only need to check the cleanup logic with a small number
286+
packagesCount := 2
287+
for i := 0; i < packagesCount; i++ {
288+
uploadURL := fmt.Sprintf("%s/pool/%s/%s/upload", rootURL, "test", "main")
289+
req := NewRequestWithBody(t, "PUT", uploadURL, createArchive(packageName, "1.0."+strconv.Itoa(i), "all")).AddBasicAuth(user.Name)
290+
MakeRequest(t, req, http.StatusCreated)
291+
}
292+
req := NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release", rootURL, "test"))
293+
MakeRequest(t, req, http.StatusOK)
294+
295+
err = packages_cleanup_service.CleanupTask(db.DefaultContext, 0)
296+
assert.NoError(t, err)
297+
298+
req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release", rootURL, "test"))
299+
MakeRequest(t, req, http.StatusNotFound)
300+
})
266301
}

0 commit comments

Comments
 (0)