summaryrefslogtreecommitdiffstats
path: root/models/db/index.go
blob: 259ddd6ade7ac2d666acac6728558132992db28e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package db

import (
	"context"
	"errors"
	"fmt"
	"strconv"

	"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, etc.
type ResourceIndex struct {
	GroupID  int64 `xorm:"pk"`
	MaxIndex int64 `xorm:"index"`
}

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")
)

// SyncMaxResourceIndex sync the max index with the resource
func SyncMaxResourceIndex(ctx context.Context, tableName string, groupID, maxIndex int64) (err error) {
	e := GetEngine(ctx)

	// try to update the max_index and acquire the write-lock for the record
	res, err := e.Exec(fmt.Sprintf("UPDATE %s SET max_index=? WHERE group_id=? AND max_index<?", tableName), maxIndex, groupID, maxIndex)
	if err != nil {
		return err
	}
	affected, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if affected == 0 {
		// 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
		_, errIns := e.Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) VALUES (?, ?)", tableName), groupID, maxIndex)
		var savedIdx int64
		has, err := e.SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id=?", tableName), groupID).Get(&savedIdx)
		if err != nil {
			return err
		}
		// if the record still doesn't exist, there must be some errors (insert error)
		if !has {
			if errIns == nil {
				return errors.New("impossible error when SyncMaxResourceIndex, insert succeeded but no record is saved")
			}
			return errIns
		}
	}
	return nil
}

func postgresGetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) {
	res, err := GetEngine(ctx).Query(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+
		"VALUES (?,1) ON CONFLICT (group_id) DO UPDATE SET max_index = %s.max_index+1 RETURNING max_index",
		tableName, tableName), groupID)
	if err != nil {
		return 0, err
	}
	if len(res) == 0 {
		return 0, ErrGetResourceIndexFailed
	}
	return strconv.ParseInt(string(res[0]["max_index"]), 10, 64)
}

func mysqlGetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) {
	if _, err := GetEngine(ctx).Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+
		"VALUES (?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1",
		tableName), groupID); err != nil {
		return 0, err
	}

	var idx int64
	_, err := GetEngine(ctx).SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ?", tableName), groupID).Get(&idx)
	if err != nil {
		return 0, err
	}
	if idx == 0 {
		return 0, errors.New("cannot get the correct index")
	}
	return idx, nil
}

// GetNextResourceIndex generates a resource index, it must run in the same transaction where the resource is created
func GetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) {
	switch {
	case setting.Database.Type.IsPostgreSQL():
		return postgresGetNextResourceIndex(ctx, tableName, groupID)
	case setting.Database.Type.IsMySQL():
		return mysqlGetNextResourceIndex(ctx, tableName, groupID)
	}

	e := GetEngine(ctx)

	// try to update the max_index to next value, and acquire the write-lock for the record
	res, err := e.Exec(fmt.Sprintf("UPDATE %s SET max_index=max_index+1 WHERE group_id=?", tableName), groupID)
	if err != nil {
		return 0, err
	}
	affected, err := res.RowsAffected()
	if err != nil {
		return 0, err
	}
	if affected == 0 {
		// this slow path is only for the first time of creating a resource index
		_, errIns := e.Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) VALUES (?, 0)", tableName), groupID)
		res, err = e.Exec(fmt.Sprintf("UPDATE %s SET max_index=max_index+1 WHERE group_id=?", tableName), groupID)
		if err != nil {
			return 0, err
		}
		affected, err = res.RowsAffected()
		if err != nil {
			return 0, err
		}
		// if the update still can not update any records, the record must not exist and there must be some errors (insert error)
		if affected == 0 {
			if errIns == nil {
				return 0, errors.New("impossible error when GetNextResourceIndex, insert and update both succeeded but no record is updated")
			}
			return 0, errIns
		}
	}

	// now, the new index is in database (protected by the transaction and write-lock)
	var newIdx int64
	has, err := e.SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id=?", tableName), groupID).Get(&newIdx)
	if err != nil {
		return 0, err
	}
	if !has {
		return 0, errors.New("impossible error when GetNextResourceIndex, upsert succeeded but no record can be selected")
	}
	return newIdx, nil
}

// DeleteResourceIndex delete resource index
func DeleteResourceIndex(ctx context.Context, tableName string, groupID int64) error {
	_, err := Exec(ctx, fmt.Sprintf("DELETE FROM %s WHERE group_id=?", tableName), groupID)
	return err
}