1// Copyright 2021 The Gitea Authors. All rights reserved. 2// Use of this source code is governed by a MIT-style 3// license that can be found in the LICENSE file. 4 5package db 6 7import ( 8 "errors" 9 "fmt" 10 11 "code.gitea.io/gitea/modules/setting" 12) 13 14// ResourceIndex represents a resource index which could be used as issue/release and others 15// We can create different tables i.e. issue_index, release_index and etc. 16type ResourceIndex struct { 17 GroupID int64 `xorm:"pk"` 18 MaxIndex int64 `xorm:"index"` 19} 20 21// UpsertResourceIndex the function will not return until it acquires the lock or receives an error. 22func UpsertResourceIndex(e Engine, tableName string, groupID int64) (err error) { 23 // An atomic UPSERT operation (INSERT/UPDATE) is the only operation 24 // that ensures that the key is actually locked. 25 switch { 26 case setting.Database.UseSQLite3 || setting.Database.UsePostgreSQL: 27 _, err = e.Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ 28 "VALUES (?,1) ON CONFLICT (group_id) DO UPDATE SET max_index = %s.max_index+1", 29 tableName, tableName), groupID) 30 case setting.Database.UseMySQL: 31 _, err = e.Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ 32 "VALUES (?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1", tableName), 33 groupID) 34 case setting.Database.UseMSSQL: 35 // https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/ 36 _, err = e.Exec(fmt.Sprintf("MERGE %s WITH (HOLDLOCK) as target "+ 37 "USING (SELECT ? AS group_id) AS src "+ 38 "ON src.group_id = target.group_id "+ 39 "WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 "+ 40 "WHEN NOT MATCHED THEN INSERT (group_id, max_index) "+ 41 "VALUES (src.group_id, 1);", tableName), 42 groupID) 43 default: 44 return fmt.Errorf("database type not supported") 45 } 46 return 47} 48 49var ( 50 // ErrResouceOutdated represents an error when request resource outdated 51 ErrResouceOutdated = errors.New("resource outdated") 52 // ErrGetResourceIndexFailed represents an error when resource index retries 3 times 53 ErrGetResourceIndexFailed = errors.New("get resource index failed") 54) 55 56const ( 57 // MaxDupIndexAttempts max retry times to create index 58 MaxDupIndexAttempts = 3 59) 60 61// GetNextResourceIndex retried 3 times to generate a resource index 62func GetNextResourceIndex(tableName string, groupID int64) (int64, error) { 63 for i := 0; i < MaxDupIndexAttempts; i++ { 64 idx, err := getNextResourceIndex(tableName, groupID) 65 if err == ErrResouceOutdated { 66 continue 67 } 68 if err != nil { 69 return 0, err 70 } 71 return idx, nil 72 } 73 return 0, ErrGetResourceIndexFailed 74} 75 76// DeleteResouceIndex delete resource index 77func DeleteResouceIndex(e Engine, tableName string, groupID int64) error { 78 _, err := e.Exec(fmt.Sprintf("DELETE FROM %s WHERE group_id=?", tableName), groupID) 79 return err 80} 81 82// getNextResourceIndex return the next index 83func getNextResourceIndex(tableName string, groupID int64) (int64, error) { 84 sess := x.NewSession() 85 defer sess.Close() 86 if err := sess.Begin(); err != nil { 87 return 0, err 88 } 89 var preIdx int64 90 _, err := sess.SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ?", tableName), groupID).Get(&preIdx) 91 if err != nil { 92 return 0, err 93 } 94 95 if err := UpsertResourceIndex(sess, tableName, groupID); err != nil { 96 return 0, err 97 } 98 99 var curIdx int64 100 has, err := sess.SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ? AND max_index=?", tableName), groupID, preIdx+1).Get(&curIdx) 101 if err != nil { 102 return 0, err 103 } 104 if !has { 105 return 0, ErrResouceOutdated 106 } 107 if err := sess.Commit(); err != nil { 108 return 0, err 109 } 110 return curIdx, nil 111} 112