@@ -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,85 @@ 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
68
- }
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 or might be larger, it's safe to try to insert it again and then check whether the record exists
47
+ _ , errIns := e .Exec (fmt .Sprintf ("INSERT INTO %s (group_id, max_index) VALUES (?, ?)" , tableName ), groupID , maxIndex )
48
+ var savedIdx int64
49
+ has , err := e .SQL (fmt .Sprintf ("SELECT max_index FROM %s WHERE group_id=?" , tableName ), groupID ).Get (& savedIdx )
69
50
if err != nil {
70
- return 0 , err
51
+ return err
52
+ }
53
+ // if the record still doesn't exist, there must be some errors (insert error)
54
+ if ! has {
55
+ if errIns == nil {
56
+ return errors .New ("impossible error when SyncMaxResourceIndex, insert succeeded but no record is saved" )
57
+ }
58
+ return errIns
71
59
}
72
- return idx , nil
73
60
}
74
- return 0 , ErrGetResourceIndexFailed
61
+ return nil
75
62
}
76
63
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
- }
64
+ // GetNextResourceIndex generates a resource index, it must run in the same transaction where the resource is created
65
+ func GetNextResourceIndex (ctx context.Context , tableName string , groupID int64 ) (int64 , error ) {
66
+ e := GetEngine (ctx )
82
67
83
- // getNextResourceIndex return the next index
84
- func getNextResourceIndex (tableName string , groupID int64 ) (int64 , error ) {
85
- ctx , commiter , err := TxContext ()
68
+ // try to update the max_index to next value, and acquire the write-lock for the record
69
+ res , err := e .Exec (fmt .Sprintf ("UPDATE %s SET max_index=max_index+1 WHERE group_id=?" , tableName ), groupID )
86
70
if err != nil {
87
71
return 0 , err
88
72
}
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 {
73
+ affected , err := res .RowsAffected ()
74
+ if err != nil {
92
75
return 0 , err
93
76
}
94
-
95
- if err := UpsertResourceIndex (ctx , tableName , groupID ); err != nil {
96
- return 0 , err
77
+ if affected == 0 {
78
+ // this slow path is only for the first time of creating a resource index
79
+ _ , errIns := e .Exec (fmt .Sprintf ("INSERT INTO %s (group_id, max_index) VALUES (?, 0)" , tableName ), groupID )
80
+ res , err = e .Exec (fmt .Sprintf ("UPDATE %s SET max_index=max_index+1 WHERE group_id=?" , tableName ), groupID )
81
+ if err != nil {
82
+ return 0 , err
83
+ }
84
+ affected , err = res .RowsAffected ()
85
+ if err != nil {
86
+ return 0 , err
87
+ }
88
+ // if the update still can not update any records, the record must not exist and there must be some errors (insert error)
89
+ if affected == 0 {
90
+ if errIns == nil {
91
+ return 0 , errors .New ("impossible error when GetNextResourceIndex, insert and update both succeeded but no record is updated" )
92
+ }
93
+ return 0 , errIns
94
+ }
97
95
}
98
96
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 )
97
+ // now, the new index is in database (protected by the transaction and write-lock)
98
+ var newIdx int64
99
+ has , err := e .SQL (fmt .Sprintf ("SELECT max_index FROM %s WHERE group_id=?" , tableName ), groupID ).Get (& newIdx )
101
100
if err != nil {
102
101
return 0 , err
103
102
}
104
103
if ! has {
105
- return 0 , ErrResouceOutdated
106
- }
107
- if err := commiter .Commit (); err != nil {
108
- return 0 , err
104
+ return 0 , errors .New ("impossible error when GetNextResourceIndex, upsert succeeded but no record can be selected" )
109
105
}
110
- return curIdx , nil
106
+ return newIdx , nil
107
+ }
108
+
109
+ // DeleteResourceIndex delete resource index
110
+ func DeleteResourceIndex (ctx context.Context , tableName string , groupID int64 ) error {
111
+ _ , err := Exec (ctx , fmt .Sprintf ("DELETE FROM %s WHERE group_id=?" , tableName ), groupID )
112
+ return err
111
113
}
0 commit comments