Skip to content

Commit

Permalink
Add loader function with expiration (bluele#29)
Browse files Browse the repository at this point in the history
* Add LoaderExpireFunc

Replace loaderFunc with loaderExpireFunc
Extend builder
Extend load function for baseCache

* Add expiration handling to getWithLoader for arc cache

* Add expiration handling to getWithLoader for lfu cache

* Add expiration handling to getWithLoader for lru

* Add expiration handling to getWithLoader for simple cache

* Add tests for new LoaderExpireFunc

* Add docs for LoaderExpireFunc

* Add LoaderExpireFunc example to README.md
  • Loading branch information
anpryl authored and bluele committed May 28, 2017
1 parent b9993a9 commit 278acfb
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 38 deletions.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,48 @@ func main() {
Get: ok
```

### Automatically load value with expiration

```go
package main

import (
"github.com/bluele/gcache"
"fmt"
)

func main() {
gc := gcache.New(20).
LRU().
LoaderExpireFunc(func(key interface{}) (interface{}, *time.Duration, error) {
expire := 1 * time.Second
return "ok", &expire, nil
}).
EvictedFunc(func(key, value interface{}) {
fmt.Println("evicted key:", key)
}).
Build()
value, err := gc.Get("key")
if err != nil {
panic(err)
}
fmt.Println("Get:", value)
time.Sleep(1 * time.Second)
value, err := gc.Get("key")
if err != nil {
panic(err)
}
fmt.Println("Get:", value)
}
```

```
Get: ok
evicted key: key
Get: ok
```


## Cache Algorithm

* Least-Frequently Used (LFU)
Expand Down
10 changes: 7 additions & 3 deletions arc.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,19 +229,23 @@ func (c *ARC) getValue(key interface{}, onLoad bool) (interface{}, error) {
}

func (c *ARC) getWithLoader(key interface{}, isWait bool) (interface{}, error) {
if c.loaderFunc == nil {
if c.loaderExpireFunc == nil {
return nil, KeyNotFoundError
}
value, _, err := c.load(key, func(v interface{}, e error) (interface{}, error) {
value, _, err := c.load(key, func(v interface{}, expiration *time.Duration, e error) (interface{}, error) {
if e != nil {
return nil, e
}
c.mu.Lock()
_, err := c.set(key, v)
defer c.mu.Unlock()
item, err := c.set(key, v)
if err != nil {
return nil, err
}
if expiration != nil {
t := time.Now().Add(*expiration)
item.(*arcItem).expiration = &t
}
return v, nil
}, isWait)
if err != nil {
Expand Down
64 changes: 38 additions & 26 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,36 @@ type Cache interface {
}

type baseCache struct {
size int
loaderFunc LoaderFunc
evictedFunc EvictedFunc
addedFunc AddedFunc
deserializeFunc DeserializeFunc
serializeFunc SerializeFunc
expiration *time.Duration
mu sync.RWMutex
loadGroup Group
size int
loaderExpireFunc LoaderExpireFunc
evictedFunc EvictedFunc
addedFunc AddedFunc
deserializeFunc DeserializeFunc
serializeFunc SerializeFunc
expiration *time.Duration
mu sync.RWMutex
loadGroup Group
*stats
}

type (
LoaderFunc func(interface{}) (interface{}, error)
EvictedFunc func(interface{}, interface{})
AddedFunc func(interface{}, interface{})
DeserializeFunc func(interface{}, interface{}) (interface{}, error)
SerializeFunc func(interface{}, interface{}) (interface{}, error)
LoaderFunc func(interface{}) (interface{}, error)
LoaderExpireFunc func(interface{}) (interface{}, *time.Duration, error)
EvictedFunc func(interface{}, interface{})
AddedFunc func(interface{}, interface{})
DeserializeFunc func(interface{}, interface{}) (interface{}, error)
SerializeFunc func(interface{}, interface{}) (interface{}, error)
)

type CacheBuilder struct {
tp string
size int
loaderFunc LoaderFunc
evictedFunc EvictedFunc
addedFunc AddedFunc
expiration *time.Duration
deserializeFunc DeserializeFunc
serializeFunc SerializeFunc
tp string
size int
loaderExpireFunc LoaderExpireFunc
evictedFunc EvictedFunc
addedFunc AddedFunc
expiration *time.Duration
deserializeFunc DeserializeFunc
serializeFunc SerializeFunc
}

func New(size int) *CacheBuilder {
Expand All @@ -75,7 +76,18 @@ func New(size int) *CacheBuilder {
// Set a loader function.
// loaderFunc: create a new value with this function if cached value is expired.
func (cb *CacheBuilder) LoaderFunc(loaderFunc LoaderFunc) *CacheBuilder {
cb.loaderFunc = loaderFunc
cb.loaderExpireFunc = func(k interface{}) (interface{}, *time.Duration, error) {
v, err := loaderFunc(k)
return v, nil, err
}
return cb
}

// Set a loader function with expiration.
// loaderExpireFunc: create a new value with this function if cached value is expired.
// If nil returned instead of time.Duration from loaderExpireFunc than value will never expire.
func (cb *CacheBuilder) LoaderExpireFunc(loaderExpireFunc LoaderExpireFunc) *CacheBuilder {
cb.loaderExpireFunc = loaderExpireFunc
return cb
}

Expand Down Expand Up @@ -146,7 +158,7 @@ func (cb *CacheBuilder) build() Cache {

func buildCache(c *baseCache, cb *CacheBuilder) {
c.size = cb.size
c.loaderFunc = cb.loaderFunc
c.loaderExpireFunc = cb.loaderExpireFunc
c.expiration = cb.expiration
c.addedFunc = cb.addedFunc
c.deserializeFunc = cb.deserializeFunc
Expand All @@ -156,9 +168,9 @@ func buildCache(c *baseCache, cb *CacheBuilder) {
}

// load a new value using by specified key.
func (c *baseCache) load(key interface{}, cb func(interface{}, error) (interface{}, error), isWait bool) (interface{}, bool, error) {
func (c *baseCache) load(key interface{}, cb func(interface{}, *time.Duration, error) (interface{}, error), isWait bool) (interface{}, bool, error) {
v, called, err := c.loadGroup.Do(key, func() (interface{}, error) {
return cb(c.loaderFunc(key))
return cb(c.loaderExpireFunc(key))
}, isWait)
if err != nil {
return nil, called, err
Expand Down
88 changes: 88 additions & 0 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,94 @@ func TestLoaderFunc(t *testing.T) {
}
}

func TestLoaderExpireFuncWithoutExpire(t *testing.T) {
size := 2
var testCaches = []*gcache.CacheBuilder{
gcache.New(size).Simple(),
gcache.New(size).LRU(),
gcache.New(size).LFU(),
gcache.New(size).ARC(),
}
for _, builder := range testCaches {
var testCounter int64
counter := 1000
cache := builder.
LoaderExpireFunc(func(key interface{}) (interface{}, *time.Duration, error) {
return atomic.AddInt64(&testCounter, 1), nil, nil
}).
EvictedFunc(func(key, value interface{}) {
panic(key)
}).Build()

var wg sync.WaitGroup
for i := 0; i < counter; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := cache.Get(0)
if err != nil {
t.Error(err)
}
}()
}

wg.Wait()

if testCounter != 1 {
t.Errorf("testCounter != %v", testCounter)
}
}
}

func TestLoaderExpireFuncWithExpire(t *testing.T) {
size := 2
var testCaches = []*gcache.CacheBuilder{
gcache.New(size).Simple(),
gcache.New(size).LRU(),
gcache.New(size).LFU(),
gcache.New(size).ARC(),
}
for _, builder := range testCaches {
var testCounter int64
counter := 1000
expire := 200 * time.Millisecond
cache := builder.
LoaderExpireFunc(func(key interface{}) (interface{}, *time.Duration, error) {
return atomic.AddInt64(&testCounter, 1), &expire, nil
}).
Build()

var wg sync.WaitGroup
for i := 0; i < counter; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := cache.Get(0)
if err != nil {
t.Error(err)
}
}()
}
time.Sleep(expire) // Waiting for key expiration
for i := 0; i < counter; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := cache.Get(0)
if err != nil {
t.Error(err)
}
}()
}

wg.Wait()

if testCounter != 2 {
t.Errorf("testCounter != %v", testCounter)
}
}
}

func TestDeserializeFunc(t *testing.T) {
var cases = []struct {
tp string
Expand Down
10 changes: 7 additions & 3 deletions lfu.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,19 +154,23 @@ func (c *LFUCache) getValue(key interface{}, onLoad bool) (interface{}, error) {
}

func (c *LFUCache) getWithLoader(key interface{}, isWait bool) (interface{}, error) {
if c.loaderFunc == nil {
if c.loaderExpireFunc == nil {
return nil, KeyNotFoundError
}
value, _, err := c.load(key, func(v interface{}, e error) (interface{}, error) {
value, _, err := c.load(key, func(v interface{}, expiration *time.Duration, e error) (interface{}, error) {
if e != nil {
return nil, e
}
c.mu.Lock()
_, err := c.set(key, v)
defer c.mu.Unlock()
item, err := c.set(key, v)
if err != nil {
return nil, err
}
if expiration != nil {
t := time.Now().Add(*expiration)
item.(*lfuItem).expiration = &t
}
return v, nil
}, isWait)
if err != nil {
Expand Down
10 changes: 7 additions & 3 deletions lru.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,19 +147,23 @@ func (c *LRUCache) getValue(key interface{}, onLoad bool) (interface{}, error) {
}

func (c *LRUCache) getWithLoader(key interface{}, isWait bool) (interface{}, error) {
if c.loaderFunc == nil {
if c.loaderExpireFunc == nil {
return nil, KeyNotFoundError
}
value, _, err := c.load(key, func(v interface{}, e error) (interface{}, error) {
value, _, err := c.load(key, func(v interface{}, expiration *time.Duration, e error) (interface{}, error) {
if e != nil {
return nil, e
}
c.mu.Lock()
_, err := c.set(key, v)
defer c.mu.Unlock()
item, err := c.set(key, v)
if err != nil {
return nil, err
}
if expiration != nil {
t := time.Now().Add(*expiration)
item.(*lruItem).expiration = &t
}
return v, nil
}, isWait)
if err != nil {
Expand Down
10 changes: 7 additions & 3 deletions simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,23 @@ func (c *SimpleCache) getValue(key interface{}, onLoad bool) (interface{}, error
}

func (c *SimpleCache) getWithLoader(key interface{}, isWait bool) (interface{}, error) {
if c.loaderFunc == nil {
if c.loaderExpireFunc == nil {
return nil, KeyNotFoundError
}
value, _, err := c.load(key, func(v interface{}, e error) (interface{}, error) {
value, _, err := c.load(key, func(v interface{}, expiration *time.Duration, e error) (interface{}, error) {
if e != nil {
return nil, e
}
c.mu.Lock()
_, err := c.set(key, v)
defer c.mu.Unlock()
item, err := c.set(key, v)
if err != nil {
return nil, err
}
if expiration != nil {
t := time.Now().Add(*expiration)
item.(*simpleItem).expiration = &t
}
return v, nil
}, isWait)
if err != nil {
Expand Down

0 comments on commit 278acfb

Please sign in to comment.