add Cache304 as an alternative to the server-side kataras/iris/cache middleware - it can perform better with less server overheat but it comes with a cost of 304 instead of 200 so custom clients must make that check

Former-commit-id: b0ba68c528c870fe060e2825c35689771a1d3680
This commit is contained in:
Gerasimos (Makis) Maropoulos 2018-01-25 16:19:45 +02:00
parent befb1f0c08
commit 969c2e87d4
8 changed files with 140 additions and 30 deletions

View File

@ -382,6 +382,7 @@ The `httptest` package is your way for end-to-end HTTP testing, it uses the http
iris cache library lives on its own [package](https://github.com/kataras/iris/tree/master/cache).
- [Simple](cache/simple/main.go)
- [Client-Side (304)](cache/client-side/main.go) - part of the iris context core
> You're free to use your own favourite caching package if you'd like so.

View File

@ -6,22 +6,34 @@
package main
import (
"fmt"
"time"
"github.com/kataras/iris"
)
var modtime = time.Now()
func greet(ctx iris.Context) {
ctx.Header("X-Custom", "my custom header")
response := fmt.Sprintf("Hello World! %s", time.Now())
ctx.WriteWithExpiration([]byte(response), modtime)
}
const refreshEvery = 10 * time.Second
func main() {
app := iris.New()
app.Use(iris.Cache304(refreshEvery))
// same as:
// app.Use(func(ctx iris.Context) {
// now := time.Now()
// if modified, err := ctx.CheckIfModifiedSince(now.Add(-refresh)); !modified && err == nil {
// ctx.WriteNotModified()
// return
// }
// ctx.SetLastModified(now)
// ctx.Next()
// })
app.Get("/", greet)
app.Run(iris.Addr(":8080"))
}
func greet(ctx iris.Context) {
ctx.Header("X-Custom", "my custom header")
ctx.Writef("Hello World! %s", time.Now())
}

View File

@ -73,3 +73,8 @@ func writeMarkdown(ctx iris.Context) {
ctx.Markdown(markdownContents)
}
/* Note that `StaticWeb` does use the browser's disk caching by-default
therefore, register the cache handler AFTER any StaticWeb calls,
for a faster solution that server doesn't need to keep track of the response
navigate to https://github.com/kataras/iris/blob/master/_examples/cache/client-side/main.go */

11
cache/cache.go vendored
View File

@ -1,4 +1,7 @@
/* Package cache provides cache capabilities with rich support of options and rules.
/* Package cache provides server-side caching capabilities with rich support of options and rules.
Use it for server-side caching, see the `iris#Cache304` for an alternative approach that
may fit your needs most.
Example code:
@ -37,6 +40,9 @@ import (
//
// All types of response can be cached, templates, json, text, anything.
//
// Use it for server-side caching, see the `iris#Cache304` for an alternative approach that
// may fit your needs most.
//
// You can add validators with this function.
func Cache(expiration time.Duration) *client.Handler {
return client.NewHandler(expiration)
@ -49,6 +55,9 @@ func Cache(expiration time.Duration) *client.Handler {
//
// All types of response can be cached, templates, json, text, anything.
//
// Use it for server-side caching, see the `iris#Cache304` for an alternative approach that
// may fit your needs most.
//
// 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

View File

@ -163,7 +163,7 @@ func (h *Handler) ServeHTTP(ctx context.Context) {
// if it's valid then just write the cached results
entry.CopyHeaders(ctx.ResponseWriter().Header(), response.Headers())
context.SetLastModified(ctx, e.LastModified)
ctx.SetLastModified(e.LastModified)
ctx.StatusCode(response.StatusCode())
ctx.Write(response.Body())

View File

@ -639,6 +639,39 @@ type Context interface {
//
// Returns the number of bytes written and any write error encountered.
WriteString(body string) (int, error)
// SetLastModified sets the "Last-Modified" based on the "modtime" input.
// If "modtime" is zero then it does nothing.
//
// It's mostly internally on core/router and context packages.
//
// Note that modtime.UTC() is being used instead of just modtime, so
// you don't have to know the internals in order to make that works.
SetLastModified(modtime time.Time)
// CheckIfModifiedSince checks if the response is modified since the "modtime".
// Note that it has nothing to do with server-side caching.
// It does those checks by checking if the "If-Modified-Since" request header
// sent by client or a previous server response header
// (e.g with WriteWithExpiration or StaticEmbedded or Favicon etc.)
// is a valid one and it's before the "modtime".
//
// A check for !modtime && err == nil is necessary to make sure that
// it's not modified since, because it may return false but without even
// had the chance to check the client-side (request) header due to some errors,
// like the HTTP Method is not "GET" or "HEAD" or if the "modtime" is zero
// or if parsing time from the header failed.
//
// It's mostly used internally, e.g. `context#WriteWithExpiration`.
//
// Note that modtime.UTC() is being used instead of just modtime, so
// you don't have to know the internals in order to make that works.
CheckIfModifiedSince(modtime time.Time) (bool, error)
// WriteNotModified sends a 304 "Not Modified" status code to the client,
// it makes sure that the content type, the content length headers
// and any "ETag" are removed before the response sent.
//
// It's mostly used internally on core/router/fs.go and context methods.
WriteNotModified()
// WriteWithExpiration like Write but it sends with an expiration datetime
// which is refreshed every package-level `StaticCacheDuration` field.
WriteWithExpiration(body []byte, modtime time.Time) (int, error)
@ -913,6 +946,35 @@ var LimitRequestBodySize = func(maxRequestBodySizeBytes int64) Handler {
}
}
// Cache304 sends a `StatusNotModified` (304) whenever
// the "If-Modified-Since" request header (time) is before the
// time.Now() + expiresEvery (always compared to their UTC values).
// Use this `context#Cache304` instead of the "github.com/kataras/iris/cache" or iris.Cache
// for better performance.
// Clients that are compatible with the http RCF (all browsers are and tools like postman)
// will handle the caching.
// The only disadvantage of using that instead of server-side caching
// is that this method will send a 304 status code instead of 200,
// So, if you use it side by side with other micro services
// you have to check for that status code as well for a valid response.
//
// Developers are free to extend this method's behavior
// by watching system directories changes manually and use of the `ctx.WriteWithExpiration`
// with a "modtime" based on the file modified date,
// simillary to the `StaticWeb`(StaticWeb sends an OK(200) and browser disk caching instead of 304).
var Cache304 = func(expiresEvery time.Duration) Handler {
return func(ctx Context) {
now := time.Now()
if modified, err := ctx.CheckIfModifiedSince(now.Add(-expiresEvery)); !modified && err == nil {
ctx.WriteNotModified()
return
}
ctx.SetLastModified(now)
ctx.Next()
}
}
// Gzip is a middleware which enables writing
// using gzip compression, if client supports.
var Gzip = func(ctx Context) {
@ -2092,9 +2154,9 @@ var FormatTime = func(ctx Context, t time.Time) string {
// If "modtime" is zero then it does nothing.
//
// It's mostly internally on core/router and context packages.
func SetLastModified(ctx Context, modtime time.Time) {
func (ctx *context) SetLastModified(modtime time.Time) {
if !IsZeroTime(modtime) {
ctx.Header(lastModifiedHeaderKey, FormatTime(ctx, modtime)) // or modtime.UTC()?
ctx.Header(lastModifiedHeaderKey, FormatTime(ctx, modtime.UTC())) // or modtime.UTC()?
}
}
@ -2112,7 +2174,7 @@ func SetLastModified(ctx Context, modtime time.Time) {
// or if parsing time from the header failed.
//
// It's mostly used internally, e.g. `context#WriteWithExpiration`.
func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) {
func (ctx *context) CheckIfModifiedSince(modtime time.Time) (bool, error) {
if method := ctx.Method(); method != http.MethodGet && method != http.MethodHead {
return false, errors.New("skip: method")
}
@ -2126,7 +2188,7 @@ func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) {
}
// sub-second precision, so
// use mtime < t+1s instead of mtime <= t to check for unmodified.
if modtime.Before(t.Add(1 * time.Second)) {
if modtime.UTC().Before(t.Add(1 * time.Second)) {
return false, nil
}
return true, nil
@ -2137,7 +2199,7 @@ func CheckIfModifiedSince(ctx Context, modtime time.Time) (bool, error) {
// and any "ETag" are removed before the response sent.
//
// It's mostly used internally on core/router/fs.go and context methods.
func WriteNotModified(ctx Context) {
func (ctx *context) WriteNotModified() {
// RFC 7232 section 4.1:
// a sender SHOULD NOT generate representation metadata other than the
// above listed fields unless said metadata exists for the purpose of
@ -2155,12 +2217,12 @@ func WriteNotModified(ctx Context) {
// WriteWithExpiration like Write but it sends with an expiration datetime
// which is refreshed every package-level `StaticCacheDuration` field.
func (ctx *context) WriteWithExpiration(body []byte, modtime time.Time) (int, error) {
if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil {
WriteNotModified(ctx)
if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil {
ctx.WriteNotModified()
return 0, nil
}
SetLastModified(ctx, modtime)
ctx.SetLastModified(modtime)
return ctx.writer.Write(body)
}
@ -2750,13 +2812,13 @@ const (
// You can define your own "Content-Type" header also, after this function call
// Doesn't implements resuming (by range), use ctx.SendFile instead
func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error {
if modified, err := CheckIfModifiedSince(ctx, modtime); !modified && err == nil {
WriteNotModified(ctx)
if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil {
ctx.WriteNotModified()
return nil
}
ctx.ContentType(filename)
SetLastModified(ctx, modtime)
ctx.SetLastModified(modtime)
var out io.Writer
if gzipCompression && ctx.ClientSupportsGzip() {
ctx.writer.Header().Add(varyHeaderKey, acceptEncodingHeaderKey)

View File

@ -411,7 +411,7 @@ func detectOrWriteContentType(ctx context.Context, name string, content io.ReadS
// content must be seeked to the beginning of the file.
// The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response.
func serveContent(ctx context.Context, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) (string, int) /* we could use the TransactionErrResult but prefer not to create new objects for each of the errors on static file handlers*/ {
context.SetLastModified(ctx, modtime)
ctx.SetLastModified(modtime)
done, rangeReq := checkPreconditions(ctx, modtime)
if done {
return "", http.StatusNotModified
@ -651,15 +651,15 @@ func checkPreconditions(ctx context.Context, modtime time.Time) (done bool, rang
switch checkIfNoneMatch(ctx) {
case condFalse:
if ctx.Method() == http.MethodGet || ctx.Method() == http.MethodHead {
context.WriteNotModified(ctx)
ctx.WriteNotModified()
return true, ""
}
ctx.StatusCode(http.StatusPreconditionFailed)
return true, ""
case condNone:
if modified, err := context.CheckIfModifiedSince(ctx, modtime); !modified && err == nil {
context.WriteNotModified(ctx)
if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil {
ctx.WriteNotModified()
return true, ""
}
}
@ -785,11 +785,11 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo
if !showList {
return "", http.StatusForbidden
}
if modified, err := context.CheckIfModifiedSince(ctx, d.ModTime()); !modified && err == nil {
context.WriteNotModified(ctx)
if modified, err := ctx.CheckIfModifiedSince(d.ModTime()); !modified && err == nil {
ctx.WriteNotModified()
return "", http.StatusNotModified
}
ctx.Header("Last-Modified", d.ModTime().UTC().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat()))
ctx.SetLastModified(d.ModTime())
return dirList(ctx, f)
}
@ -801,7 +801,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo
}
// else, set the last modified as "serveContent" does.
context.SetLastModified(ctx, d.ModTime())
ctx.SetLastModified(d.ModTime())
// write the file to the response writer.
contents, err := ioutil.ReadAll(f)

23
iris.go
View File

@ -360,11 +360,32 @@ var (
//
// A shortcut for the `handlerconv#FromStd`.
FromStd = handlerconv.FromStd
// Cache is a middleware providing cache functionalities
// Cache is a middleware providing server-side cache functionalities
// to the next handlers, can be used as: `app.Get("/", iris.Cache, aboutHandler)`.
// It should be used after Static methods.
// See `context#Cache304` for an alternative, faster way.
//
// Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/#caching
Cache = cache.Handler
// Cache304 sends a `StatusNotModified` (304) whenever
// the "If-Modified-Since" request header (time) is before the
// time.Now() + expiresEvery (always compared to their UTC values).
// Use this, which is a shortcut of the, `context#Cache304` instead of the "github.com/kataras/iris/cache" or iris.Cache
// for better performance.
// Clients that are compatible with the http RCF (all browsers are and tools like postman)
// will handle the caching.
// The only disadvantage of using that instead of server-side caching
// is that this method will send a 304 status code instead of 200,
// So, if you use it side by side with other micro services
// you have to check for that status code as well for a valid response.
//
// Developers are free to extend this method's behavior
// by watching system directories changes manually and use of the `ctx.WriteWithExpiration`
// with a "modtime" based on the file modified date,
// simillary to the `StaticWeb`(StaticWeb sends an OK(200) and browser disk caching instead of 304).
//
// A shortcut of the `context#Cache304`.
Cache304 = context.Cache304
)
// SPA accepts an "assetHandler" which can be the result of an