mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 18:51:03 +01:00
6f22da7622
Remote Cache is not inside docs or history because it's not ready for production yet, however it seems to works fine, but until I test it on more than 2kk requests I will not add this on history or docs. The upcoming 24 hours I will upload a relative package which will combine all cache features to one
385 lines
13 KiB
Go
385 lines
13 KiB
Go
package iris
|
|
|
|
import (
|
|
"github.com/valyala/fasthttp"
|
|
"net/url"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type (
|
|
// CacheServiceAPI describes the cache service which caches the whole response body
|
|
CacheServiceAPI interface {
|
|
// 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
|
|
// ServeRemoteCache creates & returns a new handler which saves cache by POST method and serves a cache entry by GET method to clients
|
|
// usually set it with iris.Any,
|
|
// but developer is able to set different paths for save or get cache entries: using the iris.Post/.Get(...,iris.ServeRemote())
|
|
// CacheRemote IS not ready for production yet and that's why it is not in docs or history yet.
|
|
// propably this method will go to another package which will be ready at the next 24 hours,
|
|
// because it can work both on iris and raw net/http, lets no limit it:)
|
|
ServeRemoteCache(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
|
|
gcDuration time.Duration
|
|
}
|
|
|
|
cacheEntry struct {
|
|
statusCode int
|
|
contentType string
|
|
body []byte
|
|
// we could have a new Timer foreach cache entry in order to be persise on the expiration but this will cost us a lot of performance,
|
|
// (the ticker should be stopped if delete or key ovveride and so on...)
|
|
// but I chosen to just have a generic timer with its tick on the lowest 'expires' of all cache entries that cache keeps
|
|
expires time.Time
|
|
}
|
|
)
|
|
|
|
func (e *cacheEntry) serve(ctx *Context) {
|
|
ctx.SetContentType(e.contentType)
|
|
ctx.SetStatusCode(e.statusCode)
|
|
ctx.RequestCtx.Write(e.body)
|
|
}
|
|
|
|
var _ CacheServiceAPI = &cacheService{}
|
|
|
|
func newCacheService() *cacheService {
|
|
cs := &cacheService{
|
|
cache: make(map[string]*cacheEntry),
|
|
mu: sync.RWMutex{},
|
|
gcDuration: -1, // will set as the lowest of the cache entries, if not set then the cache doesn't starts its garbage collector
|
|
}
|
|
|
|
return cs
|
|
}
|
|
|
|
// start called last (after the lowest cache gc duration has been setted by the Cache funcs)
|
|
func (cs *cacheService) start() {
|
|
if cs.gcDuration > 0 {
|
|
// start the timer to check for expirated cache entries
|
|
tick := time.Tick(cs.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 && time.Now().Before(v.expires) { // we check for expiration, the gc clears the cache but gc maybe late
|
|
cs.mu.RUnlock()
|
|
return v
|
|
}
|
|
cs.mu.RUnlock()
|
|
return nil
|
|
}
|
|
|
|
var minimumAllowedCacheDuration = 2 * time.Second
|
|
|
|
func validateCacheDuration(expiration time.Duration) time.Duration {
|
|
if expiration <= minimumAllowedCacheDuration {
|
|
expiration = minimumAllowedCacheDuration * 2
|
|
}
|
|
return expiration
|
|
}
|
|
|
|
func validateStatusCode(statusCode int) int {
|
|
if statusCode <= 0 {
|
|
statusCode = StatusOK
|
|
}
|
|
return statusCode
|
|
}
|
|
|
|
func validateContentType(cType string) string {
|
|
if cType == "" {
|
|
cType = contentText
|
|
}
|
|
return cType
|
|
}
|
|
|
|
func (cs *cacheService) set(key string, statusCode int, cType string, body []byte, expiration time.Duration) {
|
|
entry := &cacheEntry{
|
|
statusCode: validateStatusCode(statusCode),
|
|
contentType: validateContentType(cType),
|
|
expires: time.Now().Add(validateCacheDuration(expiration)),
|
|
body: body,
|
|
}
|
|
|
|
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.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.Cache(bodyHandler, expiration)
|
|
}
|
|
|
|
func getResponseContentType(ctx *Context) string {
|
|
return validateContentType(string(ctx.Response.Header.ContentType()))
|
|
}
|
|
|
|
func getResponseStatusCode(ctx *Context) int {
|
|
return validateStatusCode(ctx.Response.StatusCode())
|
|
}
|
|
|
|
// 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 {
|
|
expiration = validateCacheDuration(expiration)
|
|
|
|
if cs.gcDuration == -1 || expiration < cs.gcDuration {
|
|
cs.gcDuration = expiration // the first time the gcDuration should be > minimumAllowedCacheDuration so:
|
|
}
|
|
|
|
h := func(ctx *Context) {
|
|
key := GetCacheKey(ctx)
|
|
if v := cs.get(key); v != nil {
|
|
v.serve(ctx)
|
|
return
|
|
}
|
|
|
|
// if not found then serve the handler and collect its results after
|
|
bodyHandler.Serve(ctx)
|
|
|
|
if expiration <= minimumAllowedCacheDuration {
|
|
// try to set the expiraion from header
|
|
expiration = time.Duration(ctx.MaxAge()) * time.Second
|
|
}
|
|
|
|
cType := getResponseContentType(ctx)
|
|
statusCode := getResponseStatusCode(ctx)
|
|
body := ctx.Response.Body()
|
|
// 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, body, expiration)
|
|
}
|
|
|
|
return h
|
|
}
|
|
|
|
// GetRemoteCacheKey returns the context's cache key,
|
|
// differs from GetCacheKey is that this method parses the query arguments
|
|
// because this key must be sent to an external server
|
|
func GetRemoteCacheKey(ctx *Context) string {
|
|
return url.QueryEscape(ctx.Request.URI().String())
|
|
}
|
|
|
|
const (
|
|
queryCacheKey = "cache_key"
|
|
queryCacheDuration = "cache_duration"
|
|
queryCacheStatusCode = "cache_status_code"
|
|
queryCacheContentType = "cache_content_type"
|
|
requestCacheTimeout = 5 * time.Second
|
|
statusCacheSucceed = StatusOK
|
|
statusCacheFailed = StatusBadRequest
|
|
)
|
|
|
|
// RemoteCache accepts the remote server address and path of the external cache service, the body handler and optional an expiration
|
|
// the last 2 receivers works like .Cache(...) function
|
|
//
|
|
// Note: Remotecache is a global function, usage:
|
|
// app.Get("/", iris.RemoteCache("http://127.0.0.1:8888/cache", bodyHandler, time.Duration(15)*time.Second))
|
|
//
|
|
// IT IS NOT READY FOR PRODUCTION YET, READ THE HISTORY.md for the available working cache methods
|
|
func RemoteCache(cacheServerAddr string, bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc {
|
|
client := fasthttp.Client{}
|
|
// buf := utils.NewBufferPool(10)
|
|
cacheDurationStr := strconv.Itoa(int(expiration.Seconds()))
|
|
h := func(ctx *Context) {
|
|
req := fasthttp.AcquireRequest()
|
|
req.SetRequestURI(cacheServerAddr)
|
|
req.Header.SetMethodBytes(MethodGetBytes)
|
|
req.URI().QueryArgs().Add(queryCacheKey, GetRemoteCacheKey(ctx))
|
|
|
|
res := fasthttp.AcquireResponse()
|
|
err := client.DoTimeout(req, res, requestCacheTimeout)
|
|
if err != nil || res.StatusCode() == statusCacheFailed {
|
|
// if not found on cache, then execute the handler and save the cache to the remote server
|
|
bodyHandler.Serve(ctx)
|
|
// save to the remote cache
|
|
req.Header.SetMethodBytes(MethodPostBytes)
|
|
args := req.URI().QueryArgs()
|
|
args.Add(queryCacheDuration, cacheDurationStr)
|
|
statusCode := strconv.Itoa(ctx.Response.StatusCode())
|
|
args.Add(queryCacheStatusCode, statusCode)
|
|
cType := string(ctx.Response.Header.Peek(contentType))
|
|
args.Add(queryCacheContentType, cType)
|
|
|
|
req.SetBody(ctx.Response.Body())
|
|
go func() {
|
|
client.DoTimeout(req, res, requestCacheTimeout)
|
|
fasthttp.ReleaseRequest(req)
|
|
fasthttp.ReleaseResponse(res)
|
|
}()
|
|
|
|
} else {
|
|
// get the status code , content type and the write the response body
|
|
statusCode := res.StatusCode()
|
|
cType := res.Header.ContentType()
|
|
ctx.SetStatusCode(statusCode)
|
|
ctx.Response.Header.SetContentTypeBytes(cType)
|
|
|
|
ctx.RequestCtx.Write(res.Body())
|
|
|
|
fasthttp.ReleaseRequest(req)
|
|
fasthttp.ReleaseResponse(res)
|
|
}
|
|
|
|
}
|
|
return h
|
|
}
|
|
|
|
// ServeRemoteCache usage: iris.Any("/cacheservice", iris.ServeRemote())
|
|
// client does an http request to retrieve cached body from the external/remote server which keeps the cache service.
|
|
//
|
|
// if is GET method request then gets from cache
|
|
// if it's POST method request then its saves to the cache
|
|
// if it's DELETE method request then its invalidates/removes from cache manually
|
|
// the content type and the status are setted inside the caller's handler
|
|
// this is not like cs.Cache, it's useful only when you separate your servers to achieve horizontal scaling
|
|
//
|
|
// 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.ServeRemoteCache instead of iris.ServeRemoteCache
|
|
func ServeRemoteCache(gcDuration time.Duration) HandlerFunc {
|
|
return Default.ServeRemoteCache(gcDuration)
|
|
}
|
|
|
|
// ServeRemoteCache usage: iris.Any("/cacheservice", iris.ServeRemoteCache())
|
|
// client does an http request to retrieve cached body from the external/remote server which keeps the cache service.
|
|
//
|
|
// if is GET method request then gets from cache
|
|
// if it's POST method request then its saves to the cache
|
|
// if it's DELETE method request then its invalidates/removes from cache manually
|
|
// the content type and the status are setted inside the caller's handler
|
|
// this is not like cs.Cache, it's useful only when you separate your servers to achieve horizontal scaling
|
|
//
|
|
// 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.ServeRemoteCache instead of iris.ServeRemoteCache
|
|
func (cs *cacheService) ServeRemoteCache(gcDuration time.Duration) HandlerFunc {
|
|
cs.gcDuration = validateCacheDuration(gcDuration)
|
|
// the service started at pre-listen state(on .Build) if gcDuration > 0
|
|
h := func(ctx *Context) {
|
|
key := ctx.URLParam(queryCacheKey)
|
|
if key == "" {
|
|
ctx.SetStatusCode(statusCacheFailed)
|
|
return
|
|
}
|
|
|
|
if ctx.IsGet() {
|
|
if v := cs.get(key); v != nil {
|
|
v.serve(ctx)
|
|
return
|
|
}
|
|
} else if ctx.IsPost() {
|
|
// get the cache expiration via url param
|
|
expirationSeconds, err := ctx.URLParamInt64(queryCacheDuration)
|
|
// get the body from the requested body
|
|
body := ctx.Request.Body()
|
|
if len(body) == 0 {
|
|
ctx.SetStatusCode(statusCacheFailed)
|
|
return
|
|
|
|
}
|
|
// get the expiration from the "cache-control's maxage" if no url param is setted
|
|
if expirationSeconds <= 0 || err != nil {
|
|
expirationSeconds = ctx.MaxAge()
|
|
}
|
|
|
|
// if not setted then try to get it via
|
|
if expirationSeconds <= 0 {
|
|
expirationSeconds = int64(minimumAllowedCacheDuration.Seconds())
|
|
}
|
|
|
|
cacheDuration := validateCacheDuration(time.Duration(expirationSeconds) * time.Second)
|
|
statusCode, _ := ctx.URLParamInt(queryCacheDuration)
|
|
statusCode = validateStatusCode(statusCode)
|
|
cType := validateContentType(ctx.URLParam(queryCacheContentType))
|
|
|
|
cs.set(key, statusCode, cType, body, cacheDuration)
|
|
|
|
ctx.SetStatusCode(statusCacheSucceed)
|
|
return
|
|
} else if ctx.IsDelete() {
|
|
cs.remove(key)
|
|
ctx.SetStatusCode(statusCacheSucceed)
|
|
return
|
|
}
|
|
|
|
ctx.SetStatusCode(statusCacheFailed)
|
|
}
|
|
|
|
return h
|
|
}
|