Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
abihf committed Apr 16, 2020
1 parent 0d1f00f commit 0680133
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 0 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,25 @@
# cache-loader
Golang Cache Loader

## Feature
* Thread safe
* Fetch once even when concurrent process request same key.
* stale-while-revalidate when item is expired

## Example

```go
func main() {
itemLoader := loader.NewLRU(fetchItem, 5 * time.Minute, 1000)
item, err := loader.Get("key")
// use item
}

func fetchItem(key interface{}) (interface{}, error) {
res, err := http.Get("https://example.com/item/" + key)
if err != nil {
return nil, err
}
return processResponse(res)
}
```
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/abihf/cache-loader

go 1.14

require github.com/hashicorp/golang-lru v0.5.4
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
104 changes: 104 additions & 0 deletions loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package loader

import (
"sync"
"time"
)

// LoadFunc loads the value based on key
type LoadFunc func(key interface{}) (interface{}, error)

// Cache stores the items
// you can use ARCCache or TwoQueueCache from github.com/hashicorp/golang-lru
type Cache interface {
Add(key, value interface{})
Get(key interface{}) (value interface{}, ok bool)
Remove(key interface{})
}

// Loader manage items in cache and fetch them if not exist
type Loader struct {
fn LoadFunc
cache Cache
ttl time.Duration

mutex sync.Mutex
}

// New creates new Loader
func New(fn LoadFunc, ttl time.Duration, cache Cache) *Loader {
return &Loader{
fn: fn,
cache: cache,
ttl: ttl,

mutex: sync.Mutex{},
}
}

// Get the item.
// If it doesn't exist on cache, Loader will call LoadFunc once even when other go routine access the same key.
// If the item is expired, it will return old value while loading new one.
func (l *Loader) Get(key interface{}) (interface{}, error) {
l.mutex.Lock()
cached, ok := l.cache.Get(key)
if ok {
defer l.mutex.Unlock()

item := cached.(*cacheItem)
item.mutex.Lock()
defer item.mutex.Unlock()

if item.expire.Before(time.Now()) && !item.isFetching {
item.isFetching = true // so other thread don't fetch
go l.refetch(key, item)
}
return item.value, nil
}

item := &cacheItem{isFetching: true, mutex: sync.Mutex{}}
item.mutex.Lock()
defer item.mutex.Unlock()
defer func() {
item.isFetching = false
}()
l.cache.Add(key, item)
l.mutex.Unlock()

value, err := l.fn(key)
if err != nil {
l.cache.Remove(key)
return nil, err
}
item.value = value
item.updateExpire(l.ttl)
return value, nil
}

func (l *Loader) refetch(key interface{}, item *cacheItem) {
item.isFetching = true // to make sure, lol
defer func() {
item.isFetching = false
}()

value, err := l.fn(key)
if err != nil {
l.cache.Remove(key)
return
}
item.value = value
item.updateExpire(l.ttl)
}

type cacheItem struct {
value interface{}
expire time.Time

mutex sync.Mutex
isFetching bool
}

func (i *cacheItem) updateExpire(ttl time.Duration) {
newExpire := time.Now().Add(ttl)
i.expire = newExpire
}
13 changes: 13 additions & 0 deletions lru.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package loader

import (
"time"

lru "github.com/hashicorp/golang-lru"
)

// NewLRU creates Loader with lru based cache
func NewLRU(fn LoadFunc, ttl time.Duration, size int) *Loader {
cache, _ := lru.NewARC(size)
return New(fn, ttl, cache)
}

0 comments on commit 0680133

Please sign in to comment.