-
Notifications
You must be signed in to change notification settings - Fork 276
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
959 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 |
---|---|---|
@@ -0,0 +1,2 @@ | ||
*/Godeps/* | ||
!*/Godeps/Godeps.json |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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,154 @@ | ||
package cache | ||
|
||
import ( | ||
"bytes" | ||
"crypto/sha1" | ||
"errors" | ||
"github.com/gin-gonic/gin" | ||
"net/url" | ||
"net/http" | ||
"io" | ||
"time" | ||
) | ||
|
||
const ( | ||
DEFAULT = time.Duration(0) | ||
FOREVER = time.Duration(-1) | ||
CACHE_MIDDLEWARE_KEY = "gincontrib.cache" | ||
) | ||
|
||
var ( | ||
PageCachePrefix = "gincontrib.page.cahe" | ||
ErrCacheMiss = errors.New("cache: key not found.") | ||
ErrNotStored = errors.New("cache: not stored.") | ||
ErrNotSupport = errors.New("cache: not support.") | ||
) | ||
|
||
type CacheStore interface { | ||
Get(key string, value interface{}) error | ||
Set(key string, value interface{}, expire time.Duration) error | ||
Add(key string, value interface{}, expire time.Duration) error | ||
Replace(key string, data interface{}, expire time.Duration) error | ||
Delete(key string) error | ||
Increment(key string, data uint64) (uint64, error) | ||
Decrement(key string, data uint64) (uint64, error) | ||
Flush() error | ||
} | ||
|
||
type responseCache struct { | ||
status int | ||
header http.Header | ||
data []byte | ||
} | ||
|
||
type cachedWriter struct { | ||
gin.ResponseWriter | ||
status int | ||
written bool | ||
store CacheStore | ||
expire time.Duration | ||
key string | ||
} | ||
|
||
func urlEscape(prefix string, u string) string { | ||
key := url.QueryEscape(u) | ||
if len(key) > 200 { | ||
h := sha1.New() | ||
io.WriteString(h, u) | ||
key = string(h.Sum(nil)) | ||
} | ||
var buffer bytes.Buffer | ||
buffer.WriteString(prefix) | ||
buffer.WriteString(":") | ||
buffer.WriteString(key) | ||
return buffer.String() | ||
} | ||
|
||
func newCachedWriter(store CacheStore, expire time.Duration, writer gin.ResponseWriter, key string) *cachedWriter { | ||
return &cachedWriter{writer, 0, false, store, expire, key} | ||
} | ||
|
||
func (w *cachedWriter) WriteHeader(code int) { | ||
w.status = code | ||
w.written = true | ||
w.ResponseWriter.WriteHeader(code) | ||
} | ||
|
||
func (w *cachedWriter) Status() int { | ||
return w.status | ||
} | ||
|
||
func (w *cachedWriter) Written() bool { | ||
return w.written | ||
} | ||
|
||
func (w *cachedWriter) Write(data []byte) (int, error) { | ||
ret, err := w.ResponseWriter.Write(data) | ||
if err == nil { | ||
//cache response | ||
store := w.store | ||
val := responseCache{ | ||
w.status, | ||
w.Header(), | ||
data, | ||
} | ||
err = store.Set(w.key, val, w.expire) | ||
if err != nil { | ||
// need logger | ||
} | ||
} | ||
return ret, err | ||
} | ||
|
||
// Cache Middleware | ||
func Cache(store *CacheStore) gin.HandlerFunc { | ||
return func(c *gin.Context) { | ||
c.Set(CACHE_MIDDLEWARE_KEY, store) | ||
c.Next() | ||
} | ||
} | ||
|
||
func SiteCache(store CacheStore, expire time.Duration) gin.HandlerFunc { | ||
|
||
return func(c *gin.Context) { | ||
var cache responseCache | ||
url := c.Req.URL | ||
key := urlEscape(PageCachePrefix, url.RequestURI()) | ||
if err := store.Get(key, &cache); err != nil { | ||
c.Next() | ||
} else { | ||
c.Writer.WriteHeader(cache.status) | ||
for k, vals := range cache.header { | ||
for _, v := range vals { | ||
c.Writer.Header().Add(k, v) | ||
} | ||
} | ||
c.Writer.Write(cache.data) | ||
} | ||
} | ||
} | ||
|
||
// Cache Decorator | ||
func CachePage(store CacheStore, expire time.Duration, handle gin.HandlerFunc) gin.HandlerFunc { | ||
|
||
return func(c *gin.Context) { | ||
var cache responseCache | ||
url := c.Req.URL | ||
key := urlEscape(PageCachePrefix, url.RequestURI()) | ||
if err := store.Get(key, &cache); err != nil { | ||
// replace writer | ||
writer := newCachedWriter(store, expire, c.Writer, key) | ||
c.Writer = writer | ||
handle(c) | ||
} else { | ||
c.Writer.WriteHeader(cache.status) | ||
for k, vals := range cache.header { | ||
for _, v := range vals { | ||
c.Writer.Header().Add(k, v) | ||
} | ||
} | ||
c.Writer.Write(cache.data) | ||
} | ||
} | ||
} | ||
|
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,203 @@ | ||
package cache | ||
|
||
import ( | ||
"math" | ||
"testing" | ||
"time" | ||
) | ||
|
||
type cacheFactory func(*testing.T, time.Duration) CacheStore | ||
|
||
// 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(2 * 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(2 * 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(2 * 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(2 * 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 ErrCacheMiss for non-existent key: %s", err) | ||
} | ||
|
||
err = cache.Delete("notexist") | ||
if err != ErrCacheMiss { | ||
t.Errorf("Expected ErrCacheMiss 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(2 * 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(2 * 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) | ||
} | ||
} | ||
|
Oops, something went wrong.