1// Copyright 2021 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package cache
5
6import (
7 "strconv"
8 "sync"
9 "time"
10
11 "forgejo.org/modules/json"
12
13 mc "code.forgejo.org/go-chi/cache"
14 lru "github.com/hashicorp/golang-lru/v2"
15)
16
17// TwoQueueCache represents a LRU 2Q cache adapter implementation
18type TwoQueueCache struct {
19 lock sync.Mutex
20 cache *lru.TwoQueueCache[string, any]
21 interval int
22}
23
24// TwoQueueCacheConfig describes the configuration for TwoQueueCache
25type TwoQueueCacheConfig struct {
26 Size int `ini:"SIZE" json:"size"`
27 RecentRatio float64 `ini:"RECENT_RATIO" json:"recent_ratio"`
28 GhostRatio float64 `ini:"GHOST_RATIO" json:"ghost_ratio"`
29}
30
31// MemoryItem represents a memory cache item.
32type MemoryItem struct {
33 Val any
34 Created int64
35 Timeout int64
36}
37
38func (item *MemoryItem) hasExpired() bool {
39 return item.Timeout > 0 &&
40 (time.Now().Unix()-item.Created) >= item.Timeout
41}
42
43var _ mc.Cache = &TwoQueueCache{}
44
45// Put puts value into cache with key and expire time.
46func (c *TwoQueueCache) Put(key string, val any, timeout int64) error {
47 item := &MemoryItem{
48 Val: val,
49 Created: time.Now().Unix(),
50 Timeout: timeout,
51 }
52 c.lock.Lock()
53 defer c.lock.Unlock()
54 c.cache.Add(key, item)
55 return nil
56}
57
58// Get gets cached value by given key.
59func (c *TwoQueueCache) Get(key string) any {
60 c.lock.Lock()
61 defer c.lock.Unlock()
62 cached, ok := c.cache.Get(key)
63 if !ok {
64 return nil
65 }
66 item, ok := cached.(*MemoryItem)
67
68 if !ok || item.hasExpired() {
69 c.cache.Remove(key)
70 return nil
71 }
72
73 return item.Val
74}
75
76// Delete deletes cached value by given key.
77func (c *TwoQueueCache) Delete(key string) error {
78 c.lock.Lock()
79 defer c.lock.Unlock()
80 c.cache.Remove(key)
81 return nil
82}
83
84// Incr increases cached int-type value by given key as a counter.
85func (c *TwoQueueCache) Incr(key string) error {
86 c.lock.Lock()
87 defer c.lock.Unlock()
88 cached, ok := c.cache.Get(key)
89 if !ok {
90 return nil
91 }
92 item, ok := cached.(*MemoryItem)
93
94 if !ok || item.hasExpired() {
95 c.cache.Remove(key)
96 return nil
97 }
98
99 var err error
100 item.Val, err = mc.Incr(item.Val)
101 return err
102}
103
104// Decr decreases cached int-type value by given key as a counter.
105func (c *TwoQueueCache) Decr(key string) error {
106 c.lock.Lock()
107 defer c.lock.Unlock()
108 cached, ok := c.cache.Get(key)
109 if !ok {
110 return nil
111 }
112 item, ok := cached.(*MemoryItem)
113
114 if !ok || item.hasExpired() {
115 c.cache.Remove(key)
116 return nil
117 }
118
119 var err error
120 item.Val, err = mc.Decr(item.Val)
121 return err
122}
123
124// IsExist returns true if cached value exists.
125func (c *TwoQueueCache) IsExist(key string) bool {
126 c.lock.Lock()
127 defer c.lock.Unlock()
128 cached, ok := c.cache.Peek(key)
129 if !ok {
130 return false
131 }
132 item, ok := cached.(*MemoryItem)
133 if !ok || item.hasExpired() {
134 c.cache.Remove(key)
135 return false
136 }
137
138 return true
139}
140
141// Flush deletes all cached data.
142func (c *TwoQueueCache) Flush() error {
143 c.lock.Lock()
144 defer c.lock.Unlock()
145 c.cache.Purge()
146 return nil
147}
148
149func (c *TwoQueueCache) checkAndInvalidate(key string) {
150 c.lock.Lock()
151 defer c.lock.Unlock()
152 cached, ok := c.cache.Peek(key)
153 if !ok {
154 return
155 }
156 item, ok := cached.(*MemoryItem)
157 if !ok || item.hasExpired() {
158 c.cache.Remove(key)
159 }
160}
161
162func (c *TwoQueueCache) startGC() {
163 if c.interval < 0 {
164 return
165 }
166 for _, key := range c.cache.Keys() {
167 c.checkAndInvalidate(key)
168 }
169 time.AfterFunc(time.Duration(c.interval)*time.Second, c.startGC)
170}
171
172// StartAndGC starts GC routine based on config string settings.
173func (c *TwoQueueCache) StartAndGC(opts mc.Options) error {
174 var err error
175 size := 50000
176 if opts.AdapterConfig != "" {
177 size, err = strconv.Atoi(opts.AdapterConfig)
178 }
179 if err != nil {
180 if !json.Valid([]byte(opts.AdapterConfig)) {
181 return err
182 }
183
184 cfg := &TwoQueueCacheConfig{
185 Size: 50000,
186 RecentRatio: lru.Default2QRecentRatio,
187 GhostRatio: lru.Default2QGhostEntries,
188 }
189 _ = json.Unmarshal([]byte(opts.AdapterConfig), cfg)
190 c.cache, err = lru.New2QParams[string, any](cfg.Size, cfg.RecentRatio, cfg.GhostRatio)
191 } else {
192 c.cache, err = lru.New2Q[string, any](size)
193 }
194 c.interval = opts.Interval
195 if c.interval > 0 {
196 go c.startGC()
197 }
198 return err
199}
200
201// Ping tests if the cache is alive.
202func (c *TwoQueueCache) Ping() error {
203 return mc.GenericPing(c)
204}
205
206func init() {
207 mc.Register("twoqueue", &TwoQueueCache{})
208}