Skip to content

Commit

Permalink
limit in lru cache and a lot of tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Sebastian Mancke committed Jun 22, 2016
1 parent 785ecb7 commit c2cc797
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 72 deletions.
105 changes: 95 additions & 10 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,114 @@
package cache

import (
"github.com/hashicorp/golang-lru"
"github.com/Sirupsen/logrus"
"github.com/hashicorp/golang-lru/simplelru"
"github.com/tarent/lib-compose/logging"
"sync"
"sync/atomic"
"time"
)

// Cache is a LRU cache with the following features
// - limits on max entries
// - memory size limit
// - ttl for entries
type Cache struct {
lruBackend *lru.Cache
lock sync.RWMutex
lruBackend *simplelru.LRU
maxAge time.Duration
maxSizeBytes int32
currentSizeBytes int32
}

// NewCache creates a cache with max 100MB and max 10.000 Entries
func NewCache(entrySize int) *Cache {
arc, err := lru.New(entrySize)
type CacheEntry struct {
key string
label string
size int
fetchTime time.Time
cacheObject interface{}
hits int
}

// NewCache creates a new cache
func NewCache(maxEntries int, maxSizeBytes int32, maxAge time.Duration) *Cache {
c := &Cache{
maxAge: maxAge,
maxSizeBytes: maxSizeBytes,
}

var err error
c.lruBackend, err = simplelru.NewLRU(maxEntries, simplelru.EvictCallback(c.onEvicted))
if err != nil {
panic(err)
}
return &Cache{
lruBackend: arc,
return c
}

// LogEvery Start a Goroutine, which logs statisitcs periodically.
func (c *Cache) LogEvery(d time.Duration) {
go c.logEvery(d)
}

func (c *Cache) logEvery(d time.Duration) {
for {
select {
case <-time.After(d):
logging.Logger.WithFields(logrus.Fields{
"type": "metric",
"matric_name": "cachestatus",
"cache_entries": c.Len(),
"cache_size_bytes": c.SizeByte(),
}).Infof("cache status #%v, %vbytes", c.Len(), c.SizeByte())
}
}
}

func (c *Cache) onEvicted(key, value interface{}) {
entry := value.(*CacheEntry)
atomic.AddInt32(&c.currentSizeBytes, -1*int32(entry.size))
}

func (c *Cache) Get(key string) (interface{}, bool) {
return c.lruBackend.Get(key)
c.lock.Lock()
defer c.lock.Unlock()
e, found := c.lruBackend.Get(key)
if found {
entry := e.(*CacheEntry)
if time.Since(entry.fetchTime) < c.maxAge {
entry.hits++
return entry.cacheObject, true
}
}
return nil, false
}

func (c *Cache) Set(key string, label string, sizeBytes int, cacheObject interface{}) {
entry := &CacheEntry{
key: key,
label: label,
size: sizeBytes,
fetchTime: time.Now(),
cacheObject: cacheObject,
}
c.lock.Lock()
defer c.lock.Unlock()
atomic.AddInt32(&c.currentSizeBytes, int32(sizeBytes))
c.lruBackend.Add(key, entry)

for atomic.LoadInt32(&c.currentSizeBytes) > int32(c.maxSizeBytes) {
c.lruBackend.RemoveOldest()
}
}

// SizeByte returns the total memory consumption of the cache
func (c *Cache) SizeByte() int {
return int(atomic.LoadInt32(&c.currentSizeBytes))
}

func (c *Cache) Set(key string, sizeBytes int, cacheObject interface{}) {
c.lruBackend.Add(key, cacheObject)
// Len returns the total number of entries in the cache
func (c *Cache) Len() int {
c.lock.RLock()
defer c.lock.RUnlock()
return c.lruBackend.Len()
}
7 changes: 4 additions & 3 deletions cache/cache_strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,14 @@ func (tcs *CacheStrategy) IsCachable(method string, url string, statusCode int,
req := &http.Request{Method: method, Header: requestHeader}
reasons, _, err := cacheobject.UsingRequestResponse(req, statusCode, responseHeader, true)
if err != nil {
logging.Logger.WithError(err).Warnf("error checking cachability fot %v %v: %v", method, url, err)
logging.Logger.WithError(err).Warnf("error checking cachability for %v %v: %v", method, url, err)
return false
}

for _, foundReason := range reasons {
if !tcs.isReasonIgnorable(foundReason) {
logging.Logger.WithField("notCachableReason", foundReason).Debugf("ressource not cachable %v %v: %v", method, url, foundReason)
logging.Logger.WithField("notCachableReason", foundReason).
WithField("type", "cacheinfo").
Debugf("ressource not cachable %v %v: %v", method, url, foundReason)
return false
}
}
Expand Down
24 changes: 11 additions & 13 deletions composition/cache_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,39 @@ package composition

import (
"github.com/tarent/lib-compose/cache"
"github.com/tarent/lib-compose/logging"
"strings"
"time"
)

type CachingContentLoader struct {
httpContentLoader ContentLoader
fileContentLoader ContentLoader
cache *cache.Cache
cache Cache
}

func NewCachingContentLoader() *CachingContentLoader {
c := cache.NewCache(1000, 50*1024*1024, time.Minute*20)
c.LogEvery(time.Second * 5)
return &CachingContentLoader{
httpContentLoader: NewHttpContentLoader(),
fileContentLoader: NewFileContentLoader(),
cache: cache.NewCache(10000),
cache: c,
}
}

func (loader *CachingContentLoader) Load(fd *FetchDefinition) (Content, error) {
hash := fd.Hash()

if c, exist := loader.cache.Get(hash); exist {
println("found: " + fd.URL + " " + hash)
return c.(Content), nil
} else {
println("not found: " + fd.URL + " " + hash)

if cFromCache, exist := loader.cache.Get(hash); exist {
logging.Cacheinfo(fd.URL, true)
return cFromCache.(Content), nil
}

logging.Cacheinfo(fd.URL, false)
c, err := loader.load(fd)
if err == nil {
if fd.IsCachable(c.HttpStatusCode(), c.HttpHeader()) {
println("Set: " + fd.URL + " " + hash)
loader.cache.Set(hash, c.MemorySize(), c)
} else {
println("Not cachable: " + fd.URL + " " + hash)
loader.cache.Set(hash, fd.URL, c.MemorySize(), c)
}
}
return c, err
Expand Down
44 changes: 0 additions & 44 deletions composition/content_fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,10 @@ package composition
import (
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
"time"
)

func Test_ContentFetcher_FetchDefinitionHash(t *testing.T) {
a := assert.New(t)
tests := []struct {
fd1 *FetchDefinition
fd2 *FetchDefinition
eq bool
}{
{
NewFetchDefinition("/foo"),
NewFetchDefinition("/foo"),
true,
},
{
NewFetchDefinition("/foo"),
NewFetchDefinition("/bar"),
false,
},
{
&FetchDefinition{
URL: "/foo",
Timeout: time.Second,
Header: http.Header{"Some": {"header"}},
Required: false,
},
&FetchDefinition{
URL: "/foo",
Timeout: time.Second * 42,
Header: http.Header{"Some": {"header"}},
Required: true,
},
true,
},
}

for _, t := range tests {
if t.eq {
a.Equal(t.fd1.Hash(), t.fd2.Hash())
} else {
a.NotEqual(t.fd1.Hash(), t.fd2.Hash())
}
}
}

func Test_ContentFetcher_FetchingWithDependency(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
Expand Down
42 changes: 41 additions & 1 deletion composition/interface_mocks_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Automatically generated by MockGen. DO NOT EDIT!
// Source: github.com/tarent/lib-compose/composition (interfaces: Fragment,ContentLoader,Content,ContentMerger,ContentParser,ResponseProcessor)
// Source: github.com/tarent/lib-compose/composition (interfaces: Fragment,ContentLoader,Content,ContentMerger,ContentParser,ResponseProcessor,Cache)

package composition

Expand Down Expand Up @@ -314,3 +314,43 @@ func (_m *MockResponseProcessor) Process(_param0 *http.Response, _param1 string)
func (_mr *_MockResponseProcessorRecorder) Process(arg0, arg1 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "Process", arg0, arg1)
}

// Mock of Cache interface
type MockCache struct {
ctrl *gomock.Controller
recorder *_MockCacheRecorder
}

// Recorder for MockCache (not exported)
type _MockCacheRecorder struct {
mock *MockCache
}

func NewMockCache(ctrl *gomock.Controller) *MockCache {
mock := &MockCache{ctrl: ctrl}
mock.recorder = &_MockCacheRecorder{mock}
return mock
}

func (_m *MockCache) EXPECT() *_MockCacheRecorder {
return _m.recorder
}

func (_m *MockCache) Get(_param0 string) (interface{}, bool) {
ret := _m.ctrl.Call(_m, "Get", _param0)
ret0, _ := ret[0].(interface{})
ret1, _ := ret[1].(bool)
return ret0, ret1
}

func (_mr *_MockCacheRecorder) Get(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "Get", arg0)
}

func (_m *MockCache) Set(_param0 string, _param1 string, _param2 int, _param3 interface{}) {
_m.ctrl.Call(_m, "Set", _param0, _param1, _param2, _param3)
}

func (_mr *_MockCacheRecorder) Set(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "Set", arg0, arg1, arg2, arg3)
}
7 changes: 6 additions & 1 deletion composition/interfaces.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package composition

//go:generate go get github.com/golang/mock/mockgen
//go:generate mockgen -self_package composition -package composition -destination interface_mocks_test.go github.com/tarent/lib-compose/composition Fragment,ContentLoader,Content,ContentMerger,ContentParser,ResponseProcessor
//go:generate mockgen -self_package composition -package composition -destination interface_mocks_test.go github.com/tarent/lib-compose/composition Fragment,ContentLoader,Content,ContentMerger,ContentParser,ResponseProcessor,Cache
//go:generate sed -ie "s/composition .github.com\\/tarent\\/lib-compose\\/composition.//g;s/composition\\.//g" interface_mocks_test.go
import (
"io"
Expand Down Expand Up @@ -100,3 +100,8 @@ type ErrorHandler interface {
// handle http request errors
Handle(err error, w http.ResponseWriter, r *http.Request)
}

type Cache interface {
Get(hash string) (cacheObject interface{}, found bool)
Set(hash string, label string, memorySize int, cacheObject interface{})
}
17 changes: 17 additions & 0 deletions logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,23 @@ func Call(r *http.Request, resp *http.Response, start time.Time, err error) {
Logger.WithFields(fields).Warn("call, but no response given")
}

// Cacheinfo logs the hit information a accessing a ressource
func Cacheinfo(url string, hit bool) {
var msg string
if hit {
msg = fmt.Sprintf("cache hit: %v", url)
} else {
msg = fmt.Sprintf("cache miss: %v", url)
}
Logger.WithFields(
logrus.Fields{
"type": "cacheinfo",
"url": url,
"hit": hit,
}).
Debug(msg)
}

// Return a log entry for application logs,
// prefilled with the correlation ids out of the supplied request.
func Application(r *http.Request) *logrus.Entry {
Expand Down
27 changes: 27 additions & 0 deletions logging/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,33 @@ func Test_Logger_LifecycleStop(t *testing.T) {
a.Equal("b666", data["build_number"])
}

func Test_Logger_Cacheinfo(t *testing.T) {
a := assert.New(t)

// given a logger
Set("debug", false)
defer Set("info", false)
b := bytes.NewBuffer(nil)
Logger.Out = b

// when a positive cachinfo is logged
Cacheinfo("/foo", true)

// then: it is logged
data := mapFromBuffer(b)
a.Equal("/foo", data["url"])
a.Equal("cacheinfo", data["type"])
a.Equal(true, data["hit"])
a.Equal("cache hit: /foo", data["message"])

b.Reset()
// logging a non hit
Cacheinfo("/foo", false)
data = mapFromBuffer(b)
a.Equal(false, data["hit"])
a.Equal("cache miss: /foo", data["message"])
}

func logRecordFromBuffer(b *bytes.Buffer) *logReccord {
data := &logReccord{}
err := json.Unmarshal(b.Bytes(), data)
Expand Down

0 comments on commit c2cc797

Please sign in to comment.