2q.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. // Copyright (c) HashiCorp, Inc.
  2. // SPDX-License-Identifier: MPL-2.0
  3. package lru
  4. import (
  5. "errors"
  6. "sync"
  7. "github.com/hashicorp/golang-lru/v2/simplelru"
  8. )
  9. const (
  10. // Default2QRecentRatio is the ratio of the 2Q cache dedicated
  11. // to recently added entries that have only been accessed once.
  12. Default2QRecentRatio = 0.25
  13. // Default2QGhostEntries is the default ratio of ghost
  14. // entries kept to track entries recently evicted
  15. Default2QGhostEntries = 0.50
  16. )
  17. // TwoQueueCache is a thread-safe fixed size 2Q cache.
  18. // 2Q is an enhancement over the standard LRU cache
  19. // in that it tracks both frequently and recently used
  20. // entries separately. This avoids a burst in access to new
  21. // entries from evicting frequently used entries. It adds some
  22. // additional tracking overhead to the standard LRU cache, and is
  23. // computationally about 2x the cost, and adds some metadata over
  24. // head. The ARCCache is similar, but does not require setting any
  25. // parameters.
  26. type TwoQueueCache[K comparable, V any] struct {
  27. size int
  28. recentSize int
  29. recentRatio float64
  30. ghostRatio float64
  31. recent simplelru.LRUCache[K, V]
  32. frequent simplelru.LRUCache[K, V]
  33. recentEvict simplelru.LRUCache[K, struct{}]
  34. lock sync.RWMutex
  35. }
  36. // New2Q creates a new TwoQueueCache using the default
  37. // values for the parameters.
  38. func New2Q[K comparable, V any](size int) (*TwoQueueCache[K, V], error) {
  39. return New2QParams[K, V](size, Default2QRecentRatio, Default2QGhostEntries)
  40. }
  41. // New2QParams creates a new TwoQueueCache using the provided
  42. // parameter values.
  43. func New2QParams[K comparable, V any](size int, recentRatio, ghostRatio float64) (*TwoQueueCache[K, V], error) {
  44. if size <= 0 {
  45. return nil, errors.New("invalid size")
  46. }
  47. if recentRatio < 0.0 || recentRatio > 1.0 {
  48. return nil, errors.New("invalid recent ratio")
  49. }
  50. if ghostRatio < 0.0 || ghostRatio > 1.0 {
  51. return nil, errors.New("invalid ghost ratio")
  52. }
  53. // Determine the sub-sizes
  54. recentSize := int(float64(size) * recentRatio)
  55. evictSize := int(float64(size) * ghostRatio)
  56. // Allocate the LRUs
  57. recent, err := simplelru.NewLRU[K, V](size, nil)
  58. if err != nil {
  59. return nil, err
  60. }
  61. frequent, err := simplelru.NewLRU[K, V](size, nil)
  62. if err != nil {
  63. return nil, err
  64. }
  65. recentEvict, err := simplelru.NewLRU[K, struct{}](evictSize, nil)
  66. if err != nil {
  67. return nil, err
  68. }
  69. // Initialize the cache
  70. c := &TwoQueueCache[K, V]{
  71. size: size,
  72. recentSize: recentSize,
  73. recentRatio: recentRatio,
  74. ghostRatio: ghostRatio,
  75. recent: recent,
  76. frequent: frequent,
  77. recentEvict: recentEvict,
  78. }
  79. return c, nil
  80. }
  81. // Get looks up a key's value from the cache.
  82. func (c *TwoQueueCache[K, V]) Get(key K) (value V, ok bool) {
  83. c.lock.Lock()
  84. defer c.lock.Unlock()
  85. // Check if this is a frequent value
  86. if val, ok := c.frequent.Get(key); ok {
  87. return val, ok
  88. }
  89. // If the value is contained in recent, then we
  90. // promote it to frequent
  91. if val, ok := c.recent.Peek(key); ok {
  92. c.recent.Remove(key)
  93. c.frequent.Add(key, val)
  94. return val, ok
  95. }
  96. // No hit
  97. return
  98. }
  99. // Add adds a value to the cache.
  100. func (c *TwoQueueCache[K, V]) Add(key K, value V) {
  101. c.lock.Lock()
  102. defer c.lock.Unlock()
  103. // Check if the value is frequently used already,
  104. // and just update the value
  105. if c.frequent.Contains(key) {
  106. c.frequent.Add(key, value)
  107. return
  108. }
  109. // Check if the value is recently used, and promote
  110. // the value into the frequent list
  111. if c.recent.Contains(key) {
  112. c.recent.Remove(key)
  113. c.frequent.Add(key, value)
  114. return
  115. }
  116. // If the value was recently evicted, add it to the
  117. // frequently used list
  118. if c.recentEvict.Contains(key) {
  119. c.ensureSpace(true)
  120. c.recentEvict.Remove(key)
  121. c.frequent.Add(key, value)
  122. return
  123. }
  124. // Add to the recently seen list
  125. c.ensureSpace(false)
  126. c.recent.Add(key, value)
  127. }
  128. // ensureSpace is used to ensure we have space in the cache
  129. func (c *TwoQueueCache[K, V]) ensureSpace(recentEvict bool) {
  130. // If we have space, nothing to do
  131. recentLen := c.recent.Len()
  132. freqLen := c.frequent.Len()
  133. if recentLen+freqLen < c.size {
  134. return
  135. }
  136. // If the recent buffer is larger than
  137. // the target, evict from there
  138. if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) {
  139. k, _, _ := c.recent.RemoveOldest()
  140. c.recentEvict.Add(k, struct{}{})
  141. return
  142. }
  143. // Remove from the frequent list otherwise
  144. c.frequent.RemoveOldest()
  145. }
  146. // Len returns the number of items in the cache.
  147. func (c *TwoQueueCache[K, V]) Len() int {
  148. c.lock.RLock()
  149. defer c.lock.RUnlock()
  150. return c.recent.Len() + c.frequent.Len()
  151. }
  152. // Resize changes the cache size.
  153. func (c *TwoQueueCache[K, V]) Resize(size int) (evicted int) {
  154. c.lock.Lock()
  155. defer c.lock.Unlock()
  156. // Recalculate the sub-sizes
  157. recentSize := int(float64(size) * c.recentRatio)
  158. evictSize := int(float64(size) * c.ghostRatio)
  159. c.size = size
  160. c.recentSize = recentSize
  161. // ensureSpace
  162. diff := c.recent.Len() + c.frequent.Len() - size
  163. if diff < 0 {
  164. diff = 0
  165. }
  166. for i := 0; i < diff; i++ {
  167. c.ensureSpace(true)
  168. }
  169. // Reallocate the LRUs
  170. c.recent.Resize(size)
  171. c.frequent.Resize(size)
  172. c.recentEvict.Resize(evictSize)
  173. return diff
  174. }
  175. // Keys returns a slice of the keys in the cache.
  176. // The frequently used keys are first in the returned slice.
  177. func (c *TwoQueueCache[K, V]) Keys() []K {
  178. c.lock.RLock()
  179. defer c.lock.RUnlock()
  180. k1 := c.frequent.Keys()
  181. k2 := c.recent.Keys()
  182. return append(k1, k2...)
  183. }
  184. // Values returns a slice of the values in the cache.
  185. // The frequently used values are first in the returned slice.
  186. func (c *TwoQueueCache[K, V]) Values() []V {
  187. c.lock.RLock()
  188. defer c.lock.RUnlock()
  189. v1 := c.frequent.Values()
  190. v2 := c.recent.Values()
  191. return append(v1, v2...)
  192. }
  193. // Remove removes the provided key from the cache.
  194. func (c *TwoQueueCache[K, V]) Remove(key K) {
  195. c.lock.Lock()
  196. defer c.lock.Unlock()
  197. if c.frequent.Remove(key) {
  198. return
  199. }
  200. if c.recent.Remove(key) {
  201. return
  202. }
  203. if c.recentEvict.Remove(key) {
  204. return
  205. }
  206. }
  207. // Purge is used to completely clear the cache.
  208. func (c *TwoQueueCache[K, V]) Purge() {
  209. c.lock.Lock()
  210. defer c.lock.Unlock()
  211. c.recent.Purge()
  212. c.frequent.Purge()
  213. c.recentEvict.Purge()
  214. }
  215. // Contains is used to check if the cache contains a key
  216. // without updating recency or frequency.
  217. func (c *TwoQueueCache[K, V]) Contains(key K) bool {
  218. c.lock.RLock()
  219. defer c.lock.RUnlock()
  220. return c.frequent.Contains(key) || c.recent.Contains(key)
  221. }
  222. // Peek is used to inspect the cache value of a key
  223. // without updating recency or frequency.
  224. func (c *TwoQueueCache[K, V]) Peek(key K) (value V, ok bool) {
  225. c.lock.RLock()
  226. defer c.lock.RUnlock()
  227. if val, ok := c.frequent.Peek(key); ok {
  228. return val, ok
  229. }
  230. return c.recent.Peek(key)
  231. }