Skip to content

Commit bab5cd1

Browse files
authored
Merge branch 'main' into lunny/more_refactor
2 parents 91f25d1 + 4e5d4d0 commit bab5cd1

File tree

6 files changed

+179
-96
lines changed

6 files changed

+179
-96
lines changed

models/db/index.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ var (
2323
ErrGetResourceIndexFailed = errors.New("get resource index failed")
2424
)
2525

26-
const (
27-
// MaxDupIndexAttempts max retry times to create index
28-
MaxDupIndexAttempts = 3
29-
)
30-
3126
// SyncMaxResourceIndex sync the max index with the resource
3227
func SyncMaxResourceIndex(ctx context.Context, tableName string, groupID, maxIndex int64) (err error) {
3328
e := GetEngine(ctx)

models/git/commit_status.go

Lines changed: 36 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package git
66
import (
77
"context"
88
"crypto/sha1"
9+
"errors"
910
"fmt"
1011
"net/url"
1112
"strings"
@@ -48,79 +49,49 @@ func init() {
4849
db.RegisterModel(new(CommitStatusIndex))
4950
}
5051

51-
// upsertCommitStatusIndex the function will not return until it acquires the lock or receives an error.
52-
func upsertCommitStatusIndex(ctx context.Context, repoID int64, sha string) (err error) {
53-
// An atomic UPSERT operation (INSERT/UPDATE) is the only operation
54-
// that ensures that the key is actually locked.
55-
switch {
56-
case setting.Database.UseSQLite3 || setting.Database.UsePostgreSQL:
57-
_, err = db.Exec(ctx, "INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+
58-
"VALUES (?,?,1) ON CONFLICT (repo_id,sha) DO UPDATE SET max_index = `commit_status_index`.max_index+1",
59-
repoID, sha)
60-
case setting.Database.UseMySQL:
61-
_, err = db.Exec(ctx, "INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+
62-
"VALUES (?,?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1",
63-
repoID, sha)
64-
case setting.Database.UseMSSQL:
65-
// https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/
66-
_, err = db.Exec(ctx, "MERGE `commit_status_index` WITH (HOLDLOCK) as target "+
67-
"USING (SELECT ? AS repo_id, ? AS sha) AS src "+
68-
"ON src.repo_id = target.repo_id AND src.sha = target.sha "+
69-
"WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 "+
70-
"WHEN NOT MATCHED THEN INSERT (repo_id, sha, max_index) "+
71-
"VALUES (src.repo_id, src.sha, 1);",
72-
repoID, sha)
73-
default:
74-
return fmt.Errorf("database type not supported")
75-
}
76-
return err
77-
}
78-
7952
// GetNextCommitStatusIndex retried 3 times to generate a resource index
80-
func GetNextCommitStatusIndex(repoID int64, sha string) (int64, error) {
81-
for i := 0; i < db.MaxDupIndexAttempts; i++ {
82-
idx, err := getNextCommitStatusIndex(repoID, sha)
83-
if err == db.ErrResouceOutdated {
84-
continue
85-
}
86-
if err != nil {
87-
return 0, err
88-
}
89-
return idx, nil
90-
}
91-
return 0, db.ErrGetResourceIndexFailed
92-
}
53+
func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
54+
e := db.GetEngine(ctx)
9355

94-
// getNextCommitStatusIndex return the next index
95-
func getNextCommitStatusIndex(repoID int64, sha string) (int64, error) {
96-
ctx, commiter, err := db.TxContext(db.DefaultContext)
56+
// try to update the max_index to next value, and acquire the write-lock for the record
57+
res, err := e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha)
9758
if err != nil {
9859
return 0, err
9960
}
100-
defer commiter.Close()
101-
102-
var preIdx int64
103-
_, err = db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?", repoID, sha).Get(&preIdx)
61+
affected, err := res.RowsAffected()
10462
if err != nil {
10563
return 0, err
10664
}
107-
108-
if err := upsertCommitStatusIndex(ctx, repoID, sha); err != nil {
109-
return 0, err
65+
if affected == 0 {
66+
// this slow path is only for the first time of creating a resource index
67+
_, errIns := e.Exec("INSERT INTO `commit_status_index` (repo_id, sha, max_index) VALUES (?, ?, 0)", repoID, sha)
68+
res, err = e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha)
69+
if err != nil {
70+
return 0, err
71+
}
72+
affected, err = res.RowsAffected()
73+
if err != nil {
74+
return 0, err
75+
}
76+
// if the update still can not update any records, the record must not exist and there must be some errors (insert error)
77+
if affected == 0 {
78+
if errIns == nil {
79+
return 0, errors.New("impossible error when GetNextCommitStatusIndex, insert and update both succeeded but no record is updated")
80+
}
81+
return 0, errIns
82+
}
11083
}
11184

112-
var curIdx int64
113-
has, err := db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ? AND max_index=?", repoID, sha, preIdx+1).Get(&curIdx)
85+
// now, the new index is in database (protected by the transaction and write-lock)
86+
var newIdx int64
87+
has, err := e.SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id=? AND sha=?", repoID, sha).Get(&newIdx)
11488
if err != nil {
11589
return 0, err
11690
}
11791
if !has {
118-
return 0, db.ErrResouceOutdated
92+
return 0, errors.New("impossible error when GetNextCommitStatusIndex, upsert succeeded but no record can be selected")
11993
}
120-
if err := commiter.Commit(); err != nil {
121-
return 0, err
122-
}
123-
return curIdx, nil
94+
return newIdx, nil
12495
}
12596

12697
func (status *CommitStatus) loadAttributes(ctx context.Context) (err error) {
@@ -290,18 +261,18 @@ func NewCommitStatus(opts NewCommitStatusOptions) error {
290261
return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA)
291262
}
292263

293-
// Get the next Status Index
294-
idx, err := GetNextCommitStatusIndex(opts.Repo.ID, opts.SHA)
295-
if err != nil {
296-
return fmt.Errorf("generate commit status index failed: %w", err)
297-
}
298-
299264
ctx, committer, err := db.TxContext(db.DefaultContext)
300265
if err != nil {
301266
return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", opts.Repo.ID, opts.Creator.ID, opts.SHA, err)
302267
}
303268
defer committer.Close()
304269

270+
// Get the next Status Index
271+
idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA)
272+
if err != nil {
273+
return fmt.Errorf("generate commit status index failed: %w", err)
274+
}
275+
305276
opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description)
306277
opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context)
307278
opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL)
@@ -315,7 +286,7 @@ func NewCommitStatus(opts NewCommitStatusOptions) error {
315286

316287
// Insert new CommitStatus
317288
if _, err = db.GetEngine(ctx).Insert(opts.CommitStatus); err != nil {
318-
return fmt.Errorf("Insert CommitStatus[%s, %s]: %w", repoPath, opts.SHA, err)
289+
return fmt.Errorf("insert CommitStatus[%s, %s]: %w", repoPath, opts.SHA, err)
319290
}
320291

321292
return committer.Commit()

modules/storage/helper.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
package storage
55

66
import (
7+
"fmt"
8+
"io"
9+
"net/url"
10+
"os"
711
"reflect"
812

913
"code.gitea.io/gitea/modules/json"
@@ -61,3 +65,31 @@ func toConfig(exemplar, cfg interface{}) (interface{}, error) {
6165
}
6266
return newVal.Elem().Interface(), nil
6367
}
68+
69+
var uninitializedStorage = discardStorage("uninitialized storage")
70+
71+
type discardStorage string
72+
73+
func (s discardStorage) Open(_ string) (Object, error) {
74+
return nil, fmt.Errorf("%s", s)
75+
}
76+
77+
func (s discardStorage) Save(_ string, _ io.Reader, _ int64) (int64, error) {
78+
return 0, fmt.Errorf("%s", s)
79+
}
80+
81+
func (s discardStorage) Stat(_ string) (os.FileInfo, error) {
82+
return nil, fmt.Errorf("%s", s)
83+
}
84+
85+
func (s discardStorage) Delete(_ string) error {
86+
return fmt.Errorf("%s", s)
87+
}
88+
89+
func (s discardStorage) URL(_, _ string) (*url.URL, error) {
90+
return nil, fmt.Errorf("%s", s)
91+
}
92+
93+
func (s discardStorage) IterateObjects(_ func(string, Object) error) error {
94+
return fmt.Errorf("%s", s)
95+
}

modules/storage/helper_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package storage
5+
6+
import (
7+
"bytes"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_discardStorage(t *testing.T) {
14+
tests := []discardStorage{
15+
uninitializedStorage,
16+
discardStorage("empty"),
17+
}
18+
for _, tt := range tests {
19+
t.Run(string(tt), func(t *testing.T) {
20+
{
21+
got, err := tt.Open("path")
22+
assert.Nil(t, got)
23+
assert.Error(t, err, string(tt))
24+
}
25+
{
26+
got, err := tt.Save("path", bytes.NewReader([]byte{0}), 1)
27+
assert.Equal(t, int64(0), got)
28+
assert.Error(t, err, string(tt))
29+
}
30+
{
31+
got, err := tt.Stat("path")
32+
assert.Nil(t, got)
33+
assert.Error(t, err, string(tt))
34+
}
35+
{
36+
err := tt.Delete("path")
37+
assert.Error(t, err, string(tt))
38+
}
39+
{
40+
got, err := tt.URL("path", "name")
41+
assert.Nil(t, got)
42+
assert.Errorf(t, err, string(tt))
43+
}
44+
{
45+
err := tt.IterateObjects(func(_ string, _ Object) error { return nil })
46+
assert.Error(t, err, string(tt))
47+
}
48+
})
49+
}
50+
}

modules/storage/storage.go

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -110,46 +110,38 @@ func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) err
110110

111111
var (
112112
// Attachments represents attachments storage
113-
Attachments ObjectStorage
113+
Attachments ObjectStorage = uninitializedStorage
114114

115115
// LFS represents lfs storage
116-
LFS ObjectStorage
116+
LFS ObjectStorage = uninitializedStorage
117117

118118
// Avatars represents user avatars storage
119-
Avatars ObjectStorage
119+
Avatars ObjectStorage = uninitializedStorage
120120
// RepoAvatars represents repository avatars storage
121-
RepoAvatars ObjectStorage
121+
RepoAvatars ObjectStorage = uninitializedStorage
122122

123123
// RepoArchives represents repository archives storage
124-
RepoArchives ObjectStorage
124+
RepoArchives ObjectStorage = uninitializedStorage
125125

126126
// Packages represents packages storage
127-
Packages ObjectStorage
127+
Packages ObjectStorage = uninitializedStorage
128128
)
129129

130130
// Init init the stoarge
131131
func Init() error {
132-
if err := initAttachments(); err != nil {
133-
return err
134-
}
135-
136-
if err := initAvatars(); err != nil {
137-
return err
138-
}
139-
140-
if err := initRepoAvatars(); err != nil {
141-
return err
142-
}
143-
144-
if err := initLFS(); err != nil {
145-
return err
146-
}
147-
148-
if err := initRepoArchives(); err != nil {
149-
return err
132+
for _, f := range []func() error{
133+
initAttachments,
134+
initAvatars,
135+
initRepoAvatars,
136+
initLFS,
137+
initRepoArchives,
138+
initPackages,
139+
} {
140+
if err := f(); err != nil {
141+
return err
142+
}
150143
}
151-
152-
return initPackages()
144+
return nil
153145
}
154146

155147
// NewStorage takes a storage type and some config and returns an ObjectStorage or an error
@@ -172,12 +164,20 @@ func initAvatars() (err error) {
172164
}
173165

174166
func initAttachments() (err error) {
167+
if !setting.Attachment.Enabled {
168+
Attachments = discardStorage("Attachment isn't enabled")
169+
return nil
170+
}
175171
log.Info("Initialising Attachment storage with type: %s", setting.Attachment.Storage.Type)
176172
Attachments, err = NewStorage(setting.Attachment.Storage.Type, &setting.Attachment.Storage)
177173
return err
178174
}
179175

180176
func initLFS() (err error) {
177+
if !setting.LFS.StartServer {
178+
LFS = discardStorage("LFS isn't enabled")
179+
return nil
180+
}
181181
log.Info("Initialising LFS storage with type: %s", setting.LFS.Storage.Type)
182182
LFS, err = NewStorage(setting.LFS.Storage.Type, &setting.LFS.Storage)
183183
return err
@@ -196,6 +196,10 @@ func initRepoArchives() (err error) {
196196
}
197197

198198
func initPackages() (err error) {
199+
if !setting.Packages.Enabled {
200+
Packages = discardStorage("Packages isn't enabled")
201+
return nil
202+
}
199203
log.Info("Initialising Packages storage with type: %s", setting.Packages.Storage.Type)
200204
Packages, err = NewStorage(setting.Packages.Storage.Type, &setting.Packages.Storage)
201205
return err

tests/integration/repo_commits_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
package integration
55

66
import (
7+
"fmt"
78
"net/http"
89
"net/http/httptest"
910
"path"
11+
"sync"
1012
"testing"
1113

1214
"code.gitea.io/gitea/modules/json"
@@ -114,3 +116,32 @@ func TestRepoCommitsWithStatusFailure(t *testing.T) {
114116
func TestRepoCommitsWithStatusWarning(t *testing.T) {
115117
doTestRepoCommitWithStatus(t, "warning", "gitea-exclamation", "yellow")
116118
}
119+
120+
func TestRepoCommitsStatusParallel(t *testing.T) {
121+
defer tests.PrepareTestEnv(t)()
122+
123+
session := loginUser(t, "user2")
124+
125+
// Request repository commits page
126+
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
127+
resp := session.MakeRequest(t, req, http.StatusOK)
128+
129+
doc := NewHTMLParser(t, resp.Body)
130+
// Get first commit URL
131+
commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href")
132+
assert.True(t, exists)
133+
assert.NotEmpty(t, commitURL)
134+
135+
var wg sync.WaitGroup
136+
for i := 0; i < 10; i++ {
137+
wg.Add(1)
138+
go func(t *testing.T, i int) {
139+
t.Run(fmt.Sprintf("ParallelCreateStatus_%d", i), func(t *testing.T) {
140+
runBody := doAPICreateCommitStatus(NewAPITestContext(t, "user2", "repo1"), path.Base(commitURL), api.CommitStatusState("pending"))
141+
runBody(t)
142+
wg.Done()
143+
})
144+
}(t, i)
145+
}
146+
wg.Wait()
147+
}

0 commit comments

Comments
 (0)