@@ -8,45 +8,15 @@ import (
88 "context"
99 "errors"
1010 "fmt"
11-
12- "code.gitea.io/gitea/modules/setting"
1311)
1412
1513// 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.
1715type ResourceIndex struct {
1816 GroupID int64 `xorm:"pk"`
1917 MaxIndex int64 `xorm:"index"`
2018}
2119
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-
5020var (
5121 // ErrResouceOutdated represents an error when request resource outdated
5222 ErrResouceOutdated = errors .New ("resource outdated" )
@@ -59,53 +29,85 @@ const (
5929 MaxDupIndexAttempts = 3
6030)
6131
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 )
6950 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
7159 }
72- return idx , nil
7360 }
74- return 0 , ErrGetResourceIndexFailed
61+ return nil
7562}
7663
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 )
8267
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 )
8670 if err != nil {
8771 return 0 , err
8872 }
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 {
9275 return 0 , err
9376 }
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+ }
9795 }
9896
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 )
101100 if err != nil {
102101 return 0 , err
103102 }
104103 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" )
109105 }
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
111113}
0 commit comments