Skip to content

Commit

Permalink
Add a Cache interface, implemented by an in-memory cache and a memcac…
Browse files Browse the repository at this point in the history
…hed client.

- The cache implementation may be configured from app.conf
- The cache handles serialization -- applications may cache any type (interface{})
- Serialization is done using gob encoding, except when storing []byte or ints
  • Loading branch information
robfig committed Mar 8, 2013
1 parent 1768509 commit 7363aa2
Show file tree
Hide file tree
Showing 10 changed files with 781 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
@@ -1 +1,3 @@
language: go
services:
- memcache # github.com/robfig/revel/cache
125 changes: 125 additions & 0 deletions cache/cache.go
@@ -0,0 +1,125 @@
package cache

import (
"errors"
"time"
)

// Length of time to cache an item.
const (
DEFAULT = time.Duration(0)
FOREVER = time.Duration(-1)
)

// Cache is an interface to an expiring cache. It behaves (and is modeled) like
// the Memcached interface.
//
// Many callers will make exclusive use of Set and Get, but more exotic
// functions are also available.
//
// Example
//
// Here is a typical Get/Set interaction:
//
// var items []*Item
// if err := cache.Get("items", &items); err != nil {
// items = loadItems()
// go cache.Set("items", items, cache.DEFAULT)
// }
//
// Note that the caller will frequently not wait for Set() to complete.
//
// Errors
//
// It is assumed that callers will infrequently check returned errors, since any
// request should be fulfillable without finding anything in the cache. As a
// result, all errors other than ErrCacheMiss and ErrNotStored will be logged to
// revel.ERROR, so that the developer does not need to check the return value to
// discover things like deserialization or connection errors.
type Cache interface {
// Set the given key/value in the cache. overwriting any existing value
// associated with that key.
//
// Returns:
// - nil on success
// - an implementation specific error otherwise
Set(key string, value interface{}, expires time.Duration) error

// Get the content associated with the given key. decoding it into the given
// pointer.
//
// Returns:
// - nil if the value was successfully retrieved and ptrValue set
// - ErrCacheMiss if the value was not in the cache
// - an implementation specific error otherwise
Get(key string, ptrValue interface{}) error

// Delete the given key from the cache.
//
// Returns:
// - nil on a successful delete
// - ErrCacheMiss if the value was not in the cache
// - an implementation specific error otherwise
Delete(key string) error

// Add the given key/value to the cache ONLY IF the key does not already exist.
//
// Returns:
// - nil if the value was added to the cache
// - ErrNotStored if the key was already present in the cache
// - an implementation-specific error otherwise
Add(key string, value interface{}, expires time.Duration) error

// Set the given key/value in the cache ONLY IF the key already exists.
//
// Returns:
// - nil if the value was replaced
// - ErrNotStored if the key does not exist in the cache
// - an implementation specific error otherwise
Replace(key string, value interface{}, expires time.Duration) error

// Increment the value stored at the given key by the given amount.
// The value silently wraps around upon exceeding the uint64 range.
//
// Returns the new counter value if the operation was successful, or:
// - ErrCacheMiss if the key was not found in the cache
// - an implementation specific error otherwise
Increment(key string, n uint64) (newValue uint64, err error)

// Decrement the value stored at the given key by the given amount.
// The value is capped at 0 on underflow, with no error returned.
//
// Returns the new counter value if the operation was successful, or:
// - ErrCacheMiss if the key was not found in the cache
// - an implementation specific error otherwise
Decrement(key string, n uint64) (newValue uint64, err error)

// Expire all cache entries immediately.
// This is not implemented for the memcached cache (intentionally).
// Returns an implementation specific error if the operation failed.
Flush() error
}

var (
Instance Cache

ErrCacheMiss = errors.New("revel/cache: key not found.")
ErrNotStored = errors.New("revel/cache: not stored.")
)

// The package implements the Cache interface (as sugar).

func Get(key string, ptrValue interface{}) error { return Instance.Get(key, ptrValue) }
func Delete(key string) error { return Instance.Delete(key) }
func Increment(key string, n uint64) (newValue uint64, err error) { return Instance.Increment(key, n) }
func Decrement(key string, n uint64) (newValue uint64, err error) { return Instance.Decrement(key, n) }
func Flush() error { return Instance.Flush() }
func Set(key string, value interface{}, expires time.Duration) error {
return Instance.Set(key, value, expires)
}
func Add(key string, value interface{}, expires time.Duration) error {
return Instance.Add(key, value, expires)
}
func Replace(key string, value interface{}, expires time.Duration) error {
return Instance.Replace(key, value, expires)
}
206 changes: 206 additions & 0 deletions cache/cache_test.go
@@ -0,0 +1,206 @@
package cache

import (
"math"
"testing"
"time"
)

// Tests against a generic Cache interface.
// They should pass for all implementations.
type cacheFactory func(*testing.T, time.Duration) Cache

// Test typical cache interactions
func typicalGetSet(t *testing.T, newCache cacheFactory) {
var err error
cache := newCache(t, time.Hour)

value := "foo"
if err = cache.Set("value", value, DEFAULT); err != nil {
t.Errorf("Error setting a value: %s", err)
}

value = ""
err = cache.Get("value", &value)
if err != nil {
t.Errorf("Error getting a value: %s", err)
}
if value != "foo" {
t.Errorf("Expected to get foo back, got %s", value)
}
}

// Test the increment-decrement cases
func incrDecr(t *testing.T, newCache cacheFactory) {
var err error
cache := newCache(t, time.Hour)

// Normal increment / decrement operation.
if err = cache.Set("int", 10, DEFAULT); err != nil {
t.Errorf("Error setting int: %s", err)
}
newValue, err := cache.Increment("int", 50)
if err != nil {
t.Errorf("Error incrementing int: %s", err)
}
if newValue != 60 {
t.Errorf("Expected 60, was %d", newValue)
}

if newValue, err = cache.Decrement("int", 50); err != nil {
t.Errorf("Error decrementing: %s", err)
}
if newValue != 10 {
t.Errorf("Expected 10, was %d", newValue)
}

// Increment wraparound
newValue, err = cache.Increment("int", math.MaxUint64-5)
if err != nil {
t.Errorf("Error wrapping around: %s", err)
}
if newValue != 4 {
t.Errorf("Expected wraparound 4, got %d", newValue)
}

// Decrement capped at 0
newValue, err = cache.Decrement("int", 25)
if err != nil {
t.Errorf("Error decrementing below 0: %s", err)
}
if newValue != 0 {
t.Errorf("Expected capped at 0, got %d", newValue)
}
}

func expiration(t *testing.T, newCache cacheFactory) {
// memcached does not support expiration times less than 1 second.
var err error
cache := newCache(t, time.Second)

// Test Set w/ DEFAULT
value := 10
cache.Set("int", value, DEFAULT)
time.Sleep(time.Second)
err = cache.Get("int", &value)
if err != ErrCacheMiss {
t.Errorf("Expected CacheMiss, but got: %s", err)
}

// Test Set w/ short time
cache.Set("int", value, time.Second)
time.Sleep(time.Second)
err = cache.Get("int", &value)
if err != ErrCacheMiss {
t.Errorf("Expected CacheMiss, but got: %s", err)
}

// Test Set w/ longer time.
cache.Set("int", value, time.Hour)
time.Sleep(time.Second)
err = cache.Get("int", &value)
if err != nil {
t.Errorf("Expected to get the value, but got: %s", err)
}

// Test Set w/ forever.
cache.Set("int", value, FOREVER)
time.Sleep(time.Second)
err = cache.Get("int", &value)
if err != nil {
t.Errorf("Expected to get the value, but got: %s", err)
}
}

func emptyCache(t *testing.T, newCache cacheFactory) {
var err error
cache := newCache(t, time.Hour)

err = cache.Get("notexist", 0)
if err == nil {
t.Errorf("Error expected for non-existent key")
}
if err != ErrCacheMiss {
t.Errorf("Expected ErrNotExists for non-existent key: %s", err)
}

err = cache.Delete("notexist")
if err != ErrCacheMiss {
t.Errorf("Expected ErrNotExists for non-existent key: %s", err)
}

_, err = cache.Increment("notexist", 1)
if err != ErrCacheMiss {
t.Errorf("Expected cache miss incrementing non-existent key: %s", err)
}

_, err = cache.Decrement("notexist", 1)
if err != ErrCacheMiss {
t.Errorf("Expected cache miss decrementing non-existent key: %s", err)
}
}

func testReplace(t *testing.T, newCache cacheFactory) {
var err error
cache := newCache(t, time.Hour)

// Replace in an empty cache.
if err = cache.Replace("notexist", 1, FOREVER); err != ErrNotStored {
t.Errorf("Replace in empty cache: expected ErrNotStored, got: %s", err)
}

// Set a value of 1, and replace it with 2
if err = cache.Set("int", 1, time.Second); err != nil {
t.Errorf("Unexpected error: %s", err)
}

if err = cache.Replace("int", 2, time.Second); err != nil {
t.Errorf("Unexpected error: %s", err)
}
var i int
if err = cache.Get("int", &i); err != nil {
t.Errorf("Unexpected error getting a replaced item: %s", err)
}
if i != 2 {
t.Errorf("Expected 2, got %d", i)
}

// Wait for it to expire and replace with 3 (unsuccessfully).
time.Sleep(time.Second)
if err = cache.Replace("int", 3, time.Second); err != ErrNotStored {
t.Errorf("Expected ErrNotStored, got: %s", err)
}
if err = cache.Get("int", &i); err != ErrCacheMiss {
t.Errorf("Expected cache miss, got: %s", err)
}
}

func testAdd(t *testing.T, newCache cacheFactory) {
var err error
cache := newCache(t, time.Hour)

// Add to an empty cache.
if err = cache.Add("int", 1, time.Second); err != nil {
t.Errorf("Unexpected error adding to empty cache: %s", err)
}

// Try to add again. (fail)
if err = cache.Add("int", 2, time.Second); err != ErrNotStored {
t.Errorf("Expected ErrNotStored adding dupe to cache: %s", err)
}

// Wait for it to expire, and add again.
time.Sleep(time.Second)
if err = cache.Add("int", 3, time.Second); err != nil {
t.Errorf("Unexpected error adding to cache: %s", err)
}

// Get and verify the value.
var i int
if err = cache.Get("int", &i); err != nil {
t.Errorf("Unexpected error: %s", err)
}
if i != 3 {
t.Errorf("Expected 3, got: %d", i)
}
}
34 changes: 34 additions & 0 deletions cache/init.go
@@ -0,0 +1,34 @@
package cache

import (
"github.com/robfig/revel"
"strings"
"time"
)

func init() {
revel.OnAppStart(func() {
// Set the default expiration time.
defaultExpiration := time.Hour // The default for the default is one hour.
if expireStr, found := revel.Config.String("cache.expires"); found {
var err error
if defaultExpiration, err = time.ParseDuration(expireStr); err != nil {
panic("Could not parse default cache expiration duration " + expireStr + ": " + err.Error())
}
}

// Use memcached?
if revel.Config.BoolDefault("cache.memcached", false) {
hosts := strings.Split(revel.Config.StringDefault("cache.hosts", ""), ",")
if len(hosts) == 0 {
panic("Memcache enabled but no memcached hosts specified!")
}

Instance = NewMemcachedCache(hosts, defaultExpiration)
return
}

// By default, use the in-memory cache.
Instance = NewInMemoryCache(defaultExpiration)
})
}

0 comments on commit 7363aa2

Please sign in to comment.