mirror of
https://github.com/kataras/iris.git
synced 2025-03-15 05:16:28 +01:00
Implement Cache as https://github.com/kataras/iris/issues/513
I love coding unique Iris staff!!!
This commit is contained in:
parent
6d65c00423
commit
d32ae1377c
128
HISTORY.md
128
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`.
|
**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, "<h1>Hello!</h1>")
|
||||||
|
}, -1))
|
||||||
|
|
||||||
|
iris.Listen(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## v4 -> 5.0.1
|
## v4 -> 5.0.1
|
||||||
|
|
||||||
|
|
13
README.md
13
README.md
|
@ -19,7 +19,7 @@
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/kataras/iris/releases"><img src="https://img.shields.io/badge/%20version%20-%205.0.1%20-blue.svg?style=flat-square" alt="Releases"></a>
|
<a href="https://github.com/kataras/iris/releases"><img src="https://img.shields.io/badge/%20version%20-%205.1.0%20-blue.svg?style=flat-square" alt="Releases"></a>
|
||||||
|
|
||||||
<a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/%20examples-repository-3362c2.svg?style=flat-square" alt="Examples"></a>
|
<a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/%20examples-repository-3362c2.svg?style=flat-square" alt="Examples"></a>
|
||||||
|
|
||||||
|
@ -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
|
- [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
|
- [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
|
## 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 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.
|
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
|
Testing
|
||||||
|
@ -876,13 +876,14 @@ I recommend writing your API tests using this new library, [httpexpect](https://
|
||||||
Versioning
|
Versioning
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Current: **5.0.1**
|
Current: **5.1.0**
|
||||||
|
|
||||||
Todo
|
Todo
|
||||||
------------
|
------------
|
||||||
|
|
||||||
- [ ] Server-side React render, as requested [here](https://github.com/kataras/iris/issues/503)
|
- [ ] 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)!
|
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
|
[Travis]: http://travis-ci.org/kataras/iris
|
||||||
[License Widget]: https://img.shields.io/badge/license-Apache%20Version%202-E91E63.svg?style=flat-square
|
[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
|
[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
|
[Release]: https://github.com/kataras/iris/releases
|
||||||
[Chat Widget]: https://img.shields.io/badge/community-chat%20-00BCD4.svg?style=flat-square
|
[Chat Widget]: https://img.shields.io/badge/community-chat%20-00BCD4.svg?style=flat-square
|
||||||
[Chat]: https://kataras.rocket.chat/channel/iris
|
[Chat]: https://kataras.rocket.chat/channel/iris
|
||||||
|
|
202
cache.go
Normal file
202
cache.go
Normal file
|
@ -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
|
||||||
|
}
|
92
cache_test.go
Normal file
92
cache_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -180,12 +180,12 @@ type Configuration struct {
|
||||||
LoggerPreffix string
|
LoggerPreffix string
|
||||||
|
|
||||||
// DisableTemplateEngines set to true to disable loading the default template engine (html/template) and disallow the use of iris.UseEngine
|
// 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
|
DisableTemplateEngines bool
|
||||||
|
|
||||||
// IsDevelopment iris will act like a developer, for example
|
// IsDevelopment iris will act like a developer, for example
|
||||||
// If true then re-builds the templates on each request
|
// If true then re-builds the templates on each request
|
||||||
// default is false
|
// Defaults to false
|
||||||
IsDevelopment bool
|
IsDevelopment bool
|
||||||
|
|
||||||
// TimeFormat time format for any kind of datetime parsing
|
// TimeFormat time format for any kind of datetime parsing
|
||||||
|
@ -193,17 +193,26 @@ type Configuration struct {
|
||||||
|
|
||||||
// Charset character encoding for various rendering
|
// Charset character encoding for various rendering
|
||||||
// used for templates and the rest of the responses
|
// used for templates and the rest of the responses
|
||||||
// defaults to "UTF-8"
|
// Defaults to "UTF-8"
|
||||||
Charset string
|
Charset string
|
||||||
|
|
||||||
// Gzip enables gzip compression on your Render actions, this includes any type of render, templates and pure/raw content
|
// 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})
|
// 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
|
Gzip bool
|
||||||
|
|
||||||
// Sessions contains the configs for sessions
|
// Sessions contains the configs for sessions
|
||||||
Sessions SessionsConfiguration
|
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 contains the configs for Websocket's server integration
|
||||||
Websocket WebsocketConfiguration
|
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
|
// OptionIsDevelopment iris will act like a developer, for example
|
||||||
// If true then re-builds the templates on each request
|
// If true then re-builds the templates on each request
|
||||||
// Default is false
|
// Default is false
|
||||||
|
@ -542,6 +565,7 @@ func DefaultConfiguration() Configuration {
|
||||||
Charset: DefaultCharset,
|
Charset: DefaultCharset,
|
||||||
Gzip: false,
|
Gzip: false,
|
||||||
Sessions: DefaultSessionsConfiguration(),
|
Sessions: DefaultSessionsConfiguration(),
|
||||||
|
CacheGCDuration: minimumAllowedCacheDuration,
|
||||||
Websocket: DefaultWebsocketConfiguration(),
|
Websocket: DefaultWebsocketConfiguration(),
|
||||||
Other: options.Options{},
|
Other: options.Options{},
|
||||||
}
|
}
|
||||||
|
@ -552,7 +576,7 @@ func DefaultConfiguration() Configuration {
|
||||||
// first is the cookieName, the session's name (string) ["mysessionsecretcookieid"]
|
// first is the cookieName, the session's name (string) ["mysessionsecretcookieid"]
|
||||||
// second enable if you want to decode the cookie's key also
|
// second enable if you want to decode the cookie's key also
|
||||||
// third is the time which the client's cookie expires
|
// 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
|
// 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
|
// 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
|
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
|
// 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 {
|
OptionSessionsDisableSubdomainPersistence = func(val bool) OptionSet {
|
||||||
return func(c *Configuration) {
|
return func(c *Configuration) {
|
||||||
c.Sessions.DisableSubdomainPersistence = val
|
c.Sessions.DisableSubdomainPersistence = val
|
||||||
|
@ -660,7 +684,7 @@ type WebsocketConfiguration struct {
|
||||||
MaxMessageSize int64
|
MaxMessageSize int64
|
||||||
// BinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text
|
// 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
|
// see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more
|
||||||
// defaults to false
|
// Defaults to false
|
||||||
BinaryMessages bool
|
BinaryMessages bool
|
||||||
// Endpoint is the path which the websocket server will listen for clients/connections
|
// 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.
|
// 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
|
// 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
|
// see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more
|
||||||
// defaults to false
|
// Defaults to false
|
||||||
OptionWebsocketBinaryMessages = func(val bool) OptionSet {
|
OptionWebsocketBinaryMessages = func(val bool) OptionSet {
|
||||||
return func(c *Configuration) {
|
return func(c *Configuration) {
|
||||||
c.Websocket.BinaryMessages = val
|
c.Websocket.BinaryMessages = val
|
||||||
|
|
27
context.go
27
context.go
|
@ -12,6 +12,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -62,6 +63,8 @@ const (
|
||||||
ifModifiedSince = "If-Modified-Since"
|
ifModifiedSince = "If-Modified-Since"
|
||||||
// ContentDisposition "Content-Disposition"
|
// ContentDisposition "Content-Disposition"
|
||||||
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 used inside the Context, is the number which shows us that the context's middleware manualy stop the execution
|
||||||
stopExecutionPosition = 255
|
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
|
// Log logs to the iris defined logger
|
||||||
func (ctx *Context) Log(format string, a ...interface{}) {
|
func (ctx *Context) Log(format string, a ...interface{}) {
|
||||||
ctx.framework.Logger.Printf(format, a...)
|
ctx.framework.Logger.Printf(format, a...)
|
||||||
|
|
|
@ -1,16 +1,6 @@
|
||||||
// Black-box Testing
|
// Black-box Testing
|
||||||
package iris_test
|
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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
|
21
iris.go
21
iris.go
|
@ -79,7 +79,7 @@ const (
|
||||||
// IsLongTermSupport flag is true when the below version number is a long-term-support version
|
// IsLongTermSupport flag is true when the below version number is a long-term-support version
|
||||||
IsLongTermSupport = false
|
IsLongTermSupport = false
|
||||||
// Version is the current version number of the Iris web framework
|
// Version is the current version number of the Iris web framework
|
||||||
Version = "5.0.1"
|
Version = "5.1.0"
|
||||||
|
|
||||||
banner = ` _____ _
|
banner = ` _____ _
|
||||||
|_ _| (_)
|
|_ _| (_)
|
||||||
|
@ -142,6 +142,7 @@ type (
|
||||||
// FrameworkAPI contains the main Iris Public API
|
// FrameworkAPI contains the main Iris Public API
|
||||||
FrameworkAPI interface {
|
FrameworkAPI interface {
|
||||||
MuxAPI
|
MuxAPI
|
||||||
|
CacheService
|
||||||
Set(...OptionSetter)
|
Set(...OptionSetter)
|
||||||
Must(error)
|
Must(error)
|
||||||
Build()
|
Build()
|
||||||
|
@ -175,6 +176,7 @@ type (
|
||||||
// Implements the FrameworkAPI
|
// Implements the FrameworkAPI
|
||||||
Framework struct {
|
Framework struct {
|
||||||
*muxAPI
|
*muxAPI
|
||||||
|
CacheService
|
||||||
// HTTP Server runtime fields is the iris' defined main server, developer can use unlimited number of servers
|
// 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
|
// note: they're available after .Build, and .Serve/Listen/ListenTLS/ListenLETSENCRYPT/ListenUNIX
|
||||||
ln net.Listener
|
ln net.Listener
|
||||||
|
@ -193,11 +195,10 @@ type (
|
||||||
sessions sessions.Sessions
|
sessions sessions.Sessions
|
||||||
serializers serializer.Serializers
|
serializers serializer.Serializers
|
||||||
templates *templateEngines
|
templates *templateEngines
|
||||||
// configuration by instance.Logger.Config
|
Logger *log.Logger
|
||||||
Logger *log.Logger
|
Plugins PluginContainer
|
||||||
Plugins PluginContainer
|
Websocket *WebsocketServer
|
||||||
Websocket *WebsocketServer
|
SSH *SSHServer
|
||||||
SSH *SSHServer
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -231,6 +232,8 @@ func New(setters ...OptionSetter) *Framework {
|
||||||
"url": s.URL,
|
"url": s.URL,
|
||||||
"urlpath": s.Path,
|
"urlpath": s.Path,
|
||||||
})
|
})
|
||||||
|
// set the cache service
|
||||||
|
s.CacheService = newCacheService()
|
||||||
}
|
}
|
||||||
|
|
||||||
// websocket & sessions
|
// websocket & sessions
|
||||||
|
@ -350,6 +353,9 @@ func (s *Framework) Build() {
|
||||||
s.sessions.Set(s.Config.Sessions, sessions.DisableAutoGC(false))
|
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 != "" {
|
if s.Config.Websocket.Endpoint != "" {
|
||||||
// register the websocket server and listen to websocket connections when/if $instance.Websocket.OnConnection called by the dev
|
// 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)
|
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 {
|
func (s *Framework) SerializeToString(keyOrContentType string, obj interface{}, options ...map[string]interface{}) string {
|
||||||
res, err := s.serializers.SerializeToString(keyOrContentType, obj, options...)
|
res, err := s.serializers.SerializeToString(keyOrContentType, obj, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if s.Config.IsDevelopment {
|
||||||
|
s.Logger.Printf("Error on SerializeToString, Key(content-type): %s. Trace: %s\n", keyOrContentType, err)
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|
Loading…
Reference in New Issue
Block a user