From d32ae1377c6eeb817704b4400b064617fb909070 Mon Sep 17 00:00:00 2001 From: Gerasimos Maropoulos Date: Thu, 27 Oct 2016 03:17:09 +0300 Subject: [PATCH] Implement Cache as https://github.com/kataras/iris/issues/513 I love coding unique Iris staff!!! --- HISTORY.md | 128 ++++++++++++++++++++++++++++++ README.md | 13 +-- cache.go | 202 +++++++++++++++++++++++++++++++++++++++++++++++ cache_test.go | 92 +++++++++++++++++++++ configuration.go | 40 ++++++++-- context.go | 27 +++++++ context_test.go | 10 --- iris.go | 21 +++-- 8 files changed, 503 insertions(+), 30 deletions(-) create mode 100644 cache.go create mode 100644 cache_test.go diff --git a/HISTORY.md b/HISTORY.md index a4206c94..1956ac4d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,134 @@ **How to upgrade**: remove your `$GOPATH/src/github.com/kataras` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`. +## 5.0.1 -> 5.1.0 + +- **NEW FEATURE**: `CacheService` simple, cache service for your app's static body content(can work as external service if you are doing horizontal scaling, the `Cache` is just a `Handler` :) ) + +Cache any content, templates, static files, even the error handlers, anything. + +> Bombardier: 5 million requests and 100k clients per second to this markdown static content(look below) with cache(3 seconds) can be served up to ~x12 times faster. Imagine what happens with bigger content like full page and templates! + + +**OUTLINE** +```go + +// Cache is just a wrapper for a route's handler which you want to enable body caching +// Usage: iris.Get("/", iris.Cache(func(ctx *iris.Context){ +// ctx.WriteString("Hello, world!") // or a template or anything else +// }, time.Duration(10*time.Second))) // duration of expiration +// if <=time.Second then it tries to find it though request header's "cache-control" maxage value +// +// Note that it depends on a station instance's cache service. +// Do not try to call it from default' station if you use the form of app := iris.New(), +// use the app.Cache instead of iris.Cache +Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc + +// InvalidateCache clears the cache body for a specific key(request uri, can be retrieved by GetCacheKey(ctx)) +// +// Note that it depends on a station instance's cache service. +// Do not try to call it from default' station if you use the form of app := iris.New(), +// use the app.Cache instead of iris.Cache +InvalidateCache(key string) + +// GetCacheKey returns the cache key(string) from a Context +GetCacheKey(ctx *Context) string + +``` + +**OVERVIEW** + +```go +iris.Get("/hi", iris.Cache(func(c *iris.Context) { + c.WriteString("Hi this is a big content, do not try cache on small content it will not make any significant difference!") +}, time.Duration(10)*time.Second)) + +``` + +[EXAMPLE](https://github.com/iris-contrib/examples/tree/master/cache_body): + + + + + + +```go +package main + +import ( + "github.com/kataras/iris" + "time" +) + +var testMarkdownContents = `## Hello Markdown from Iris + +This is an example of Markdown with Iris + + + +Features +-------- + +All features of Sundown are supported, including: + +* **Compatibility**. The Markdown v1.0.3 test suite passes with + the --tidy option. Without --tidy, the differences are + mostly in whitespace and entity escaping, where blackfriday is + more consistent and cleaner. + +* **Common extensions**, including table support, fenced code + blocks, autolinks, strikethroughs, non-strict emphasis, etc. + +* **Safety**. Blackfriday is paranoid when parsing, making it safe + to feed untrusted user input without fear of bad things + happening. The test suite stress tests this and there are no + known inputs that make it crash. If you find one, please let me + know and send me the input that does it. + + NOTE: "safety" in this context means *runtime safety only*. In order to + protect yourself against JavaScript injection in untrusted content, see + [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). + +* **Fast processing**. It is fast enough to render on-demand in + most web applications without having to cache the output. + +* **Thread safety**. You can run multiple parsers in different + goroutines without ill effect. There is no dependence on global + shared state. + +* **Minimal dependencies**. Blackfriday only depends on standard + library packages in Go. The source code is pretty + self-contained, so it is easy to add to any project, including + Google App Engine projects. + +* **Standards compliant**. Output successfully validates using the + W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. + + [this is a link](https://github.com/kataras/iris) ` + +func main() { + // if this is not setted then iris set this duration to the lowest expiration entry from the cache + 5 seconds + // recommentation is to left as it's or + // iris.Config.CacheGCDuration = time.Duration(5) * time.Minute + + bodyHandler := func(ctx *iris.Context) { + ctx.Markdown(iris.StatusOK, testMarkdownContents) + } + + expiration := time.Duration(5 * time.Second) + + iris.Get("/", iris.Cache(bodyHandler, expiration)) + + // if expiration is <=time.Second then the cache tries to set the expiration from the "cache-control" maxage header's value(in seconds) + // // if this header doesn't founds then the default is 5 minutes + iris.Get("/cache_control", iris.Cache(func(ctx *iris.Context) { + ctx.HTML(iris.StatusOK, "

Hello!

") + }, -1)) + + iris.Listen(":8080") +} + +``` ## v4 -> 5.0.1 diff --git a/README.md b/README.md index e0142ddd..5565c39b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@
-Releases +Releases Examples @@ -53,7 +53,7 @@ Ideally suited for both experienced and novice Developers. - [20 Minute URL Shortener with Go and Redis - Iris+Docker+Redis](https://www.kieranajp.uk/articles/build-url-shortener-api-golang/) by Kieran Patel - [The fastest web framework for Go](http://marcoscleison.xyz/the-fastest-web-framework-for-go-in-this-earth/) by Marcos Cleison -- [Iris vs Nginx vs Php vs Nodejs](https://www.ntossapo.me/2016/08/13/nginx-vs-nginx-php-fpm-vs-go-iris-vs-express-with-wrk/) by Tossapon Nuanchuay +- [Iris vs Nginx vs Php vs Nodejs](https://translate.google.com/translate?sl=auto&tl=en&js=y&prev=_t&hl=en&ie=UTF-8&u=http://webcache.googleusercontent.com/search?q=cache:https%3A%2F%2Fwww.ntossapo.me%2F2016%2F08%2F13%2Fnginx-vs-nginx-php-fpm-vs-go-iris-vs-express-with-wrk%2F&edit-text=&act=url) by Tossapon Nuanchuay ## Feature Overview @@ -865,7 +865,7 @@ The second is an article I just found(**3 October 2016**) which compares Iris vs The results showed that the req / sec iris do best at around 70k-50k, followed by nginx and nginx-php-fpm and nodejs respectively. The error golang-iris and nginx work equally, followed by the final nginx and php-fpm at a ratio of 1: 1. -You can read the full article [here](https://translate.google.com/translate?sl=auto&tl=en&js=y&prev=_t&hl=en&ie=UTF-8&u=https%3A%2F%2Fwww.ntossapo.me%2F2016%2F08%2F13%2Fnginx-vs-nginx-php-fpm-vs-go-iris-vs-express-with-wrk%2F&edit-text=&act=url). +You can read the full article [here](https://translate.google.com/translate?sl=auto&tl=en&js=y&prev=_t&hl=en&ie=UTF-8&u=http://webcache.googleusercontent.com/search?q=cache:https%3A%2F%2Fwww.ntossapo.me%2F2016%2F08%2F13%2Fnginx-vs-nginx-php-fpm-vs-go-iris-vs-express-with-wrk%2F&edit-text=&act=url). Testing @@ -876,13 +876,14 @@ I recommend writing your API tests using this new library, [httpexpect](https:// Versioning ------------ -Current: **5.0.1** +Current: **5.1.0** Todo ------------ - [ ] Server-side React render, as requested [here](https://github.com/kataras/iris/issues/503) - +- [x] Iris command line improvements, as requested [here](https://github.com/kataras/iris/issues/506) +- [x] Cache service, simple but can make your page renders up to 10 times faster, write your suggestions [here](https://github.com/kataras/iris/issues/513) Iris is a **Community-Driven** Project, waiting for your suggestions and [feature requests](https://github.com/kataras/iris/issues?utf8=%E2%9C%93&q=label%3A%22feature%20request%22)! @@ -914,7 +915,7 @@ under the Apache Version 2 license found in the [LICENSE file](LICENSE). [Travis]: http://travis-ci.org/kataras/iris [License Widget]: https://img.shields.io/badge/license-Apache%20Version%202-E91E63.svg?style=flat-square [License]: https://github.com/kataras/iris/blob/master/LICENSE -[Release Widget]: https://img.shields.io/badge/release-V5.0.1%20-blue.svg?style=flat-square +[Release Widget]: https://img.shields.io/badge/release-V5.1.0%20-blue.svg?style=flat-square [Release]: https://github.com/kataras/iris/releases [Chat Widget]: https://img.shields.io/badge/community-chat%20-00BCD4.svg?style=flat-square [Chat]: https://kataras.rocket.chat/channel/iris diff --git a/cache.go b/cache.go new file mode 100644 index 00000000..24874ca0 --- /dev/null +++ b/cache.go @@ -0,0 +1,202 @@ +package iris + +import ( + "sync" + "time" +) + +type ( + // CacheService is the cache service which caches the whole response body + // it's an interface because you can even set your own cache service inside framework! + CacheService interface { + // Start is the method which the CacheService starts the GC(check if expiration of each entry is passed , if yes then delete it from cache) + Start(time.Duration) + // Cache accepts a route's handler which will cache its response and a time.Duration(int64) which is the expiration duration + Cache(HandlerFunc, time.Duration) HandlerFunc + // Invalidate accepts a cache key (which can be retrieved by 'GetCacheKey') and remove its cache response body + InvalidateCache(string) + } + + cacheService struct { + cache map[string]*cacheEntry + mu sync.RWMutex + // keep track of the minimum cache duration of all cache entries, this will be used when gcDuration inside .start() is <=time.Second + lowerExpiration time.Duration + } + + cacheEntry struct { + statusCode int + contentType string + value []byte + expires time.Time + } +) + +var _ CacheService = &cacheService{} + +func newCacheService() *cacheService { + cs := &cacheService{ + cache: make(map[string]*cacheEntry), + mu: sync.RWMutex{}, + lowerExpiration: time.Second, + } + + return cs +} + +// Start is not called via newCacheService because +// if gcDuration is <=time.Second +// then start should check and set the gcDuration from the TOTAL CACHE ENTRIES lowest expiration duration +func (cs *cacheService) Start(gcDuration time.Duration) { + + if gcDuration <= minimumAllowedCacheDuration { + gcDuration = cs.lowerExpiration + } + + // start the timer to check for expirated cache entries + tick := time.Tick(gcDuration) + go func() { + for range tick { + cs.mu.Lock() + now := time.Now() + for k, v := range cs.cache { + if now.After(v.expires) { + delete(cs.cache, k) + } + } + cs.mu.Unlock() + } + }() + +} + +func (cs *cacheService) get(key string) *cacheEntry { + cs.mu.RLock() + if v, ok := cs.cache[key]; ok { + cs.mu.RUnlock() + return v + } + cs.mu.RUnlock() + return nil +} + +// we don't set it to zero value, just 2050 year is enough xD +var expiresNever = time.Date(2050, time.January, 10, 23, 0, 0, 0, time.UTC) +var minimumAllowedCacheDuration = time.Second + +func (cs *cacheService) set(key string, statusCode int, contentType string, value []byte, expiration time.Duration) { + if statusCode == 0 { + statusCode = StatusOK + } + if contentType == "" { + contentType = contentText + } + + entry := &cacheEntry{contentType: contentType, statusCode: statusCode, value: value} + + if expiration <= minimumAllowedCacheDuration { + // Cache function tries to set the expiration(seconds) from header "cache-control" if expiration <=minimumAllowedCacheDuration + // but if cache-control is missing then set it to 5 minutes + expiration = 5 * time.Minute + } + + entry.expires = time.Now().Add(expiration) + + cs.mu.Lock() + cs.cache[key] = entry + cs.mu.Unlock() +} + +func (cs *cacheService) remove(key string) { + cs.mu.Lock() + delete(cs.cache, key) + cs.mu.Unlock() +} + +// GetCacheKey returns the cache key(string) from a context +// it's just the RequestURI +func GetCacheKey(ctx *Context) string { + return string(ctx.Request.URI().RequestURI()) +} + +// InvalidateCache clears the cache body for a specific key(request uri, can be retrieved by GetCacheKey(ctx)) +// +// Note that it depends on a station instance's cache service. +// Do not try to call it from default' station if you use the form of app := iris.New(), +// use the app.InvalidateCache instead of iris.InvalidateCache +// +// Example: https://github.com/iris-contrib/examples/tree/master/cache_body +func InvalidateCache(key string) { + Default.CacheService.InvalidateCache(key) +} + +// InvalidateCache clears the cache body for a specific key(request uri, can be retrieved by GetCacheKey(ctx)) +// +// Note that it depends on a station instance's cache service. +// Do not try to call it from default' station if you use the form of app := iris.New(), +// use the app.Cache instead of iris.Cache +// +// Example: https://github.com/iris-contrib/examples/tree/master/cache_body +func (cs *cacheService) InvalidateCache(key string) { + cs.remove(key) +} + +// Cache is just a wrapper for a route's handler which you want to enable body caching +// Usage: iris.Get("/", iris.Cache(func(ctx *iris.Context){ +// ctx.WriteString("Hello, world!") // or a template or anything else +// }, time.Duration(10*time.Second))) // duration of expiration +// if <=time.Second then it tries to find it though request header's "cache-control" maxage value +// +// Note that it depends on a station instance's cache service. +// Do not try to call it from default' station if you use the form of app := iris.New(), +// use the app.Cache instead of iris.Cache +// +// Example: https://github.com/iris-contrib/examples/tree/master/cache_body +func Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc { + return Default.CacheService.Cache(bodyHandler, expiration) +} + +// Cache is just a wrapper for a route's handler which you want to enable body caching +// Usage: iris.Get("/", iris.Cache(func(ctx *iris.Context){ +// ctx.WriteString("Hello, world!") // or a template or anything else +// }, time.Duration(10*time.Second))) // duration of expiration +// if <=time.Second then it tries to find it though request header's "cache-control" maxage value +// +// Note that it depends on a station instance's cache service. +// Do not try to call it from default' station if you use the form of app := iris.New(), +// use the app.Cache instead of iris.Cache +// +// Example: https://github.com/iris-contrib/examples/tree/master/cache_body +func (cs *cacheService) Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc { + + // the first time the lowerExpiration should be > time.Second, so: + if cs.lowerExpiration == time.Second { + cs.lowerExpiration = expiration + } else if expiration > time.Second && expiration < cs.lowerExpiration { + cs.lowerExpiration = expiration + } + + h := func(ctx *Context) { + key := GetCacheKey(ctx) + if v := cs.get(key); v != nil { + ctx.SetContentType(v.contentType) + ctx.SetStatusCode(v.statusCode) + ctx.RequestCtx.Write(v.value) + return + } + + // if not found then serve this: + bodyHandler.Serve(ctx) + if expiration <= minimumAllowedCacheDuration { + // try to set the expiraion from header + expiration = time.Duration(ctx.MaxAge()) * time.Second + } + + cType := string(ctx.Response.Header.Peek(contentType)) + statusCode := ctx.RequestCtx.Response.StatusCode() + // and set the cache value as its response body in a goroutine, because we want to exit from the route's handler as soon as possible + go cs.set(key, statusCode, cType, ctx.Response.Body(), expiration) + } + + return h +} diff --git a/cache_test.go b/cache_test.go new file mode 100644 index 00000000..54558170 --- /dev/null +++ b/cache_test.go @@ -0,0 +1,92 @@ +package iris_test + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/httptest" + "testing" + "time" +) + +var testMarkdownContents = `## Hello Markdown from Iris + +This is an example of Markdown with Iris + + + +Features +-------- + +All features of Sundown are supported, including: + +* **Compatibility**. The Markdown v1.0.3 test suite passes with + the --tidy option. Without --tidy, the differences are + mostly in whitespace and entity escaping, where blackfriday is + more consistent and cleaner. + +* **Common extensions**, including table support, fenced code + blocks, autolinks, strikethroughs, non-strict emphasis, etc. + +* **Safety**. Blackfriday is paranoid when parsing, making it safe + to feed untrusted user input without fear of bad things + happening. The test suite stress tests this and there are no + known inputs that make it crash. If you find one, please let me + know and send me the input that does it. + + NOTE: "safety" in this context means *runtime safety only*. In order to + protect yourself against JavaScript injection in untrusted content, see + [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). + +* **Fast processing**. It is fast enough to render on-demand in + most web applications without having to cache the output. + +* **Thread safety**. You can run multiple parsers in different + goroutines without ill effect. There is no dependence on global + shared state. + +* **Minimal dependencies**. Blackfriday only depends on standard + library packages in Go. The source code is pretty + self-contained, so it is easy to add to any project, including + Google App Engine projects. + +* **Standards compliant**. Output successfully validates using the + W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. + + [this is a link](https://github.com/kataras/iris) ` + +// 10 seconds test +// EXAMPLE: https://github.com/iris-contrib/examples/tree/master/cache_body +func TestCacheBody(t *testing.T) { + iris.ResetDefault() + iris.Config.CacheGCDuration = time.Duration(2) * time.Second + iris.Config.IsDevelopment = true + defer iris.Close() + var i = 1 + bodyHandler := func(ctx *iris.Context) { + if i%2 == 0 { // only for testing + ctx.SetStatusCode(iris.StatusNoContent) + i++ + return + } + i++ + ctx.Markdown(iris.StatusOK, testMarkdownContents) + } + + expiration := time.Duration(3 * time.Second) + + iris.Get("/", iris.Cache(bodyHandler, expiration)) + + e := httptest.New(iris.Default, t) + + expectedBody := iris.SerializeToString("text/markdown", testMarkdownContents) + + e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) + e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) // the cache still son the corrrect body so no StatusNoContent fires + time.Sleep(time.Duration(5) * time.Second) // 4 depends on the CacheGCDuration not the expiration + + // the cache should be cleared and now i = 2 then it should run the iris.StatusNoContent with empty body ( we don't use the EmitError) + e.GET("/").Expect().Status(iris.StatusNoContent).Body().Empty() + time.Sleep(time.Duration(5) * time.Second) + + e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) + e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) +} diff --git a/configuration.go b/configuration.go index fa6fbaa3..50f718bb 100644 --- a/configuration.go +++ b/configuration.go @@ -180,12 +180,12 @@ type Configuration struct { LoggerPreffix string // DisableTemplateEngines set to true to disable loading the default template engine (html/template) and disallow the use of iris.UseEngine - // default is false + // Defaults to false DisableTemplateEngines bool // IsDevelopment iris will act like a developer, for example // If true then re-builds the templates on each request - // default is false + // Defaults to false IsDevelopment bool // TimeFormat time format for any kind of datetime parsing @@ -193,17 +193,26 @@ type Configuration struct { // Charset character encoding for various rendering // used for templates and the rest of the responses - // defaults to "UTF-8" + // Defaults to "UTF-8" Charset string // Gzip enables gzip compression on your Render actions, this includes any type of render, templates and pure/raw content // If you don't want to enable it globaly, you could just use the third parameter on context.Render("myfileOrResponse", structBinding{}, iris.RenderOptions{"gzip": true}) - // defaults to false + // Defaults to false Gzip bool // Sessions contains the configs for sessions Sessions SessionsConfiguration + // CacheGCDuration the cache gc duration, + // when this duration is passed then the cache is checking for each of the cache entries' expiration field + // this clears only the Cached handlers, so if you don't want cache then don't pass your handler arround the Cache wrapper + // it's like the session's GcDuration field + // + // Is if your app is big and not very changable (like a blog) set this duration big , like 5 hours + // + // Defaults to Auto, Auto means that is setted by the lowest expiration of all cach entries + CacheGCDuration time.Duration // Websocket contains the configs for Websocket's server integration Websocket WebsocketConfiguration @@ -425,6 +434,20 @@ var ( } } + // OptionCacheGCDuration ses the cache gc duration, + // when this duration is passed then the cache is checking for each of the cache entries' expiration field + // this clears only the Cached handlers, so if you don't want cache then don't pass your handler arround the Cache wrapper + // it's like the session's GcDuration field + // + // Is if your app is big and not very changable (like a blog) set this duration big , like 5 hours + // + // Defaults to Auto, Auto means that is setted by the lowest expiration of all cach entries + OptionCacheGCDuration = func(val time.Duration) OptionSet { + return func(c *Configuration) { + c.CacheGCDuration = val + } + } + // OptionIsDevelopment iris will act like a developer, for example // If true then re-builds the templates on each request // Default is false @@ -542,6 +565,7 @@ func DefaultConfiguration() Configuration { Charset: DefaultCharset, Gzip: false, Sessions: DefaultSessionsConfiguration(), + CacheGCDuration: minimumAllowedCacheDuration, Websocket: DefaultWebsocketConfiguration(), Other: options.Options{}, } @@ -552,7 +576,7 @@ func DefaultConfiguration() Configuration { // first is the cookieName, the session's name (string) ["mysessionsecretcookieid"] // second enable if you want to decode the cookie's key also // third is the time which the client's cookie expires -// forth is the cookie length (sessionid) int, defaults to 32, do not change if you don't have any reason to do +// forth is the cookie length (sessionid) int, Defaults to 32, do not change if you don't have any reason to do // fifth is the gcDuration (time.Duration) when this time passes it removes the unused sessions from the memory until the user come back // sixth is the DisableSubdomainPersistence which you can set it to true in order dissallow your q subdomains to have access to the session cook type SessionsConfiguration sessions.Config @@ -608,7 +632,7 @@ var ( } // OptionSessionsDisableSubdomainPersistence set it to true in order dissallow your q subdomains to have access to the session cookie - // defaults to false + // Defaults to false OptionSessionsDisableSubdomainPersistence = func(val bool) OptionSet { return func(c *Configuration) { c.Sessions.DisableSubdomainPersistence = val @@ -660,7 +684,7 @@ type WebsocketConfiguration struct { MaxMessageSize int64 // BinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text // see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more - // defaults to false + // Defaults to false BinaryMessages bool // Endpoint is the path which the websocket server will listen for clients/connections // Default value is empty string, if you don't set it the Websocket server is disabled. @@ -717,7 +741,7 @@ var ( } // OptionWebsocketBinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text // see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more - // defaults to false + // Defaults to false OptionWebsocketBinaryMessages = func(val bool) OptionSet { return func(c *Configuration) { c.Websocket.BinaryMessages = val diff --git a/context.go b/context.go index 55140c50..221e862d 100644 --- a/context.go +++ b/context.go @@ -12,6 +12,7 @@ import ( "os" "path" "reflect" + "regexp" "runtime" "strconv" "strings" @@ -62,6 +63,8 @@ const ( ifModifiedSince = "If-Modified-Since" // ContentDisposition "Content-Disposition" contentDisposition = "Content-Disposition" + // CacheControl "Cache-Control" + cacheControl = "Cache-Control" // stopExecutionPosition used inside the Context, is the number which shows us that the context's middleware manualy stop the execution stopExecutionPosition = 255 @@ -1109,6 +1112,30 @@ func (ctx *Context) SessionDestroy() { } +var maxAgeExp = regexp.MustCompile(`maxage=(\d+)`) + +// MaxAge returns the "cache-control" request header's value +// seconds as int64 +// if header not found or parse failed then it returns -1 +func (ctx *Context) MaxAge() int64 { + header := ctx.RequestHeader(cacheControl) + if header == "" { + return -1 + } + m := maxAgeExp.FindStringSubmatch(header) + if len(m) == 2 { + if v, err := strconv.Atoi(m[1]); err == nil { + return int64(v) + } + } + return -1 +} + +// InvalidateCache clears the cache manually for this request uri context's handler's route +func (ctx *Context) InvalidateCache() { + ctx.framework.CacheService.InvalidateCache(GetCacheKey(ctx)) +} + // Log logs to the iris defined logger func (ctx *Context) Log(format string, a ...interface{}) { ctx.framework.Logger.Printf(format, a...) diff --git a/context_test.go b/context_test.go index 5f5b053d..9f112e99 100644 --- a/context_test.go +++ b/context_test.go @@ -1,16 +1,6 @@ // Black-box Testing package iris_test -/* -The most part of the context covered, -the other part contains serving static methods, -find remote ip, GetInt and the view engine rendering(templates) -I am not waiting unexpected behaviors from the rest of the funcs, -so that's all with context's tests. - -CONTRIBUTE & DISCUSSION ABOUT TESTS TO: https://github.com/iris-contrib/tests -*/ - import ( "encoding/json" "encoding/xml" diff --git a/iris.go b/iris.go index 34c6d873..b60bd1da 100644 --- a/iris.go +++ b/iris.go @@ -79,7 +79,7 @@ const ( // IsLongTermSupport flag is true when the below version number is a long-term-support version IsLongTermSupport = false // Version is the current version number of the Iris web framework - Version = "5.0.1" + Version = "5.1.0" banner = ` _____ _ |_ _| (_) @@ -142,6 +142,7 @@ type ( // FrameworkAPI contains the main Iris Public API FrameworkAPI interface { MuxAPI + CacheService Set(...OptionSetter) Must(error) Build() @@ -175,6 +176,7 @@ type ( // Implements the FrameworkAPI Framework struct { *muxAPI + CacheService // HTTP Server runtime fields is the iris' defined main server, developer can use unlimited number of servers // note: they're available after .Build, and .Serve/Listen/ListenTLS/ListenLETSENCRYPT/ListenUNIX ln net.Listener @@ -193,11 +195,10 @@ type ( sessions sessions.Sessions serializers serializer.Serializers templates *templateEngines - // configuration by instance.Logger.Config - Logger *log.Logger - Plugins PluginContainer - Websocket *WebsocketServer - SSH *SSHServer + Logger *log.Logger + Plugins PluginContainer + Websocket *WebsocketServer + SSH *SSHServer } ) @@ -231,6 +232,8 @@ func New(setters ...OptionSetter) *Framework { "url": s.URL, "urlpath": s.Path, }) + // set the cache service + s.CacheService = newCacheService() } // websocket & sessions @@ -350,6 +353,9 @@ func (s *Framework) Build() { s.sessions.Set(s.Config.Sessions, sessions.DisableAutoGC(false)) } + // set the cache gc duration and start service + s.CacheService.Start(s.Config.CacheGCDuration) + if s.Config.Websocket.Endpoint != "" { // register the websocket server and listen to websocket connections when/if $instance.Websocket.OnConnection called by the dev s.Websocket.RegisterTo(s, s.Config.Websocket) @@ -1111,6 +1117,9 @@ func SerializeToString(keyOrContentType string, obj interface{}, options ...map[ func (s *Framework) SerializeToString(keyOrContentType string, obj interface{}, options ...map[string]interface{}) string { res, err := s.serializers.SerializeToString(keyOrContentType, obj, options...) if err != nil { + if s.Config.IsDevelopment { + s.Logger.Printf("Error on SerializeToString, Key(content-type): %s. Trace: %s\n", keyOrContentType, err) + } return "" } return res