github.com/maypok86/otter@v1.2.1/internal/core/cache.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 core 16 17 import ( 18 "sync" 19 "time" 20 21 "github.com/maypok86/otter/internal/expiry" 22 "github.com/maypok86/otter/internal/generated/node" 23 "github.com/maypok86/otter/internal/hashtable" 24 "github.com/maypok86/otter/internal/lossy" 25 "github.com/maypok86/otter/internal/queue" 26 "github.com/maypok86/otter/internal/s3fifo" 27 "github.com/maypok86/otter/internal/stats" 28 "github.com/maypok86/otter/internal/unixtime" 29 "github.com/maypok86/otter/internal/xmath" 30 "github.com/maypok86/otter/internal/xruntime" 31 ) 32 33 // DeletionCause the cause why a cached entry was deleted. 34 type DeletionCause uint8 35 36 const ( 37 // Explicit the entry was manually deleted by the user. 38 Explicit DeletionCause = iota 39 // Replaced the entry itself was not actually deleted, but its value was replaced by the user. 40 Replaced 41 // Size the entry was evicted due to size constraints. 42 Size 43 // Expired the entry's expiration timestamp has passed. 44 Expired 45 ) 46 47 const ( 48 minWriteBufferCapacity uint32 = 4 49 ) 50 51 func zeroValue[V any]() V { 52 var zero V 53 return zero 54 } 55 56 func getTTL(ttl time.Duration) uint32 { 57 return uint32((ttl + time.Second - 1) / time.Second) 58 } 59 60 func getExpiration(ttl time.Duration) uint32 { 61 return unixtime.Now() + getTTL(ttl) 62 } 63 64 // Config is a set of cache settings. 65 type Config[K comparable, V any] struct { 66 Capacity int 67 InitialCapacity *int 68 StatsEnabled bool 69 TTL *time.Duration 70 WithVariableTTL bool 71 CostFunc func(key K, value V) uint32 72 WithCost bool 73 DeletionListener func(key K, value V, cause DeletionCause) 74 } 75 76 type expiryPolicy[K comparable, V any] interface { 77 Add(n node.Node[K, V]) 78 Delete(n node.Node[K, V]) 79 RemoveExpired(expired []node.Node[K, V]) []node.Node[K, V] 80 Clear() 81 } 82 83 // Cache is a structure performs a best-effort bounding of a hash table using eviction algorithm 84 // to determine which entries to evict when the capacity is exceeded. 85 type Cache[K comparable, V any] struct { 86 nodeManager *node.Manager[K, V] 87 hashmap *hashtable.Map[K, V] 88 policy *s3fifo.Policy[K, V] 89 expiryPolicy expiryPolicy[K, V] 90 stats *stats.Stats 91 readBuffers []*lossy.Buffer[K, V] 92 writeBuffer *queue.Growable[task[K, V]] 93 evictionMutex sync.Mutex 94 closeOnce sync.Once 95 doneClear chan struct{} 96 costFunc func(key K, value V) uint32 97 deletionListener func(key K, value V, cause DeletionCause) 98 capacity int 99 mask uint32 100 ttl uint32 101 withExpiration bool 102 isClosed bool 103 } 104 105 // NewCache returns a new cache instance based on the settings from Config. 106 func NewCache[K comparable, V any](c Config[K, V]) *Cache[K, V] { 107 parallelism := xruntime.Parallelism() 108 roundedParallelism := int(xmath.RoundUpPowerOf2(parallelism)) 109 maxWriteBufferCapacity := uint32(128 * roundedParallelism) 110 readBuffersCount := 4 * roundedParallelism 111 112 nodeManager := node.NewManager[K, V](node.Config{ 113 WithExpiration: c.TTL != nil || c.WithVariableTTL, 114 WithCost: c.WithCost, 115 }) 116 117 readBuffers := make([]*lossy.Buffer[K, V], 0, readBuffersCount) 118 for i := 0; i < readBuffersCount; i++ { 119 readBuffers = append(readBuffers, lossy.New[K, V](nodeManager)) 120 } 121 122 var hashmap *hashtable.Map[K, V] 123 if c.InitialCapacity == nil { 124 hashmap = hashtable.New[K, V](nodeManager) 125 } else { 126 hashmap = hashtable.NewWithSize[K, V](nodeManager, *c.InitialCapacity) 127 } 128 129 var expPolicy expiryPolicy[K, V] 130 switch { 131 case c.TTL != nil: 132 expPolicy = expiry.NewFixed[K, V]() 133 case c.WithVariableTTL: 134 expPolicy = expiry.NewVariable[K, V](nodeManager) 135 default: 136 expPolicy = expiry.NewDisabled[K, V]() 137 } 138 139 cache := &Cache[K, V]{ 140 nodeManager: nodeManager, 141 hashmap: hashmap, 142 policy: s3fifo.NewPolicy[K, V](c.Capacity), 143 expiryPolicy: expPolicy, 144 readBuffers: readBuffers, 145 writeBuffer: queue.NewGrowable[task[K, V]](minWriteBufferCapacity, maxWriteBufferCapacity), 146 doneClear: make(chan struct{}), 147 mask: uint32(readBuffersCount - 1), 148 costFunc: c.CostFunc, 149 deletionListener: c.DeletionListener, 150 capacity: c.Capacity, 151 } 152 153 if c.StatsEnabled { 154 cache.stats = stats.New() 155 } 156 if c.TTL != nil { 157 cache.ttl = getTTL(*c.TTL) 158 } 159 160 cache.withExpiration = c.TTL != nil || c.WithVariableTTL 161 162 if cache.withExpiration { 163 unixtime.Start() 164 go cache.cleanup() 165 } 166 167 go cache.process() 168 169 return cache 170 } 171 172 func (c *Cache[K, V]) getReadBufferIdx() int { 173 return int(xruntime.Fastrand() & c.mask) 174 } 175 176 // Has checks if there is an item with the given key in the cache. 177 func (c *Cache[K, V]) Has(key K) bool { 178 _, ok := c.Get(key) 179 return ok 180 } 181 182 // Get returns the value associated with the key in this cache. 183 func (c *Cache[K, V]) Get(key K) (V, bool) { 184 n, ok := c.GetNode(key) 185 if !ok { 186 return zeroValue[V](), false 187 } 188 189 return n.Value(), true 190 } 191 192 // GetNode returns the node associated with the key in this cache. 193 func (c *Cache[K, V]) GetNode(key K) (node.Node[K, V], bool) { 194 n, ok := c.hashmap.Get(key) 195 if !ok || !n.IsAlive() { 196 c.stats.IncMisses() 197 return nil, false 198 } 199 200 if n.HasExpired() { 201 c.writeBuffer.Push(newDeleteTask(n)) 202 c.stats.IncMisses() 203 return nil, false 204 } 205 206 c.afterGet(n) 207 c.stats.IncHits() 208 209 return n, true 210 } 211 212 // GetNodeQuietly returns the node associated with the key in this cache. 213 // 214 // Unlike GetNode, this function does not produce any side effects 215 // such as updating statistics or the eviction policy. 216 func (c *Cache[K, V]) GetNodeQuietly(key K) (node.Node[K, V], bool) { 217 n, ok := c.hashmap.Get(key) 218 if !ok || !n.IsAlive() || n.HasExpired() { 219 return nil, false 220 } 221 222 return n, true 223 } 224 225 func (c *Cache[K, V]) afterGet(got node.Node[K, V]) { 226 idx := c.getReadBufferIdx() 227 pb := c.readBuffers[idx].Add(got) 228 if pb != nil { 229 c.evictionMutex.Lock() 230 c.policy.Read(pb.Returned) 231 c.evictionMutex.Unlock() 232 233 c.readBuffers[idx].Free() 234 } 235 } 236 237 // Set associates the value with the key in this cache. 238 // 239 // If it returns false, then the key-value item had too much cost and the Set was dropped. 240 func (c *Cache[K, V]) Set(key K, value V) bool { 241 return c.set(key, value, c.defaultExpiration(), false) 242 } 243 244 func (c *Cache[K, V]) defaultExpiration() uint32 { 245 if c.ttl == 0 { 246 return 0 247 } 248 249 return unixtime.Now() + c.ttl 250 } 251 252 // SetWithTTL associates the value with the key in this cache and sets the custom ttl for this key-value item. 253 // 254 // If it returns false, then the key-value item had too much cost and the SetWithTTL was dropped. 255 func (c *Cache[K, V]) SetWithTTL(key K, value V, ttl time.Duration) bool { 256 return c.set(key, value, getExpiration(ttl), false) 257 } 258 259 // SetIfAbsent if the specified key is not already associated with a value associates it with the given value. 260 // 261 // If the specified key is not already associated with a value, then it returns false. 262 // 263 // Also, it returns false if the key-value item had too much cost and the SetIfAbsent was dropped. 264 func (c *Cache[K, V]) SetIfAbsent(key K, value V) bool { 265 return c.set(key, value, c.defaultExpiration(), true) 266 } 267 268 // SetIfAbsentWithTTL if the specified key is not already associated with a value associates it with the given value 269 // and sets the custom ttl for this key-value item. 270 // 271 // If the specified key is not already associated with a value, then it returns false. 272 // 273 // Also, it returns false if the key-value item had too much cost and the SetIfAbsent was dropped. 274 func (c *Cache[K, V]) SetIfAbsentWithTTL(key K, value V, ttl time.Duration) bool { 275 return c.set(key, value, getExpiration(ttl), true) 276 } 277 278 func (c *Cache[K, V]) set(key K, value V, expiration uint32, onlyIfAbsent bool) bool { 279 cost := c.costFunc(key, value) 280 if int(cost) > c.policy.MaxAvailableCost() { 281 c.stats.IncRejectedSets() 282 return false 283 } 284 285 n := c.nodeManager.Create(key, value, expiration, cost) 286 if onlyIfAbsent { 287 res := c.hashmap.SetIfAbsent(n) 288 if res == nil { 289 // insert 290 c.writeBuffer.Push(newAddTask(n)) 291 return true 292 } 293 c.stats.IncRejectedSets() 294 return false 295 } 296 297 evicted := c.hashmap.Set(n) 298 if evicted != nil { 299 // update 300 evicted.Die() 301 c.writeBuffer.Push(newUpdateTask(n, evicted)) 302 } else { 303 // insert 304 c.writeBuffer.Push(newAddTask(n)) 305 } 306 307 return true 308 } 309 310 // Delete deletes the association for this key from the cache. 311 func (c *Cache[K, V]) Delete(key K) { 312 c.afterDelete(c.hashmap.Delete(key)) 313 } 314 315 func (c *Cache[K, V]) deleteNode(n node.Node[K, V]) { 316 c.afterDelete(c.hashmap.DeleteNode(n)) 317 } 318 319 func (c *Cache[K, V]) afterDelete(deleted node.Node[K, V]) { 320 if deleted != nil { 321 deleted.Die() 322 c.writeBuffer.Push(newDeleteTask(deleted)) 323 } 324 } 325 326 // DeleteByFunc deletes the association for this key from the cache when the given function returns true. 327 func (c *Cache[K, V]) DeleteByFunc(f func(key K, value V) bool) { 328 c.hashmap.Range(func(n node.Node[K, V]) bool { 329 if !n.IsAlive() || n.HasExpired() { 330 return true 331 } 332 333 if f(n.Key(), n.Value()) { 334 c.deleteNode(n) 335 } 336 337 return true 338 }) 339 } 340 341 func (c *Cache[K, V]) notifyDeletion(key K, value V, cause DeletionCause) { 342 if c.deletionListener == nil { 343 return 344 } 345 346 c.deletionListener(key, value, cause) 347 } 348 349 func (c *Cache[K, V]) cleanup() { 350 bufferCapacity := 64 351 expired := make([]node.Node[K, V], 0, bufferCapacity) 352 for { 353 time.Sleep(time.Second) 354 355 c.evictionMutex.Lock() 356 if c.isClosed { 357 return 358 } 359 360 expired = c.expiryPolicy.RemoveExpired(expired) 361 for _, n := range expired { 362 c.policy.Delete(n) 363 } 364 365 c.evictionMutex.Unlock() 366 367 for _, n := range expired { 368 c.hashmap.DeleteNode(n) 369 n.Die() 370 c.notifyDeletion(n.Key(), n.Value(), Expired) 371 } 372 373 expired = clearBuffer(expired) 374 if cap(expired) > 3*bufferCapacity { 375 expired = make([]node.Node[K, V], 0, bufferCapacity) 376 } 377 } 378 } 379 380 func (c *Cache[K, V]) process() { 381 bufferCapacity := 64 382 buffer := make([]task[K, V], 0, bufferCapacity) 383 deleted := make([]node.Node[K, V], 0, bufferCapacity) 384 i := 0 385 for { 386 t := c.writeBuffer.Pop() 387 388 if t.isClear() || t.isClose() { 389 buffer = clearBuffer(buffer) 390 c.writeBuffer.Clear() 391 392 c.evictionMutex.Lock() 393 c.policy.Clear() 394 c.expiryPolicy.Clear() 395 if t.isClose() { 396 c.isClosed = true 397 } 398 c.evictionMutex.Unlock() 399 400 c.doneClear <- struct{}{} 401 if t.isClose() { 402 break 403 } 404 continue 405 } 406 407 buffer = append(buffer, t) 408 i++ 409 if i >= bufferCapacity { 410 i -= bufferCapacity 411 412 c.evictionMutex.Lock() 413 414 for _, t := range buffer { 415 n := t.node() 416 switch { 417 case t.isDelete(): 418 c.expiryPolicy.Delete(n) 419 c.policy.Delete(n) 420 case t.isAdd(): 421 if n.IsAlive() { 422 c.expiryPolicy.Add(n) 423 deleted = c.policy.Add(deleted, n) 424 } 425 case t.isUpdate(): 426 oldNode := t.oldNode() 427 c.expiryPolicy.Delete(oldNode) 428 c.policy.Delete(oldNode) 429 if n.IsAlive() { 430 c.expiryPolicy.Add(n) 431 deleted = c.policy.Add(deleted, n) 432 } 433 } 434 } 435 436 for _, n := range deleted { 437 c.expiryPolicy.Delete(n) 438 } 439 440 c.evictionMutex.Unlock() 441 442 for _, t := range buffer { 443 switch { 444 case t.isDelete(): 445 n := t.node() 446 c.notifyDeletion(n.Key(), n.Value(), Explicit) 447 case t.isUpdate(): 448 n := t.oldNode() 449 c.notifyDeletion(n.Key(), n.Value(), Replaced) 450 } 451 } 452 453 for _, n := range deleted { 454 c.hashmap.DeleteNode(n) 455 n.Die() 456 c.notifyDeletion(n.Key(), n.Value(), Size) 457 c.stats.IncEvictedCount() 458 c.stats.AddEvictedCost(n.Cost()) 459 } 460 461 buffer = clearBuffer(buffer) 462 deleted = clearBuffer(deleted) 463 if cap(deleted) > 3*bufferCapacity { 464 deleted = make([]node.Node[K, V], 0, bufferCapacity) 465 } 466 } 467 } 468 } 469 470 // Range iterates over all items in the cache. 471 // 472 // Iteration stops early when the given function returns false. 473 func (c *Cache[K, V]) Range(f func(key K, value V) bool) { 474 c.hashmap.Range(func(n node.Node[K, V]) bool { 475 if !n.IsAlive() || n.HasExpired() { 476 return true 477 } 478 479 return f(n.Key(), n.Value()) 480 }) 481 } 482 483 // Clear clears the hash table, all policies, buffers, etc. 484 // 485 // NOTE: this operation must be performed when no requests are made to the cache otherwise the behavior is undefined. 486 func (c *Cache[K, V]) Clear() { 487 c.clear(newClearTask[K, V]()) 488 } 489 490 func (c *Cache[K, V]) clear(t task[K, V]) { 491 c.hashmap.Clear() 492 for i := 0; i < len(c.readBuffers); i++ { 493 c.readBuffers[i].Clear() 494 } 495 496 c.writeBuffer.Push(t) 497 <-c.doneClear 498 499 c.stats.Clear() 500 } 501 502 // Close clears the hash table, all policies, buffers, etc and stop all goroutines. 503 // 504 // NOTE: this operation must be performed when no requests are made to the cache otherwise the behavior is undefined. 505 func (c *Cache[K, V]) Close() { 506 c.closeOnce.Do(func() { 507 c.clear(newCloseTask[K, V]()) 508 if c.withExpiration { 509 unixtime.Stop() 510 } 511 }) 512 } 513 514 // Size returns the current number of items in the cache. 515 func (c *Cache[K, V]) Size() int { 516 return c.hashmap.Size() 517 } 518 519 // Capacity returns the cache capacity. 520 func (c *Cache[K, V]) Capacity() int { 521 return c.capacity 522 } 523 524 // Stats returns a current snapshot of this cache's cumulative statistics. 525 func (c *Cache[K, V]) Stats() *stats.Stats { 526 return c.stats 527 } 528 529 // WithExpiration returns true if the cache was configured with the expiration policy enabled. 530 func (c *Cache[K, V]) WithExpiration() bool { 531 return c.withExpiration 532 } 533 534 func clearBuffer[T any](buffer []T) []T { 535 var zero T 536 for i := 0; i < len(buffer); i++ { 537 buffer[i] = zero 538 } 539 return buffer[:0] 540 }