2021-06-14 05:22:55 +03:00
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"errors"
"fmt"
"code.gitea.io/gitea/modules/setting"
)
// ResourceIndex represents a resource index which could be used as issue/release and others
// We can create different tables i.e. issue_index, release_index and etc.
type ResourceIndex struct {
2021-08-25 11:42:51 +03:00
GroupID int64 ` xorm:"pk" `
2021-06-14 05:22:55 +03:00
MaxIndex int64 ` xorm:"index" `
}
// IssueIndex represents the issue index table
type IssueIndex ResourceIndex
// upsertResourceIndex the function will not return until it acquires the lock or receives an error.
func upsertResourceIndex ( e Engine , tableName string , groupID int64 ) ( err error ) {
// An atomic UPSERT operation (INSERT/UPDATE) is the only operation
// that ensures that the key is actually locked.
switch {
case setting . Database . UseSQLite3 || setting . Database . UsePostgreSQL :
_ , err = e . Exec ( fmt . Sprintf ( "INSERT INTO %s (group_id, max_index) " +
"VALUES (?,1) ON CONFLICT (group_id) DO UPDATE SET max_index = %s.max_index+1" ,
tableName , tableName ) , groupID )
case setting . Database . UseMySQL :
_ , err = e . Exec ( fmt . Sprintf ( "INSERT INTO %s (group_id, max_index) " +
"VALUES (?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1" , tableName ) ,
groupID )
case setting . Database . UseMSSQL :
// https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/
_ , err = e . Exec ( fmt . Sprintf ( "MERGE %s WITH (HOLDLOCK) as target " +
"USING (SELECT ? AS group_id) AS src " +
"ON src.group_id = target.group_id " +
"WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 " +
"WHEN NOT MATCHED THEN INSERT (group_id, max_index) " +
"VALUES (src.group_id, 1);" , tableName ) ,
groupID )
default :
return fmt . Errorf ( "database type not supported" )
}
return
}
var (
// ErrResouceOutdated represents an error when request resource outdated
ErrResouceOutdated = errors . New ( "resource outdated" )
// ErrGetResourceIndexFailed represents an error when resource index retries 3 times
ErrGetResourceIndexFailed = errors . New ( "get resource index failed" )
)
const (
maxDupIndexAttempts = 3
)
// GetNextResourceIndex retried 3 times to generate a resource index
func GetNextResourceIndex ( tableName string , groupID int64 ) ( int64 , error ) {
for i := 0 ; i < maxDupIndexAttempts ; i ++ {
idx , err := getNextResourceIndex ( tableName , groupID )
if err == ErrResouceOutdated {
continue
}
if err != nil {
return 0 , err
}
return idx , nil
}
return 0 , ErrGetResourceIndexFailed
}
// deleteResouceIndex delete resource index
func deleteResouceIndex ( e Engine , tableName string , groupID int64 ) error {
_ , err := e . Exec ( fmt . Sprintf ( "DELETE FROM %s WHERE group_id=?" , tableName ) , groupID )
return err
}
// getNextResourceIndex return the next index
func getNextResourceIndex ( tableName string , groupID int64 ) ( int64 , error ) {
sess := x . NewSession ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
return 0 , err
}
var preIdx int64
_ , err := sess . SQL ( fmt . Sprintf ( "SELECT max_index FROM %s WHERE group_id = ?" , tableName ) , groupID ) . Get ( & preIdx )
if err != nil {
return 0 , err
}
if err := upsertResourceIndex ( sess , tableName , groupID ) ; err != nil {
return 0 , err
}
var curIdx int64
has , err := sess . SQL ( fmt . Sprintf ( "SELECT max_index FROM %s WHERE group_id = ? AND max_index=?" , tableName ) , groupID , preIdx + 1 ) . Get ( & curIdx )
if err != nil {
return 0 , err
}
if ! has {
return 0 , ErrResouceOutdated
}
if err := sess . Commit ( ) ; err != nil {
return 0 , err
}
return curIdx , nil
}