github.com/maypok86/otter@v1.2.1/cache_test.go (about) 1 // Copyright (c) 2023 Alexey Mayshev. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package otter 16 17 import ( 18 "container/heap" 19 "fmt" 20 "math/rand" 21 "sync" 22 "testing" 23 "time" 24 25 "github.com/maypok86/otter/internal/xruntime" 26 ) 27 28 func TestCache_Set(t *testing.T) { 29 const size = 256 30 var mutex sync.Mutex 31 m := make(map[DeletionCause]int) 32 c, err := MustBuilder[int, int](size). 33 WithTTL(time.Minute). 34 CollectStats(). 35 DeletionListener(func(key int, value int, cause DeletionCause) { 36 mutex.Lock() 37 m[cause]++ 38 mutex.Unlock() 39 }). 40 Build() 41 if err != nil { 42 t.Fatalf("can not create cache: %v", err) 43 } 44 45 for i := 0; i < size; i++ { 46 c.Set(i, i) 47 } 48 49 // update 50 for i := 0; i < size; i++ { 51 c.Set(i, i) 52 } 53 54 parallelism := xruntime.Parallelism() 55 var wg sync.WaitGroup 56 for i := 0; i < int(parallelism); i++ { 57 wg.Add(1) 58 go func() { 59 defer wg.Done() 60 61 r := rand.New(rand.NewSource(time.Now().UnixNano())) 62 for a := 0; a < 10000; a++ { 63 k := r.Int() % 100 64 val, ok := c.Get(k) 65 if !ok { 66 err = fmt.Errorf("expected %d but got nil", k) 67 break 68 } 69 if val != k { 70 err = fmt.Errorf("expected %d but got %d", k, val) 71 break 72 } 73 } 74 }() 75 } 76 wg.Wait() 77 78 if err != nil { 79 t.Fatalf("not found key: %v", err) 80 } 81 ratio := c.Stats().Ratio() 82 if ratio != 1.0 { 83 t.Fatalf("cache hit ratio should be 1.0, but got %v", ratio) 84 } 85 86 mutex.Lock() 87 defer mutex.Unlock() 88 if len(m) != 1 || m[Replaced] != size { 89 t.Fatalf("cache was supposed to replace %d, but replaced %d entries", size, m[Replaced]) 90 } 91 } 92 93 func TestCache_SetIfAbsent(t *testing.T) { 94 const size = 100 95 c, err := MustBuilder[int, int](size).WithTTL(time.Minute).CollectStats().Build() 96 if err != nil { 97 t.Fatalf("can not create cache: %v", err) 98 } 99 100 for i := 0; i < size; i++ { 101 if !c.SetIfAbsent(i, i) { 102 t.Fatalf("set was dropped. key: %d", i) 103 } 104 } 105 106 for i := 0; i < size; i++ { 107 if !c.Has(i) { 108 t.Fatalf("key should exists: %d", i) 109 } 110 } 111 112 for i := 0; i < size; i++ { 113 if c.SetIfAbsent(i, i) { 114 t.Fatalf("set wasn't dropped. key: %d", i) 115 } 116 } 117 118 c.Clear() 119 120 cc, err := MustBuilder[int, int](size).WithVariableTTL().CollectStats().Build() 121 if err != nil { 122 t.Fatalf("can not create cache: %v", err) 123 } 124 125 for i := 0; i < size; i++ { 126 if !cc.SetIfAbsent(i, i, time.Hour) { 127 t.Fatalf("set was dropped. key: %d", i) 128 } 129 } 130 131 for i := 0; i < size; i++ { 132 if !cc.Has(i) { 133 t.Fatalf("key should exists: %d", i) 134 } 135 } 136 137 for i := 0; i < size; i++ { 138 if cc.SetIfAbsent(i, i, time.Second) { 139 t.Fatalf("set wasn't dropped. key: %d", i) 140 } 141 } 142 143 if hits := cc.Stats().Hits(); hits != size { 144 t.Fatalf("hit ratio should be 100%%. Hits: %d", hits) 145 } 146 147 cc.Close() 148 } 149 150 func TestCache_SetWithTTL(t *testing.T) { 151 size := 256 152 var mutex sync.Mutex 153 m := make(map[DeletionCause]int) 154 c, err := MustBuilder[int, int](size). 155 InitialCapacity(size). 156 WithTTL(time.Second). 157 DeletionListener(func(key int, value int, cause DeletionCause) { 158 mutex.Lock() 159 m[cause]++ 160 mutex.Unlock() 161 }). 162 Build() 163 if err != nil { 164 t.Fatalf("can not create builder: %v", err) 165 } 166 167 for i := 0; i < size; i++ { 168 c.Set(i, i) 169 } 170 171 time.Sleep(3 * time.Second) 172 for i := 0; i < size; i++ { 173 if c.Has(i) { 174 t.Fatalf("key should be expired: %d", i) 175 } 176 } 177 178 time.Sleep(10 * time.Millisecond) 179 180 if cacheSize := c.Size(); cacheSize != 0 { 181 t.Fatalf("c.Size() = %d, want = %d", cacheSize, 0) 182 } 183 184 mutex.Lock() 185 if e := m[Expired]; len(m) != 1 || e != size { 186 mutex.Unlock() 187 t.Fatalf("cache was supposed to expire %d, but expired %d entries", size, e) 188 } 189 mutex.Unlock() 190 191 m = make(map[DeletionCause]int) 192 cc, err := MustBuilder[int, int](size). 193 WithVariableTTL(). 194 CollectStats(). 195 DeletionListener(func(key int, value int, cause DeletionCause) { 196 mutex.Lock() 197 m[cause]++ 198 mutex.Unlock() 199 }). 200 Build() 201 if err != nil { 202 t.Fatalf("can not create builder: %v", err) 203 } 204 205 for i := 0; i < size; i++ { 206 cc.Set(i, i, 5*time.Second) 207 } 208 209 time.Sleep(7 * time.Second) 210 211 for i := 0; i < size; i++ { 212 if cc.Has(i) { 213 t.Fatalf("key should be expired: %d", i) 214 } 215 } 216 217 time.Sleep(10 * time.Millisecond) 218 219 if cacheSize := cc.Size(); cacheSize != 0 { 220 t.Fatalf("c.Size() = %d, want = %d", cacheSize, 0) 221 } 222 if misses := cc.Stats().Misses(); misses != int64(size) { 223 t.Fatalf("c.Stats().Misses() = %d, want = %d", misses, size) 224 } 225 mutex.Lock() 226 defer mutex.Unlock() 227 if len(m) != 1 || m[Expired] != size { 228 t.Fatalf("cache was supposed to expire %d, but expired %d entries", size, m[Expired]) 229 } 230 } 231 232 func TestCache_Delete(t *testing.T) { 233 size := 256 234 var mutex sync.Mutex 235 m := make(map[DeletionCause]int) 236 c, err := MustBuilder[int, int](size). 237 InitialCapacity(size). 238 WithTTL(time.Hour). 239 DeletionListener(func(key int, value int, cause DeletionCause) { 240 mutex.Lock() 241 m[cause]++ 242 mutex.Unlock() 243 }). 244 Build() 245 if err != nil { 246 t.Fatalf("can not create builder: %v", err) 247 } 248 249 for i := 0; i < size; i++ { 250 c.Set(i, i) 251 } 252 253 for i := 0; i < size; i++ { 254 if !c.Has(i) { 255 t.Fatalf("key should exists: %d", i) 256 } 257 } 258 259 for i := 0; i < size; i++ { 260 c.Delete(i) 261 } 262 263 for i := 0; i < size; i++ { 264 if c.Has(i) { 265 t.Fatalf("key should not exists: %d", i) 266 } 267 } 268 269 time.Sleep(time.Second) 270 271 mutex.Lock() 272 defer mutex.Unlock() 273 if len(m) != 1 || m[Explicit] != size { 274 t.Fatalf("cache was supposed to delete %d, but deleted %d entries", size, m[Explicit]) 275 } 276 } 277 278 func TestCache_DeleteByFunc(t *testing.T) { 279 size := 256 280 var mutex sync.Mutex 281 m := make(map[DeletionCause]int) 282 c, err := MustBuilder[int, int](size). 283 InitialCapacity(size). 284 WithTTL(time.Hour). 285 DeletionListener(func(key int, value int, cause DeletionCause) { 286 mutex.Lock() 287 m[cause]++ 288 mutex.Unlock() 289 }). 290 Build() 291 if err != nil { 292 t.Fatalf("can not create builder: %v", err) 293 } 294 295 for i := 0; i < size; i++ { 296 c.Set(i, i) 297 } 298 299 c.DeleteByFunc(func(key int, value int) bool { 300 return key%2 == 1 301 }) 302 303 c.Range(func(key int, value int) bool { 304 if key%2 == 1 { 305 t.Fatalf("key should be odd, but got: %d", key) 306 } 307 return true 308 }) 309 310 time.Sleep(time.Second) 311 312 expected := size / 2 313 mutex.Lock() 314 defer mutex.Unlock() 315 if len(m) != 1 || m[Explicit] != expected { 316 t.Fatalf("cache was supposed to delete %d, but deleted %d entries", expected, m[Explicit]) 317 } 318 } 319 320 func TestCache_Advanced(t *testing.T) { 321 size := 256 322 defaultTTL := time.Hour 323 c, err := MustBuilder[int, int](size). 324 WithTTL(defaultTTL). 325 Build() 326 if err != nil { 327 t.Fatalf("can not create builder: %v", err) 328 } 329 330 for i := 0; i < size; i++ { 331 c.Set(i, i) 332 } 333 334 k1 := 4 335 v1, ok := c.Extension().GetQuietly(k1) 336 if !ok { 337 t.Fatalf("not found key %d", k1) 338 } 339 340 e1, ok := c.Extension().GetEntryQuietly(k1) 341 if !ok { 342 t.Fatalf("not found key %d", k1) 343 } 344 345 e2, ok := c.Extension().GetEntry(k1) 346 if !ok { 347 t.Fatalf("not found key %d", k1) 348 } 349 350 time.Sleep(time.Second) 351 352 isValidEntries := e1.Key() == k1 && 353 e1.Value() == v1 && 354 e1.Cost() == 1 && 355 e1 == e2 && 356 e1.TTL() < defaultTTL && 357 !e1.HasExpired() 358 359 if !isValidEntries { 360 t.Fatalf("found not valid entries. e1: %+v, e2: %+v, v1:%d", e1, e2, v1) 361 } 362 363 if _, ok := c.Extension().GetQuietly(size); ok { 364 t.Fatalf("found not valid key: %d", size) 365 } 366 if _, ok := c.Extension().GetEntryQuietly(size); ok { 367 t.Fatalf("found not valid key: %d", size) 368 } 369 if _, ok := c.Extension().GetEntry(size); ok { 370 t.Fatalf("found not valid key: %d", size) 371 } 372 } 373 374 func TestCache_Ratio(t *testing.T) { 375 var mutex sync.Mutex 376 m := make(map[DeletionCause]int) 377 c, err := MustBuilder[uint64, uint64](100). 378 CollectStats(). 379 DeletionListener(func(key uint64, value uint64, cause DeletionCause) { 380 mutex.Lock() 381 m[cause]++ 382 mutex.Unlock() 383 }). 384 Build() 385 if err != nil { 386 t.Fatalf("can not create cache: %v", err) 387 } 388 389 z := rand.NewZipf(rand.New(rand.NewSource(time.Now().UnixNano())), 1.0001, 1, 1000) 390 391 o := newOptimal(100) 392 for i := 0; i < 10000; i++ { 393 k := z.Uint64() 394 395 o.Get(k) 396 if !c.Has(k) { 397 c.Set(k, k) 398 } 399 } 400 401 t.Logf("actual size: %d, capacity: %d", c.Size(), c.Capacity()) 402 t.Logf("actual: %.2f, optimal: %.2f", c.Stats().Ratio(), o.Ratio()) 403 404 mutex.Lock() 405 defer mutex.Unlock() 406 t.Logf("evicted: %d", m[Size]) 407 if len(m) != 1 || m[Size] <= 0 || m[Size] > 5000 { 408 t.Fatalf("cache was supposed to evict positive number of entries, but evicted %d entries", m[Size]) 409 } 410 } 411 412 type optimal struct { 413 capacity uint64 414 hits map[uint64]uint64 415 access []uint64 416 } 417 418 func newOptimal(capacity uint64) *optimal { 419 return &optimal{ 420 capacity: capacity, 421 hits: make(map[uint64]uint64), 422 access: make([]uint64, 0), 423 } 424 } 425 426 func (o *optimal) Get(key uint64) { 427 o.hits[key]++ 428 o.access = append(o.access, key) 429 } 430 431 func (o *optimal) Ratio() float64 { 432 look := make(map[uint64]struct{}, o.capacity) 433 data := &optimalHeap{} 434 heap.Init(data) 435 hits := 0 436 misses := 0 437 for _, key := range o.access { 438 if _, has := look[key]; has { 439 hits++ 440 continue 441 } 442 if uint64(data.Len()) >= o.capacity { 443 victim := heap.Pop(data) 444 delete(look, victim.(*optimalItem).key) 445 } 446 misses++ 447 look[key] = struct{}{} 448 heap.Push(data, &optimalItem{key, o.hits[key]}) 449 } 450 if hits == 0 && misses == 0 { 451 return 0.0 452 } 453 return float64(hits) / float64(hits+misses) 454 } 455 456 type optimalItem struct { 457 key uint64 458 hits uint64 459 } 460 461 type optimalHeap []*optimalItem 462 463 func (h optimalHeap) Len() int { return len(h) } 464 func (h optimalHeap) Less(i, j int) bool { return h[i].hits < h[j].hits } 465 func (h optimalHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 466 467 func (h *optimalHeap) Push(x any) { 468 *h = append(*h, x.(*optimalItem)) 469 } 470 471 func (h *optimalHeap) Pop() any { 472 old := *h 473 n := len(old) 474 x := old[n-1] 475 *h = old[0 : n-1] 476 return x 477 }