1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-18 02:17:05 +00:00

make cache package to work across multi handlers, remove the old 'WrapHandler' and keep the cache.Handler as documented only

Former-commit-id: b030cd92d26a9f646b060e379b3702b9a677749b
This commit is contained in:
Gerasimos (Makis) Maropoulos
2018-01-01 21:53:12 +02:00
parent 9e6691e5ce
commit b43b25626b
8 changed files with 215 additions and 99 deletions

2
cache/LICENSE vendored
View File

@@ -1,4 +1,4 @@
Copyright (c) 2017 The Iris Cache Authors. All rights reserved.
Copyright (c) 2017-2018 The Iris Cache Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are

34
cache/cache.go vendored
View File

@@ -7,18 +7,17 @@ Example code:
"time"
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/cache"
)
func main(){
app := iris.Default()
cachedHandler := cache.WrapHandler(h, 2 *time.Minute)
app.Get("/hello", cachedHandler)
middleware := cache.Handler(2 *time.Minute)
app.Get("/hello", middleware, h)
app.Run(iris.Addr(":8080"))
}
func h(ctx context.Context) {
func h(ctx iris.Context) {
ctx.HTML("<h1> Hello, this should be cached. Every 2 minutes it will be refreshed, check your browser's inspector</h1>")
}
*/
@@ -32,46 +31,29 @@ import (
"github.com/kataras/iris/context"
)
// Cache accepts two parameters
// first is the context.Handler which you want to cache its result
// the second is, optional, the cache Entry's expiration duration
// Cache accepts the cache expiration duration
// if the expiration <=2 seconds then expiration is taken by the "cache-control's maxage" header
// returns context.Handler, which you can use as your default router or per-route handler
//
// All types of response can be cached, templates, json, text, anything.
//
// You can add validators with this function.
func Cache(bodyHandler context.Handler, expiration time.Duration) *client.Handler {
return client.NewHandler(bodyHandler, expiration)
}
// WrapHandler accepts two parameters
// first is the context.Handler which you want to cache its result
// the second is, optional, the cache Entry's expiration duration
// if the expiration <=2 seconds then expiration is taken by the "cache-control's maxage" header
// returns context.Handler, which you can use as your default router or per-route handler
//
// All types of response can be cached, templates, json, text, anything.
//
// it returns a context.Handler, for more options use the `Cache`
func WrapHandler(bodyHandler context.Handler, expiration time.Duration) context.Handler {
return Cache(bodyHandler, expiration).ServeHTTP
func Cache(expiration time.Duration) *client.Handler {
return client.NewHandler(expiration)
}
// Handler accepts one single parameter:
// the cache Entry's expiration duration
// the cache expiration duration
// if the expiration <=2 seconds then expiration is taken by the "cache-control's maxage" header
// returns context.Handler.
//
// It's the same as Cache and WrapHandler but it sets the "bodyHandler" to the next handler in the chain.
//
// All types of response can be cached, templates, json, text, anything.
//
// it returns a context.Handler which can be used as a middleware, for more options use the `Cache`.
//
// Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/#caching
func Handler(expiration time.Duration) context.Handler {
h := WrapHandler(nil, expiration)
h := Cache(expiration).ServeHTTP
return h
}

70
cache/cache_test.go vendored
View File

@@ -23,10 +23,10 @@ var (
errTestFailed = errors.New("expected the main handler to be executed %d times instead of %d")
)
func runTest(e *httpexpect.Expect, counterPtr *uint32, expectedBodyStr string, nocache string) error {
e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
func runTest(e *httpexpect.Expect, path string, counterPtr *uint32, expectedBodyStr string, nocache string) error {
e.GET(path).Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
time.Sleep(cacheDuration / 5) // lets wait for a while, cache should be saved and ready
e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
e.GET(path).Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
counter := atomic.LoadUint32(counterPtr)
if counter > 1 {
// n should be 1 because it doesn't changed after the first call
@@ -35,19 +35,19 @@ func runTest(e *httpexpect.Expect, counterPtr *uint32, expectedBodyStr string, n
time.Sleep(cacheDuration)
// cache should be cleared now
e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
e.GET(path).Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
time.Sleep(cacheDuration / 5)
// let's call again , the cache should be saved
e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
e.GET(path).Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
counter = atomic.LoadUint32(counterPtr)
if counter != 2 {
return errTestFailed.Format(2, counter)
}
// we have cache response saved for the "/" path, we have some time more here, but here
// we have cache response saved for the path, we have some time more here, but here
// we will make the requestS with some of the deniers options
e.GET("/").WithHeader("max-age", "0").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
e.GET("/").WithHeader("Authorization", "basic or anything").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
e.GET(path).WithHeader("max-age", "0").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
e.GET(path).WithHeader("Authorization", "basic or anything").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
counter = atomic.LoadUint32(counterPtr)
if counter != 4 {
return errTestFailed.Format(4, counter)
@@ -71,8 +71,8 @@ func runTest(e *httpexpect.Expect, counterPtr *uint32, expectedBodyStr string, n
return errTestFailed.Format(6, counter)
}
// let's call again the "/", the expiration is not passed so it should be cached
e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
// let's call again the path the expiration is not passed so it should be cached
e.GET(path).Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
counter = atomic.LoadUint32(counterPtr)
if counter != 6 {
return errTestFailed.Format(6, counter)
@@ -88,19 +88,19 @@ func TestNoCache(t *testing.T) {
app := iris.New()
var n uint32
app.Get("/", cache.WrapHandler(func(ctx context.Context) {
app.Get("/", cache.Handler(cacheDuration), func(ctx context.Context) {
atomic.AddUint32(&n, 1)
ctx.Write([]byte(expectedBodyStr))
}, cacheDuration))
})
app.Get("/nocache", cache.WrapHandler(func(ctx context.Context) {
app.Get("/nocache", cache.Handler(cacheDuration), func(ctx context.Context) {
cache.NoCache(ctx) // <----
atomic.AddUint32(&n, 1)
ctx.Write([]byte(expectedBodyStr))
}, cacheDuration))
})
e := httptest.New(t, app)
if err := runTest(e, &n, expectedBodyStr, "/nocache"); err != nil {
if err := runTest(e, "/", &n, expectedBodyStr, "/nocache"); err != nil {
t.Fatalf(t.Name()+": %v", err)
}
@@ -117,11 +117,25 @@ func TestCache(t *testing.T) {
ctx.Write([]byte(expectedBodyStr))
})
var (
n2 uint32
expectedBodyStr2 = "This is the other"
)
app.Get("/other", func(ctx context.Context) {
atomic.AddUint32(&n2, 1)
ctx.Write([]byte(expectedBodyStr2))
})
e := httptest.New(t, app)
if err := runTest(e, &n, expectedBodyStr, ""); err != nil {
if err := runTest(e, "/", &n, expectedBodyStr, ""); err != nil {
t.Fatalf(t.Name()+": %v", err)
}
if err := runTest(e, "/other", &n2, expectedBodyStr2, ""); err != nil {
t.Fatalf(t.Name()+" other: %v", err)
}
}
func TestCacheHandlerParallel(t *testing.T) {
@@ -138,10 +152,10 @@ func TestCacheValidator(t *testing.T) {
ctx.Write([]byte(expectedBodyStr))
}
validCache := cache.Cache(h, cacheDuration)
app.Get("/", validCache.ServeHTTP)
validCache := cache.Cache(cacheDuration)
app.Get("/", validCache.ServeHTTP, h)
managedCache := cache.Cache(h, cacheDuration)
managedCache := cache.Cache(cacheDuration)
managedCache.AddRule(rule.Validator([]rule.PreValidator{
func(ctx context.Context) bool {
if ctx.Request().URL.Path == "/invalid" {
@@ -151,12 +165,7 @@ func TestCacheValidator(t *testing.T) {
},
}, nil))
managedCache2 := cache.Cache(func(ctx context.Context) {
atomic.AddUint32(&n, 1)
ctx.Header("DONT", "DO not cache that response even if it was claimed")
ctx.Write([]byte(expectedBodyStr))
}, cacheDuration)
managedCache2 := cache.Cache(cacheDuration)
managedCache2.AddRule(rule.Validator(nil,
[]rule.PostValidator{
func(ctx context.Context) bool {
@@ -168,10 +177,15 @@ func TestCacheValidator(t *testing.T) {
},
))
app.Get("/valid", validCache.ServeHTTP)
app.Get("/valid", validCache.ServeHTTP, h)
app.Get("/invalid", managedCache.ServeHTTP)
app.Get("/invalid2", managedCache2.ServeHTTP)
app.Get("/invalid", managedCache.ServeHTTP, h)
app.Get("/invalid2", managedCache2.ServeHTTP, func(ctx context.Context) {
atomic.AddUint32(&n, 1)
ctx.Header("DONT", "DO not cache that response even if it was claimed")
ctx.Write([]byte(expectedBodyStr))
})
e := httptest.New(t, app)

View File

@@ -1,6 +1,7 @@
package client
import (
"sync"
"time"
"github.com/kataras/iris/cache/cfg"
@@ -10,34 +11,27 @@ import (
)
// Handler the local cache service handler contains
// the original bodyHandler, the memory cache entry and
// the original response, the memory cache entry and
// the validator for each of the incoming requests and post responses
type Handler struct {
// bodyHandler the original route's handler.
// If nil then it tries to take the next handler from the chain.
bodyHandler context.Handler
// Rule optional validators for pre cache and post cache actions
//
// See more at ruleset.go
rule rule.Rule
// entry is the memory cache entry
entry *entry.Entry
// when expires.
expiration time.Duration
// entries the memory cache stored responses.
entries map[string]*entry.Entry
mu sync.RWMutex
}
// NewHandler returns a new cached handler for the "bodyHandler"
// which expires every "expiration".
func NewHandler(bodyHandler context.Handler,
expiration time.Duration) *Handler {
e := entry.NewEntry(expiration)
func NewHandler(expiration time.Duration) *Handler {
return &Handler{
bodyHandler: bodyHandler,
rule: DefaultRuleSet,
entry: e,
rule: DefaultRuleSet,
expiration: expiration,
entries: make(map[string]*entry.Entry, 0),
}
}
@@ -66,35 +60,53 @@ func (h *Handler) AddRule(r rule.Rule) *Handler {
return h
}
var emptyHandler = func(ctx context.Context) {
ctx.StatusCode(500)
ctx.WriteString("cache: empty body handler")
ctx.StopExecution()
}
func (h *Handler) ServeHTTP(ctx context.Context) {
// check for pre-cache validators, if at least one of them return false
// for this specific request, then skip the whole cache
bodyHandler := h.bodyHandler
bodyHandler := ctx.NextHandler()
if bodyHandler == nil {
if nextHandler := ctx.NextHandler(); nextHandler != nil {
// skip prepares the context to move to the next handler if the "nextHandler" has a ctx.Next() inside it,
// even if it's not executed because it's cached.
ctx.Skip()
bodyHandler = nextHandler
} else {
ctx.StatusCode(500)
ctx.WriteString("cache: empty body handler")
ctx.StopExecution()
return
}
emptyHandler(ctx)
return
}
// skip prepares the context to move to the next handler if the "nextHandler" has a ctx.Next() inside it,
// even if it's not executed because it's cached.
ctx.Skip()
if !h.rule.Claim(ctx) {
bodyHandler(ctx)
return
}
// check if we have a stored response( it is not expired)
res, exists := h.entry.Response()
if !exists {
var (
response *entry.Response
valid = false
key = ctx.Path()
)
// if it's not exists, then execute the original handler
h.mu.RLock()
e, found := h.entries[key]
h.mu.RUnlock()
if found {
// the entry is here, .Response will give us
// if it's expired or no
response, valid = e.Response()
} else {
// create the entry now.
e = entry.NewEntry(h.expiration)
h.mu.Lock()
h.entries[key] = e
h.mu.Unlock()
}
if !valid {
// if it's expired, then execute the original handler
// with our custom response recorder response writer
// because the net/http doesn't give us
// a built'n way to get the status code & body
@@ -119,12 +131,12 @@ func (h *Handler) ServeHTTP(ctx context.Context) {
// check for an expiration time if the
// given expiration was not valid then check for GetMaxAge &
// update the response & release the recorder
h.entry.Reset(recorder.StatusCode(), recorder.Header().Get(cfg.ContentTypeHeader), body, GetMaxAge(ctx.Request()))
e.Reset(recorder.StatusCode(), recorder.Header().Get(cfg.ContentTypeHeader), body, GetMaxAge(ctx.Request()))
return
}
// if it's valid then just write the cached results
ctx.ContentType(res.ContentType())
ctx.StatusCode(res.StatusCode())
ctx.Write(res.Body())
ctx.ContentType(response.ContentType())
ctx.StatusCode(response.StatusCode())
ctx.Write(response.Body())
}