code.gitea.io/gitea@v1.22.3/models/db/index.go (about) 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package db 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "strconv" 11 12 "code.gitea.io/gitea/modules/setting" 13 ) 14 15 // 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, etc. 17 type ResourceIndex struct { 18 GroupID int64 `xorm:"pk"` 19 MaxIndex int64 `xorm:"index"` 20 } 21 22 var ( 23 // ErrResouceOutdated represents an error when request resource outdated 24 ErrResouceOutdated = errors.New("resource outdated") 25 // ErrGetResourceIndexFailed represents an error when resource index retries 3 times 26 ErrGetResourceIndexFailed = errors.New("get resource index failed") 27 ) 28 29 // SyncMaxResourceIndex sync the max index with the resource 30 func SyncMaxResourceIndex(ctx context.Context, tableName string, groupID, maxIndex int64) (err error) { 31 e := GetEngine(ctx) 32 33 // try to update the max_index and acquire the write-lock for the record 34 res, err := e.Exec(fmt.Sprintf("UPDATE %s SET max_index=? WHERE group_id=? AND max_index<?", tableName), maxIndex, groupID, maxIndex) 35 if err != nil { 36 return err 37 } 38 affected, err := res.RowsAffected() 39 if err != nil { 40 return err 41 } 42 if affected == 0 { 43 // 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 44 _, errIns := e.Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) VALUES (?, ?)", tableName), groupID, maxIndex) 45 var savedIdx int64 46 has, err := e.SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id=?", tableName), groupID).Get(&savedIdx) 47 if err != nil { 48 return err 49 } 50 // if the record still doesn't exist, there must be some errors (insert error) 51 if !has { 52 if errIns == nil { 53 return errors.New("impossible error when SyncMaxResourceIndex, insert succeeded but no record is saved") 54 } 55 return errIns 56 } 57 } 58 return nil 59 } 60 61 func postgresGetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) { 62 res, err := GetEngine(ctx).Query(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ 63 "VALUES (?,1) ON CONFLICT (group_id) DO UPDATE SET max_index = %s.max_index+1 RETURNING max_index", 64 tableName, tableName), groupID) 65 if err != nil { 66 return 0, err 67 } 68 if len(res) == 0 { 69 return 0, ErrGetResourceIndexFailed 70 } 71 return strconv.ParseInt(string(res[0]["max_index"]), 10, 64) 72 } 73 74 func mysqlGetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) { 75 if _, err := GetEngine(ctx).Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ 76 "VALUES (?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1", 77 tableName), groupID); err != nil { 78 return 0, err 79 } 80 81 var idx int64 82 _, err := GetEngine(ctx).SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ?", tableName), groupID).Get(&idx) 83 if err != nil { 84 return 0, err 85 } 86 if idx == 0 { 87 return 0, errors.New("cannot get the correct index") 88 } 89 return idx, nil 90 } 91 92 func mssqlGetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) { 93 if _, err := GetEngine(ctx).Exec(fmt.Sprintf(` 94 MERGE INTO %s WITH (HOLDLOCK) AS target 95 USING (SELECT %d AS group_id) AS source 96 (group_id) 97 ON target.group_id = source.group_id 98 WHEN MATCHED 99 THEN UPDATE 100 SET max_index = max_index + 1 101 WHEN NOT MATCHED 102 THEN INSERT (group_id, max_index) 103 VALUES (%d, 1); 104 `, tableName, groupID, groupID)); err != nil { 105 return 0, err 106 } 107 108 var idx int64 109 _, err := GetEngine(ctx).SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ?", tableName), groupID).Get(&idx) 110 if err != nil { 111 return 0, err 112 } 113 if idx == 0 { 114 return 0, errors.New("cannot get the correct index") 115 } 116 return idx, nil 117 } 118 119 // GetNextResourceIndex generates a resource index, it must run in the same transaction where the resource is created 120 func GetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) { 121 switch { 122 case setting.Database.Type.IsPostgreSQL(): 123 return postgresGetNextResourceIndex(ctx, tableName, groupID) 124 case setting.Database.Type.IsMySQL(): 125 return mysqlGetNextResourceIndex(ctx, tableName, groupID) 126 case setting.Database.Type.IsMSSQL(): 127 return mssqlGetNextResourceIndex(ctx, tableName, groupID) 128 } 129 130 e := GetEngine(ctx) 131 132 // try to update the max_index to next value, and acquire the write-lock for the record 133 res, err := e.Exec(fmt.Sprintf("UPDATE %s SET max_index=max_index+1 WHERE group_id=?", tableName), groupID) 134 if err != nil { 135 return 0, err 136 } 137 affected, err := res.RowsAffected() 138 if err != nil { 139 return 0, err 140 } 141 if affected == 0 { 142 // this slow path is only for the first time of creating a resource index 143 _, errIns := e.Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) VALUES (?, 0)", tableName), groupID) 144 res, err = e.Exec(fmt.Sprintf("UPDATE %s SET max_index=max_index+1 WHERE group_id=?", tableName), groupID) 145 if err != nil { 146 return 0, err 147 } 148 affected, err = res.RowsAffected() 149 if err != nil { 150 return 0, err 151 } 152 // if the update still can not update any records, the record must not exist and there must be some errors (insert error) 153 if affected == 0 { 154 if errIns == nil { 155 return 0, errors.New("impossible error when GetNextResourceIndex, insert and update both succeeded but no record is updated") 156 } 157 return 0, errIns 158 } 159 } 160 161 // now, the new index is in database (protected by the transaction and write-lock) 162 var newIdx int64 163 has, err := e.SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id=?", tableName), groupID).Get(&newIdx) 164 if err != nil { 165 return 0, err 166 } 167 if !has { 168 return 0, errors.New("impossible error when GetNextResourceIndex, upsert succeeded but no record can be selected") 169 } 170 return newIdx, nil 171 } 172 173 // DeleteResourceIndex delete resource index 174 func DeleteResourceIndex(ctx context.Context, tableName string, groupID int64) error { 175 _, err := Exec(ctx, fmt.Sprintf("DELETE FROM %s WHERE group_id=?", tableName), groupID) 176 return err 177 }