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

Commit

Permalink
feat: added redis storage (#25)
Browse files Browse the repository at this point in the history
Co-authored-by: o.musin <[email protected]>
  • Loading branch information
Oleg and o.musin authored Jul 6, 2020
1 parent bf43fca commit e926f2b
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 21 deletions.
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))
}

0 comments on commit e926f2b

Please sign in to comment.