* Cache last commit to accelerate the repository directory page visit * Default use default cache configuration * add tests for last commit cache * Simplify last commit cache * Revert Enabled back * Change the last commit cache default ttl to 8760h * Fix testtags/v1.12.0-dev
@@ -383,6 +383,7 @@ relation to port exhaustion. | |||||
## Cache (`cache`) | ## Cache (`cache`) | ||||
- `ENABLED`: **true**: Enable the cache. | |||||
- `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, or `memcache`. | - `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, or `memcache`. | ||||
- `INTERVAL`: **60**: Garbage Collection interval (sec), for memory cache only. | - `INTERVAL`: **60**: Garbage Collection interval (sec), for memory cache only. | ||||
- `HOST`: **\<empty\>**: Connection string for `redis` and `memcache`. | - `HOST`: **\<empty\>**: Connection string for `redis` and `memcache`. | ||||
@@ -390,6 +391,12 @@ relation to port exhaustion. | |||||
- Memcache: `127.0.0.1:9090;127.0.0.1:9091` | - Memcache: `127.0.0.1:9090;127.0.0.1:9091` | ||||
- `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to 0 disables caching. | - `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to 0 disables caching. | ||||
## Cache - LastCommitCache settings (`cache.last_commit`) | |||||
- `ENABLED`: **true**: Enable the cache. | |||||
- `ITEM_TTL`: **8760h**: Time to keep items in cache if not used, Setting it to 0 disables caching. | |||||
- `COMMITS_COUNT`: **1000**: Only enable the cache when repository's commits count great than. | |||||
## Session (`session`) | ## Session (`session`) | ||||
- `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, mysql, couchbase, memcache, nodb, postgres\]. | - `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, mysql, couchbase, memcache, nodb, postgres\]. | ||||
@@ -148,6 +148,7 @@ menu: | |||||
## Cache (`cache`) | ## Cache (`cache`) | ||||
- `ENABLED`: **true**: 是否启用。 | |||||
- `ADAPTER`: **memory**: 缓存引擎,可以为 `memory`, `redis` 或 `memcache`。 | - `ADAPTER`: **memory**: 缓存引擎,可以为 `memory`, `redis` 或 `memcache`。 | ||||
- `INTERVAL`: **60**: 只对内存缓存有效,GC间隔,单位秒。 | - `INTERVAL`: **60**: 只对内存缓存有效,GC间隔,单位秒。 | ||||
- `HOST`: **\<empty\>**: 针对redis和memcache有效,主机地址和端口。 | - `HOST`: **\<empty\>**: 针对redis和memcache有效,主机地址和端口。 | ||||
@@ -155,6 +156,12 @@ menu: | |||||
- Memache: `127.0.0.1:9090;127.0.0.1:9091` | - Memache: `127.0.0.1:9090;127.0.0.1:9091` | ||||
- `ITEM_TTL`: **16h**: 缓存项目失效时间,设置为 0 则禁用缓存。 | - `ITEM_TTL`: **16h**: 缓存项目失效时间,设置为 0 则禁用缓存。 | ||||
## Cache - LastCommitCache settings (`cache.last_commit`) | |||||
- `ENABLED`: **true**: 是否启用。 | |||||
- `ITEM_TTL`: **8760h**: 缓存项目失效时间,设置为 0 则禁用缓存。 | |||||
- `COMMITS_COUNT`: **1000**: 仅当仓库的提交数大于时才启用缓存。 | |||||
## Session (`session`) | ## Session (`session`) | ||||
- `PROVIDER`: Session 内容存储方式,可选 `memory`, `file`, `redis` 或 `mysql`。 | - `PROVIDER`: Session 内容存储方式,可选 `memory`, `file`, `redis` 或 `mysql`。 | ||||
@@ -7,8 +7,10 @@ package integrations | |||||
import ( | import ( | ||||
"fmt" | "fmt" | ||||
"net/http" | "net/http" | ||||
"path" | |||||
"strings" | "strings" | ||||
"testing" | "testing" | ||||
"time" | |||||
"code.gitea.io/gitea/modules/setting" | "code.gitea.io/gitea/modules/setting" | ||||
@@ -29,12 +31,71 @@ func TestViewRepo(t *testing.T) { | |||||
session.MakeRequest(t, req, http.StatusNotFound) | session.MakeRequest(t, req, http.StatusNotFound) | ||||
} | } | ||||
func TestViewRepo2(t *testing.T) { | |||||
func testViewRepo(t *testing.T) { | |||||
defer prepareTestEnv(t)() | defer prepareTestEnv(t)() | ||||
req := NewRequest(t, "GET", "/user3/repo3") | req := NewRequest(t, "GET", "/user3/repo3") | ||||
session := loginUser(t, "user2") | session := loginUser(t, "user2") | ||||
session.MakeRequest(t, req, http.StatusOK) | |||||
resp := session.MakeRequest(t, req, http.StatusOK) | |||||
htmlDoc := NewHTMLParser(t, resp.Body) | |||||
files := htmlDoc.doc.Find("#repo-files-table > TBODY > TR") | |||||
type file struct { | |||||
fileName string | |||||
commitID string | |||||
commitMsg string | |||||
commitTime string | |||||
} | |||||
var items []file | |||||
files.Each(func(i int, s *goquery.Selection) { | |||||
tds := s.Find("td") | |||||
var f file | |||||
tds.Each(func(i int, s *goquery.Selection) { | |||||
if i == 0 { | |||||
f.fileName = strings.TrimSpace(s.Text()) | |||||
} else if i == 1 { | |||||
a := s.Find("a") | |||||
f.commitMsg = strings.TrimSpace(a.Text()) | |||||
l, _ := a.Attr("href") | |||||
f.commitID = path.Base(l) | |||||
} | |||||
}) | |||||
f.commitTime, _ = s.Find("span.time-since").Attr("title") | |||||
items = append(items, f) | |||||
}) | |||||
assert.EqualValues(t, []file{ | |||||
{ | |||||
fileName: "doc", | |||||
commitID: "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6", | |||||
commitMsg: "init project", | |||||
commitTime: time.Date(2017, time.June, 14, 13, 54, 21, 0, time.UTC).Format(time.RFC1123), | |||||
}, | |||||
{ | |||||
fileName: "README.md", | |||||
commitID: "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6", | |||||
commitMsg: "init project", | |||||
commitTime: time.Date(2017, time.June, 14, 13, 54, 21, 0, time.UTC).Format(time.RFC1123), | |||||
}, | |||||
}, items) | |||||
} | |||||
func TestViewRepo2(t *testing.T) { | |||||
// no last commit cache | |||||
testViewRepo(t) | |||||
// enable last commit cache for all repositories | |||||
oldCommitsCount := setting.CacheService.LastCommit.CommitsCount | |||||
setting.CacheService.LastCommit.CommitsCount = 0 | |||||
// first view will not hit the cache | |||||
testViewRepo(t) | |||||
// second view will hit the cache | |||||
testViewRepo(t) | |||||
setting.CacheService.LastCommit.CommitsCount = oldCommitsCount | |||||
} | } | ||||
func TestViewRepo3(t *testing.T) { | func TestViewRepo3(t *testing.T) { | ||||
@@ -16,20 +16,28 @@ import ( | |||||
_ "gitea.com/macaron/cache/redis" | _ "gitea.com/macaron/cache/redis" | ||||
) | ) | ||||
var conn mc.Cache | |||||
var ( | |||||
conn mc.Cache | |||||
) | |||||
func newCache(cacheConfig setting.Cache) (mc.Cache, error) { | |||||
return mc.NewCacher(cacheConfig.Adapter, mc.Options{ | |||||
Adapter: cacheConfig.Adapter, | |||||
AdapterConfig: cacheConfig.Conn, | |||||
Interval: cacheConfig.Interval, | |||||
}) | |||||
} | |||||
// NewContext start cache service | // NewContext start cache service | ||||
func NewContext() error { | func NewContext() error { | ||||
if setting.CacheService == nil || conn != nil { | |||||
return nil | |||||
var err error | |||||
if conn == nil && setting.CacheService.Enabled { | |||||
if conn, err = newCache(setting.CacheService.Cache); err != nil { | |||||
return err | |||||
} | |||||
} | } | ||||
var err error | |||||
conn, err = mc.NewCacher(setting.CacheService.Adapter, mc.Options{ | |||||
Adapter: setting.CacheService.Adapter, | |||||
AdapterConfig: setting.CacheService.Conn, | |||||
Interval: setting.CacheService.Interval, | |||||
}) | |||||
return err | return err | ||||
} | } | ||||
@@ -0,0 +1,64 @@ | |||||
// Copyright 2020 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 cache | |||||
import ( | |||||
"fmt" | |||||
"code.gitea.io/gitea/modules/git" | |||||
"code.gitea.io/gitea/modules/log" | |||||
mc "gitea.com/macaron/cache" | |||||
"gopkg.in/src-d/go-git.v4/plumbing/object" | |||||
) | |||||
// LastCommitCache represents a cache to store last commit | |||||
type LastCommitCache struct { | |||||
repoPath string | |||||
ttl int64 | |||||
repo *git.Repository | |||||
commitCache map[string]*object.Commit | |||||
mc.Cache | |||||
} | |||||
// NewLastCommitCache creates a new last commit cache for repo | |||||
func NewLastCommitCache(repoPath string, gitRepo *git.Repository, ttl int64) *LastCommitCache { | |||||
return &LastCommitCache{ | |||||
repoPath: repoPath, | |||||
repo: gitRepo, | |||||
commitCache: make(map[string]*object.Commit), | |||||
ttl: ttl, | |||||
Cache: conn, | |||||
} | |||||
} | |||||
// Get get the last commit information by commit id and entry path | |||||
func (c LastCommitCache) Get(ref, entryPath string) (*object.Commit, error) { | |||||
v := c.Cache.Get(fmt.Sprintf("last_commit:%s:%s:%s", c.repoPath, ref, entryPath)) | |||||
if vs, ok := v.(string); ok { | |||||
log.Trace("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs) | |||||
if commit, ok := c.commitCache[vs]; ok { | |||||
log.Trace("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs) | |||||
return commit, nil | |||||
} | |||||
id, err := c.repo.ConvertToSHA1(vs) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
commit, err := c.repo.GoGitRepo().CommitObject(id) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
c.commitCache[vs] = commit | |||||
return commit, nil | |||||
} | |||||
return nil, nil | |||||
} | |||||
// Put put the last commit id with commit and entry path | |||||
func (c LastCommitCache) Put(ref, entryPath, commitID string) error { | |||||
log.Trace("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID) | |||||
return c.Cache.Put(fmt.Sprintf("last_commit:%s:%s:%s", c.repoPath, ref, entryPath), commitID, c.ttl) | |||||
} |
@@ -4,8 +4,10 @@ | |||||
package git | package git | ||||
import "gopkg.in/src-d/go-git.v4/plumbing/object" | |||||
// LastCommitCache cache | // LastCommitCache cache | ||||
type LastCommitCache interface { | type LastCommitCache interface { | ||||
Get(repoPath, ref, entryPath string) (*Commit, error) | |||||
Put(repoPath, ref, entryPath string, commit *Commit) error | |||||
Get(ref, entryPath string) (*object.Commit, error) | |||||
Put(ref, entryPath, commitID string) error | |||||
} | } |
@@ -5,6 +5,8 @@ | |||||
package git | package git | ||||
import ( | import ( | ||||
"path" | |||||
"github.com/emirpasic/gods/trees/binaryheap" | "github.com/emirpasic/gods/trees/binaryheap" | ||||
"gopkg.in/src-d/go-git.v4/plumbing" | "gopkg.in/src-d/go-git.v4/plumbing" | ||||
"gopkg.in/src-d/go-git.v4/plumbing/object" | "gopkg.in/src-d/go-git.v4/plumbing/object" | ||||
@@ -30,7 +32,29 @@ func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache LastCom | |||||
return nil, nil, err | return nil, nil, err | ||||
} | } | ||||
revs, err := getLastCommitForPaths(c, treePath, entryPaths) | |||||
var revs map[string]*object.Commit | |||||
if cache != nil { | |||||
var unHitPaths []string | |||||
revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, cache) | |||||
if err != nil { | |||||
return nil, nil, err | |||||
} | |||||
if len(unHitPaths) > 0 { | |||||
revs2, err := getLastCommitForPaths(c, treePath, unHitPaths) | |||||
if err != nil { | |||||
return nil, nil, err | |||||
} | |||||
for k, v := range revs2 { | |||||
if err := cache.Put(commit.ID.String(), path.Join(treePath, k), v.ID().String()); err != nil { | |||||
return nil, nil, err | |||||
} | |||||
revs[k] = v | |||||
} | |||||
} | |||||
} else { | |||||
revs, err = getLastCommitForPaths(c, treePath, entryPaths) | |||||
} | |||||
if err != nil { | if err != nil { | ||||
return nil, nil, err | return nil, nil, err | ||||
} | } | ||||
@@ -127,6 +151,25 @@ func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[ | |||||
return hashes, nil | return hashes, nil | ||||
} | } | ||||
func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache LastCommitCache) (map[string]*object.Commit, []string, error) { | |||||
var unHitEntryPaths []string | |||||
var results = make(map[string]*object.Commit) | |||||
for _, p := range paths { | |||||
lastCommit, err := cache.Get(commitID, path.Join(treePath, p)) | |||||
if err != nil { | |||||
return nil, nil, err | |||||
} | |||||
if lastCommit != nil { | |||||
results[p] = lastCommit | |||||
continue | |||||
} | |||||
unHitEntryPaths = append(unHitEntryPaths, p) | |||||
} | |||||
return results, unHitEntryPaths, nil | |||||
} | |||||
func getLastCommitForPaths(c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) { | func getLastCommitForPaths(c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) { | ||||
// We do a tree traversal with nodes sorted by commit time | // We do a tree traversal with nodes sorted by commit time | ||||
heap := binaryheap.NewWith(func(a, b interface{}) int { | heap := binaryheap.NewWith(func(a, b interface{}) int { | ||||
@@ -13,31 +13,71 @@ import ( | |||||
// Cache represents cache settings | // Cache represents cache settings | ||||
type Cache struct { | type Cache struct { | ||||
Enabled bool | |||||
Adapter string | Adapter string | ||||
Interval int | Interval int | ||||
Conn string | Conn string | ||||
TTL time.Duration | |||||
TTL time.Duration `ini:"ITEM_TTL"` | |||||
} | } | ||||
var ( | var ( | ||||
// CacheService the global cache | // CacheService the global cache | ||||
CacheService *Cache | |||||
CacheService = struct { | |||||
Cache | |||||
LastCommit struct { | |||||
Enabled bool | |||||
TTL time.Duration `ini:"ITEM_TTL"` | |||||
CommitsCount int64 | |||||
} `ini:"cache.last_commit"` | |||||
}{ | |||||
Cache: Cache{ | |||||
Enabled: true, | |||||
Adapter: "memory", | |||||
Interval: 60, | |||||
TTL: 16 * time.Hour, | |||||
}, | |||||
LastCommit: struct { | |||||
Enabled bool | |||||
TTL time.Duration `ini:"ITEM_TTL"` | |||||
CommitsCount int64 | |||||
}{ | |||||
Enabled: true, | |||||
TTL: 8760 * time.Hour, | |||||
CommitsCount: 1000, | |||||
}, | |||||
} | |||||
) | ) | ||||
func newCacheService() { | func newCacheService() { | ||||
sec := Cfg.Section("cache") | sec := Cfg.Section("cache") | ||||
CacheService = &Cache{ | |||||
Adapter: sec.Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache"}), | |||||
if err := sec.MapTo(&CacheService); err != nil { | |||||
log.Fatal("Failed to map Cache settings: %v", err) | |||||
} | } | ||||
CacheService.Adapter = sec.Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache"}) | |||||
switch CacheService.Adapter { | switch CacheService.Adapter { | ||||
case "memory": | case "memory": | ||||
CacheService.Interval = sec.Key("INTERVAL").MustInt(60) | |||||
case "redis", "memcache": | case "redis", "memcache": | ||||
CacheService.Conn = strings.Trim(sec.Key("HOST").String(), "\" ") | CacheService.Conn = strings.Trim(sec.Key("HOST").String(), "\" ") | ||||
case "": // disable cache | |||||
CacheService.Enabled = false | |||||
default: | default: | ||||
log.Fatal("Unknown cache adapter: %s", CacheService.Adapter) | log.Fatal("Unknown cache adapter: %s", CacheService.Adapter) | ||||
} | } | ||||
CacheService.TTL = sec.Key("ITEM_TTL").MustDuration(16 * time.Hour) | |||||
log.Info("Cache Service Enabled") | |||||
if CacheService.Enabled { | |||||
log.Info("Cache Service Enabled") | |||||
} | |||||
sec = Cfg.Section("cache.last_commit") | |||||
if !CacheService.Enabled { | |||||
CacheService.LastCommit.Enabled = false | |||||
} | |||||
CacheService.LastCommit.CommitsCount = sec.Key("COMMITS_COUNT").MustInt64(1000) | |||||
if CacheService.LastCommit.Enabled { | |||||
log.Info("Last Commit Cache Service Enabled") | |||||
} | |||||
} | } |
@@ -17,6 +17,7 @@ import ( | |||||
"code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
"code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
"code.gitea.io/gitea/modules/cache" | |||||
"code.gitea.io/gitea/modules/charset" | "code.gitea.io/gitea/modules/charset" | ||||
"code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
"code.gitea.io/gitea/modules/git" | "code.gitea.io/gitea/modules/git" | ||||
@@ -49,8 +50,13 @@ func renderDirectory(ctx *context.Context, treeLink string) { | |||||
} | } | ||||
entries.CustomSort(base.NaturalSortLess) | entries.CustomSort(base.NaturalSortLess) | ||||
var c git.LastCommitCache | |||||
if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount { | |||||
c = cache.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, int64(setting.CacheService.LastCommit.TTL.Seconds())) | |||||
} | |||||
var latestCommit *git.Commit | var latestCommit *git.Commit | ||||
ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(ctx.Repo.Commit, ctx.Repo.TreePath, nil) | |||||
ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(ctx.Repo.Commit, ctx.Repo.TreePath, c) | |||||
if err != nil { | if err != nil { | ||||
ctx.ServerError("GetCommitsInfo", err) | ctx.ServerError("GetCommitsInfo", err) | ||||
return | return | ||||