Skip to content
This repository has been archived by the owner on Mar 8, 2024. It is now read-only.

added redis storage #25

Merged
merged 1 commit into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ var (
ErrFailedToSaveToCache = errors.New("Failed to save item")
// ErrCacheMissed will throw if an item can't be retrieved (due to invalid, or missing)
ErrCacheMissed = errors.New("Cache is missing")
// ErrStorageInternal will throw when some internal error in storage occurred
ErrStorageInternal = errors.New("Internal error in storage")
)

// Cache storage type
Expand Down
80 changes: 80 additions & 0 deletions cache/redis/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package redis

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/bxcodec/httpcache/cache"
"github.com/go-redis/redis/v8"
)

// CacheOptions for storing data for Redis connections
type CacheOptions struct {
Addr string
Password string
DB int // 0 for default DB
}

type redisCache struct {
ctx context.Context
cache *redis.Client
expiryTime time.Duration
}

// NewCache will return the redis cache handler
func NewCache(ctx context.Context, c *redis.Client, exptime time.Duration) cache.ICacheInteractor {
return &redisCache{
ctx: ctx,
cache: c,
expiryTime: exptime,
}
}

func (i *redisCache) Set(key string, value cache.CachedResponse) (err error) {
valueJSON, _ := json.Marshal(value)
set := i.cache.Set(i.ctx, key, string(valueJSON), i.expiryTime*time.Second)
if err := set.Err(); err != nil {
fmt.Println(err)
return cache.ErrStorageInternal
}
return nil
}

func (i *redisCache) Get(key string) (res cache.CachedResponse, err error) {
get := i.cache.Do(i.ctx, "get", key)
if err = get.Err(); err != nil {
if err == redis.Nil {
return cache.CachedResponse{}, cache.ErrCacheMissed
}
return cache.CachedResponse{}, cache.ErrStorageInternal
}
val := get.Val().(string)
err = json.Unmarshal([]byte(val), &res)
if err != nil {
return cache.CachedResponse{}, cache.ErrStorageInternal
}
return
}

func (i *redisCache) Delete(key string) (err error) {
// deleting in redis equal to setting expiration time for key to 0
set := i.cache.Set(i.ctx, key, nil, 0)
if err := set.Err(); err != nil {
return cache.ErrStorageInternal
}
return nil
}

func (i *redisCache) Origin() string {
return cache.CacheRedis
}

func (i *redisCache) Flush() error {
flush := i.cache.FlushAll(i.ctx)
if err := flush.Err(); err != nil {
return cache.ErrStorageInternal
}
return nil
}
66 changes: 66 additions & 0 deletions cache/redis/redis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package redis_test

import (
"context"
"testing"
"time"

"github.com/alicebob/miniredis"
"github.com/bxcodec/httpcache/cache"
rediscache "github.com/bxcodec/httpcache/cache/redis"
"github.com/go-redis/redis/v8"
)

func TestCacheRedis(t *testing.T) {
s, err := miniredis.Run()
if err != nil {
panic(err)
}
defer s.Close()
c := redis.NewClient(&redis.Options{
Addr: s.Addr(),
Password: "", // no password set
DB: 0, // use default DB
})

cacheObj := rediscache.NewCache(context.Background(), c, 15)
testKey := "KEY"
testVal := cache.CachedResponse{
DumpedResponse: nil,
RequestURI: "http://bxcodec.io",
RequestMethod: "GET",
CachedTime: time.Now(),
}

// Try to SET item
err = cacheObj.Set(testKey, testVal)
if err != nil {
t.Fatalf("expected %v, got %v", nil, err)
}

// try to GET item from cache
res, err := cacheObj.Get(testKey)
if err != nil {
t.Fatalf("expected %v, got %v", nil, err)
}
// assert the content
if res.RequestURI != testVal.RequestURI {
t.Fatalf("expected %v, got %v", testVal.RequestURI, res.RequestURI)
}
// assert the content
if res.RequestMethod != testVal.RequestMethod {
t.Fatalf("expected %v, got %v", testVal.RequestMethod, res.RequestMethod)
}

// try to DELETE the item
err = cacheObj.Delete(testKey)
if err != nil {
t.Fatalf("expected %v, got %v", nil, err)
}

// try to re-GET item from cache after deleted
res, err = cacheObj.Get(testKey)
if err == nil {
t.Fatalf("expected %v, got %v", err, nil)
}
}
80 changes: 60 additions & 20 deletions example_inmemory_storage_test.go → example_storages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/bxcodec/httpcache"
"github.com/bxcodec/httpcache/cache/redis"
)

func Example_inMemoryStorageDefault() {
Expand All @@ -16,27 +17,42 @@ func Example_inMemoryStorageDefault() {
log.Fatal(err)
}

for i := 0; i < 100; i++ {
startTime := time.Now()
req, err := http.NewRequest("GET", "https://bxcodec.io", nil)
if err != nil {
log.Fatal((err))
}
res, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Response time: %v micro-second\n", time.Since(startTime).Microseconds())
fmt.Println("Status Code", res.StatusCode)
time.Sleep(time.Second * 1)
fmt.Println("Sequence >>> ", i)
if i%5 == 0 {
err := handler.CacheInteractor.Flush()
if err != nil {
log.Fatal(err)
}
}
processCachedRequest(client, handler)
// Example Output:
/*
2020/06/21 13:14:51 Cache item's missing failed to retrieve from cache, trying with a live version
Response time: 940086 micro-second
Status Code 200
Sequence >>> 0
2020/06/21 13:14:53 Cache item's missing failed to retrieve from cache, trying with a live version
Response time: 73679 micro-second
Status Code 200
Sequence >>> 1
Response time: 126 micro-second
Status Code 200
Sequence >>> 2
Response time: 96 micro-second
Status Code 200
Sequence >>> 3
Response time: 102 micro-second
Status Code 200
Sequence >>> 4
Response time: 94 micro-second
Status Code 200
Sequence >>> 5
*/
}

func Example_redisStorage() {
client := &http.Client{}
handler, err := httpcache.NewWithRedisCache(client, true, &redis.CacheOptions{
Addr: "localhost:6379",
}, time.Second*15)
if err != nil {
log.Fatal(err)
}

processCachedRequest(client, handler)
// Example Output:
/*
2020/06/21 13:14:51 Cache item's missing failed to retrieve from cache, trying with a live version
Expand All @@ -61,3 +77,27 @@ func Example_inMemoryStorageDefault() {
Sequence >>> 5
*/
}

func processCachedRequest(client *http.Client, handler *httpcache.CacheHandler) {
for i := 0; i < 100; i++ {
startTime := time.Now()
req, err := http.NewRequest("GET", "https://bxcodec.io", nil)
if err != nil {
log.Fatal((err))
}
res, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Response time: %v micro-second\n", time.Since(startTime).Microseconds())
fmt.Println("Status Code", res.StatusCode)
time.Sleep(time.Second * 1)
fmt.Println("Sequence >>> ", i)
if i%5 == 0 {
err := handler.CacheInteractor.Flush()
if err != nil {
log.Fatal(err)
}
}
}
}
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ module github.com/bxcodec/httpcache
go 1.13

require (
github.com/alicebob/miniredis v2.5.0+incompatible
github.com/alicebob/miniredis/v2 v2.13.0
github.com/bxcodec/gotcha v1.0.0-beta.2
github.com/go-redis/redis/v8 v8.0.0-beta.5
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/stretchr/testify v1.4.0
github.com/stretchr/testify v1.5.1
golang.org/x/net v0.0.0-20190923162816-aa69164e4478
)
21 changes: 21 additions & 0 deletions httpcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
inmemcache "github.com/bxcodec/gotcha/cache"
"github.com/bxcodec/httpcache/cache"
"github.com/bxcodec/httpcache/cache/inmem"
rediscache "github.com/bxcodec/httpcache/cache/redis"
"github.com/go-redis/redis/v8"
"golang.org/x/net/context"
)

// NewWithCustomStorageCache will initiate the httpcache with your defined cache storage
Expand Down Expand Up @@ -40,3 +43,21 @@ func NewWithInmemoryCache(client *http.Client, rfcCompliance bool, duration ...t

return newClient(client, rfcCompliance, inmem.NewCache(c))
}

// NewWithRedisCache will create a complete cache-support of HTTP client with using redis cache.
// If the duration not set, the cache will use LFU algorithm
func NewWithRedisCache(client *http.Client, rfcCompliance bool, options *rediscache.CacheOptions,
duration ...time.Duration) (cachedHandler *CacheHandler, err error) {
var ctx = context.Background()
var expiryTime time.Duration
if len(duration) > 0 {
expiryTime = duration[0]
}
c := redis.NewClient(&redis.Options{
Addr: options.Addr,
Password: options.Password,
DB: options.DB,
})

return newClient(client, rfcCompliance, rediscache.NewCache(ctx, c, expiryTime))
}