From e5b64a5580799616ba688609750876767a6870e8 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 24 Jan 2020 13:59:33 +1100 Subject: [PATCH] (COMMON): Basic (Least Recently Used) LRU caching system (#420) * started adding basic lru cache system * Added basic LRU cache including Add Get Remove Contains ContainsOrAdd Clear * wording changes on comments * removed exported var's in strut as they are not required * Added README * README updates * rm line :D * swapped to mutex over rwmutex updated comments * unexported getNewest & getOldest * unexported getNewest & getOldest * Updated comments and cited references in source * updated comments --- common/cache/README.md | 67 +++++++++++++++++ common/cache/cache.go | 75 +++++++++++++++++++ common/cache/cache_test.go | 145 ++++++++++++++++++++++++++++++++++++ common/cache/cache_types.go | 25 +++++++ common/cache/lru.go | 106 ++++++++++++++++++++++++++ 5 files changed, 418 insertions(+) create mode 100644 common/cache/README.md create mode 100644 common/cache/cache.go create mode 100644 common/cache/cache_test.go create mode 100644 common/cache/cache_types.go create mode 100644 common/cache/lru.go diff --git a/common/cache/README.md b/common/cache/README.md new file mode 100644 index 00000000..8703f2f9 --- /dev/null +++ b/common/cache/README.md @@ -0,0 +1,67 @@ +# GoCryptoTrader package cache + + + + +[![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/portfolio) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This cache package is part of the GoCryptoTrader codebase. + +## This is still in active development + +You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Current Features for cache package + ++ Basic LRU cache system with both goroutine safe (via mutex locking) and non-goroutine safe options + +## How to use + +##### Basic Usage: + +```go +package main + +import ("github.com/thrasher-corp/gocryptotrader/common/cache") + +func main() { + lruCache := cache.New(5) + lruCache.Add("hello", "world") + c := lruCache.Contains("hello") + if !c { + fmt.Println("expected cache to contain \"hello\" key") + } + + v := lruCache.Get("hello") + if v == nil { + fmt.Println("expected cache to contain \"hello\" key") + } + fmt.Println(v) +} +``` +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** + diff --git a/common/cache/cache.go b/common/cache/cache.go new file mode 100644 index 00000000..038141aa --- /dev/null +++ b/common/cache/cache.go @@ -0,0 +1,75 @@ +package cache + +// New returns a new concurrent-safe LRU cache with input capacity +func New(capacity uint64) *LRUCache { + return &LRUCache{ + lru: NewLRUCache(capacity), + } +} + +// Add new entry to Cache return true if entry removed +func (l *LRUCache) Add(k, v interface{}) { + l.m.Lock() + l.lru.Add(k, v) + l.m.Unlock() +} + +// Get looks up a key's value from the cache. +func (l *LRUCache) Get(key interface{}) (value interface{}) { + l.m.Lock() + defer l.m.Unlock() + return l.lru.Get(key) +} + +// GetOldest looks up old key's value from the cache. +func (l *LRUCache) getOldest() (key, value interface{}) { + l.m.Lock() + defer l.m.Unlock() + return l.lru.getOldest() +} + +// getNewest looks up a key's value from the cache. +func (l *LRUCache) getNewest() (key, value interface{}) { + l.m.Lock() + defer l.m.Unlock() + return l.lru.getNewest() +} + +// ContainsOrAdd checks if cache contains key if not adds to cache +func (l *LRUCache) ContainsOrAdd(key, value interface{}) bool { + l.m.Lock() + defer l.m.Unlock() + if l.lru.Contains(key) { + return true + } + l.lru.Add(key, value) + return false +} + +// Contains checks if cache contains key +func (l *LRUCache) Contains(key interface{}) bool { + l.m.Lock() + defer l.m.Unlock() + return l.lru.Contains(key) +} + +// Remove entry from cache +func (l *LRUCache) Remove(key interface{}) bool { + l.m.Lock() + defer l.m.Unlock() + return l.lru.Remove(key) +} + +// Clear is used to clear the cache. +func (l *LRUCache) Clear() { + l.m.Lock() + l.lru.Clear() + l.m.Unlock() +} + +// Len returns the number of items in the cache. +func (l *LRUCache) Len() uint64 { + l.m.Lock() + defer l.m.Unlock() + return l.lru.Len() +} diff --git a/common/cache/cache_test.go b/common/cache/cache_test.go new file mode 100644 index 00000000..6ac4cd9a --- /dev/null +++ b/common/cache/cache_test.go @@ -0,0 +1,145 @@ +package cache + +import ( + "testing" +) + +func TestCache(t *testing.T) { + lruCache := New(5) + lruCache.Add("hello", "world") + c := lruCache.Contains("hello") + if !c { + t.Fatal("expected cache to contain \"hello\" key") + } + + v := lruCache.Get("hello") + if v == nil { + t.Fatal("expected cache to contain \"hello\" key") + } + if v.(string) != "world" { + t.Fatal("expected \"hello\" key to contain value \"world\"") + } + + r := lruCache.Remove("hello") + if !r { + t.Fatal("expected \"hello\" key to be removed from cache") + } + + v = lruCache.Get("hello") + if v != nil { + t.Fatal("expected cache to not contain \"hello\" key") + } +} + +func TestContainsOrAdd(t *testing.T) { + lruCache := New(5) + + if lruCache.ContainsOrAdd("hello", "world") { + t.Fatal("expected ContainsOrAdd() to add new key when not found") + } + + if !lruCache.ContainsOrAdd("hello", "world") { + t.Fatal("expected ContainsOrAdd() to return true when key found") + } +} + +func TestClear(t *testing.T) { + lruCache := New(5) + for x := 0; x < 5; x++ { + lruCache.Add(x, x) + } + if lruCache.Len() != 5 { + t.Fatal("expected cache to have 5 entries") + } + lruCache.Clear() + if lruCache.Len() != 0 { + t.Fatal("expected cache to have 0 entries") + } +} + +func TestAdd(t *testing.T) { + lruCache := New(2) + lruCache.Add(1, 1) + lruCache.Add(2, 2) + if lruCache.Len() != 2 { + t.Fatal("expected cache to have 2 entries") + } + lruCache.Add(3, 3) + if lruCache.Len() != 2 { + t.Fatal("expected cache to have 2 entries") + } + + v := lruCache.Get(1) + if v != nil { + t.Fatal("expected cache to no longer contain \"1\" key") + } + v = lruCache.Get(2) + if v == nil { + t.Fatal("expected cache to contain \"2\" key") + } + if v.(int) != 2 { + t.Fatal("expected \"2\" key to contain value \"2\"") + } + k, v := lruCache.getNewest() + if k.(int) != 2 { + t.Fatal("expected latest key to be 2") + } + if v.(int) != 2 { + t.Fatal("expected latest value to be 2") + } + lruCache.Add(3, 3) + k, _ = lruCache.getNewest() + if k.(int) != 3 { + t.Fatal("expected latest key to be 3") + } + k, _ = lruCache.getOldest() + if k.(int) != 2 { + t.Fatal("expected oldest key to be 2") + } + k, v = lruCache.getOldest() + if k.(int) != 2 { + t.Fatal("expected oldest key to be 2") + } + if v.(int) != 2 { + t.Fatal("expected latest value to be 2") + } + lruCache.Add(2, 2) + k, _ = lruCache.getNewest() + if k.(int) != 2 { + t.Fatal("expected latest key to be 2") + } + k, _ = lruCache.getOldest() + if k.(int) != 3 { + t.Fatal("expected oldest key to be 3") + } +} + +func TestRemove(t *testing.T) { + lruCache := New(2) + lruCache.Add(1, 1) + + v := lruCache.Remove(1) + if !v { + t.Fatal("expected remove on valid key to return true") + } + v = lruCache.Remove(2) + if v { + t.Fatal("expected remove on invalid key to return false") + } +} + +func TestGetNewest(t *testing.T) { + lruCache := New(2) + k, _ := lruCache.getNewest() + if k != nil { + t.Fatal("expected GetNewest() on empty cache to return nil") + } +} + +func TestGetOldest(t *testing.T) { + lruCache := New(2) + k, _ := lruCache.getOldest() + if k != nil { + t.Fatal("expected GetOldest() on empty cache to return nil") + } +} diff --git a/common/cache/cache_types.go b/common/cache/cache_types.go new file mode 100644 index 00000000..64605340 --- /dev/null +++ b/common/cache/cache_types.go @@ -0,0 +1,25 @@ +package cache + +import ( + "container/list" + "sync" +) + +// LRUCache thread safe fixed size LRU cache +type LRUCache struct { + lru *LRU + m sync.Mutex +} + +// LRU non-thread safe fixed size LRU cache +type LRU struct { + Cap uint64 + l *list.List + items map[interface{}]*list.Element +} + +// item holds key/value for the cache +type item struct { + key interface{} + value interface{} +} diff --git a/common/cache/lru.go b/common/cache/lru.go new file mode 100644 index 00000000..995d05b4 --- /dev/null +++ b/common/cache/lru.go @@ -0,0 +1,106 @@ +/* + LRU Cache package + + Based off information obtained from: + + https://girai.dev/blog/lru-cache-implementation-in-go/ + https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU) +*/ + +package cache + +import "container/list" + +// NewLRUCache returns a new non-concurrent-safe LRU cache with input capacity +func NewLRUCache(capacity uint64) *LRU { + return &LRU{ + Cap: capacity, + l: list.New(), + items: make(map[interface{}]*list.Element), + } +} + +// Add adds a value to the cache +func (l *LRU) Add(key, value interface{}) { + if f, o := l.items[key]; o { + l.l.MoveToFront(f) + f.Value.(*item).value = value + return + } + + newItem := &item{key, value} + itemList := l.l.PushFront(newItem) + l.items[key] = itemList + if l.Len() > l.Cap { + l.removeOldestEntry() + } +} + +// Get returns keys value from cache if found +func (l *LRU) Get(key interface{}) interface{} { + if i, f := l.items[key]; f { + l.l.MoveToFront(i) + return i.Value.(*item).value + } + return nil +} + +// GetOldest returns the oldest entry +func (l *LRU) getOldest() (key, value interface{}) { + x := l.l.Back() + if x != nil { + return x.Value.(*item).key, x.Value.(*item).value + } + return +} + +// GetNewest returns the newest entry +func (l *LRU) getNewest() (key, value interface{}) { + x := l.l.Front() + if x != nil { + return x.Value.(*item).key, x.Value.(*item).value + } + return +} + +// Contains check if key is in cache this does not update LRU +func (l *LRU) Contains(key interface{}) (f bool) { + _, f = l.items[key] + return +} + +// Remove removes key from the cache, if the key was removed. +func (l *LRU) Remove(key interface{}) bool { + if i, f := l.items[key]; f { + l.removeElement(i) + return true + } + return false +} + +// Clear is used to completely clear the cache. +func (l *LRU) Clear() { + for x := range l.items { + delete(l.items, l.items[x]) + } + l.l.Init() +} + +// Len returns length of l +func (l *LRU) Len() uint64 { + return uint64(l.l.Len()) +} + +// removeOldest removes the oldest item from the cache. +func (l *LRU) removeOldestEntry() { + i := l.l.Back() + if i != nil { + l.removeElement(i) + } +} + +// removeElement element from the cache +func (l *LRU) removeElement(e *list.Element) { + l.l.Remove(e) + delete(l.items, e.Value.(*item).key) +}