use built-in cache package to wrap external go-chi cache packagetags/v1.22.0-rc1
desc: do not use the internal package, use AddXxx function instead | desc: do not use the internal package, use AddXxx function instead | ||||
- pkg: gopkg.in/ini.v1 | - pkg: gopkg.in/ini.v1 | ||||
desc: do not use the ini package, use gitea's config system instead | desc: do not use the ini package, use gitea's config system instead | ||||
- pkg: gitea.com/go-chi/cache | |||||
desc: do not use the go-chi cache package, use gitea's cache system | |||||
issues: | issues: | ||||
max-issues-per-linter: 0 | max-issues-per-linter: 0 |
package cache | package cache | ||||
import ( | import ( | ||||
"fmt" | |||||
"strconv" | "strconv" | ||||
"time" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
mc "gitea.com/go-chi/cache" | |||||
_ "gitea.com/go-chi/cache/memcache" // memcache plugin for cache | |||||
) | ) | ||||
var conn mc.Cache | |||||
func newCache(cacheConfig setting.Cache) (mc.Cache, error) { | |||||
return mc.NewCacher(mc.Options{ | |||||
Adapter: cacheConfig.Adapter, | |||||
AdapterConfig: cacheConfig.Conn, | |||||
Interval: cacheConfig.Interval, | |||||
}) | |||||
} | |||||
var defaultCache StringCache | |||||
// Init start cache service | // Init start cache service | ||||
func Init() error { | func Init() error { | ||||
var err error | |||||
if conn == nil { | |||||
if conn, err = newCache(setting.CacheService.Cache); err != nil { | |||||
if defaultCache == nil { | |||||
c, err := NewStringCache(setting.CacheService.Cache) | |||||
if err != nil { | |||||
return err | return err | ||||
} | } | ||||
if err = conn.Ping(); err != nil { | |||||
for i := 0; i < 10; i++ { | |||||
if err = c.Ping(); err == nil { | |||||
break | |||||
} | |||||
time.Sleep(time.Second) | |||||
} | |||||
if err != nil { | |||||
return err | return err | ||||
} | } | ||||
defaultCache = c | |||||
} | } | ||||
return err | |||||
return nil | |||||
} | } | ||||
// GetCache returns the currently configured cache | // GetCache returns the currently configured cache | ||||
func GetCache() mc.Cache { | |||||
return conn | |||||
func GetCache() StringCache { | |||||
return defaultCache | |||||
} | } | ||||
// GetString returns the key value from cache with callback when no key exists in cache | // GetString returns the key value from cache with callback when no key exists in cache | ||||
func GetString(key string, getFunc func() (string, error)) (string, error) { | func GetString(key string, getFunc func() (string, error)) (string, error) { | ||||
if conn == nil || setting.CacheService.TTL == 0 { | |||||
if defaultCache == nil || setting.CacheService.TTL == 0 { | |||||
return getFunc() | return getFunc() | ||||
} | } | ||||
cached := conn.Get(key) | |||||
if cached == nil { | |||||
cached, exist := defaultCache.Get(key) | |||||
if !exist { | |||||
value, err := getFunc() | value, err := getFunc() | ||||
if err != nil { | if err != nil { | ||||
return value, err | return value, err | ||||
} | } | ||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) | |||||
} | |||||
if value, ok := cached.(string); ok { | |||||
return value, nil | |||||
} | |||||
if stringer, ok := cached.(fmt.Stringer); ok { | |||||
return stringer.String(), nil | |||||
} | |||||
return fmt.Sprintf("%s", cached), nil | |||||
} | |||||
// GetInt returns key value from cache with callback when no key exists in cache | |||||
func GetInt(key string, getFunc func() (int, error)) (int, error) { | |||||
if conn == nil || setting.CacheService.TTL == 0 { | |||||
return getFunc() | |||||
} | |||||
cached := conn.Get(key) | |||||
if cached == nil { | |||||
value, err := getFunc() | |||||
if err != nil { | |||||
return value, err | |||||
} | |||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) | |||||
} | |||||
switch v := cached.(type) { | |||||
case int: | |||||
return v, nil | |||||
case string: | |||||
value, err := strconv.Atoi(v) | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
return value, nil | |||||
default: | |||||
value, err := getFunc() | |||||
if err != nil { | |||||
return value, err | |||||
} | |||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) | |||||
return value, defaultCache.Put(key, value, setting.CacheService.TTLSeconds()) | |||||
} | } | ||||
return cached, nil | |||||
} | } | ||||
// GetInt64 returns key value from cache with callback when no key exists in cache | // GetInt64 returns key value from cache with callback when no key exists in cache | ||||
func GetInt64(key string, getFunc func() (int64, error)) (int64, error) { | func GetInt64(key string, getFunc func() (int64, error)) (int64, error) { | ||||
if conn == nil || setting.CacheService.TTL == 0 { | |||||
return getFunc() | |||||
} | |||||
cached := conn.Get(key) | |||||
if cached == nil { | |||||
value, err := getFunc() | |||||
if err != nil { | |||||
return value, err | |||||
} | |||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) | |||||
s, err := GetString(key, func() (string, error) { | |||||
v, err := getFunc() | |||||
return strconv.FormatInt(v, 10), err | |||||
}) | |||||
if err != nil { | |||||
return 0, err | |||||
} | } | ||||
switch v := conn.Get(key).(type) { | |||||
case int64: | |||||
return v, nil | |||||
case string: | |||||
value, err := strconv.ParseInt(v, 10, 64) | |||||
if err != nil { | |||||
return 0, err | |||||
} | |||||
return value, nil | |||||
default: | |||||
value, err := getFunc() | |||||
if err != nil { | |||||
return value, err | |||||
} | |||||
return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) | |||||
if s == "" { | |||||
return 0, nil | |||||
} | } | ||||
return strconv.ParseInt(s, 10, 64) | |||||
} | } | ||||
// Remove key from cache | // Remove key from cache | ||||
func Remove(key string) { | func Remove(key string) { | ||||
if conn == nil { | |||||
if defaultCache == nil { | |||||
return | return | ||||
} | } | ||||
_ = conn.Delete(key) | |||||
_ = defaultCache.Delete(key) | |||||
} | } |
"code.gitea.io/gitea/modules/graceful" | "code.gitea.io/gitea/modules/graceful" | ||||
"code.gitea.io/gitea/modules/nosql" | "code.gitea.io/gitea/modules/nosql" | ||||
"gitea.com/go-chi/cache" | |||||
"gitea.com/go-chi/cache" //nolint:depguard | |||||
"github.com/redis/go-redis/v9" | "github.com/redis/go-redis/v9" | ||||
) | ) | ||||
) | ) | ||||
func createTestCache() { | func createTestCache() { | ||||
conn, _ = newCache(setting.Cache{ | |||||
defaultCache, _ = NewStringCache(setting.Cache{ | |||||
Adapter: "memory", | Adapter: "memory", | ||||
TTL: time.Minute, | TTL: time.Minute, | ||||
}) | }) | ||||
assert.NoError(t, Init()) | assert.NoError(t, Init()) | ||||
setting.CacheService.Cache = setting.Cache{Adapter: "redis", Conn: "some random string"} | setting.CacheService.Cache = setting.Cache{Adapter: "redis", Conn: "some random string"} | ||||
con, err := newCache(setting.Cache{ | |||||
con, err := NewStringCache(setting.Cache{ | |||||
Adapter: "rand", | Adapter: "rand", | ||||
Conn: "false conf", | Conn: "false conf", | ||||
Interval: 100, | Interval: 100, | ||||
Remove("key") | Remove("key") | ||||
} | } | ||||
func TestGetInt(t *testing.T) { | |||||
createTestCache() | |||||
data, err := GetInt("key", func() (int, error) { | |||||
return 0, fmt.Errorf("some error") | |||||
}) | |||||
assert.Error(t, err) | |||||
assert.Equal(t, 0, data) | |||||
data, err = GetInt("key", func() (int, error) { | |||||
return 0, nil | |||||
}) | |||||
assert.NoError(t, err) | |||||
assert.Equal(t, 0, data) | |||||
data, err = GetInt("key", func() (int, error) { | |||||
return 100, nil | |||||
}) | |||||
assert.NoError(t, err) | |||||
assert.Equal(t, 0, data) | |||||
Remove("key") | |||||
data, err = GetInt("key", func() (int, error) { | |||||
return 100, nil | |||||
}) | |||||
assert.NoError(t, err) | |||||
assert.Equal(t, 100, data) | |||||
data, err = GetInt("key", func() (int, error) { | |||||
return 0, fmt.Errorf("some error") | |||||
}) | |||||
assert.NoError(t, err) | |||||
assert.Equal(t, 100, data) | |||||
Remove("key") | |||||
} | |||||
func TestGetInt64(t *testing.T) { | func TestGetInt64(t *testing.T) { | ||||
createTestCache() | createTestCache() | ||||
"code.gitea.io/gitea/modules/json" | "code.gitea.io/gitea/modules/json" | ||||
mc "gitea.com/go-chi/cache" | |||||
mc "gitea.com/go-chi/cache" //nolint:depguard | |||||
lru "github.com/hashicorp/golang-lru/v2" | lru "github.com/hashicorp/golang-lru/v2" | ||||
) | ) | ||||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||||
// SPDX-License-Identifier: MIT | |||||
package cache | |||||
import ( | |||||
"errors" | |||||
"strings" | |||||
"code.gitea.io/gitea/modules/json" | |||||
"code.gitea.io/gitea/modules/setting" | |||||
"code.gitea.io/gitea/modules/util" | |||||
chi_cache "gitea.com/go-chi/cache" //nolint:depguard | |||||
) | |||||
type GetJSONError struct { | |||||
err error | |||||
cachedError string // Golang error can't be stored in cache, only the string message could be stored | |||||
} | |||||
func (e *GetJSONError) ToError() error { | |||||
if e.err != nil { | |||||
return e.err | |||||
} | |||||
return errors.New("cached error: " + e.cachedError) | |||||
} | |||||
type StringCache interface { | |||||
Ping() error | |||||
Get(key string) (string, bool) | |||||
Put(key, value string, ttl int64) error | |||||
Delete(key string) error | |||||
IsExist(key string) bool | |||||
PutJSON(key string, v any, ttl int64) error | |||||
GetJSON(key string, ptr any) (exist bool, err *GetJSONError) | |||||
ChiCache() chi_cache.Cache | |||||
} | |||||
type stringCache struct { | |||||
chiCache chi_cache.Cache | |||||
} | |||||
func NewStringCache(cacheConfig setting.Cache) (StringCache, error) { | |||||
adapter := util.IfZero(cacheConfig.Adapter, "memory") | |||||
interval := util.IfZero(cacheConfig.Interval, 60) | |||||
cc, err := chi_cache.NewCacher(chi_cache.Options{ | |||||
Adapter: adapter, | |||||
AdapterConfig: cacheConfig.Conn, | |||||
Interval: interval, | |||||
}) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return &stringCache{chiCache: cc}, nil | |||||
} | |||||
func (sc *stringCache) Ping() error { | |||||
return sc.chiCache.Ping() | |||||
} | |||||
func (sc *stringCache) Get(key string) (string, bool) { | |||||
v := sc.chiCache.Get(key) | |||||
if v == nil { | |||||
return "", false | |||||
} | |||||
s, ok := v.(string) | |||||
return s, ok | |||||
} | |||||
func (sc *stringCache) Put(key, value string, ttl int64) error { | |||||
return sc.chiCache.Put(key, value, ttl) | |||||
} | |||||
func (sc *stringCache) Delete(key string) error { | |||||
return sc.chiCache.Delete(key) | |||||
} | |||||
func (sc *stringCache) IsExist(key string) bool { | |||||
return sc.chiCache.IsExist(key) | |||||
} | |||||
const cachedErrorPrefix = "<CACHED-ERROR>:" | |||||
func (sc *stringCache) PutJSON(key string, v any, ttl int64) error { | |||||
var s string | |||||
switch v := v.(type) { | |||||
case error: | |||||
s = cachedErrorPrefix + v.Error() | |||||
default: | |||||
b, err := json.Marshal(v) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
s = util.UnsafeBytesToString(b) | |||||
} | |||||
return sc.chiCache.Put(key, s, ttl) | |||||
} | |||||
func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSONError) { | |||||
s, ok := sc.Get(key) | |||||
if !ok || s == "" { | |||||
return false, nil | |||||
} | |||||
s, isCachedError := strings.CutPrefix(s, cachedErrorPrefix) | |||||
if isCachedError { | |||||
return true, &GetJSONError{cachedError: s} | |||||
} | |||||
if err := json.Unmarshal(util.UnsafeStringToBytes(s), ptr); err != nil { | |||||
return false, &GetJSONError{err: err} | |||||
} | |||||
return true, nil | |||||
} | |||||
func (sc *stringCache) ChiCache() chi_cache.Cache { | |||||
return sc.chiCache | |||||
} |
"crypto/sha256" | "crypto/sha256" | ||||
"fmt" | "fmt" | ||||
"code.gitea.io/gitea/modules/cache" | |||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
) | ) | ||||
// Cache represents a caching interface | |||||
type Cache interface { | |||||
// Put puts value into cache with key and expire time. | |||||
Put(key string, val any, timeout int64) error | |||||
// Get gets cached value by given key. | |||||
Get(key string) any | |||||
} | |||||
func getCacheKey(repoPath, commitID, entryPath string) string { | func getCacheKey(repoPath, commitID, entryPath string) string { | ||||
hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, commitID, entryPath))) | hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, commitID, entryPath))) | ||||
return fmt.Sprintf("last_commit:%x", hashBytes) | return fmt.Sprintf("last_commit:%x", hashBytes) | ||||
ttl func() int64 | ttl func() int64 | ||||
repo *Repository | repo *Repository | ||||
commitCache map[string]*Commit | commitCache map[string]*Commit | ||||
cache Cache | |||||
cache cache.StringCache | |||||
} | } | ||||
// NewLastCommitCache creates a new last commit cache for repo | // NewLastCommitCache creates a new last commit cache for repo | ||||
func NewLastCommitCache(count int64, repoPath string, gitRepo *Repository, cache Cache) *LastCommitCache { | |||||
func NewLastCommitCache(count int64, repoPath string, gitRepo *Repository, cache cache.StringCache) *LastCommitCache { | |||||
if cache == nil { | if cache == nil { | ||||
return nil | return nil | ||||
} | } | ||||
return nil, nil | return nil, nil | ||||
} | } | ||||
commitID, ok := c.cache.Get(getCacheKey(c.repoPath, ref, entryPath)).(string) | |||||
commitID, ok := c.cache.Get(getCacheKey(c.repoPath, ref, entryPath)) | |||||
if !ok || commitID == "" { | if !ok || commitID == "" { | ||||
return nil, nil | return nil, nil | ||||
} | } |
nodeInfoUsage := structs.NodeInfoUsage{} | nodeInfoUsage := structs.NodeInfoUsage{} | ||||
if setting.Federation.ShareUserStatistics { | if setting.Federation.ShareUserStatistics { | ||||
var cached bool | |||||
nodeInfoUsage, cached = ctx.Cache.Get(cacheKeyNodeInfoUsage).(structs.NodeInfoUsage) | |||||
cached, _ := ctx.Cache.GetJSON(cacheKeyNodeInfoUsage, &nodeInfoUsage) | |||||
if !cached { | if !cached { | ||||
usersTotal := int(user_model.CountUsers(ctx, nil)) | usersTotal := int(user_model.CountUsers(ctx, nil)) | ||||
now := time.Now() | now := time.Now() | ||||
LocalComments: int(allComments), | LocalComments: int(allComments), | ||||
} | } | ||||
if err := ctx.Cache.Put(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil { | |||||
if err := ctx.Cache.PutJSON(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil { | |||||
ctx.InternalServerError(err) | ctx.InternalServerError(err) | ||||
return | return | ||||
} | } |
"code.gitea.io/gitea/models/unit" | "code.gitea.io/gitea/models/unit" | ||||
user_model "code.gitea.io/gitea/models/user" | user_model "code.gitea.io/gitea/models/user" | ||||
mc "code.gitea.io/gitea/modules/cache" | |||||
"code.gitea.io/gitea/modules/cache" | |||||
"code.gitea.io/gitea/modules/git" | "code.gitea.io/gitea/modules/git" | ||||
"code.gitea.io/gitea/modules/gitrepo" | "code.gitea.io/gitea/modules/gitrepo" | ||||
"code.gitea.io/gitea/modules/httpcache" | "code.gitea.io/gitea/modules/httpcache" | ||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
"code.gitea.io/gitea/modules/web" | "code.gitea.io/gitea/modules/web" | ||||
web_types "code.gitea.io/gitea/modules/web/types" | web_types "code.gitea.io/gitea/modules/web/types" | ||||
"gitea.com/go-chi/cache" | |||||
) | ) | ||||
// APIContext is a specific context for API service | // APIContext is a specific context for API service | ||||
type APIContext struct { | type APIContext struct { | ||||
*Base | *Base | ||||
Cache cache.Cache | |||||
Cache cache.StringCache | |||||
Doer *user_model.User // current signed-in user | Doer *user_model.User // current signed-in user | ||||
IsSigned bool | IsSigned bool | ||||
base, baseCleanUp := NewBaseContext(w, req) | base, baseCleanUp := NewBaseContext(w, req) | ||||
ctx := &APIContext{ | ctx := &APIContext{ | ||||
Base: base, | Base: base, | ||||
Cache: mc.GetCache(), | |||||
Cache: cache.GetCache(), | |||||
Repo: &Repository{PullRequest: &PullRequest{}}, | Repo: &Repository{PullRequest: &PullRequest{}}, | ||||
Org: &APIOrganization{}, | Org: &APIOrganization{}, | ||||
} | } |
cpt = captcha.NewCaptcha(captcha.Options{ | cpt = captcha.NewCaptcha(captcha.Options{ | ||||
SubURL: setting.AppSubURL, | SubURL: setting.AppSubURL, | ||||
}) | }) | ||||
cpt.Store = cache.GetCache() | |||||
cpt.Store = cache.GetCache().ChiCache() | |||||
}) | }) | ||||
return cpt | return cpt | ||||
} | } |
"code.gitea.io/gitea/models/unit" | "code.gitea.io/gitea/models/unit" | ||||
user_model "code.gitea.io/gitea/models/user" | user_model "code.gitea.io/gitea/models/user" | ||||
mc "code.gitea.io/gitea/modules/cache" | |||||
"code.gitea.io/gitea/modules/cache" | |||||
"code.gitea.io/gitea/modules/gitrepo" | "code.gitea.io/gitea/modules/gitrepo" | ||||
"code.gitea.io/gitea/modules/httpcache" | "code.gitea.io/gitea/modules/httpcache" | ||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
"code.gitea.io/gitea/modules/web/middleware" | "code.gitea.io/gitea/modules/web/middleware" | ||||
web_types "code.gitea.io/gitea/modules/web/types" | web_types "code.gitea.io/gitea/modules/web/types" | ||||
"gitea.com/go-chi/cache" | |||||
"gitea.com/go-chi/session" | "gitea.com/go-chi/session" | ||||
) | ) | ||||
Render Render | Render Render | ||||
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData` | PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData` | ||||
Cache cache.Cache | |||||
Cache cache.StringCache | |||||
Csrf CSRFProtector | Csrf CSRFProtector | ||||
Flash *middleware.Flash | Flash *middleware.Flash | ||||
Session session.Store | Session session.Store | ||||
Render: render, | Render: render, | ||||
Session: session, | Session: session, | ||||
Cache: mc.GetCache(), | |||||
Cache: cache.GetCache(), | |||||
Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"), | Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"), | ||||
Repo: &Repository{PullRequest: &PullRequest{}}, | Repo: &Repository{PullRequest: &PullRequest{}}, | ||||
Org: &Organization{}, | Org: &Organization{}, |
"code.gitea.io/gitea/modules/queue" | "code.gitea.io/gitea/modules/queue" | ||||
repo_module "code.gitea.io/gitea/modules/repository" | repo_module "code.gitea.io/gitea/modules/repository" | ||||
"code.gitea.io/gitea/modules/timeutil" | "code.gitea.io/gitea/modules/timeutil" | ||||
"code.gitea.io/gitea/modules/util" | |||||
webhook_module "code.gitea.io/gitea/modules/webhook" | webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
notify_service "code.gitea.io/gitea/services/notify" | notify_service "code.gitea.io/gitea/services/notify" | ||||
files_service "code.gitea.io/gitea/services/repository/files" | files_service "code.gitea.io/gitea/services/repository/files" | ||||
// getDivergenceFromCache gets the divergence from cache | // getDivergenceFromCache gets the divergence from cache | ||||
func getDivergenceFromCache(repoID int64, branchName string) (*git.DivergeObject, bool) { | func getDivergenceFromCache(repoID int64, branchName string) (*git.DivergeObject, bool) { | ||||
data := cache.GetCache().Get(getDivergenceCacheKey(repoID, branchName)) | |||||
data, ok := cache.GetCache().Get(getDivergenceCacheKey(repoID, branchName)) | |||||
res := git.DivergeObject{ | res := git.DivergeObject{ | ||||
Ahead: -1, | Ahead: -1, | ||||
Behind: -1, | Behind: -1, | ||||
} | } | ||||
s, ok := data.([]byte) | |||||
if !ok || len(s) == 0 { | |||||
if !ok || data == "" { | |||||
return &res, false | return &res, false | ||||
} | } | ||||
if err := json.Unmarshal(s, &res); err != nil { | |||||
if err := json.Unmarshal(util.UnsafeStringToBytes(data), &res); err != nil { | |||||
log.Error("json.UnMarshal failed: %v", err) | log.Error("json.UnMarshal failed: %v", err) | ||||
return &res, false | return &res, false | ||||
} | } | ||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
return cache.GetCache().Put(getDivergenceCacheKey(repoID, branchName), bs, 30*24*60*60) | |||||
return cache.GetCache().Put(getDivergenceCacheKey(repoID, branchName), util.UnsafeBytesToString(bs), 30*24*60*60) | |||||
} | } | ||||
func DelDivergenceFromCache(repoID int64, branchName string) error { | func DelDivergenceFromCache(repoID int64, branchName string) error { |
func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheValue { | func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheValue { | ||||
c := cache.GetCache() | c := cache.GetCache() | ||||
statusStr, ok := c.Get(getCacheKey(repoID, branchName)).(string) | |||||
statusStr, ok := c.Get(getCacheKey(repoID, branchName)) | |||||
if ok && statusStr != "" { | if ok && statusStr != "" { | ||||
var cv commitStatusCacheValue | var cv commitStatusCacheValue | ||||
err := json.Unmarshal([]byte(statusStr), &cv) | err := json.Unmarshal([]byte(statusStr), &cv) |
"code.gitea.io/gitea/models/avatars" | "code.gitea.io/gitea/models/avatars" | ||||
repo_model "code.gitea.io/gitea/models/repo" | repo_model "code.gitea.io/gitea/models/repo" | ||||
user_model "code.gitea.io/gitea/models/user" | user_model "code.gitea.io/gitea/models/user" | ||||
"code.gitea.io/gitea/modules/cache" | |||||
"code.gitea.io/gitea/modules/git" | "code.gitea.io/gitea/modules/git" | ||||
"code.gitea.io/gitea/modules/gitrepo" | "code.gitea.io/gitea/modules/gitrepo" | ||||
"code.gitea.io/gitea/modules/graceful" | "code.gitea.io/gitea/modules/graceful" | ||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
api "code.gitea.io/gitea/modules/structs" | api "code.gitea.io/gitea/modules/structs" | ||||
"gitea.com/go-chi/cache" | |||||
) | ) | ||||
const ( | const ( | ||||
} | } | ||||
// GetContributorStats returns contributors stats for git commits for given revision or default branch | // GetContributorStats returns contributors stats for git commits for given revision or default branch | ||||
func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) { | |||||
func GetContributorStats(ctx context.Context, cache cache.StringCache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) { | |||||
// as GetContributorStats is resource intensive we cache the result | // as GetContributorStats is resource intensive we cache the result | ||||
cacheKey := fmt.Sprintf(contributorStatsCacheKey, repo.FullName(), revision) | cacheKey := fmt.Sprintf(contributorStatsCacheKey, repo.FullName(), revision) | ||||
if !cache.IsExist(cacheKey) { | if !cache.IsExist(cacheKey) { | ||||
genReady := make(chan struct{}) | genReady := make(chan struct{}) | ||||
// dont start multible async generations | |||||
// dont start multiple async generations | |||||
_, run := generateLock.Load(cacheKey) | _, run := generateLock.Load(cacheKey) | ||||
if run { | if run { | ||||
return nil, ErrAwaitGeneration | return nil, ErrAwaitGeneration | ||||
} | } | ||||
} | } | ||||
// TODO: renew timeout of cache cache.UpdateTimeout(cacheKey, contributorStatsCacheTimeout) | // TODO: renew timeout of cache cache.UpdateTimeout(cacheKey, contributorStatsCacheTimeout) | ||||
switch v := cache.Get(cacheKey).(type) { | |||||
case error: | |||||
return nil, v | |||||
case map[string]*ContributorData: | |||||
return v, nil | |||||
default: | |||||
return nil, fmt.Errorf("unexpected type in cache detected") | |||||
var res map[string]*ContributorData | |||||
if _, cacheErr := cache.GetJSON(cacheKey, &res); cacheErr != nil { | |||||
return nil, fmt.Errorf("cached error: %w", cacheErr.ToError()) | |||||
} | } | ||||
return res, nil | |||||
} | } | ||||
// getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision | // getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision | ||||
return extendedCommitStats, nil | return extendedCommitStats, nil | ||||
} | } | ||||
func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey string, repo *repo_model.Repository, revision string) { | |||||
func generateContributorStats(genDone chan struct{}, cache cache.StringCache, cacheKey string, repo *repo_model.Repository, revision string) { | |||||
ctx := graceful.GetManager().HammerContext() | ctx := graceful.GetManager().HammerContext() | ||||
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) | gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) | ||||
if err != nil { | if err != nil { | ||||
err := fmt.Errorf("OpenRepository: %w", err) | |||||
_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout) | |||||
_ = cache.PutJSON(cacheKey, fmt.Errorf("OpenRepository: %w", err), contributorStatsCacheTimeout) | |||||
return | return | ||||
} | } | ||||
defer closer.Close() | defer closer.Close() | ||||
} | } | ||||
extendedCommitStats, err := getExtendedCommitStats(gitRepo, revision) | extendedCommitStats, err := getExtendedCommitStats(gitRepo, revision) | ||||
if err != nil { | if err != nil { | ||||
err := fmt.Errorf("ExtendedCommitStats: %w", err) | |||||
_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout) | |||||
_ = cache.PutJSON(cacheKey, fmt.Errorf("ExtendedCommitStats: %w", err), contributorStatsCacheTimeout) | |||||
return | return | ||||
} | } | ||||
if len(extendedCommitStats) == 0 { | if len(extendedCommitStats) == 0 { | ||||
err := fmt.Errorf("no commit stats returned for revision '%s'", revision) | |||||
_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout) | |||||
_ = cache.PutJSON(cacheKey, fmt.Errorf("no commit stats returned for revision '%s'", revision), contributorStatsCacheTimeout) | |||||
return | return | ||||
} | } | ||||
total.TotalCommits++ | total.TotalCommits++ | ||||
} | } | ||||
_ = cache.Put(cacheKey, contributorsCommitStats, contributorStatsCacheTimeout) | |||||
_ = cache.PutJSON(cacheKey, contributorsCommitStats, contributorStatsCacheTimeout) | |||||
generateLock.Delete(cacheKey) | generateLock.Delete(cacheKey) | ||||
if genDone != nil { | if genDone != nil { | ||||
genDone <- struct{}{} | genDone <- struct{}{} |
"code.gitea.io/gitea/models/db" | "code.gitea.io/gitea/models/db" | ||||
repo_model "code.gitea.io/gitea/models/repo" | repo_model "code.gitea.io/gitea/models/repo" | ||||
"code.gitea.io/gitea/models/unittest" | "code.gitea.io/gitea/models/unittest" | ||||
"code.gitea.io/gitea/modules/git" | |||||
"code.gitea.io/gitea/modules/cache" | |||||
"code.gitea.io/gitea/modules/setting" | |||||
"gitea.com/go-chi/cache" | |||||
"github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
) | ) | ||||
assert.NoError(t, unittest.PrepareTestDatabase()) | assert.NoError(t, unittest.PrepareTestDatabase()) | ||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) | repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) | ||||
assert.NoError(t, repo.LoadOwner(db.DefaultContext)) | assert.NoError(t, repo.LoadOwner(db.DefaultContext)) | ||||
mockCache, err := cache.NewCacher(cache.Options{ | |||||
Adapter: "memory", | |||||
Interval: 24 * 60, | |||||
}) | |||||
mockCache, err := cache.NewStringCache(setting.Cache{}) | |||||
assert.NoError(t, err) | assert.NoError(t, err) | ||||
generateContributorStats(nil, mockCache, "key", repo, "404ref") | generateContributorStats(nil, mockCache, "key", repo, "404ref") | ||||
err, isErr := mockCache.Get("key").(error) | |||||
assert.True(t, isErr) | |||||
assert.ErrorAs(t, err, &git.ErrNotExist{}) | |||||
var data map[string]*ContributorData | |||||
_, getErr := mockCache.GetJSON("key", &data) | |||||
assert.NotNil(t, getErr) | |||||
assert.ErrorContains(t, getErr.ToError(), "object does not exist") | |||||
generateContributorStats(nil, mockCache, "key2", repo, "master") | generateContributorStats(nil, mockCache, "key2", repo, "master") | ||||
data, isData := mockCache.Get("key2").(map[string]*ContributorData) | |||||
assert.True(t, isData) | |||||
exist, _ := mockCache.GetJSON("key2", &data) | |||||
assert.True(t, exist) | |||||
var keys []string | var keys []string | ||||
for k := range data { | for k := range data { | ||||
keys = append(keys, k) | keys = append(keys, k) |