Skip to content

Commit bd820aa

Browse files
authored
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set a context cache to do that. i.e. Some pages may load a user from a database with the same id in different areas on the same page. But the code is hidden in two different deep logic. How should we share the user? As a result of this PR, now if both entry functions accept `context.Context` as the first parameter and we just need to refactor `GetUserByID` to reuse the user from the context cache. Then it will not be loaded twice on an HTTP request. But of course, sometimes we would like to reload an object from the database, that's why `RemoveContextData` is also exposed. The core context cache is here. It defines a new context ```go type cacheContext struct { ctx context.Context data map[any]map[any]any lock sync.RWMutex } var cacheContextKey = struct{}{} func WithCacheContext(ctx context.Context) context.Context { return context.WithValue(ctx, cacheContextKey, &cacheContext{ ctx: ctx, data: make(map[any]map[any]any), }) } ``` Then you can use the below 4 methods to read/write/del the data within the same context. ```go func GetContextData(ctx context.Context, tp, key any) any func SetContextData(ctx context.Context, tp, key, value any) func RemoveContextData(ctx context.Context, tp, key any) func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error) ``` Then let's take a look at how `system.GetString` implement it. ```go func GetSetting(ctx context.Context, key string) (string, error) { return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) { return cache.GetString(genSettingCacheKey(key), func() (string, error) { res, err := GetSettingNoCache(ctx, key) if err != nil { return "", err } return res.SettingValue, nil }) }) } ``` First, it will check if context data include the setting object with the key. If not, it will query from the global cache which may be memory or a Redis cache. If not, it will get the object from the database. In the end, if the object gets from the global cache or database, it will be set into the context cache. An object stored in the context cache will only be destroyed after the context disappeared.
1 parent 03638f9 commit bd820aa

File tree

150 files changed

+663
-516
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

150 files changed

+663
-516
lines changed

cmd/admin_user_delete.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func runDeleteUser(c *cli.Context) error {
5757
var err error
5858
var user *user_model.User
5959
if c.IsSet("email") {
60-
user, err = user_model.GetUserByEmail(c.String("email"))
60+
user, err = user_model.GetUserByEmail(ctx, c.String("email"))
6161
} else if c.IsSet("username") {
6262
user, err = user_model.GetUserByName(ctx, c.String("username"))
6363
} else {

models/activities/repo_activity.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,12 @@ func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository
9797
}
9898
users := make(map[int64]*ActivityAuthorData)
9999
var unknownUserID int64
100-
unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink()
100+
unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink(ctx)
101101
for _, v := range code.Authors {
102102
if len(v.Email) == 0 {
103103
continue
104104
}
105-
u, err := user_model.GetUserByEmail(v.Email)
105+
u, err := user_model.GetUserByEmail(ctx, v.Email)
106106
if u == nil || user_model.IsErrUserNotExist(err) {
107107
unknownUserID--
108108
users[unknownUserID] = &ActivityAuthorData{
@@ -119,7 +119,7 @@ func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository
119119
users[u.ID] = &ActivityAuthorData{
120120
Name: u.DisplayName(),
121121
Login: u.LowerName,
122-
AvatarLink: u.AvatarLink(),
122+
AvatarLink: u.AvatarLink(ctx),
123123
HomeLink: u.HomeLink(),
124124
Commits: v.Commits,
125125
}

models/asymkey/gpg_key_commit_verification.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package asymkey
55

66
import (
7+
"context"
78
"fmt"
89
"hash"
910
"strings"
@@ -70,14 +71,14 @@ const (
7071
)
7172

7273
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
73-
func ParseCommitsWithSignature(oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error)) []*SignCommit {
74+
func ParseCommitsWithSignature(ctx context.Context, oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error)) []*SignCommit {
7475
newCommits := make([]*SignCommit, 0, len(oldCommits))
7576
keyMap := map[string]bool{}
7677

7778
for _, c := range oldCommits {
7879
signCommit := &SignCommit{
7980
UserCommit: c,
80-
Verification: ParseCommitWithSignature(c.Commit),
81+
Verification: ParseCommitWithSignature(ctx, c.Commit),
8182
}
8283

8384
_ = CalculateTrustStatus(signCommit.Verification, repoTrustModel, isOwnerMemberCollaborator, &keyMap)
@@ -88,13 +89,13 @@ func ParseCommitsWithSignature(oldCommits []*user_model.UserCommit, repoTrustMod
8889
}
8990

9091
// ParseCommitWithSignature check if signature is good against keystore.
91-
func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
92+
func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerification {
9293
var committer *user_model.User
9394
if c.Committer != nil {
9495
var err error
9596
// Find Committer account
96-
committer, err = user_model.GetUserByEmail(c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not
97-
if err != nil { // Skipping not user for committer
97+
committer, err = user_model.GetUserByEmail(ctx, c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not
98+
if err != nil { // Skipping not user for committer
9899
committer = &user_model.User{
99100
Name: c.Committer.Name,
100101
Email: c.Committer.Email,

models/avatars/avatar.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,13 @@ func generateRecognizedAvatarURL(u url.URL, size int) string {
147147
// generateEmailAvatarLink returns a email avatar link.
148148
// if final is true, it may use a slow path (eg: query DNS).
149149
// if final is false, it always uses a fast path.
150-
func generateEmailAvatarLink(email string, size int, final bool) string {
150+
func generateEmailAvatarLink(ctx context.Context, email string, size int, final bool) string {
151151
email = strings.TrimSpace(email)
152152
if email == "" {
153153
return DefaultAvatarLink()
154154
}
155155

156-
enableFederatedAvatar := system_model.GetSettingBool(system_model.KeyPictureEnableFederatedAvatar)
156+
enableFederatedAvatar := system_model.GetSettingBool(ctx, system_model.KeyPictureEnableFederatedAvatar)
157157

158158
var err error
159159
if enableFederatedAvatar && system_model.LibravatarService != nil {
@@ -174,7 +174,7 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
174174
return urlStr
175175
}
176176

177-
disableGravatar := system_model.GetSettingBool(system_model.KeyPictureDisableGravatar)
177+
disableGravatar := system_model.GetSettingBool(ctx, system_model.KeyPictureDisableGravatar)
178178
if !disableGravatar {
179179
// copy GravatarSourceURL, because we will modify its Path.
180180
avatarURLCopy := *system_model.GravatarSourceURL
@@ -186,11 +186,11 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
186186
}
187187

188188
// GenerateEmailAvatarFastLink returns a avatar link (fast, the link may be a delegated one: "/avatar/${hash}")
189-
func GenerateEmailAvatarFastLink(email string, size int) string {
190-
return generateEmailAvatarLink(email, size, false)
189+
func GenerateEmailAvatarFastLink(ctx context.Context, email string, size int) string {
190+
return generateEmailAvatarLink(ctx, email, size, false)
191191
}
192192

193193
// GenerateEmailAvatarFinalLink returns a avatar final link (maybe slow)
194-
func GenerateEmailAvatarFinalLink(email string, size int) string {
195-
return generateEmailAvatarLink(email, size, true)
194+
func GenerateEmailAvatarFinalLink(ctx context.Context, email string, size int) string {
195+
return generateEmailAvatarLink(ctx, email, size, true)
196196
}

models/avatars/avatar_test.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
avatars_model "code.gitea.io/gitea/models/avatars"
10+
"code.gitea.io/gitea/models/db"
1011
system_model "code.gitea.io/gitea/models/system"
1112
"code.gitea.io/gitea/modules/setting"
1213

@@ -16,15 +17,15 @@ import (
1617
const gravatarSource = "https://secure.gravatar.com/avatar/"
1718

1819
func disableGravatar(t *testing.T) {
19-
err := system_model.SetSettingNoVersion(system_model.KeyPictureEnableFederatedAvatar, "false")
20+
err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureEnableFederatedAvatar, "false")
2021
assert.NoError(t, err)
21-
err = system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, "true")
22+
err = system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "true")
2223
assert.NoError(t, err)
2324
system_model.LibravatarService = nil
2425
}
2526

2627
func enableGravatar(t *testing.T) {
27-
err := system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, "false")
28+
err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "false")
2829
assert.NoError(t, err)
2930
setting.GravatarSource = gravatarSource
3031
err = system_model.Init()
@@ -47,11 +48,11 @@ func TestSizedAvatarLink(t *testing.T) {
4748

4849
disableGravatar(t)
4950
assert.Equal(t, "/testsuburl/assets/img/avatar_default.png",
50-
avatars_model.GenerateEmailAvatarFastLink("gitea@example.com", 100))
51+
avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100))
5152

5253
enableGravatar(t)
5354
assert.Equal(t,
5455
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100",
55-
avatars_model.GenerateEmailAvatarFastLink("gitea@example.com", 100),
56+
avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100),
5657
)
5758
}

models/git/commit_status.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,8 @@ func hashCommitStatusContext(context string) string {
351351
func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository) []*SignCommitWithStatuses {
352352
return ParseCommitsWithStatus(ctx,
353353
asymkey_model.ParseCommitsWithSignature(
354-
user_model.ValidateCommitsWithEmails(commits),
354+
ctx,
355+
user_model.ValidateCommitsWithEmails(ctx, commits),
355356
repo.GetTrustModel(),
356357
func(user *user_model.User) (bool, error) {
357358
return repo_model.IsOwnerMemberCollaborator(repo, user.ID)

models/organization/org.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,8 @@ func (org *Organization) hasMemberWithUserID(ctx context.Context, userID int64)
156156
}
157157

158158
// AvatarLink returns the full avatar link with http host
159-
func (org *Organization) AvatarLink() string {
160-
return org.AsUser().AvatarLink()
159+
func (org *Organization) AvatarLink(ctx context.Context) string {
160+
return org.AsUser().AvatarLink(ctx)
161161
}
162162

163163
// HTMLURL returns the organization's full link.

models/repo/avatar.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,7 @@ func (repo *Repository) relAvatarLink(ctx context.Context) string {
8585
}
8686

8787
// AvatarLink returns a link to the repository's avatar.
88-
func (repo *Repository) AvatarLink() string {
89-
return repo.avatarLink(db.DefaultContext)
90-
}
91-
92-
// avatarLink returns user avatar absolute link.
93-
func (repo *Repository) avatarLink(ctx context.Context) string {
88+
func (repo *Repository) AvatarLink(ctx context.Context) string {
9489
link := repo.relAvatarLink(ctx)
9590
// we only prepend our AppURL to our known (relative, internal) avatar link to get an absolute URL
9691
if strings.HasPrefix(link, "/") && !strings.HasPrefix(link, "//") {

models/system/setting.go

+33-26
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ func IsErrDataExpired(err error) bool {
8080
}
8181

8282
// GetSettingNoCache returns specific setting without using the cache
83-
func GetSettingNoCache(key string) (*Setting, error) {
84-
v, err := GetSettings([]string{key})
83+
func GetSettingNoCache(ctx context.Context, key string) (*Setting, error) {
84+
v, err := GetSettings(ctx, []string{key})
8585
if err != nil {
8686
return nil, err
8787
}
@@ -91,27 +91,31 @@ func GetSettingNoCache(key string) (*Setting, error) {
9191
return v[strings.ToLower(key)], nil
9292
}
9393

94+
const contextCacheKey = "system_setting"
95+
9496
// GetSetting returns the setting value via the key
95-
func GetSetting(key string) (string, error) {
96-
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
97-
res, err := GetSettingNoCache(key)
98-
if err != nil {
99-
return "", err
100-
}
101-
return res.SettingValue, nil
97+
func GetSetting(ctx context.Context, key string) (string, error) {
98+
return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
99+
return cache.GetString(genSettingCacheKey(key), func() (string, error) {
100+
res, err := GetSettingNoCache(ctx, key)
101+
if err != nil {
102+
return "", err
103+
}
104+
return res.SettingValue, nil
105+
})
102106
})
103107
}
104108

105109
// GetSettingBool return bool value of setting,
106110
// none existing keys and errors are ignored and result in false
107-
func GetSettingBool(key string) bool {
108-
s, _ := GetSetting(key)
111+
func GetSettingBool(ctx context.Context, key string) bool {
112+
s, _ := GetSetting(ctx, key)
109113
v, _ := strconv.ParseBool(s)
110114
return v
111115
}
112116

113117
// GetSettings returns specific settings
114-
func GetSettings(keys []string) (map[string]*Setting, error) {
118+
func GetSettings(ctx context.Context, keys []string) (map[string]*Setting, error) {
115119
for i := 0; i < len(keys); i++ {
116120
keys[i] = strings.ToLower(keys[i])
117121
}
@@ -161,16 +165,17 @@ func GetAllSettings() (AllSettings, error) {
161165
}
162166

163167
// DeleteSetting deletes a specific setting for a user
164-
func DeleteSetting(setting *Setting) error {
168+
func DeleteSetting(ctx context.Context, setting *Setting) error {
169+
cache.RemoveContextData(ctx, contextCacheKey, setting.SettingKey)
165170
cache.Remove(genSettingCacheKey(setting.SettingKey))
166171
_, err := db.GetEngine(db.DefaultContext).Delete(setting)
167172
return err
168173
}
169174

170-
func SetSettingNoVersion(key, value string) error {
171-
s, err := GetSettingNoCache(key)
175+
func SetSettingNoVersion(ctx context.Context, key, value string) error {
176+
s, err := GetSettingNoCache(ctx, key)
172177
if IsErrSettingIsNotExist(err) {
173-
return SetSetting(&Setting{
178+
return SetSetting(ctx, &Setting{
174179
SettingKey: key,
175180
SettingValue: value,
176181
})
@@ -179,11 +184,11 @@ func SetSettingNoVersion(key, value string) error {
179184
return err
180185
}
181186
s.SettingValue = value
182-
return SetSetting(s)
187+
return SetSetting(ctx, s)
183188
}
184189

185190
// SetSetting updates a users' setting for a specific key
186-
func SetSetting(setting *Setting) error {
191+
func SetSetting(ctx context.Context, setting *Setting) error {
187192
if err := upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil {
188193
return err
189194
}
@@ -192,9 +197,11 @@ func SetSetting(setting *Setting) error {
192197

193198
cc := cache.GetCache()
194199
if cc != nil {
195-
return cc.Put(genSettingCacheKey(setting.SettingKey), setting.SettingValue, setting_module.CacheService.TTLSeconds())
200+
if err := cc.Put(genSettingCacheKey(setting.SettingKey), setting.SettingValue, setting_module.CacheService.TTLSeconds()); err != nil {
201+
return err
202+
}
196203
}
197-
204+
cache.SetContextData(ctx, contextCacheKey, setting.SettingKey, setting.SettingValue)
198205
return nil
199206
}
200207

@@ -244,7 +251,7 @@ var (
244251

245252
func Init() error {
246253
var disableGravatar bool
247-
disableGravatarSetting, err := GetSettingNoCache(KeyPictureDisableGravatar)
254+
disableGravatarSetting, err := GetSettingNoCache(db.DefaultContext, KeyPictureDisableGravatar)
248255
if IsErrSettingIsNotExist(err) {
249256
disableGravatar = setting_module.GetDefaultDisableGravatar()
250257
disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)}
@@ -255,7 +262,7 @@ func Init() error {
255262
}
256263

257264
var enableFederatedAvatar bool
258-
enableFederatedAvatarSetting, err := GetSettingNoCache(KeyPictureEnableFederatedAvatar)
265+
enableFederatedAvatarSetting, err := GetSettingNoCache(db.DefaultContext, KeyPictureEnableFederatedAvatar)
259266
if IsErrSettingIsNotExist(err) {
260267
enableFederatedAvatar = setting_module.GetDefaultEnableFederatedAvatar(disableGravatar)
261268
enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)}
@@ -268,13 +275,13 @@ func Init() error {
268275
if setting_module.OfflineMode {
269276
disableGravatar = true
270277
enableFederatedAvatar = false
271-
if !GetSettingBool(KeyPictureDisableGravatar) {
272-
if err := SetSettingNoVersion(KeyPictureDisableGravatar, "true"); err != nil {
278+
if !GetSettingBool(db.DefaultContext, KeyPictureDisableGravatar) {
279+
if err := SetSettingNoVersion(db.DefaultContext, KeyPictureDisableGravatar, "true"); err != nil {
273280
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureDisableGravatar, err)
274281
}
275282
}
276-
if GetSettingBool(KeyPictureEnableFederatedAvatar) {
277-
if err := SetSettingNoVersion(KeyPictureEnableFederatedAvatar, "false"); err != nil {
283+
if GetSettingBool(db.DefaultContext, KeyPictureEnableFederatedAvatar) {
284+
if err := SetSettingNoVersion(db.DefaultContext, KeyPictureEnableFederatedAvatar, "false"); err != nil {
278285
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err)
279286
}
280287
}

models/system/setting_test.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88
"testing"
99

10+
"code.gitea.io/gitea/models/db"
1011
"code.gitea.io/gitea/models/system"
1112
"code.gitea.io/gitea/models/unittest"
1213

@@ -20,24 +21,24 @@ func TestSettings(t *testing.T) {
2021
newSetting := &system.Setting{SettingKey: keyName, SettingValue: "50"}
2122

2223
// create setting
23-
err := system.SetSetting(newSetting)
24+
err := system.SetSetting(db.DefaultContext, newSetting)
2425
assert.NoError(t, err)
2526
// test about saving unchanged values
26-
err = system.SetSetting(newSetting)
27+
err = system.SetSetting(db.DefaultContext, newSetting)
2728
assert.NoError(t, err)
2829

2930
// get specific setting
30-
settings, err := system.GetSettings([]string{keyName})
31+
settings, err := system.GetSettings(db.DefaultContext, []string{keyName})
3132
assert.NoError(t, err)
3233
assert.Len(t, settings, 1)
3334
assert.EqualValues(t, newSetting.SettingValue, settings[strings.ToLower(keyName)].SettingValue)
3435

3536
// updated setting
3637
updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: settings[strings.ToLower(keyName)].Version}
37-
err = system.SetSetting(updatedSetting)
38+
err = system.SetSetting(db.DefaultContext, updatedSetting)
3839
assert.NoError(t, err)
3940

40-
value, err := system.GetSetting(keyName)
41+
value, err := system.GetSetting(db.DefaultContext, keyName)
4142
assert.NoError(t, err)
4243
assert.EqualValues(t, updatedSetting.SettingValue, value)
4344

@@ -48,7 +49,7 @@ func TestSettings(t *testing.T) {
4849
assert.EqualValues(t, updatedSetting.SettingValue, settings[strings.ToLower(updatedSetting.SettingKey)].SettingValue)
4950

5051
// delete setting
51-
err = system.DeleteSetting(&system.Setting{SettingKey: strings.ToLower(keyName)})
52+
err = system.DeleteSetting(db.DefaultContext, &system.Setting{SettingKey: strings.ToLower(keyName)})
5253
assert.NoError(t, err)
5354
settings, err = system.GetAllSettings()
5455
assert.NoError(t, err)

0 commit comments

Comments
 (0)