Skip to content

Commit b2c4870

Browse files
lunnylafriks
andauthored
Fix parallel creating commit status bug with tests (#21911)
This PR is a follow up of #21469 Co-authored-by: Lauris BH <[email protected]>
1 parent 67881ae commit b2c4870

File tree

3 files changed

+67
-70
lines changed

3 files changed

+67
-70
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()

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)