From 420b3c41d0f17b1026ecd3fe9c394185cd9ada8c Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 1 Jan 2018 21:53:12 +0200 Subject: [PATCH] make cache package to work across multi handlers, remove the old 'WrapHandler' and keep the cache.Handler as documented only --- .../embedding-files-into-app/main_test.go | 6 +- .../basic/main_test.go | 2 + .../main_test.go | 7 +- _examples/mvc/cache/main.go | 105 ++++++++++++++++++ cache/LICENSE | 2 +- cache/cache.go | 34 ++---- cache/cache_test.go | 70 +++++++----- cache/client/handler.go | 88 ++++++++------- 8 files changed, 215 insertions(+), 99 deletions(-) create mode 100644 _examples/mvc/cache/main.go diff --git a/_examples/file-server/embedding-files-into-app/main_test.go b/_examples/file-server/embedding-files-into-app/main_test.go index 3d74f17533..10aaf0ec89 100644 --- a/_examples/file-server/embedding-files-into-app/main_test.go +++ b/_examples/file-server/embedding-files-into-app/main_test.go @@ -34,9 +34,9 @@ func (r resource) loadFromBase(dir string) string { } result := string(b) - //if runtime.GOOS != "windows" { - // result = strings.Replace(result, "\n", "\r\n", -1) - //} + if runtime.GOOS != "windows" { + result = strings.Replace(result, "\n", "\r\n", -1) + } return result } diff --git a/_examples/file-server/single-page-application/basic/main_test.go b/_examples/file-server/single-page-application/basic/main_test.go index d0a383b6dd..922356a029 100644 --- a/_examples/file-server/single-page-application/basic/main_test.go +++ b/_examples/file-server/single-page-application/basic/main_test.go @@ -3,6 +3,7 @@ package main import ( "io/ioutil" "path/filepath" + "runtime" "strings" "testing" @@ -35,6 +36,7 @@ func (r resource) loadFromBase(dir string) string { } result := string(b) + return result } diff --git a/_examples/file-server/single-page-application/embedded-single-page-application/main_test.go b/_examples/file-server/single-page-application/embedded-single-page-application/main_test.go index 77b6fe48eb..2fe89b4078 100644 --- a/_examples/file-server/single-page-application/embedded-single-page-application/main_test.go +++ b/_examples/file-server/single-page-application/embedded-single-page-application/main_test.go @@ -3,6 +3,7 @@ package main import ( "io/ioutil" "path/filepath" + "runtime" "strings" "testing" @@ -34,9 +35,9 @@ func (r resource) loadFromBase(dir string) string { panic(fullpath + " failed with error: " + err.Error()) } result := string(b) - // if runtime.GOOS != "windows" { - // result = strings.Replace(result, "\n", "\r\n", -1) - // } + if runtime.GOOS != "windows" { + result = strings.Replace(result, "\n", "\r\n", -1) + } return result } diff --git a/_examples/mvc/cache/main.go b/_examples/mvc/cache/main.go new file mode 100644 index 0000000000..56a61b6bec --- /dev/null +++ b/_examples/mvc/cache/main.go @@ -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 +} diff --git a/cache/LICENSE b/cache/LICENSE index 52f7b63ec1..6beda715ed 100644 --- a/cache/LICENSE +++ b/cache/LICENSE @@ -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 diff --git a/cache/cache.go b/cache/cache.go index 1f129b4a71..09d5ab919a 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -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("

Hello, this should be cached. Every 2 minutes it will be refreshed, check your browser's inspector

") } */ @@ -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 } diff --git a/cache/cache_test.go b/cache/cache_test.go index 12d448c6ad..8c92b08fb0 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -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, 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)) - app.Get("/invalid", managedCache.ServeHTTP) - app.Get("/invalid2", managedCache2.ServeHTTP) + }) e := httptest.New(t, app) diff --git a/cache/client/handler.go b/cache/client/handler.go index 6438bbb326..9b41e53829 100644 --- a/cache/client/handler.go +++ b/cache/client/handler.go @@ -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() + ) + + 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 it's not exists, then execute the original handler + 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()) }