memory cache

pull/99/head
Karolis Rusenas 2017-08-13 22:06:56 +01:00
parent 931f1695a9
commit c0d2188932
2 changed files with 292 additions and 0 deletions

208
cache/memory/memory.go vendored Normal file
View File

@ -0,0 +1,208 @@
package memory
import (
"context"
"fmt"
"strings"
"time"
)
type requestType int
// Request types
const (
GET requestType = iota
SET
DELETE
EXPIRE
COPY
)
type (
// Value - value is stored together with access and creation time
Value struct {
ctime time.Time
atime time.Time
value []byte
}
// Cache - cache container with a map of values and defaults
Cache struct {
cache map[string]*Value
ctimeExpiry time.Duration // creation time
atimeExpiry time.Duration // access time
expiryTick time.Duration
requestChannel chan *request
}
request struct {
requestType
key string
value []byte
responseChannel chan *response
}
response struct {
error
existingValue []byte
mapCopy map[string][]byte
value []byte
}
)
func (c *Cache) isOld(v *Value) bool {
if (c.ctimeExpiry != time.Duration(0)) && (time.Now().Sub(v.ctime) > c.ctimeExpiry) {
return true
}
if (c.atimeExpiry != time.Duration(0)) && (time.Now().Sub(v.atime) > c.atimeExpiry) {
return true
}
return false
}
// NewMemoryCache - creates new cache
func NewMemoryCache(ctimeExpiry, atimeExpiry, expiryTick time.Duration) *Cache {
c := &Cache{
cache: make(map[string]*Value),
ctimeExpiry: ctimeExpiry,
atimeExpiry: atimeExpiry,
expiryTick: expiryTick,
requestChannel: make(chan *request),
}
go c.service()
if ctimeExpiry != time.Duration(0) || atimeExpiry != time.Duration(0) {
go c.expiryService()
}
return c
}
func (c *Cache) service() {
for {
req := <-c.requestChannel
resp := &response{}
switch req.requestType {
case GET:
val, ok := c.cache[req.key]
if !ok {
resp.error = fmt.Errorf("key '%v' not found", req.key)
} else if c.isOld(val) {
resp.error = fmt.Errorf("key '%v' expired (atime %v)", req.key, val.atime)
delete(c.cache, req.key)
} else {
// update atime
val.atime = time.Now()
c.cache[req.key] = val
resp.value = val.value
}
req.responseChannel <- resp
case SET:
c.cache[req.key] = &Value{
value: req.value,
ctime: time.Now(),
atime: time.Now(),
}
req.responseChannel <- resp
case DELETE:
delete(c.cache, req.key)
req.responseChannel <- resp
case EXPIRE:
for k, v := range c.cache {
if c.isOld(v) {
delete(c.cache, k)
}
}
// no response
case COPY:
resp.mapCopy = make(map[string][]byte)
for k, v := range c.cache {
resp.mapCopy[k] = v.value
}
req.responseChannel <- resp
default:
resp.error = fmt.Errorf("invalid request type: %v", req.requestType)
req.responseChannel <- resp
}
}
}
// Get - looks up value and returns it
func (c *Cache) Get(ctx context.Context, key string) ([]byte, error) {
respChannel := make(chan *response)
c.requestChannel <- &request{
requestType: GET,
key: key,
responseChannel: respChannel,
}
resp := <-respChannel
return resp.value, resp.error
}
// Put - sets key/string. Overwrites existing key
func (c *Cache) Put(ctx context.Context, key string, value []byte) error {
respChannel := make(chan *response)
c.requestChannel <- &request{
requestType: SET,
key: key,
value: value,
responseChannel: respChannel,
}
resp := <-respChannel
return resp.error
}
// Del - deletes key
func (c *Cache) Delete(ctx context.Context, key string) error {
respChannel := make(chan *response)
c.requestChannel <- &request{
requestType: DELETE,
key: key,
responseChannel: respChannel,
}
resp := <-respChannel
return resp.error
}
// List all values for specified prefix
func (c *Cache) List(prefix string) ([][]byte, error) {
respChannel := make(chan *response)
c.requestChannel <- &request{
requestType: COPY,
responseChannel: respChannel,
}
resp := <-respChannel
var list [][]byte
for k, v := range resp.mapCopy {
if strings.HasPrefix(k, prefix) {
list = append(list, v)
}
}
return list, nil
}
// Copy - makes a copy of inmemory map
func (c *Cache) Copy() map[string][]byte {
respChannel := make(chan *response)
c.requestChannel <- &request{
requestType: COPY,
responseChannel: respChannel,
}
resp := <-respChannel
return resp.mapCopy
}
func (c *Cache) expiryService() {
ticker := time.NewTicker(c.expiryTick)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.requestChannel <- &request{
requestType: EXPIRE,
}
}
}
}

84
cache/memory/memory_test.go vendored Normal file
View File

@ -0,0 +1,84 @@
package memory
import (
"context"
"log"
"testing"
"time"
)
func TestCacheSetGet(t *testing.T) {
c := NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
err := c.Put(context.Background(), "a", []byte("b"))
if err != nil {
t.Errorf("failed to SET a key, got error: %s", err)
}
val, err := c.Get(context.Background(), "a")
if err != nil {
t.Errorf("failed to GET a key, got error: %s", err)
}
if string(val) != "b" {
log.Panicf("value %v", val)
}
cc := c.Copy()
if len(cc) != 1 {
t.Errorf("expected 1 item, got: %d", len(cc))
}
}
func TestCacheDel(t *testing.T) {
c := NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
err := c.Put(context.Background(), "a", []byte("b"))
if err != nil {
t.Errorf("failed to SET a key, got error: %s", err)
}
val, err := c.Get(context.Background(), "a")
if err != nil {
t.Errorf("failed to GET a key, got error: %s", err)
}
if string(val) != "b" {
log.Panicf("value %v", val)
}
err = c.Delete(context.Background(), "a")
if err != nil {
t.Errorf("faield to delete entry, got error: %s", err)
}
_, err = c.Get(context.Background(), "a")
if err == nil {
t.Errorf("expected to get an error after deletion, but got nil")
}
}
func TestCacheExpiration(t *testing.T) {
c := NewMemoryCache(100*time.Millisecond, 100*time.Millisecond, 10*time.Millisecond)
err := c.Put(context.Background(), "a", []byte("b"))
if err != nil {
t.Errorf("failed to SET a key, got error: %s", err)
}
val, err := c.Get(context.Background(), "a")
if err != nil {
t.Errorf("failed to GET a key, got error: %s", err)
}
if string(val) != "b" {
log.Panicf("value %v", val)
}
time.Sleep(200 * time.Millisecond)
_, err = c.Get(context.Background(), "a")
if err == nil {
t.Errorf("expected to get an error after deletion, but got nil")
}
}