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  }