diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 22244545f589..251ec7a80e1c 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1471,7 +1471,7 @@ PATH = ;; if the cache enabled ;ENABLED = true ;; -;; Either "memory", "redis", or "memcache", default is "memory" +;; Either "memory", "redis", "memcache", or "twoqueue". default is "memory" ;ADAPTER = memory ;; ;; For "memory" only, GC interval in seconds, default is 60 @@ -1480,6 +1480,7 @@ PATH = ;; For "redis" and "memcache", connection host address ;; redis: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180 ;; memcache: `127.0.0.1:11211` +;; twoqueue: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` ;HOST = ;; ;; Time to keep items in cache if not used, default is 16 hours. diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 741c5f292cdf..611a7a887a9f 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -590,11 +590,12 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type ## Cache (`cache`) - `ENABLED`: **true**: Enable the cache. -- `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, or `memcache`. -- `INTERVAL`: **60**: Garbage Collection interval (sec), for memory cache only. -- `HOST`: **\**: Connection string for `redis` and `memcache`. +- `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, `twoqueue` or `memcache`. (`twoqueue` represents a size limited LRU cache.) +- `INTERVAL`: **60**: Garbage Collection interval (sec), for memory and twoqueue cache only. +- `HOST`: **\**: Connection string for `redis` and `memcache`. For `twoqueue` sets configuration for the queue. - Redis: `redis://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` - Memcache: `127.0.0.1:9090;127.0.0.1:9091` + - TwoQueue LRU cache: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` representing the maximum number of objects stored in the cache. - `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to 0 disables caching. ## Cache - LastCommitCache settings (`cache.last_commit`) diff --git a/modules/cache/cache_twoqueue.go b/modules/cache/cache_twoqueue.go new file mode 100644 index 000000000000..7d8fa7c93416 --- /dev/null +++ b/modules/cache/cache_twoqueue.go @@ -0,0 +1,204 @@ +// Copyright 2021 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 ( + "strconv" + "sync" + "time" + + mc "gitea.com/go-chi/cache" + lru "github.com/hashicorp/golang-lru" + jsoniter "github.com/json-iterator/go" +) + +// TwoQueueCache represents a LRU 2Q cache adapter implementation +type TwoQueueCache struct { + lock sync.Mutex + cache *lru.TwoQueueCache + interval int +} + +// TwoQueueCacheConfig describes the configuration for TwoQueueCache +type TwoQueueCacheConfig struct { + Size int `ini:"SIZE" json:"size"` + RecentRatio float64 `ini:"RECENT_RATIO" json:"recent_ratio"` + GhostRatio float64 `ini:"GHOST_RATIO" json:"ghost_ratio"` +} + +// MemoryItem represents a memory cache item. +type MemoryItem struct { + Val interface{} + Created int64 + Timeout int64 +} + +func (item *MemoryItem) hasExpired() bool { + return item.Timeout > 0 && + (time.Now().Unix()-item.Created) >= item.Timeout +} + +var _ mc.Cache = &TwoQueueCache{} + +// Put puts value into cache with key and expire time. +func (c *TwoQueueCache) Put(key string, val interface{}, timeout int64) error { + item := &MemoryItem{ + Val: val, + Created: time.Now().Unix(), + Timeout: timeout, + } + c.lock.Lock() + defer c.lock.Unlock() + c.cache.Add(key, item) + return nil +} + +// Get gets cached value by given key. +func (c *TwoQueueCache) Get(key string) interface{} { + c.lock.Lock() + defer c.lock.Unlock() + cached, ok := c.cache.Get(key) + if !ok { + return nil + } + item, ok := cached.(*MemoryItem) + + if !ok || item.hasExpired() { + c.cache.Remove(key) + return nil + } + + return item.Val +} + +// Delete deletes cached value by given key. +func (c *TwoQueueCache) Delete(key string) error { + c.lock.Lock() + defer c.lock.Unlock() + c.cache.Remove(key) + return nil +} + +// Incr increases cached int-type value by given key as a counter. +func (c *TwoQueueCache) Incr(key string) error { + c.lock.Lock() + defer c.lock.Unlock() + cached, ok := c.cache.Get(key) + if !ok { + return nil + } + item, ok := cached.(*MemoryItem) + + if !ok || item.hasExpired() { + c.cache.Remove(key) + return nil + } + + var err error + item.Val, err = mc.Incr(item.Val) + return err +} + +// Decr decreases cached int-type value by given key as a counter. +func (c *TwoQueueCache) Decr(key string) error { + c.lock.Lock() + defer c.lock.Unlock() + cached, ok := c.cache.Get(key) + if !ok { + return nil + } + item, ok := cached.(*MemoryItem) + + if !ok || item.hasExpired() { + c.cache.Remove(key) + return nil + } + + var err error + item.Val, err = mc.Decr(item.Val) + return err +} + +// IsExist returns true if cached value exists. +func (c *TwoQueueCache) IsExist(key string) bool { + c.lock.Lock() + defer c.lock.Unlock() + cached, ok := c.cache.Peek(key) + if !ok { + return false + } + item, ok := cached.(*MemoryItem) + if !ok || item.hasExpired() { + c.cache.Remove(key) + return false + } + + return true +} + +// Flush deletes all cached data. +func (c *TwoQueueCache) Flush() error { + c.lock.Lock() + defer c.lock.Unlock() + c.cache.Purge() + return nil +} + +func (c *TwoQueueCache) checkAndInvalidate(key interface{}) { + c.lock.Lock() + defer c.lock.Unlock() + cached, ok := c.cache.Peek(key) + if !ok { + return + } + item, ok := cached.(*MemoryItem) + if !ok || item.hasExpired() { + c.cache.Remove(item) + } +} + +func (c *TwoQueueCache) startGC() { + if c.interval < 0 { + return + } + for _, key := range c.cache.Keys() { + c.checkAndInvalidate(key) + } + time.AfterFunc(time.Duration(c.interval)*time.Second, c.startGC) +} + +// StartAndGC starts GC routine based on config string settings. +func (c *TwoQueueCache) StartAndGC(opts mc.Options) error { + var err error + size := 50000 + if opts.AdapterConfig != "" { + size, err = strconv.Atoi(opts.AdapterConfig) + } + if err != nil { + json := jsoniter.ConfigCompatibleWithStandardLibrary + if !json.Valid([]byte(opts.AdapterConfig)) { + return err + } + + cfg := &TwoQueueCacheConfig{ + Size: 50000, + RecentRatio: lru.Default2QRecentRatio, + GhostRatio: lru.Default2QGhostEntries, + } + _ = json.Unmarshal([]byte(opts.AdapterConfig), cfg) + c.cache, err = lru.New2QParams(cfg.Size, cfg.RecentRatio, cfg.GhostRatio) + } else { + c.cache, err = lru.New2Q(size) + } + c.interval = opts.Interval + if c.interval > 0 { + go c.startGC() + } + return err +} + +func init() { + mc.Register("twoqueue", &TwoQueueCache{}) +} diff --git a/modules/setting/cache.go b/modules/setting/cache.go index 7bfea919618e..2bfe2318f547 100644 --- a/modules/setting/cache.go +++ b/modules/setting/cache.go @@ -58,11 +58,16 @@ func newCacheService() { log.Fatal("Failed to map Cache settings: %v", err) } - CacheService.Adapter = sec.Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache"}) + CacheService.Adapter = sec.Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache", "twoqueue"}) switch CacheService.Adapter { case "memory": case "redis", "memcache": CacheService.Conn = strings.Trim(sec.Key("HOST").String(), "\" ") + case "twoqueue": + CacheService.Conn = strings.TrimSpace(sec.Key("HOST").String()) + if CacheService.Conn == "" { + CacheService.Conn = "50000" + } case "": // disable cache CacheService.Enabled = false default: diff --git a/routers/init.go b/routers/init.go index 05dbe4bd664e..3ee7c7357285 100644 --- a/routers/init.go +++ b/routers/init.go @@ -52,7 +52,9 @@ func NewServices() { log.Fatal("repository init failed: %v", err) } mailer.NewContext() - _ = cache.NewContext() + if err := cache.NewContext(); err != nil { + log.Fatal("Unable to start cache service: %v", err) + } notification.NewContext() if err := archiver.Init(); err != nil { log.Fatal("archiver init failed: %v", err)