Merge pull request #852 from kataras/dev

make cache package to work across multi handlers.

Former-commit-id: 2aafeb75f093c973d0b05b7f94c302f64b4b70e6
This commit is contained in:
Gerasimos (Makis) Maropoulos 2018-01-01 21:55:10 +02:00 committed by GitHub
commit e30862b74b
8 changed files with 215 additions and 99 deletions

View File

@ -34,9 +34,9 @@ func (r resource) loadFromBase(dir string) string {
} }
result := string(b) result := string(b)
//if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
// result = strings.Replace(result, "\n", "\r\n", -1) result = strings.Replace(result, "\n", "\r\n", -1)
//} }
return result return result
} }

View File

@ -3,6 +3,7 @@ package main
import ( import (
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
@ -35,6 +36,7 @@ func (r resource) loadFromBase(dir string) string {
} }
result := string(b) result := string(b)
return result return result
} }

View File

@ -3,6 +3,7 @@ package main
import ( import (
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
@ -34,9 +35,9 @@ func (r resource) loadFromBase(dir string) string {
panic(fullpath + " failed with error: " + err.Error()) panic(fullpath + " failed with error: " + err.Error())
} }
result := string(b) result := string(b)
// if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
// result = strings.Replace(result, "\n", "\r\n", -1) result = strings.Replace(result, "\n", "\r\n", -1)
// } }
return result return result
} }

105
_examples/mvc/cache/main.go vendored Normal file
View File

@ -0,0 +1,105 @@
/*
If you want to use it as middleware for the entire controller
you can use its router which is just a sub router to add it as you normally do with standard API:
I'll show you 4 different methods for adding a middleware into an mvc application,
all of those 4 do exactly the same thing, select what you prefer,
I prefer the last code-snippet when I need the middleware to be registered somewhere
else as well, otherwise I am going with the first one:
```go
// 1
mvc.Configure(app.Party("/user"), func(m *mvc.Application) {
m.Router.Use(cache.Handler(10*time.Second))
})
```
```go
// 2
// same:
userRouter := app.Party("/user")
userRouter.Use(cache.Handler(10*time.Second))
mvc.Configure(userRouter, ...)
```
```go
// 3
// same:
userRouter := app.Party("/user", cache.Handler(10*time.Second))
mvc.Configure(userRouter, ...)
```
```go
// 4
// same:
app.PartyFunc("/user", func(r iris.Party){
r.Use(cache.Handler(10*time.Second))
mvc.Configure(r, ...)
})
```
If you want to use a middleware for a single route,
for a single controller's method that is already registered by the engine
and not by custom `Handle` (which you can add
the middleware there on the last parameter) and it's not depend on the `Next Handler` to do its job
then you just call it on the method:
```go
var myMiddleware := myMiddleware.New(...) // this should return an iris/context.Handler
type UserController struct{}
func (c *UserController) GetSomething(ctx iris.Context) {
// ctx.Proceed checks if myMiddleware called `ctx.Next()`
// inside it and returns true if so, otherwise false.
nextCalled := ctx.Proceed(myMiddleware)
if !nextCalled {
return
}
// else do the job here, it's allowed
}
```
And last, if you want to add a middleware on a specific method
and it depends on the next and the whole chain then you have to do it
using the `AfterActivation` like the example below:
*/
package main
import (
"time"
"github.com/kataras/iris"
"github.com/kataras/iris/cache"
"github.com/kataras/iris/mvc"
)
var cacheHandler = cache.Handler(10 * time.Second)
func main() {
app := iris.New()
// You don't have to use .Configure if you do it all in the main func
// mvc.Configure and mvc.New(...).Configure() are just helpers to split
// your code better, here we use the simplest form:
m := mvc.New(app)
m.Handle(&exampleController{})
app.Run(iris.Addr(":8080"))
}
type exampleController struct{}
func (c *exampleController) AfterActivation(a mvc.AfterActivation) {
// select the route based on the method name you want to
// modify.
index := a.GetRoute("Get")
// just prepend the handler(s) as middleware(s) you want to use.
// or append for "done" handlers.
index.Handlers = append([]iris.Handler{cacheHandler}, index.Handlers...)
}
func (c *exampleController) Get() string {
// refresh every 10 seconds and you will see different time output.
now := time.Now().Format("Mon, Jan 02 2006 15:04:05")
return "last time executed without cache: " + now
}

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 Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are modification, are permitted provided that the following conditions are

34
cache/cache.go vendored
View File

@ -7,18 +7,17 @@ Example code:
"time" "time"
"github.com/kataras/iris" "github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/cache" "github.com/kataras/iris/cache"
) )
func main(){ func main(){
app := iris.Default() app := iris.Default()
cachedHandler := cache.WrapHandler(h, 2 *time.Minute) middleware := cache.Handler(2 *time.Minute)
app.Get("/hello", cachedHandler) app.Get("/hello", middleware, h)
app.Run(iris.Addr(":8080")) 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>") 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" "github.com/kataras/iris/context"
) )
// Cache accepts two parameters // Cache accepts the cache expiration duration
// 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 // 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 // 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. // All types of response can be cached, templates, json, text, anything.
// //
// You can add validators with this function. // You can add validators with this function.
func Cache(bodyHandler context.Handler, expiration time.Duration) *client.Handler { func Cache(expiration time.Duration) *client.Handler {
return client.NewHandler(bodyHandler, expiration) return client.NewHandler(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
} }
// Handler accepts one single parameter: // 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 // if the expiration <=2 seconds then expiration is taken by the "cache-control's maxage" header
// returns context.Handler. // 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. // 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`. // 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 // Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/#caching
func Handler(expiration time.Duration) context.Handler { func Handler(expiration time.Duration) context.Handler {
h := WrapHandler(nil, expiration) h := Cache(expiration).ServeHTTP
return h 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") 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 { func runTest(e *httpexpect.Expect, path string, counterPtr *uint32, expectedBodyStr string, nocache string) error {
e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr) 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 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) counter := atomic.LoadUint32(counterPtr)
if counter > 1 { if counter > 1 {
// n should be 1 because it doesn't changed after the first call // 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) time.Sleep(cacheDuration)
// cache should be cleared now // 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) time.Sleep(cacheDuration / 5)
// let's call again , the cache should be saved // 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) counter = atomic.LoadUint32(counterPtr)
if counter != 2 { if counter != 2 {
return errTestFailed.Format(2, counter) 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 // 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(path).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("Authorization", "basic or anything").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
counter = atomic.LoadUint32(counterPtr) counter = atomic.LoadUint32(counterPtr)
if counter != 4 { if counter != 4 {
return errTestFailed.Format(4, counter) return errTestFailed.Format(4, counter)
@ -71,8 +71,8 @@ func runTest(e *httpexpect.Expect, counterPtr *uint32, expectedBodyStr string, n
return errTestFailed.Format(6, counter) return errTestFailed.Format(6, counter)
} }
// let's call again the "/", the expiration is not passed so it should be cached // let's call again the path the expiration is not passed so it should be cached
e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr) e.GET(path).Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
counter = atomic.LoadUint32(counterPtr) counter = atomic.LoadUint32(counterPtr)
if counter != 6 { if counter != 6 {
return errTestFailed.Format(6, counter) return errTestFailed.Format(6, counter)
@ -88,19 +88,19 @@ func TestNoCache(t *testing.T) {
app := iris.New() app := iris.New()
var n uint32 var n uint32
app.Get("/", cache.WrapHandler(func(ctx context.Context) { app.Get("/", cache.Handler(cacheDuration), func(ctx context.Context) {
atomic.AddUint32(&n, 1) atomic.AddUint32(&n, 1)
ctx.Write([]byte(expectedBodyStr)) 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) // <---- cache.NoCache(ctx) // <----
atomic.AddUint32(&n, 1) atomic.AddUint32(&n, 1)
ctx.Write([]byte(expectedBodyStr)) ctx.Write([]byte(expectedBodyStr))
}, cacheDuration)) })
e := httptest.New(t, app) 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) t.Fatalf(t.Name()+": %v", err)
} }
@ -117,11 +117,25 @@ func TestCache(t *testing.T) {
ctx.Write([]byte(expectedBodyStr)) 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) 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) 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) { func TestCacheHandlerParallel(t *testing.T) {
@ -138,10 +152,10 @@ func TestCacheValidator(t *testing.T) {
ctx.Write([]byte(expectedBodyStr)) ctx.Write([]byte(expectedBodyStr))
} }
validCache := cache.Cache(h, cacheDuration) validCache := cache.Cache(cacheDuration)
app.Get("/", validCache.ServeHTTP) app.Get("/", validCache.ServeHTTP, h)
managedCache := cache.Cache(h, cacheDuration) managedCache := cache.Cache(cacheDuration)
managedCache.AddRule(rule.Validator([]rule.PreValidator{ managedCache.AddRule(rule.Validator([]rule.PreValidator{
func(ctx context.Context) bool { func(ctx context.Context) bool {
if ctx.Request().URL.Path == "/invalid" { if ctx.Request().URL.Path == "/invalid" {
@ -151,12 +165,7 @@ func TestCacheValidator(t *testing.T) {
}, },
}, nil)) }, nil))
managedCache2 := cache.Cache(func(ctx context.Context) { managedCache2 := cache.Cache(cacheDuration)
atomic.AddUint32(&n, 1)
ctx.Header("DONT", "DO not cache that response even if it was claimed")
ctx.Write([]byte(expectedBodyStr))
}, cacheDuration)
managedCache2.AddRule(rule.Validator(nil, managedCache2.AddRule(rule.Validator(nil,
[]rule.PostValidator{ []rule.PostValidator{
func(ctx context.Context) bool { 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("/invalid", managedCache.ServeHTTP, h)
app.Get("/invalid2", managedCache2.ServeHTTP) 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) e := httptest.New(t, app)

View File

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