@@ -8,45 +8,15 @@ import (
8
8
"context"
9
9
"errors"
10
10
"fmt"
11
-
12
- "code.gitea.io/gitea/modules/setting"
13
11
)
14
12
15
13
// ResourceIndex represents a resource index which could be used as issue/release and others
16
- // We can create different tables i.e. issue_index, release_index and etc.
14
+ // We can create different tables i.e. issue_index, release_index, etc.
17
15
type ResourceIndex struct {
18
16
GroupID int64 `xorm:"pk"`
19
17
MaxIndex int64 `xorm:"index"`
20
18
}
21
19
22
- // UpsertResourceIndex the function will not return until it acquires the lock or receives an error.
23
- func UpsertResourceIndex (ctx context.Context , tableName string , groupID int64 ) (err error ) {
24
- // An atomic UPSERT operation (INSERT/UPDATE) is the only operation
25
- // that ensures that the key is actually locked.
26
- switch {
27
- case setting .Database .UseSQLite3 || setting .Database .UsePostgreSQL :
28
- _ , err = Exec (ctx , fmt .Sprintf ("INSERT INTO %s (group_id, max_index) " +
29
- "VALUES (?,1) ON CONFLICT (group_id) DO UPDATE SET max_index = %s.max_index+1" ,
30
- tableName , tableName ), groupID )
31
- case setting .Database .UseMySQL :
32
- _ , err = Exec (ctx , fmt .Sprintf ("INSERT INTO %s (group_id, max_index) " +
33
- "VALUES (?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1" , tableName ),
34
- groupID )
35
- case setting .Database .UseMSSQL :
36
- // https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/
37
- _ , err = Exec (ctx , fmt .Sprintf ("MERGE %s WITH (HOLDLOCK) as target " +
38
- "USING (SELECT ? AS group_id) AS src " +
39
- "ON src.group_id = target.group_id " +
40
- "WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 " +
41
- "WHEN NOT MATCHED THEN INSERT (group_id, max_index) " +
42
- "VALUES (src.group_id, 1);" , tableName ),
43
- groupID )
44
- default :
45
- return fmt .Errorf ("database type not supported" )
46
- }
47
- return err
48
- }
49
-
50
20
var (
51
21
// ErrResouceOutdated represents an error when request resource outdated
52
22
ErrResouceOutdated = errors .New ("resource outdated" )
@@ -59,53 +29,89 @@ const (
59
29
MaxDupIndexAttempts = 3
60
30
)
61
31
62
- // GetNextResourceIndex retried 3 times to generate a resource index
63
- func GetNextResourceIndex (tableName string , groupID int64 ) (int64 , error ) {
64
- for i := 0 ; i < MaxDupIndexAttempts ; i ++ {
65
- idx , err := getNextResourceIndex (tableName , groupID )
66
- if err == ErrResouceOutdated {
67
- continue
32
+ // SyncMaxResourceIndex sync the max index with the resource
33
+ func SyncMaxResourceIndex (ctx context.Context , tableName string , groupID , maxIndex int64 ) (err error ) {
34
+ e := GetEngine (ctx )
35
+
36
+ // try to update the max_index and acquire the write-lock for the record
37
+ res , err := e .Exec (fmt .Sprintf ("UPDATE %s SET max_index=? WHERE group_id=? AND max_index<?" , tableName ), maxIndex , groupID , maxIndex )
38
+ if err != nil {
39
+ return err
40
+ }
41
+ affected , err := res .RowsAffected ()
42
+ if err != nil {
43
+ return err
44
+ }
45
+ if affected == 0 {
46
+ // if nothing is updated, the record might not exist, try to insert and update it
47
+ _ , errIns := e .Exec (fmt .Sprintf ("INSERT INTO %s (group_id, max_index) VALUES (?, 0)" , tableName ), groupID )
48
+ _ , err = e .Exec (fmt .Sprintf ("UPDATE %s SET max_index=? WHERE group_id=? AND max_index<?" , tableName ), maxIndex , groupID , maxIndex )
49
+ if err != nil {
50
+ return err
68
51
}
52
+ var savedIdx int64
53
+ has , err := e .SQL (fmt .Sprintf ("SELECT max_index FROM %s WHERE group_id=?" , tableName ), groupID ).Get (& savedIdx )
69
54
if err != nil {
70
- return 0 , err
55
+ return err
56
+ }
57
+ // if the record still doesn't exist, there must be some errors (insert error)
58
+ if ! has {
59
+ if errIns == nil {
60
+ return errors .New ("impossible error when SyncMaxResourceIndex, insert and update both succeeded but no record is saved" )
61
+ }
62
+ return errIns
71
63
}
72
- return idx , nil
73
64
}
74
- return 0 , ErrGetResourceIndexFailed
65
+ return nil
75
66
}
76
67
77
- // DeleteResouceIndex delete resource index
78
- func DeleteResouceIndex (ctx context.Context , tableName string , groupID int64 ) error {
79
- _ , err := Exec (ctx , fmt .Sprintf ("DELETE FROM %s WHERE group_id=?" , tableName ), groupID )
80
- return err
81
- }
68
+ // GetNextResourceIndex generates a resource index, it must run in the same transaction where the resource is created
69
+ func GetNextResourceIndex (ctx context.Context , tableName string , groupID int64 ) (int64 , error ) {
70
+ e := GetEngine (ctx )
82
71
83
- // getNextResourceIndex return the next index
84
- func getNextResourceIndex (tableName string , groupID int64 ) (int64 , error ) {
85
- ctx , commiter , err := TxContext ()
72
+ // try to update the max_index to next value, and acquire the write-lock for the record
73
+ res , err := e .Exec (fmt .Sprintf ("UPDATE %s SET max_index=max_index+1 WHERE group_id=?" , tableName ), groupID )
86
74
if err != nil {
87
75
return 0 , err
88
76
}
89
- defer commiter .Close ()
90
- var preIdx int64
91
- if _ , err := GetEngine (ctx ).SQL (fmt .Sprintf ("SELECT max_index FROM %s WHERE group_id = ?" , tableName ), groupID ).Get (& preIdx ); err != nil {
77
+ affected , err := res .RowsAffected ()
78
+ if err != nil {
92
79
return 0 , err
93
80
}
94
-
95
- if err := UpsertResourceIndex (ctx , tableName , groupID ); err != nil {
96
- return 0 , err
81
+ if affected == 0 {
82
+ // this slow path is only for the first time of creating a resource index
83
+ _ , errIns := e .Exec (fmt .Sprintf ("INSERT INTO %s (group_id, max_index) VALUES (?, 0)" , tableName ), groupID )
84
+ res , err = e .Exec (fmt .Sprintf ("UPDATE %s SET max_index=max_index+1 WHERE group_id=?" , tableName ), groupID )
85
+ if err != nil {
86
+ return 0 , err
87
+ }
88
+ affected , err = res .RowsAffected ()
89
+ if err != nil {
90
+ return 0 , err
91
+ }
92
+ // if the update still can not update any records, the record must not exist and there must be some errors (insert error)
93
+ if affected == 0 {
94
+ if errIns == nil {
95
+ return 0 , errors .New ("impossible error when GetNextResourceIndex, insert and update both succeeded but no record is updated" )
96
+ }
97
+ return 0 , errIns
98
+ }
97
99
}
98
100
99
- var curIdx int64
100
- has , err := GetEngine (ctx ).SQL (fmt .Sprintf ("SELECT max_index FROM %s WHERE group_id = ? AND max_index=?" , tableName ), groupID , preIdx + 1 ).Get (& curIdx )
101
+ // now, the new index is in database (protected by the transaction and write-lock)
102
+ var newIdx int64
103
+ has , err := e .SQL (fmt .Sprintf ("SELECT max_index FROM %s WHERE group_id=?" , tableName ), groupID ).Get (& newIdx )
101
104
if err != nil {
102
105
return 0 , err
103
106
}
104
107
if ! has {
105
- return 0 , ErrResouceOutdated
106
- }
107
- if err := commiter .Commit (); err != nil {
108
- return 0 , err
108
+ return 0 , errors .New ("impossible error when GetNextResourceIndex, upsert succeeded but no record can be selected" )
109
109
}
110
- return curIdx , nil
110
+ return newIdx , nil
111
+ }
112
+
113
+ // DeleteResourceIndex delete resource index
114
+ func DeleteResourceIndex (ctx context.Context , tableName string , groupID int64 ) error {
115
+ _ , err := Exec (ctx , fmt .Sprintf ("DELETE FROM %s WHERE group_id=?" , tableName ), groupID )
116
+ return err
111
117
}
0 commit comments