diff --git a/cache/cache.go b/cache/cache.go index 55b3105..7de458a 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -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 diff --git a/cache/redis/redis.go b/cache/redis/redis.go new file mode 100644 index 0000000..8674641 --- /dev/null +++ b/cache/redis/redis.go @@ -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 +} diff --git a/cache/redis/redis_test.go b/cache/redis/redis_test.go new file mode 100644 index 0000000..cb470be --- /dev/null +++ b/cache/redis/redis_test.go @@ -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) + } +} diff --git a/example_inmemory_storage_test.go b/example_storages_test.go similarity index 57% rename from example_inmemory_storage_test.go rename to example_storages_test.go index 40b24b4..7d51c90 100644 --- a/example_inmemory_storage_test.go +++ b/example_storages_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/bxcodec/httpcache" + "github.com/bxcodec/httpcache/cache/redis" ) func Example_inMemoryStorageDefault() { @@ -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 @@ -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) + } + } + } +} diff --git a/go.mod b/go.mod index 89be9ff..9f75f51 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/httpcache.go b/httpcache.go index 112c6f7..8c9a7e1 100644 --- a/httpcache.go +++ b/httpcache.go @@ -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 @@ -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)) +}