forked from revel/revel
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a Cache interface, implemented by an in-memory cache and a memcac…
…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
Showing
10 changed files
with
781 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
language: go | ||
services: | ||
- memcache # github.com/robfig/revel/cache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
} |
Oops, something went wrong.