memory cache
parent
931f1695a9
commit
c0d2188932
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue