├── .gitignore ├── LICENSE ├── README.md ├── doc └── gokachu.png ├── example └── main.go ├── go.mod ├── gokachu.go ├── gokachu_test.go └── replacement_strategy.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kaan Kuscu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![gokachu](./doc/gokachu.png) 2 | 3 | [![release](https://img.shields.io/github/release/ksckaan1/gokachu.svg)](https://github.com/ksckaan1/gokachu/releases) 4 | ![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.21-%23007d9c) 5 | [![GoDoc](https://godoc.org/github.com/ksckaan1/gokachu?status.svg)](https://pkg.go.dev/github.com/ksckaan1/gokachu) 6 | [![Go report](https://goreportcard.com/badge/github.com/ksckaan1/gokachu)](https://goreportcard.com/report/github.com/ksckaan1/gokachu) 7 | ![m2s](https://img.shields.io/badge/coverage-95.9%25-green?style=flat) 8 | [![Contributors](https://img.shields.io/github/contributors/ksckaan1/gokachu)](https://github.com/ksckaan1/gokachu/graphs/contributors) 9 | [![LICENSE](https://img.shields.io/badge/LICENCE-MIT-orange?style=flat)](./LICENSE) 10 | 11 | In-memory cache with TTL and generics support. 12 | 13 | ## Features 14 | - TTL support 15 | - Generics support 16 | - Supported Cache Replacement Strategies 17 | - LRU (Least Recently Used) 18 | - MRU (Most Recently Used) 19 | - LFU (Least Frequently Used) 20 | - MFU (Most Frequently Used) 21 | - FIFO (First In First Out) 22 | - LIFO (Last In First Out) 23 | - NONE (no replacement) 24 | 25 | ## Installation 26 | 27 | ```bash 28 | go get -u github.com/ksckaan1/gokachu@latest 29 | ``` 30 | 31 | ## Example 32 | 33 | ```go 34 | package main 35 | 36 | import ( 37 | "fmt" 38 | "strings" 39 | "time" 40 | 41 | "github.com/ksckaan1/gokachu" 42 | ) 43 | 44 | func main() { 45 | cache := gokachu.New[string, string](gokachu.Config{ 46 | ReplacementStrategy: gokachu.ReplacementStrategyLRU, 47 | MaxRecordTreshold: 1_000, // When it reaches 1_000 records, 48 | CleanNum: 100, // Cleans 100 records. 49 | }) 50 | defer cache.Close() 51 | 52 | // Set with TTL 53 | cache.SetWithTTL("token/user_id:1", "eyJhbGciOiJ...", 30*time.Minute) 54 | 55 | // Set without TTL 56 | cache.Set("get_user_response/user_id:1", "John Doe") 57 | cache.Set("get_user_response/user_id:2", "Jane Doe") 58 | 59 | // Delete specific key 60 | cache.Delete("get_user_response/user_id:1") 61 | 62 | // Delete keys with "token" prefix 63 | cache.DeleteFunc(func(key, _ string) bool { 64 | return strings.HasPrefix(key, "token") 65 | }) 66 | 67 | // Get (uses comma ok idiom) 68 | fmt.Println(cache.Get("get_user_response/user_id:2")) // eyJhbGciOiJ..., true 69 | fmt.Println(cache.Get("get_user_response/user_id:1")) // "", false 70 | 71 | fmt.Println("keys", cache.Keys()) // List of keys 72 | 73 | // List only keys start with "token" 74 | filteredKeys := cache.KeysFunc(func(key, _ string) bool { 75 | return strings.HasPrefix(key, "token") 76 | }) 77 | fmt.Println("filteredKeys", filteredKeys) 78 | 79 | fmt.Println("count", cache.Count()) // Number of keys 80 | 81 | // Count only keys start with "token" 82 | filteredCount := cache.CountFunc(func(key, _ string) bool { 83 | return strings.HasPrefix(key, "token") 84 | }) 85 | fmt.Println("filteredCount", filteredCount) 86 | 87 | cache.Flush() // Deletes all keys 88 | } 89 | 90 | ``` 91 | 92 | ## Benchmark Tests 93 | 94 | ### Set With TTL / Set Without TTL 95 | ```bash 96 | goos: darwin 97 | goarch: arm64 98 | pkg: github.com/ksckaan1/gokachu 99 | BenchmarkGokachuSetWithTTL 100 | BenchmarkGokachuSetWithTTL-8 4838650 236.8 ns/op 129 B/op 4 allocs/op 101 | PASS 102 | ok github.com/ksckaan1/gokachu 1.842s 103 | ``` 104 | 105 | ### Get 106 | ```bash 107 | goos: darwin 108 | goarch: arm64 109 | pkg: github.com/ksckaan1/gokachu 110 | BenchmarkGokachuGet 111 | BenchmarkGokachuGet-8 83910825 13.98 ns/op 0 B/op 0 allocs/op 112 | PASS 113 | ok github.com/ksckaan1/gokachu 2.094s 114 | ``` 115 | 116 | -------------------------------------------------------------------------------- /doc/gokachu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksckaan1/gokachu/8d8b9a251088ac9bcfc984025a6a32f22cca5d93/doc/gokachu.png -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/ksckaan1/gokachu" 9 | ) 10 | 11 | func main() { 12 | cache := gokachu.New[string, string](gokachu.Config{ 13 | ReplacementStrategy: gokachu.ReplacementStrategyLRU, 14 | MaxRecordTreshold: 1_000, // When it reaches 1_000 records, 15 | CleanNum: 100, // Cleans 100 records. 16 | }) 17 | defer cache.Close() 18 | 19 | // Set with TTL 20 | cache.SetWithTTL("token/user_id:1", "eyJhbGciOiJ...", 30*time.Minute) 21 | 22 | // Set without TTL 23 | cache.Set("get_user_response/user_id:1", "John Doe") 24 | cache.Set("get_user_response/user_id:2", "Jane Doe") 25 | 26 | // Delete specific key 27 | cache.Delete("get_user_response/user_id:1") 28 | 29 | // Delete keys with "token" prefix 30 | cache.DeleteFunc(func(key, _ string) bool { 31 | return strings.HasPrefix(key, "token") 32 | }) 33 | 34 | // Get (uses comma ok idiom) 35 | fmt.Println(cache.Get("get_user_response/user_id:2")) // eyJhbGciOiJ..., true 36 | fmt.Println(cache.Get("get_user_response/user_id:1")) // "", false 37 | 38 | fmt.Println("keys", cache.Keys()) // List of keys 39 | 40 | // List only keys start with "token" 41 | filteredKeys := cache.KeysFunc(func(key, _ string) bool { 42 | return strings.HasPrefix(key, "token") 43 | }) 44 | fmt.Println("filteredKeys", filteredKeys) 45 | 46 | fmt.Println("count", cache.Count()) // Number of keys 47 | 48 | // Count only keys start with "token" 49 | filteredCount := cache.CountFunc(func(key, _ string) bool { 50 | return strings.HasPrefix(key, "token") 51 | }) 52 | fmt.Println("filteredCount", filteredCount) 53 | 54 | cache.Flush() // Deletes all keys 55 | } 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ksckaan1/gokachu 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /gokachu.go: -------------------------------------------------------------------------------- 1 | package gokachu 2 | 3 | import ( 4 | "container/list" 5 | "slices" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type valueWithTTL[K comparable, V any] struct { 11 | key K 12 | value V 13 | hitCount uint 14 | expireTime time.Time 15 | } 16 | 17 | type Gokachu[K comparable, V any] struct { 18 | elems *list.List // front of list == greater risk of deletion <---------list---------> back of list == less risk of deletion 19 | store map[K]*list.Element 20 | mut *sync.Mutex 21 | maxRecordThreshold int 22 | cleanNum int 23 | replacementStrategy ReplacementStrategy 24 | pollInterval time.Duration 25 | pollCancel chan struct{} 26 | wg *sync.WaitGroup 27 | } 28 | 29 | type Config struct { 30 | ReplacementStrategy ReplacementStrategy // default: ReplacementStrategyNone 31 | MaxRecordTreshold int // This parameter is used to control the maximum number of records in the cache. If the number of records exceeds this threshold, records will be deleted according to the replacement strategy. 32 | CleanNum int // This parameter is used to control the number of records to be deleted. 33 | PollInterval time.Duration // This parameter is used to control the polling interval. If value is 0, uses default = 1 second. 34 | } 35 | 36 | func New[K comparable, V any](cfg Config) *Gokachu[K, V] { 37 | pollInterval := time.Second 38 | if cfg.PollInterval > 0 { 39 | pollInterval = cfg.PollInterval 40 | } 41 | 42 | g := &Gokachu[K, V]{ 43 | elems: list.New(), 44 | store: make(map[K]*list.Element), 45 | mut: new(sync.Mutex), 46 | maxRecordThreshold: cfg.MaxRecordTreshold, 47 | cleanNum: cfg.CleanNum, 48 | replacementStrategy: cfg.ReplacementStrategy, 49 | pollInterval: pollInterval, 50 | pollCancel: make(chan struct{}), 51 | wg: new(sync.WaitGroup), 52 | } 53 | 54 | g.wg.Add(1) 55 | go g.poll() 56 | 57 | return g 58 | } 59 | 60 | // Set sets a value in the cache. 61 | func (g *Gokachu[K, V]) Set(key K, v V) { 62 | g.set(key, v, 0) 63 | } 64 | 65 | // SetWithTTL sets a value in the cache with a TTL. If the TTL is 0, the value will not expire. 66 | func (g *Gokachu[K, V]) SetWithTTL(key K, v V, ttl time.Duration) { 67 | g.set(key, v, ttl) 68 | } 69 | 70 | // Get gets a value from the cache. Returns false in second value if the key does not exist. 71 | func (g *Gokachu[K, V]) Get(key K) (V, bool) { 72 | defer g.lock()() 73 | 74 | item, ok := g.store[key] 75 | if !ok { 76 | return *new(V), false 77 | } 78 | 79 | value := item.Value.(*valueWithTTL[K, V]) 80 | 81 | if g.replacementStrategy == ReplacementStrategyMFU || g.replacementStrategy == ReplacementStrategyLFU { 82 | value.hitCount++ 83 | } 84 | 85 | switch g.replacementStrategy { 86 | case ReplacementStrategyLRU: 87 | g.elems.MoveToBack(item) 88 | case ReplacementStrategyMRU: 89 | g.elems.MoveToFront(item) 90 | case ReplacementStrategyMFU, ReplacementStrategyLFU: 91 | g.moveByHits(item) 92 | } 93 | 94 | return value.value, true 95 | } 96 | 97 | // Delete deletes a value from the cache. 98 | func (g *Gokachu[K, V]) Delete(key K) { 99 | defer g.lock()() 100 | g.elems.Remove(g.store[key]) 101 | delete(g.store, key) 102 | } 103 | 104 | // DeleteFunc deletes values from the cache for which the callback returns true. 105 | func (g *Gokachu[K, V]) DeleteFunc(cb func(key K, value V) bool) { 106 | defer g.lock()() 107 | for key, value := range g.store { 108 | if cb(key, value.Value.(*valueWithTTL[K, V]).value) { 109 | g.elems.Remove(value) 110 | delete(g.store, key) 111 | } 112 | } 113 | } 114 | 115 | // Flush deletes all values from the cache. 116 | func (g *Gokachu[K, V]) Flush() { 117 | defer g.lock()() 118 | g.elems.Init() 119 | clear(g.store) 120 | } 121 | 122 | // Keys returns all keys in the cache. 123 | func (g *Gokachu[K, V]) Keys() []K { 124 | defer g.lock()() 125 | keys := make([]K, 0, len(g.store)) 126 | for e := g.elems.Front(); e != nil; e = e.Next() { 127 | keys = append(keys, e.Value.(*valueWithTTL[K, V]).key) 128 | } 129 | return keys 130 | } 131 | 132 | // KeysFunc returns all keys in the cache for which the callback returns true. 133 | func (g *Gokachu[K, V]) KeysFunc(cb func(key K, value V) bool) []K { 134 | defer g.lock()() 135 | keys := make([]K, 0, len(g.store)) 136 | for e := g.elems.Front(); e != nil; e = e.Next() { 137 | if cb(e.Value.(*valueWithTTL[K, V]).key, e.Value.(*valueWithTTL[K, V]).value) { 138 | keys = append(keys, e.Value.(*valueWithTTL[K, V]).key) 139 | } 140 | } 141 | return slices.Clip(keys) 142 | } 143 | 144 | // Count returns the number of values in the cache. 145 | func (g *Gokachu[K, V]) Count() int { 146 | defer g.lock()() 147 | return g.elems.Len() 148 | } 149 | 150 | // CountFunc returns the number of values in the cache for which the callback returns true. 151 | func (g *Gokachu[K, V]) CountFunc(cb func(key K, value V) bool) int { 152 | defer g.lock()() 153 | count := 0 154 | for key, value := range g.store { 155 | if cb(key, value.Value.(*valueWithTTL[K, V]).value) { 156 | count++ 157 | } 158 | } 159 | return count 160 | } 161 | 162 | // Close closes the cache. 163 | func (g *Gokachu[K, V]) Close() { 164 | if g.pollCancel == nil { 165 | return 166 | } 167 | g.lock()() 168 | close(g.pollCancel) 169 | clear(g.store) 170 | g.elems.Init() 171 | g.wg.Wait() 172 | } 173 | 174 | // Poll deletes expired values from the cache with the given poll interval. If context is cancelled, the polling stops. 175 | func (g *Gokachu[K, V]) poll() { 176 | ticker := time.NewTicker(g.pollInterval) 177 | defer ticker.Stop() 178 | 179 | for { 180 | select { 181 | case <-g.pollCancel: 182 | g.pollCancel = nil 183 | g.wg.Done() 184 | return 185 | case <-ticker.C: 186 | g.mut.Lock() 187 | now := time.Now() 188 | for key := range g.store { 189 | if g.store[key].Value.(*valueWithTTL[K, V]).expireTime.IsZero() { 190 | continue 191 | } 192 | if elem := g.store[key]; elem.Value.(*valueWithTTL[K, V]).expireTime.After(now) { 193 | g.elems.Remove(elem) 194 | delete(g.store, key) 195 | } 196 | } 197 | g.mut.Unlock() 198 | } 199 | } 200 | } 201 | 202 | func (k *Gokachu[K, V]) lock() func() { 203 | k.mut.Lock() 204 | return k.mut.Unlock 205 | } 206 | 207 | func (g *Gokachu[K, V]) set(key K, v V, ttl time.Duration) { 208 | if g.pollCancel == nil { 209 | return 210 | } 211 | defer g.lock()() 212 | if g.maxRecordThreshold > 0 && g.cleanNum > 0 && g.replacementStrategy > ReplacementStrategyNone && len(g.store) >= g.maxRecordThreshold { 213 | g.clean() 214 | } 215 | 216 | exp := time.Time{} 217 | if ttl > 0 { 218 | exp = time.Now().Add(ttl) 219 | } 220 | 221 | value := &valueWithTTL[K, V]{ 222 | key: key, 223 | value: v, 224 | expireTime: exp, 225 | } 226 | 227 | // if exists 228 | if oldElem, ok := g.store[key]; ok { 229 | oldElem.Value = value 230 | switch g.replacementStrategy { 231 | case ReplacementStrategyLRU: 232 | g.elems.MoveToBack(oldElem) 233 | case ReplacementStrategyMRU: 234 | g.elems.MoveToFront(oldElem) 235 | } 236 | return 237 | } 238 | 239 | // if not exists 240 | switch g.replacementStrategy { 241 | case ReplacementStrategyFIFO, ReplacementStrategyLRU, ReplacementStrategyLFU, ReplacementStrategyMFU, ReplacementStrategyNone: 242 | g.store[key] = g.elems.PushBack(value) 243 | case ReplacementStrategyLIFO, ReplacementStrategyMRU: 244 | g.store[key] = g.elems.PushFront(value) 245 | } 246 | } 247 | 248 | func (g *Gokachu[K, V]) moveByHits(elem *list.Element) { 249 | prev := elem.Prev() 250 | next := elem.Next() 251 | switch g.replacementStrategy { 252 | case ReplacementStrategyLFU: 253 | if prev != nil && prev.Value.(*valueWithTTL[K, V]).hitCount > elem.Value.(*valueWithTTL[K, V]).hitCount { 254 | g.elems.MoveBefore(elem, prev) 255 | g.moveByHits(elem) 256 | return 257 | } 258 | 259 | if next != nil && next.Value.(*valueWithTTL[K, V]).hitCount < elem.Value.(*valueWithTTL[K, V]).hitCount { 260 | g.elems.MoveAfter(elem, next) 261 | g.moveByHits(elem) 262 | } 263 | case ReplacementStrategyMFU: 264 | if prev != nil && prev.Value.(*valueWithTTL[K, V]).hitCount < elem.Value.(*valueWithTTL[K, V]).hitCount { 265 | g.elems.MoveBefore(elem, prev) 266 | g.moveByHits(elem) 267 | return 268 | } 269 | 270 | if next != nil && next.Value.(*valueWithTTL[K, V]).hitCount > elem.Value.(*valueWithTTL[K, V]).hitCount { 271 | g.elems.MoveAfter(elem, next) 272 | g.moveByHits(elem) 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /gokachu_test.go: -------------------------------------------------------------------------------- 1 | package gokachu 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "runtime" 7 | "strings" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func BenchmarkGokachuSetWithTTL(b *testing.B) { 13 | k := New[string, string](Config{ 14 | ReplacementStrategy: ReplacementStrategyLRU, 15 | MaxRecordTreshold: 1000, 16 | CleanNum: 100, 17 | }) 18 | defer k.Close() 19 | 20 | b.ResetTimer() 21 | 22 | for i := 0; i < b.N; i++ { 23 | k.SetWithTTL(fmt.Sprint(i), "value", 30*time.Minute) 24 | } 25 | } 26 | 27 | func BenchmarkGokachuGet(b *testing.B) { 28 | k := New[string, string](Config{ 29 | ReplacementStrategy: ReplacementStrategyLRU, 30 | MaxRecordTreshold: 1000, 31 | CleanNum: 100, 32 | }) 33 | defer k.Close() 34 | 35 | k.SetWithTTL("key", "value", 30*time.Minute) 36 | 37 | b.ResetTimer() 38 | 39 | for i := 0; i < b.N; i++ { 40 | k.Get("key") 41 | } 42 | } 43 | 44 | func TestGokachuReplacementStrategies(t *testing.T) { 45 | t.Run("when reaches max record threshold, then clean", func(t *testing.T) { 46 | k := New[string, string](Config{ 47 | ReplacementStrategy: ReplacementStrategyFIFO, 48 | MaxRecordTreshold: 1000, 49 | CleanNum: 100, 50 | }) 51 | defer k.Close() 52 | 53 | for i := 0; i < 1001; i++ { 54 | k.Set(fmt.Sprint(i), "value") 55 | } 56 | 57 | if k.Count() != 901 { 58 | t.Errorf("expected count to be 901, but got %d", k.Count()) 59 | } 60 | }) 61 | 62 | t.Run("when cleans, then check sort of keys by FIFO", func(t *testing.T) { 63 | k := New[string, string](Config{ 64 | ReplacementStrategy: ReplacementStrategyFIFO, 65 | MaxRecordTreshold: 10, 66 | CleanNum: 6, 67 | }) 68 | defer k.Close() 69 | 70 | for i := 0; i < 11; i++ { 71 | k.Set(fmt.Sprint(i), "value") 72 | } 73 | 74 | if k.Count() != 5 { 75 | t.Errorf("expected count to be 5, but got %d", k.Count()) 76 | } 77 | 78 | if !reflect.DeepEqual(k.Keys(), []string{"6", "7", "8", "9", "10"}) { 79 | t.Errorf("expected keys to be [6, 7, 8, 9, 10], but got %v", k.Keys()) 80 | } 81 | }) 82 | 83 | t.Run("when cleans, then check sort of keys by LIFO", func(t *testing.T) { 84 | k := New[string, string](Config{ 85 | ReplacementStrategy: ReplacementStrategyLIFO, 86 | MaxRecordTreshold: 10, 87 | CleanNum: 6, 88 | }) 89 | defer k.Close() 90 | 91 | for i := 0; i < 11; i++ { 92 | k.Set(fmt.Sprint(i), "value") 93 | } 94 | 95 | if k.Count() != 5 { 96 | t.Errorf("expected count to be 5, but got %d", k.Count()) 97 | } 98 | 99 | if !reflect.DeepEqual(k.Keys(), []string{"10", "3", "2", "1", "0"}) { 100 | t.Errorf("expected keys to be [10 3 2 1 0], but got %v", k.Keys()) 101 | } 102 | }) 103 | 104 | t.Run("when cleans, then check sort of keys by LRU", func(t *testing.T) { 105 | k := New[string, string](Config{ 106 | ReplacementStrategy: ReplacementStrategyLRU, 107 | MaxRecordTreshold: 10, 108 | CleanNum: 6, 109 | }) 110 | defer k.Close() 111 | 112 | for i := 0; i < 10; i++ { 113 | k.Set(fmt.Sprint(i), "value") 114 | } 115 | 116 | for i := 0; i < 10; i++ { 117 | k.Get(fmt.Sprint(9 - i)) 118 | } 119 | 120 | k.Set("10", "value") 121 | 122 | if k.Count() != 5 { 123 | t.Errorf("expected count to be 5, but got %d", k.Count()) 124 | } 125 | 126 | if !reflect.DeepEqual(k.Keys(), []string{"3", "2", "1", "0", "10"}) { 127 | t.Errorf("expected keys to be [3 2 1 0 10], but got %v", k.Keys()) 128 | } 129 | }) 130 | 131 | t.Run("when cleans, then check sort of keys by MRU", func(t *testing.T) { 132 | k := New[string, string](Config{ 133 | ReplacementStrategy: ReplacementStrategyMRU, 134 | MaxRecordTreshold: 10, 135 | CleanNum: 6, 136 | }) 137 | defer k.Close() 138 | 139 | for i := 0; i < 10; i++ { 140 | k.Set(fmt.Sprint(i), "value") 141 | } 142 | 143 | for i := 0; i < 10; i++ { 144 | k.Get(fmt.Sprint(9 - i)) 145 | } 146 | 147 | k.Set("10", "value") 148 | 149 | if k.Count() != 5 { 150 | t.Errorf("expected count to be 5, but got %d", k.Count()) 151 | } 152 | 153 | if !reflect.DeepEqual(k.Keys(), []string{"10", "6", "7", "8", "9"}) { 154 | t.Errorf("expected keys to be [10 6 7 8 9], but got %v", k.Keys()) 155 | } 156 | }) 157 | 158 | t.Run("when cleans, then check sort of keys by LFU", func(t *testing.T) { 159 | k := New[string, string](Config{ 160 | ReplacementStrategy: ReplacementStrategyLFU, 161 | MaxRecordTreshold: 10, 162 | CleanNum: 6, 163 | }) 164 | defer k.Close() 165 | 166 | for i := 0; i < 10; i++ { 167 | k.Set(fmt.Sprint(i), "value") 168 | } 169 | 170 | for i := 0; i < 10; i++ { 171 | for j := 0; j < 10; j++ { 172 | if j < i { 173 | continue 174 | } 175 | k.Get(fmt.Sprint(9 - j)) 176 | } 177 | } 178 | 179 | k.Set("10", "value") 180 | 181 | if k.Count() != 5 { 182 | t.Errorf("expected count to be 5, but got %d", k.Count()) 183 | } 184 | 185 | if !reflect.DeepEqual(k.Keys(), []string{"3", "2", "1", "0", "10"}) { 186 | t.Errorf("expected keys to be [3 2 1 0 10], but got %v", k.Keys()) 187 | } 188 | }) 189 | 190 | t.Run("when cleans, then check sort of keys by MFU", func(t *testing.T) { 191 | k := New[string, string](Config{ 192 | ReplacementStrategy: ReplacementStrategyMFU, 193 | MaxRecordTreshold: 10, 194 | CleanNum: 6, 195 | }) 196 | defer k.Close() 197 | 198 | for i := 0; i < 10; i++ { 199 | k.Set(fmt.Sprint(i), "value") 200 | } 201 | 202 | for i := 0; i < 10; i++ { 203 | for j := 0; j < 10; j++ { 204 | if j < i { 205 | continue 206 | } 207 | k.Get(fmt.Sprint(9 - j)) 208 | } 209 | } 210 | 211 | k.Set("10", "value") 212 | 213 | for i := 0; i < 10; i++ { 214 | k.Get("10") 215 | } 216 | 217 | if k.Count() != 5 { 218 | t.Errorf("expected count to be 5, but got %d", k.Count()) 219 | } 220 | 221 | if !reflect.DeepEqual(k.Keys(), []string{"10", "6", "7", "8", "9"}) { 222 | t.Errorf("expected keys to be [10 6 7 8 9], but got %v", k.Keys()) 223 | } 224 | }) 225 | } 226 | 227 | func TestSet(t *testing.T) { 228 | t.Run("set without replacement", func(t *testing.T) { 229 | k := New[string, string](Config{}) 230 | defer k.Close() 231 | 232 | k.Set("key", "value") 233 | k.Set("key", "value") // already exists 234 | 235 | keys := k.Keys() 236 | if !reflect.DeepEqual(keys, []string{"key"}) { 237 | t.Errorf("expected keys to be [key], but got %v", keys) 238 | } 239 | }) 240 | 241 | t.Run("set with lru replacement", func(t *testing.T) { 242 | k := New[string, string](Config{ 243 | ReplacementStrategy: ReplacementStrategyLRU, 244 | MaxRecordTreshold: 100, 245 | CleanNum: 100, 246 | }) 247 | defer k.Close() 248 | 249 | k.Set("key", "value") 250 | k.Set("key", "value") // already exists 251 | 252 | keys := k.Keys() 253 | if !reflect.DeepEqual(keys, []string{"key"}) { 254 | t.Errorf("expected keys to be [key], but got %v", keys) 255 | } 256 | }) 257 | 258 | t.Run("set with mru replacement", func(t *testing.T) { 259 | k := New[string, string](Config{ 260 | ReplacementStrategy: ReplacementStrategyMRU, 261 | MaxRecordTreshold: 100, 262 | CleanNum: 100, 263 | }) 264 | defer k.Close() 265 | 266 | k.Set("key", "value") 267 | k.Set("key", "value") // already exists 268 | 269 | keys := k.Keys() 270 | if !reflect.DeepEqual(keys, []string{"key"}) { 271 | t.Errorf("expected keys to be [key], but got %v", keys) 272 | } 273 | }) 274 | } 275 | 276 | func TestGet(t *testing.T) { 277 | t.Run("get without replacement", func(t *testing.T) { 278 | k := New[string, string](Config{}) 279 | defer k.Close() 280 | 281 | k.Set("key", "value") 282 | 283 | v, ok := k.Get("key") 284 | if !ok { 285 | t.Errorf("expected key to exist") 286 | } 287 | 288 | if v != "value" { 289 | t.Errorf("expected value to be value, but got %s", v) 290 | } 291 | }) 292 | 293 | t.Run("get with lru replacement", func(t *testing.T) { 294 | k := New[string, string](Config{ 295 | ReplacementStrategy: ReplacementStrategyLRU, 296 | MaxRecordTreshold: 100, 297 | CleanNum: 100, 298 | }) 299 | defer k.Close() 300 | 301 | k.Set("key", "value") 302 | 303 | v, ok := k.Get("key") 304 | if !ok { 305 | t.Errorf("expected key to exist") 306 | } 307 | 308 | if v != "value" { 309 | t.Errorf("expected value to be value, but got %s", v) 310 | } 311 | }) 312 | 313 | t.Run("get with mru replacement", func(t *testing.T) { 314 | k := New[string, string](Config{ 315 | ReplacementStrategy: ReplacementStrategyMRU, 316 | MaxRecordTreshold: 100, 317 | CleanNum: 100, 318 | }) 319 | defer k.Close() 320 | 321 | k.Set("key", "value") 322 | 323 | v, ok := k.Get("key") 324 | if !ok { 325 | t.Errorf("expected key to exist") 326 | } 327 | 328 | if v != "value" { 329 | t.Errorf("expected value to be value, but got %s", v) 330 | } 331 | }) 332 | 333 | t.Run("get with lfu replacement", func(t *testing.T) { 334 | k := New[string, string](Config{ 335 | ReplacementStrategy: ReplacementStrategyLFU, 336 | MaxRecordTreshold: 100, 337 | CleanNum: 100, 338 | }) 339 | defer k.Close() 340 | 341 | k.Set("key", "value") 342 | 343 | v, ok := k.Get("key") 344 | if !ok { 345 | t.Errorf("expected key to exist") 346 | } 347 | 348 | if v != "value" { 349 | t.Errorf("expected value to be value, but got %s", v) 350 | } 351 | }) 352 | 353 | t.Run("get with mfu replacement", func(t *testing.T) { 354 | k := New[string, string](Config{ 355 | ReplacementStrategy: ReplacementStrategyMFU, 356 | MaxRecordTreshold: 100, 357 | CleanNum: 100, 358 | }) 359 | defer k.Close() 360 | 361 | k.Set("key1", "value") 362 | k.Set("key2", "value") 363 | 364 | k.Get("key1") 365 | k.Get("key2") 366 | k.Get("key2") 367 | 368 | v, ok := k.Get("key1") 369 | if !ok { 370 | t.Errorf("expected key to exist") 371 | } 372 | 373 | if v != "value" { 374 | t.Errorf("expected value to be value, but got %s", v) 375 | } 376 | }) 377 | } 378 | 379 | func TestTTL(t *testing.T) { 380 | t.Run("with TTL", func(t *testing.T) { 381 | k := New[string, string](Config{ 382 | PollInterval: 100 * time.Millisecond, 383 | }) 384 | defer k.Close() 385 | 386 | k.SetWithTTL("key", "value", 300*time.Millisecond) 387 | 388 | if _, ok := k.Get("key"); !ok { 389 | t.Errorf("expected key to exist") 390 | } 391 | 392 | time.Sleep(400 * time.Millisecond) 393 | 394 | if _, ok := k.Get("key"); ok { 395 | t.Errorf("expected key to be deleted") 396 | } 397 | }) 398 | 399 | t.Run("skip if no TTL", func(t *testing.T) { 400 | k := New[string, string](Config{ 401 | PollInterval: 100 * time.Millisecond, 402 | }) 403 | 404 | k.SetWithTTL("key1", "value", 300*time.Millisecond) 405 | k.SetWithTTL("key2", "value", 5*time.Second) 406 | k.Set("key3", "value") 407 | k.SetWithTTL("key3", "value", 0) 408 | 409 | keys := k.Keys() 410 | if !reflect.DeepEqual(keys, []string{"key1", "key2", "key3"}) { 411 | t.Errorf("expected keys to be [key1, key2, key3], but got %v", keys) 412 | } 413 | 414 | // wait until key1 is expired 415 | time.Sleep(400 * time.Millisecond) 416 | 417 | keys = k.Keys() 418 | if !reflect.DeepEqual(keys, []string{"key3"}) { 419 | t.Errorf("expected keys to be [key2, key3], but got %v", keys) 420 | } 421 | 422 | k.Close() 423 | }) 424 | } 425 | 426 | func TestClose(t *testing.T) { 427 | t.Run("check goroutine is closed", func(t *testing.T) { 428 | start := runtime.NumGoroutine() 429 | 430 | k := New[string, string](Config{}) 431 | k.Close() 432 | 433 | if start != runtime.NumGoroutine() { 434 | t.Errorf("expected %d, but got %d", start, runtime.NumGoroutine()) 435 | } 436 | }) 437 | 438 | t.Run("check if usable after close", func(t *testing.T) { 439 | k := New[string, string](Config{}) 440 | k.Close() 441 | 442 | k.Set("key", "value") 443 | 444 | if _, ok := k.Get("key"); ok { 445 | t.Errorf("expected key to be not set") 446 | } 447 | }) 448 | 449 | t.Run("close again", func(t *testing.T) { 450 | k := New[string, string](Config{}) 451 | k.Close() 452 | k.Close() 453 | }) 454 | } 455 | 456 | func TestDelete(t *testing.T) { 457 | g := New[string, string](Config{}) 458 | g.Set("key1", "value") 459 | g.Delete("key1") 460 | if _, ok := g.Get("key1"); ok { 461 | t.Errorf("expected key to be not set") 462 | } 463 | g.Close() 464 | } 465 | 466 | func TestDeleteFunc(t *testing.T) { 467 | g := New[string, string](Config{}) 468 | g.Set("a1", "a1") 469 | g.Set("a2", "a2") 470 | g.Set("a3", "a3") 471 | g.Set("b1", "b1") 472 | g.Set("b2", "b2") 473 | g.Set("b3", "b3") 474 | g.Set("c1", "c1") 475 | // delete all keys start with "a" 476 | g.DeleteFunc(func(key, _ string) bool { 477 | return strings.HasPrefix(key, "a") 478 | }) 479 | // delete all values start with "b" 480 | g.DeleteFunc(func(_, value string) bool { 481 | return strings.HasPrefix(value, "b") 482 | }) 483 | if g.elems.Len() != 1 { 484 | t.Errorf("expected elems count to be 1, but got %d", g.elems.Len()) 485 | } 486 | if len(g.store) != 1 { 487 | t.Errorf("expected store count to be 1, but got %d", g.elems.Len()) 488 | } 489 | g.Close() 490 | } 491 | 492 | func TestKeys(t *testing.T) { 493 | g := New[string, string](Config{}) 494 | g.Set("a1", "a1") 495 | g.Set("a2", "a2") 496 | g.Set("a3", "a3") 497 | g.Set("b1", "b1") 498 | g.Set("b2", "b2") 499 | g.Set("b3", "b3") 500 | g.Set("c1", "c1") 501 | keys := g.Keys() 502 | if !reflect.DeepEqual(keys, []string{"a1", "a2", "a3", "b1", "b2", "b3", "c1"}) { 503 | t.Errorf("expected keys to be [a1, a2, a3, b1, b2, b3, c1], but got %v", keys) 504 | } 505 | g.Close() 506 | } 507 | 508 | func TestKeysFunc(t *testing.T) { 509 | g := New[string, string](Config{}) 510 | g.Set("a1", "a1") 511 | g.Set("a2", "a2") 512 | g.Set("a3", "a3") 513 | g.Set("b1", "b1") 514 | g.Set("b2", "b2") 515 | g.Set("b3", "b3") 516 | g.Set("c1", "c1") 517 | keys := g.KeysFunc(func(key, _ string) bool { 518 | return strings.HasPrefix(key, "a") 519 | }) 520 | if !reflect.DeepEqual(keys, []string{"a1", "a2", "a3"}) { 521 | t.Errorf("expected keys to be [a1, a2, a3], but got %v", keys) 522 | } 523 | g.Close() 524 | } 525 | 526 | func TestCount(t *testing.T) { 527 | g := New[string, string](Config{}) 528 | g.Set("a1", "a1") 529 | g.Set("a2", "a2") 530 | g.Set("a3", "a3") 531 | g.Set("b1", "b1") 532 | g.Set("b2", "b2") 533 | g.Set("b3", "b3") 534 | g.Set("c1", "c1") 535 | count := g.Count() 536 | if count != 7 { 537 | t.Errorf("expected count to be 7, but got %d", count) 538 | } 539 | g.Close() 540 | } 541 | 542 | func TestCountFunc(t *testing.T) { 543 | g := New[string, string](Config{}) 544 | g.Set("a1", "a1") 545 | g.Set("a2", "a2") 546 | g.Set("a3", "a3") 547 | g.Set("b1", "b1") 548 | g.Set("b2", "b2") 549 | g.Set("b3", "b3") 550 | g.Set("c1", "c1") 551 | count := g.CountFunc(func(key, _ string) bool { 552 | return strings.HasPrefix(key, "a") 553 | }) 554 | if count != 3 { 555 | t.Errorf("expected count to be 3, but got %d", count) 556 | } 557 | g.Close() 558 | } 559 | 560 | func TestFlush(t *testing.T) { 561 | g := New[string, string](Config{}) 562 | g.Set("a1", "a1") 563 | g.Set("a2", "a2") 564 | g.Set("a3", "a3") 565 | g.Set("b1", "b1") 566 | g.Set("b2", "b2") 567 | g.Set("b3", "b3") 568 | g.Set("c1", "c1") 569 | g.Flush() 570 | if g.elems.Len() != 0 { 571 | t.Errorf("expected elems count to be 0, but got %d", g.elems.Len()) 572 | } 573 | if len(g.store) != 0 { 574 | t.Errorf("expected store count to be 0, but got %d", g.elems.Len()) 575 | } 576 | g.Close() 577 | } 578 | -------------------------------------------------------------------------------- /replacement_strategy.go: -------------------------------------------------------------------------------- 1 | package gokachu 2 | 3 | type ReplacementStrategy uint 4 | 5 | const ( 6 | ReplacementStrategyNone ReplacementStrategy = iota 7 | ReplacementStrategyLRU // Least Recently Used 8 | ReplacementStrategyMRU // Most Recently Used 9 | ReplacementStrategyFIFO // First In First Out 10 | ReplacementStrategyLIFO // Last In First Out 11 | ReplacementStrategyLFU // Least Frequently Used 12 | ReplacementStrategyMFU // Most Frequently Used 13 | ) 14 | 15 | func (g *Gokachu[K, V]) clean() { 16 | currentElem := g.elems.Front() 17 | deletedCount := 0 18 | for { 19 | if deletedCount >= g.cleanNum || currentElem == nil { 20 | break 21 | } 22 | delete(g.store, currentElem.Value.(*valueWithTTL[K, V]).key) 23 | nextElem := currentElem.Next() 24 | g.elems.Remove(currentElem) 25 | deletedCount++ 26 | currentElem = nextElem 27 | } 28 | } 29 | --------------------------------------------------------------------------------