Browse Source

Refactor cache and disable go-chi cache (#30417)

use built-in cache package to wrap external go-chi cache package
tags/v1.22.0-rc1
wxiaoguang 1 month ago
parent
commit
c248f010ad
No account linked to committer's email address

+ 2
- 0
.golangci.yml View File

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

+ 32
- 106
modules/cache/cache.go View File

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

+ 1
- 1
modules/cache/cache_redis.go View File

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



+ 2
- 38
modules/cache/cache_test.go View File

) )


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



+ 1
- 1
modules/cache/cache_twoqueue.go View File



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



+ 120
- 0
modules/cache/string_cache.go View File

// 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
}

+ 4
- 11
modules/git/last_commit_cache.go View File

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

+ 2
- 4
routers/api/v1/misc/nodeinfo.go View File



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
} }

+ 3
- 5
services/context/api.go View File



"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{},
} }

+ 1
- 1
services/context/captcha.go View File

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
} }

+ 3
- 4
services/context/context.go View File



"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{},

+ 5
- 6
services/repository/branch.go View File

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

+ 1
- 1
services/repository/commitstatus/commitstatus.go View File



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)

+ 12
- 20
services/repository/contributors_graph.go View File

"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{}{}

+ 9
- 11
services/repository/contributors_graph_test.go View File

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

Loading…
Cancel
Save