Files
gitea/modules/cache/cache_twoqueue.go
T
bea6fcaa84 refactor: move go-chi/cache into Gitea (#39530)
The `gitea.com/go-chi/cache` package only exists for Gitea, so it moves
into `modules/cache`.

- `ITEM_TTL = -1` disables caching as documented
- Sub-second TTLs round up instead of never expiring
- The Redis adapter no longer grows a `MacaronCache` hash
- Use two-queue cache for in-memory cache

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-02 18:16:34 +02:00

127 lines
3.1 KiB
Go

// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cache
import (
"fmt"
"strconv"
"time"
"gitea.dev/modules/json"
lru "github.com/hashicorp/golang-lru/v2"
)
const twoQueueDefaultSize = 50000
type twoQueueCache struct {
cache *lru.TwoQueueCache[string, memoryItem] // wrap the existing thread-safe 2Q (two-queue) cache directly
}
type twoQueueCacheConfig struct {
Size int `json:"size"`
RecentRatio float64 `json:"recent_ratio"`
GhostRatio float64 `json:"ghost_ratio"`
}
func newTwoQueueCache(conn string, gcInterval time.Duration) (backend, error) {
lruCache, err := newTwoQueueLRU(conn)
if err != nil {
return nil, err
}
cache := &twoQueueCache{cache: lruCache}
startGC(gcInterval, cache.deleteExpired)
return cache, nil
}
func newTwoQueueLRU(conn string) (*lru.TwoQueueCache[string, memoryItem], error) {
if conn == "" {
return lru.New2Q[string, memoryItem](twoQueueDefaultSize)
}
if size, err := strconv.Atoi(conn); err == nil {
return lru.New2Q[string, memoryItem](size)
}
if !json.Valid([]byte(conn)) {
return nil, fmt.Errorf("invalid two-queue cache HOST %q, expected a size or a JSON config", conn)
}
config := twoQueueCacheConfig{
Size: twoQueueDefaultSize,
RecentRatio: lru.Default2QRecentRatio,
GhostRatio: lru.Default2QGhostEntries,
}
_ = json.Unmarshal([]byte(conn), &config)
return lru.New2QParams[string, memoryItem](config.Size, config.RecentRatio, config.GhostRatio)
}
func (c *twoQueueCache) Get(key string) (string, bool) {
item, ok := c.cache.Get(key)
if !ok {
return "", false
}
if item.expired(timeNow()) {
c.cache.Remove(key)
return "", false
}
return item.value, true
}
func (c *twoQueueCache) Put(key, value string, ttl int64) error {
item := newMemoryItem(value, ttl)
c.cache.Add(key, item)
return nil
}
func (c *twoQueueCache) Delete(key string) error {
c.cache.Remove(key)
return nil
}
func (c *twoQueueCache) IsExist(key string) bool {
item, ok := c.cache.Peek(key)
return ok && !item.expired(timeNow())
}
func (c *twoQueueCache) Ping() error {
return nil
}
func (c *twoQueueCache) deleteExpired() {
now := timeNow()
for _, key := range c.cache.Keys() {
if item, ok := c.cache.Peek(key); ok && item.expired(now) {
// here might be a slight data-race: the item might have been removed or updated by another goroutine between the Peek and Remove calls,
// but it's acceptable since the cache is not guaranteed to be 100% accurate and any item can be evicted at any time
c.cache.Remove(key)
}
}
}
type memoryItem struct {
value string
expiresAt time.Time
}
func newMemoryItem(value string, ttl int64) memoryItem {
item := memoryItem{value: value}
if ttl > 0 {
item.expiresAt = timeNow().Add(time.Duration(ttl) * time.Second)
}
return item
}
func (item memoryItem) expired(now time.Time) bool {
return !item.expiresAt.IsZero() && !now.Before(item.expiresAt)
}
func startGC(interval time.Duration, deleteExpired func()) {
if interval <= 0 {
return
}
go func() {
for range time.Tick(interval) {
deleteExpired()
}
}()
}