github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/ttlcache/cache.go (about)

     1  package ttlcache
     2  
     3  import (
     4  	"container/list"
     5  	"context"
     6  	"fmt"
     7  	"sync"
     8  	"time"
     9  
    10  	"golang.org/x/sync/singleflight"
    11  )
    12  
    13  // Available eviction reasons.
    14  const (
    15  	EvictionReasonDeleted EvictionReason = iota + 1
    16  	EvictionReasonCapacityReached
    17  	EvictionReasonExpired
    18  )
    19  
    20  // EvictionReason is used to specify why a certain item was
    21  // evicted/deleted.
    22  type EvictionReason int
    23  
    24  // Cache is a synchronised map of items that are automatically removed
    25  // when they expire or the capacity is reached.
    26  type Cache[K comparable, V any] struct {
    27  	items struct {
    28  		mu     sync.RWMutex
    29  		values map[K]*list.Element
    30  
    31  		// a generic doubly linked list would be more convenient
    32  		// (and more performant?). It's possible that this
    33  		// will be introduced with/in go1.19+
    34  		lru      *list.List
    35  		expQueue expirationQueue[K, V]
    36  
    37  		timerCh chan time.Duration
    38  	}
    39  
    40  	metricsMu sync.RWMutex
    41  	metrics   Metrics
    42  
    43  	events struct {
    44  		insertion struct {
    45  			mu     sync.RWMutex
    46  			nextID uint64
    47  			fns    map[uint64]func(*Item[K, V])
    48  		}
    49  		eviction struct {
    50  			mu     sync.RWMutex
    51  			nextID uint64
    52  			fns    map[uint64]func(EvictionReason, *Item[K, V])
    53  		}
    54  	}
    55  
    56  	stopCh  chan struct{}
    57  	options options[K, V]
    58  }
    59  
    60  // New creates a new instance of cache.
    61  func New[K comparable, V any](opts ...Option[K, V]) *Cache[K, V] {
    62  	c := &Cache[K, V]{
    63  		stopCh: make(chan struct{}),
    64  	}
    65  	c.items.values = make(map[K]*list.Element)
    66  	c.items.lru = list.New()
    67  	c.items.expQueue = newExpirationQueue[K, V]()
    68  	c.items.timerCh = make(chan time.Duration, 1) // buffer is important
    69  	c.events.insertion.fns = make(map[uint64]func(*Item[K, V]))
    70  	c.events.eviction.fns = make(map[uint64]func(EvictionReason, *Item[K, V]))
    71  
    72  	applyOptions(&c.options, opts...)
    73  
    74  	return c
    75  }
    76  
    77  // updateExpirations updates the expiration queue and notifies
    78  // the cache auto cleaner if needed.
    79  // Not concurrently safe.
    80  func (c *Cache[K, V]) updateExpirations(fresh bool, elem *list.Element) {
    81  	var oldExpiresAt time.Time
    82  
    83  	if !c.items.expQueue.isEmpty() {
    84  		oldExpiresAt = c.items.expQueue[0].Value.(*Item[K, V]).expiresAt
    85  	}
    86  
    87  	if fresh {
    88  		c.items.expQueue.push(elem)
    89  	} else {
    90  		c.items.expQueue.update(elem)
    91  	}
    92  
    93  	newExpiresAt := c.items.expQueue[0].Value.(*Item[K, V]).expiresAt
    94  
    95  	// check if the closest/soonest expiration timestamp changed
    96  	if newExpiresAt.IsZero() || (!oldExpiresAt.IsZero() && !newExpiresAt.Before(oldExpiresAt)) {
    97  		return
    98  	}
    99  
   100  	d := time.Until(newExpiresAt)
   101  
   102  	// It's possible that the auto cleaner isn't active or
   103  	// is busy, so we need to drain the channel before
   104  	// sending a new value.
   105  	// Also, since this method is called after locking the items' mutex,
   106  	// we can be sure that there is no other concurrent call of this
   107  	// method
   108  	if len(c.items.timerCh) > 0 {
   109  		// we need to drain this channel in a select with a default
   110  		// case because it's possible that the auto cleaner
   111  		// read this channel just after we entered this if
   112  		select {
   113  		case d1 := <-c.items.timerCh:
   114  			if d1 < d {
   115  				d = d1
   116  			}
   117  		default:
   118  		}
   119  	}
   120  
   121  	// since the channel has a size 1 buffer, we can be sure
   122  	// that the line below won't block (we can't overfill the buffer
   123  	// because we just drained it)
   124  	c.items.timerCh <- d
   125  }
   126  
   127  // set creates a new item, adds it to the cache and then returns it.
   128  // Not concurrently safe.
   129  func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] {
   130  	if ttl == DefaultTTL {
   131  		ttl = c.options.ttl
   132  	}
   133  
   134  	elem := c.get(key, false)
   135  	if elem != nil {
   136  		// update/overwrite an existing item
   137  		item := elem.Value.(*Item[K, V])
   138  		item.update(value, ttl)
   139  		c.updateExpirations(false, elem)
   140  
   141  		return item
   142  	}
   143  
   144  	if c.options.capacity != 0 && uint64(len(c.items.values)) >= c.options.capacity {
   145  		// delete the oldest item
   146  		c.evict(EvictionReasonCapacityReached, c.items.lru.Back())
   147  	}
   148  
   149  	// create a new item
   150  	item := newItem(key, value, ttl)
   151  	elem = c.items.lru.PushFront(item)
   152  	c.items.values[key] = elem
   153  	c.updateExpirations(true, elem)
   154  
   155  	c.metricsMu.Lock()
   156  	c.metrics.Insertions++
   157  	c.metricsMu.Unlock()
   158  
   159  	c.events.insertion.mu.RLock()
   160  	for _, fn := range c.events.insertion.fns {
   161  		fn(item)
   162  	}
   163  	c.events.insertion.mu.RUnlock()
   164  
   165  	return item
   166  }
   167  
   168  // get retrieves an item from the cache and extends its expiration
   169  // time if 'touch' is set to true.
   170  // It returns nil if the item is not found or is expired.
   171  // Not concurrently safe.
   172  func (c *Cache[K, V]) get(key K, touch bool) *list.Element {
   173  	elem := c.items.values[key]
   174  	if elem == nil {
   175  		return nil
   176  	}
   177  
   178  	item := elem.Value.(*Item[K, V])
   179  	if item.isExpiredUnsafe() {
   180  		return nil
   181  	}
   182  
   183  	c.items.lru.MoveToFront(elem)
   184  
   185  	if touch && item.ttl > 0 {
   186  		item.touch()
   187  		c.updateExpirations(false, elem)
   188  	}
   189  
   190  	return elem
   191  }
   192  
   193  // evict deletes items from the cache.
   194  // If no items are provided, all currently present cache items
   195  // are evicted.
   196  // Not concurrently safe.
   197  func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) {
   198  	if len(elems) > 0 {
   199  		c.metricsMu.Lock()
   200  		c.metrics.Evictions += uint64(len(elems))
   201  		c.metricsMu.Unlock()
   202  
   203  		c.events.eviction.mu.RLock()
   204  		for i := range elems {
   205  			item := elems[i].Value.(*Item[K, V])
   206  			delete(c.items.values, item.key)
   207  			c.items.lru.Remove(elems[i])
   208  			c.items.expQueue.remove(elems[i])
   209  
   210  			for _, fn := range c.events.eviction.fns {
   211  				fn(reason, item)
   212  			}
   213  		}
   214  		c.events.eviction.mu.RUnlock()
   215  
   216  		return
   217  	}
   218  
   219  	c.metricsMu.Lock()
   220  	c.metrics.Evictions += uint64(len(c.items.values))
   221  	c.metricsMu.Unlock()
   222  
   223  	c.events.eviction.mu.RLock()
   224  	for _, elem := range c.items.values {
   225  		item := elem.Value.(*Item[K, V])
   226  
   227  		for _, fn := range c.events.eviction.fns {
   228  			fn(reason, item)
   229  		}
   230  	}
   231  	c.events.eviction.mu.RUnlock()
   232  
   233  	c.items.values = make(map[K]*list.Element)
   234  	c.items.lru.Init()
   235  	c.items.expQueue = newExpirationQueue[K, V]()
   236  }
   237  
   238  // Set creates a new item from the provided key and value, adds
   239  // it to the cache and then returns it. If an item associated with the
   240  // provided key already exists, the new item overwrites the existing one.
   241  func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) *Item[K, V] {
   242  	c.items.mu.Lock()
   243  	defer c.items.mu.Unlock()
   244  
   245  	return c.set(key, value, ttl)
   246  }
   247  
   248  // Get retrieves an item from the cache by the provided key.
   249  // Unless this is disabled, it also extends/touches an item's
   250  // expiration timestamp on successful retrieval.
   251  // If the item is not found, a nil value is returned.
   252  func (c *Cache[K, V]) Get(key K, opts ...Option[K, V]) *Item[K, V] {
   253  	getOpts := options[K, V]{
   254  		loader:            c.options.loader,
   255  		disableTouchOnHit: c.options.disableTouchOnHit,
   256  	}
   257  
   258  	applyOptions(&getOpts, opts...)
   259  
   260  	c.items.mu.Lock()
   261  	elem := c.get(key, !getOpts.disableTouchOnHit)
   262  	c.items.mu.Unlock()
   263  
   264  	if elem == nil {
   265  		c.metricsMu.Lock()
   266  		c.metrics.Misses++
   267  		c.metricsMu.Unlock()
   268  
   269  		if getOpts.loader != nil {
   270  			return getOpts.loader.Load(c, key)
   271  		}
   272  
   273  		return nil
   274  	}
   275  
   276  	c.metricsMu.Lock()
   277  	c.metrics.Hits++
   278  	c.metricsMu.Unlock()
   279  
   280  	return elem.Value.(*Item[K, V])
   281  }
   282  
   283  // Delete deletes an item from the cache. If the item associated with
   284  // the key is not found, the method is no-op.
   285  func (c *Cache[K, V]) Delete(key K) {
   286  	c.items.mu.Lock()
   287  	defer c.items.mu.Unlock()
   288  
   289  	elem := c.items.values[key]
   290  	if elem == nil {
   291  		return
   292  	}
   293  
   294  	c.evict(EvictionReasonDeleted, elem)
   295  }
   296  
   297  // DeleteAll deletes all items from the cache.
   298  func (c *Cache[K, V]) DeleteAll() {
   299  	c.items.mu.Lock()
   300  	c.evict(EvictionReasonDeleted)
   301  	c.items.mu.Unlock()
   302  }
   303  
   304  // DeleteExpired deletes all expired items from the cache.
   305  func (c *Cache[K, V]) DeleteExpired() {
   306  	c.items.mu.Lock()
   307  	defer c.items.mu.Unlock()
   308  
   309  	if c.items.expQueue.isEmpty() {
   310  		return
   311  	}
   312  
   313  	e := c.items.expQueue[0]
   314  	for e.Value.(*Item[K, V]).isExpiredUnsafe() {
   315  		c.evict(EvictionReasonExpired, e)
   316  
   317  		if c.items.expQueue.isEmpty() {
   318  			break
   319  		}
   320  
   321  		// expiration queue has a new root
   322  		e = c.items.expQueue[0]
   323  	}
   324  }
   325  
   326  // Touch simulates an item's retrieval without actually returning it.
   327  // Its main purpose is to extend an item's expiration timestamp.
   328  // If the item is not found, the method is no-op.
   329  func (c *Cache[K, V]) Touch(key K) {
   330  	c.items.mu.Lock()
   331  	c.get(key, true)
   332  	c.items.mu.Unlock()
   333  }
   334  
   335  // Len returns the number of items in the cache.
   336  func (c *Cache[K, V]) Len() int {
   337  	c.items.mu.RLock()
   338  	defer c.items.mu.RUnlock()
   339  
   340  	return len(c.items.values)
   341  }
   342  
   343  // Keys returns all keys currently present in the cache.
   344  func (c *Cache[K, V]) Keys() []K {
   345  	c.items.mu.RLock()
   346  	defer c.items.mu.RUnlock()
   347  
   348  	res := make([]K, 0, len(c.items.values))
   349  	for k := range c.items.values {
   350  		res = append(res, k)
   351  	}
   352  
   353  	return res
   354  }
   355  
   356  // Items returns a copy of all items in the cache.
   357  // It does not update any expiration timestamps.
   358  func (c *Cache[K, V]) Items() map[K]*Item[K, V] {
   359  	c.items.mu.RLock()
   360  	defer c.items.mu.RUnlock()
   361  
   362  	items := make(map[K]*Item[K, V], len(c.items.values))
   363  	for k := range c.items.values {
   364  		item := c.get(k, false)
   365  		if item != nil {
   366  			items[k] = item.Value.(*Item[K, V])
   367  		}
   368  	}
   369  
   370  	return items
   371  }
   372  
   373  // Metrics returns the metrics of the cache.
   374  func (c *Cache[K, V]) Metrics() Metrics {
   375  	c.metricsMu.RLock()
   376  	defer c.metricsMu.RUnlock()
   377  
   378  	return c.metrics
   379  }
   380  
   381  // Start starts an automatic cleanup process that
   382  // periodically deletes expired items.
   383  // It blocks until Stop is called.
   384  func (c *Cache[K, V]) Start() {
   385  	waitDur := func() time.Duration {
   386  		c.items.mu.RLock()
   387  		defer c.items.mu.RUnlock()
   388  
   389  		if !c.items.expQueue.isEmpty() &&
   390  			!c.items.expQueue[0].Value.(*Item[K, V]).expiresAt.IsZero() {
   391  			d := time.Until(c.items.expQueue[0].Value.(*Item[K, V]).expiresAt)
   392  			if d <= 0 {
   393  				// execute immediately
   394  				return time.Microsecond
   395  			}
   396  
   397  			return d
   398  		}
   399  
   400  		if c.options.ttl > 0 {
   401  			return c.options.ttl
   402  		}
   403  
   404  		return time.Hour
   405  	}
   406  
   407  	timer := time.NewTimer(waitDur())
   408  	stop := func() {
   409  		if !timer.Stop() {
   410  			// drain the timer chan
   411  			select {
   412  			case <-timer.C:
   413  			default:
   414  			}
   415  		}
   416  	}
   417  
   418  	defer stop()
   419  
   420  	for {
   421  		select {
   422  		case <-c.stopCh:
   423  			return
   424  		case d := <-c.items.timerCh:
   425  			stop()
   426  			timer.Reset(d)
   427  		case <-timer.C:
   428  			c.DeleteExpired()
   429  			stop()
   430  			timer.Reset(waitDur())
   431  		}
   432  	}
   433  }
   434  
   435  // Stop stops the automatic cleanup process.
   436  // It blocks until the cleanup process exits.
   437  func (c *Cache[K, V]) Stop() {
   438  	c.stopCh <- struct{}{}
   439  }
   440  
   441  // OnInsertion adds the provided function to be executed when
   442  // a new item is inserted into the cache. The function is executed
   443  // on a separate goroutine and does not block the flow of the cache
   444  // manager.
   445  // The returned function may be called to delete the subscription function
   446  // from the list of insertion subscribers.
   447  // When the returned function is called, it blocks until all instances of
   448  // the same subscription function return. A context is used to notify the
   449  // subscription function when the returned/deletion function is called.
   450  func (c *Cache[K, V]) OnInsertion(fn func(context.Context, *Item[K, V])) func() {
   451  	var (
   452  		wg          sync.WaitGroup
   453  		ctx, cancel = context.WithCancel(context.Background())
   454  	)
   455  
   456  	c.events.insertion.mu.Lock()
   457  	id := c.events.insertion.nextID
   458  	c.events.insertion.fns[id] = func(item *Item[K, V]) {
   459  		wg.Add(1)
   460  		go func() {
   461  			fn(ctx, item)
   462  			wg.Done()
   463  		}()
   464  	}
   465  	c.events.insertion.nextID++
   466  	c.events.insertion.mu.Unlock()
   467  
   468  	return func() {
   469  		cancel()
   470  
   471  		c.events.insertion.mu.Lock()
   472  		delete(c.events.insertion.fns, id)
   473  		c.events.insertion.mu.Unlock()
   474  
   475  		wg.Wait()
   476  	}
   477  }
   478  
   479  // OnEviction adds the provided function to be executed when
   480  // an item is evicted/deleted from the cache. The function is executed
   481  // on a separate goroutine and does not block the flow of the cache
   482  // manager.
   483  // The returned function may be called to delete the subscription function
   484  // from the list of eviction subscribers.
   485  // When the returned function is called, it blocks until all instances of
   486  // the same subscription function return. A context is used to notify the
   487  // subscription function when the returned/deletion function is called.
   488  func (c *Cache[K, V]) OnEviction(fn func(context.Context, EvictionReason, *Item[K, V])) func() {
   489  	var (
   490  		wg          sync.WaitGroup
   491  		ctx, cancel = context.WithCancel(context.Background())
   492  	)
   493  
   494  	c.events.eviction.mu.Lock()
   495  	id := c.events.eviction.nextID
   496  	c.events.eviction.fns[id] = func(r EvictionReason, item *Item[K, V]) {
   497  		wg.Add(1)
   498  		go func() {
   499  			fn(ctx, r, item)
   500  			wg.Done()
   501  		}()
   502  	}
   503  	c.events.eviction.nextID++
   504  	c.events.eviction.mu.Unlock()
   505  
   506  	return func() {
   507  		cancel()
   508  
   509  		c.events.eviction.mu.Lock()
   510  		delete(c.events.eviction.fns, id)
   511  		c.events.eviction.mu.Unlock()
   512  
   513  		wg.Wait()
   514  	}
   515  }
   516  
   517  // Loader is an interface that handles missing data loading.
   518  type Loader[K comparable, V any] interface {
   519  	// Load should execute a custom item retrieval logic and
   520  	// return the item that is associated with the key.
   521  	// It should return nil if the item is not found/valid.
   522  	// The method is allowed to fetch data from the cache instance
   523  	// or update it for future use.
   524  	Load(c *Cache[K, V], key K) *Item[K, V]
   525  }
   526  
   527  // LoaderFunc type is an adapter that allows the use of ordinary
   528  // functions as data loaders.
   529  type LoaderFunc[K comparable, V any] func(*Cache[K, V], K) *Item[K, V]
   530  
   531  // Load executes a custom item retrieval logic and returns the item that
   532  // is associated with the key.
   533  // It returns nil if the item is not found/valid.
   534  func (l LoaderFunc[K, V]) Load(c *Cache[K, V], key K) *Item[K, V] {
   535  	return l(c, key)
   536  }
   537  
   538  // SuppressedLoader wraps another Loader and suppresses duplicate
   539  // calls to its Load method.
   540  type SuppressedLoader[K comparable, V any] struct {
   541  	Loader[K, V]
   542  
   543  	group *singleflight.Group
   544  }
   545  
   546  // Load executes a custom item retrieval logic and returns the item that
   547  // is associated with the key.
   548  // It returns nil if the item is not found/valid.
   549  // It also ensures that only one execution of the wrapped Loader's Load
   550  // method is in-flight for a given key at a time.
   551  func (l *SuppressedLoader[K, V]) Load(c *Cache[K, V], key K) *Item[K, V] {
   552  	// there should be a better/generic way to create a
   553  	// singleflight Group's key. It's possible that a generic
   554  	// singleflight.Group will be introduced with/in go1.19+
   555  	strKey := fmt.Sprint(key)
   556  
   557  	// the error can be discarded since the singleflight.Group
   558  	// itself does not return any of its errors, it returns
   559  	// the error that we return ourselves in the func below, which
   560  	// is also nil
   561  	res, _, _ := l.group.Do(strKey, func() (interface{}, error) {
   562  		item := l.Loader.Load(c, key)
   563  		if item == nil {
   564  			return nil, nil
   565  		}
   566  
   567  		return item, nil
   568  	})
   569  	if res == nil {
   570  		return nil
   571  	}
   572  
   573  	return res.(*Item[K, V])
   574  }