some major improvements to the (server-side) cache middleware and an example of a client-side responsibility cache

Former-commit-id: 93d3a7a6f163c6d49f315f86d10e63f7b1b1d93a
This commit is contained in:
Gerasimos (Makis) Maropoulos 2018-01-25 15:09:24 +02:00
parent 981322cfd2
commit befb1f0c08
7 changed files with 90 additions and 151 deletions

27
_examples/cache/client-side/main.go vendored Normal file
View File

@ -0,0 +1,27 @@
// Package main shows how you can use the `WriteWithExpiration`
// based on the "modtime", if it's newer than the request header then
// it will refresh the contents, otherwise will let the client (99.9% the browser)
// to handle the cache mechanism, it's faster than iris.Cache because server-side
// has nothing to do and no need to store the responses in the memory.
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)
}
func main() {
app := iris.New()
app.Get("/", greet)
app.Run(iris.Addr(":8080"))
}

View File

@ -4,7 +4,6 @@ import (
"sync"
"time"
"github.com/kataras/iris/cache/cfg"
"github.com/kataras/iris/cache/client/rule"
"github.com/kataras/iris/cache/entry"
"github.com/kataras/iris/context"
@ -66,6 +65,12 @@ var emptyHandler = func(ctx context.Context) {
ctx.StopExecution()
}
func parseLifeChanger(ctx context.Context) entry.LifeChanger {
return func() time.Duration {
return time.Duration(ctx.MaxAge()) * time.Second
}
}
///TODO: debug this and re-run the parallel tests on larger scale,
// because I think we have a bug here when `core/router#StaticWeb` is used after this middleware.
func (h *Handler) ServeHTTP(ctx context.Context) {
@ -135,14 +140,19 @@ func (h *Handler) ServeHTTP(ctx context.Context) {
// no need to copy the body, its already done inside
body := recorder.Body()
if len(body) == 0 {
// if no body then just exit
// if no body then just exit.
return
}
// check for an expiration time if the
// given expiration was not valid then check for GetMaxAge &
// update the response & release the recorder
e.Reset(recorder.StatusCode(), recorder.Header().Get(cfg.ContentTypeHeader), body, GetMaxAge(ctx.Request()))
e.Reset(
recorder.StatusCode(),
recorder.Header(),
body,
parseLifeChanger(ctx),
)
// fmt.Printf("reset cache entry\n")
// fmt.Printf("key: %s\n", key)
@ -152,12 +162,13 @@ func (h *Handler) ServeHTTP(ctx context.Context) {
}
// if it's valid then just write the cached results
ctx.ContentType(response.ContentType())
entry.CopyHeaders(ctx.ResponseWriter().Header(), response.Headers())
context.SetLastModified(ctx, e.LastModified)
ctx.StatusCode(response.StatusCode())
ctx.Write(response.Body())
// fmt.Printf("key: %s\n", key)
// fmt.Printf("write content type: %s\n", response.ContentType())
// fmt.Printf("write content type: %s\n", response.Headers()["ContentType"])
// fmt.Printf("write body len: %d\n", len(response.Body()))
}

View File

@ -1,109 +0,0 @@
package client
import (
"net/http"
"sync"
)
var rpool = sync.Pool{}
// AcquireResponseRecorder returns a ResponseRecorder
func AcquireResponseRecorder(underline http.ResponseWriter) *ResponseRecorder {
v := rpool.Get()
var res *ResponseRecorder
if v != nil {
res = v.(*ResponseRecorder)
} else {
res = &ResponseRecorder{}
}
res.underline = underline
return res
}
// ReleaseResponseRecorder releases a ResponseRecorder which has been previously received by AcquireResponseRecorder
func ReleaseResponseRecorder(res *ResponseRecorder) {
res.underline = nil
res.statusCode = 0
res.chunks = res.chunks[0:0]
rpool.Put(res)
}
// ResponseRecorder is used by httpcache to be able to get the Body and the StatusCode of a request handler
type ResponseRecorder struct {
underline http.ResponseWriter
chunks [][]byte // 2d because .Write can be called more than one time in the same handler and we want to cache all of them
statusCode int // the saved status code which will be used from the cache service
}
// Body joins the chunks to one []byte slice, this is the full body
func (res *ResponseRecorder) Body() []byte {
var body []byte
for i := range res.chunks {
body = append(body, res.chunks[i]...)
}
return body
}
// ContentType returns the header's value of "Content-Type"
func (res *ResponseRecorder) ContentType() string {
return res.Header().Get("Content-Type")
}
// StatusCode returns the status code, if not given then returns 200
// but doesn't changes the existing behavior
func (res *ResponseRecorder) StatusCode() int {
if res.statusCode == 0 {
return 200
}
return res.statusCode
}
// Header returns the header map that will be sent by
// WriteHeader. Changing the header after a call to
// WriteHeader (or Write) has no effect unless the modified
// headers were declared as trailers by setting the
// "Trailer" header before the call to WriteHeader (see example).
// To suppress implicit response headers, set their value to nil.
func (res *ResponseRecorder) Header() http.Header {
return res.underline.Header()
}
// Write writes the data to the connection as part of an HTTP reply.
//
// If WriteHeader has not yet been called, Write calls
// WriteHeader(http.StatusOK) before writing the data. If the Header
// does not contain a Content-Type line, Write adds a Content-Type set
// to the result of passing the initial 512 bytes of written data to
// DetectContentType.
//
// Depending on the HTTP protocol version and the client, calling
// Write or WriteHeader may prevent future reads on the
// Request.Body. For HTTP/1.x requests, handlers should read any
// needed request body data before writing the response. Once the
// headers have been flushed (due to either an explicit Flusher.Flush
// call or writing enough data to trigger a flush), the request body
// may be unavailable. For HTTP/2 requests, the Go HTTP server permits
// handlers to continue to read the request body while concurrently
// writing the response. However, such behavior may not be supported
// by all HTTP/2 clients. Handlers should read before writing if
// possible to maximize compatibility.
func (res *ResponseRecorder) Write(contents []byte) (int, error) {
if res.statusCode == 0 { // if not setted set it here
res.WriteHeader(http.StatusOK)
}
res.chunks = append(res.chunks, contents)
return res.underline.Write(contents)
}
// WriteHeader sends an HTTP response header with status code.
// If WriteHeader is not called explicitly, the first call to Write
// will trigger an implicit WriteHeader(http.StatusOK).
// Thus explicit calls to WriteHeader are mainly used to
// send error codes.
func (res *ResponseRecorder) WriteHeader(statusCode int) {
if res.statusCode == 0 { // set it only if not setted already, we don't want logs about multiple sends
res.statusCode = statusCode
res.underline.WriteHeader(statusCode)
}
}

20
cache/client/utils.go vendored
View File

@ -1,20 +0,0 @@
package client
import (
"net/http"
"time"
"github.com/kataras/iris/cache/entry"
)
// GetMaxAge parses the "Cache-Control" header
// and returns a LifeChanger which can be passed
// to the response's Reset
func GetMaxAge(r *http.Request) entry.LifeChanger {
return func() time.Duration {
cacheControlHeader := r.Header.Get("Cache-Control")
// headerCacheDur returns the seconds
headerCacheDur := entry.ParseMaxAge(cacheControlHeader)
return time.Duration(headerCacheDur) * time.Second
}
}

31
cache/entry/entry.go vendored
View File

@ -13,6 +13,11 @@ type Entry struct {
// ExpiresAt is the time which this cache will not be available
expiresAt time.Time
// when `Reset` this value is reseting to time.Now(),
// it's used to send the "Last-Modified" header,
// some clients may need it.
LastModified time.Time
// Response the response should be served to the client
response *Response
// but we need the key to invalidate manually...xmm
@ -78,10 +83,23 @@ func (e *Entry) ChangeLifetime(fdur LifeChanger) {
}
}
// CopyHeaders clones headers "src" to "dst" .
func CopyHeaders(dst map[string][]string, src map[string][]string) {
if dst == nil || src == nil {
return
}
for k, vv := range src {
v := make([]string, len(vv))
copy(v, vv)
dst[k] = v
}
}
// Reset called each time the entry is expired
// and the handler calls this after the original handler executed
// to re-set the response with the new handler's content result
func (e *Entry) Reset(statusCode int, contentType string,
func (e *Entry) Reset(statusCode int, headers map[string][]string,
body []byte, lifeChanger LifeChanger) {
if e.response == nil {
@ -91,8 +109,10 @@ func (e *Entry) Reset(statusCode int, contentType string,
e.response.statusCode = statusCode
}
if contentType != "" {
e.response.contentType = contentType
if len(headers) > 0 {
newHeaders := make(map[string][]string, len(headers))
CopyHeaders(newHeaders, headers)
e.response.headers = newHeaders
}
e.response.body = body
@ -101,5 +121,8 @@ func (e *Entry) Reset(statusCode int, contentType string,
if lifeChanger != nil {
e.ChangeLifetime(lifeChanger)
}
e.expiresAt = time.Now().Add(e.life)
now := time.Now()
e.expiresAt = now.Add(e.life)
e.LastModified = now
}

View File

@ -1,19 +1,21 @@
package entry
import "net/http"
// Response is the cached response will be send to the clients
// its fields setted at runtime on each of the non-cached executions
// non-cached executions = first execution, and each time after
// cache expiration datetime passed
// cache expiration datetime passed.
type Response struct {
// statusCode for the response cache handler
// statusCode for the response cache handler.
statusCode int
// contentType for the response cache handler
contentType string
// body is the contents will be served by the cache handler
// body is the contents will be served by the cache handler.
body []byte
// the total headers of the response, including content type.
headers http.Header
}
// StatusCode returns a valid status code
// StatusCode returns a valid status code.
func (r *Response) StatusCode() int {
if r.statusCode <= 0 {
r.statusCode = 200
@ -22,14 +24,19 @@ func (r *Response) StatusCode() int {
}
// ContentType returns a valid content type
func (r *Response) ContentType() string {
if r.contentType == "" {
r.contentType = "text/html; charset=utf-8"
}
return r.contentType
// func (r *Response) ContentType() string {
// if r.headers == "" {
// r.contentType = "text/html; charset=utf-8"
// }
// return r.contentType
// }
// Headers returns the total headers of the cached response.
func (r *Response) Headers() http.Header {
return r.headers
}
// Body returns contents will be served by the cache handler
// Body returns contents will be served by the cache handler.
func (r *Response) Body() []byte {
return r.body
}

View File

@ -823,7 +823,7 @@ func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bo
// and the binary data inside "f".
detectOrWriteContentType(ctx, d.Name(), f)
return "", 200
return "", http.StatusOK
}
// toHTTPError returns a non-specific HTTP error message and status code