(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
This commit is contained in:
Andrew
2020-01-24 13:59:33 +11:00
committed by Adrian Gallagher
parent f6fd94ea69
commit e5b64a5580
5 changed files with 418 additions and 0 deletions

67
common/cache/README.md vendored Normal file
View File

@@ -0,0 +1,67 @@
# GoCryptoTrader package cache
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/page-logo.png?raw=true" width="350px" height="350px" hspace="70">
[![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
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB***

75
common/cache/cache.go vendored Normal file
View File

@@ -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()
}

145
common/cache/cache_test.go vendored Normal file
View File

@@ -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")
}
}

25
common/cache/cache_types.go vendored Normal file
View File

@@ -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{}
}

106
common/cache/lru.go vendored Normal file
View File

@@ -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)
}