Skip to content

Commit

Permalink
Add high-level description to README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanjcochran committed Oct 25, 2018
1 parent 4ed2a8e commit 5b73ad4
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 21 deletions.
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,41 @@
[![GoDoc](https://godoc.org/github.com/nathanjcochran/lru?status.svg)](https://godoc.org/github.com/nathanjcochran/lru)
[![license](https://img.shields.io/github/license/nathanjcochran/lru.svg?maxAge=2592000)](LICENSE)

This is a fixed-size LRU cache that supports eviction, expiration, and
out-of-band updates.
A thread-safe, fixed-size, in-memory LRU cache that supports eviction,
expiration, and out-of-band updates.

This cache is implemented as a lock-protected map whose values are elements in
*two* doubly linked lists (unlike a traditional LRU, which only uses one). The
first doubly linked list keeps track of upcoming evictions. It orders items by
how recently they have been used. When an item is added to the cache, or
retrieved via the `Get` method, it is moved to the front of this list. When an
item needs to be evicted during a call to `Add` (because the cache is full),
the item chosen for eviction is pulled from the back of this list.

The second doubly linked list keeps track of how soon items will expire. When
items are added to the cache, they are added to the front of this list. A
single worker goroutine - the "expiration worker" - reads from the back. If
the item at the back is expired, it is removed from the cache. If it is not
yet expired, the worker puts itself to sleep until it is scheduled to expire.
Note that the worker may expire items early in order to avoid putting itself
to sleep for very small amounts of time (configurable). A caveat of this
design is that all items in the cache must share the same time-to-live (i.e.
newly added items always expire after existing items).

If an update function is provided via `SetUpdateFunc`, expired items are
queued in a channel by the expiration worker, rather than simply being removed
from the cache (the default behavior). Updates are handled by a separate pool
of worker goroutines - the "update workers". These workers read from the
channel and call the user-provided update function with the key of an expired
item. Depending on the value of the status flag returned, they either update
the item with a new value, remove the item from the cache, or reset the item's
time-to-live without updating its value. If the item is not removed from the
cache, it is moved back to the front of the expiration list after the update.
Its position in the eviction list is not changed. It is also possible to
configure the cache to only update items if they have received a threshold
number of hits since the last addition/update. This ensures that unused items
are not needlessly incurring the cost of being updated.

Unit tests and benchmarks are provided.

For more information, see the [documentation](https://godoc.org/github.com/nathanjcochran/lru).
45 changes: 26 additions & 19 deletions cache.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/*
A thread-safe, fixed-size, in-memory LRU cache that supports eviction,
expiration, and out-of-band updates.
For more information, see: https://github.com/nathanjcochran/lru.
*/
package lru

import (
Expand Down Expand Up @@ -41,14 +47,15 @@ type UpdateFunc func(key interface{}) (newVal interface{}, status Status)
// SetOnExpire, and SetOnBufferFull.
type Callback func(key, val interface{})

// Cache is a thread-safe fixed size LRU cache with a single worker goroutine
// that checks for expired items. If an UpdateFunc is provided (see
// SetUpdateFunc), updates are queued in a buffer (see SetBufferSize) and
// processed out-of-band via separate worker goroutines (see SetWorkers). If
// no UpdateFunc is provided, expired items are simply dropped from the cache
// (in which case, SetBufferSize and SetWorkers have no effect). Updates are
// not performed if the expired entry has fewer than a threshold number of
// hits since the last update (see SetUpdateThreshold).
// Cache is a thread-safe fixed-size LRU cache with a single worker goroutine
// that checks for expired items (see SetTTL and SetTTLMargin). If an
// UpdateFunc is provided (see SetUpdateFunc), updates are queued in a buffer
// (see SetBufferSize) and processed out-of-band via separate worker
// goroutines (see SetWorkers). If no UpdateFunc is provided, expired items
// are simply dropped from the cache (in which case, SetBufferSize and
// SetWorkers have no effect). Updates are not performed if the expired entry
// has fewer than a threshold number of hits since the last update (see
// SetUpdateThreshold).
type Cache struct {
size int
ttl time.Duration
Expand Down Expand Up @@ -393,7 +400,7 @@ func (c *Cache) Purge() {
// Purge).
func (c *Cache) Stop() {
close(c.quitChan)
for i := 0; i < c.workers+1; i++ {
for i := 0; i < c.workers+1; i++ { // +1 for expiration worker
<-c.doneChan
}
if c.workers > 0 {
Expand Down Expand Up @@ -498,16 +505,6 @@ func (c *Cache) updateWorker() {
}
}

func (c *Cache) drainUpdates() {
for {
select {
case <-c.updateChan:
default:
return
}
}
}

func (c *Cache) handleUpdate(key interface{}) {
// Call user-provided update function:
newVal, status := c.update(key)
Expand Down Expand Up @@ -539,6 +536,16 @@ func (c *Cache) handleUpdate(key interface{}) {
}
}

func (c *Cache) drainUpdates() {
for {
select {
case <-c.updateChan:
default:
return
}
}
}

func (c *Cache) evict() {
e := c.evtBack()
if e != &c.evtRoot {
Expand Down

0 comments on commit 5b73ad4

Please sign in to comment.