mirror of
https://github.com/kataras/iris.git
synced 2026-01-08 20:41:57 +00:00
improve cache handler, embracing #2210 too
This commit is contained in:
123
cache/entry/entry.go
vendored
123
cache/entry/entry.go
vendored
@@ -3,15 +3,14 @@ package entry
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12/cache/cfg"
|
||||
"github.com/kataras/iris/v12/core/memstore"
|
||||
)
|
||||
|
||||
// Entry is the cache entry
|
||||
// contains the expiration datetime and the response
|
||||
type Entry struct {
|
||||
life time.Duration
|
||||
// ExpiresAt is the time which this cache will not be available
|
||||
expiresAt time.Time
|
||||
lifeTime *memstore.LifeTime
|
||||
|
||||
// when `Reset` this value is reseting to time.Now(),
|
||||
// it's used to send the "Last-Modified" header,
|
||||
@@ -25,104 +24,30 @@ type Entry struct {
|
||||
// of store map
|
||||
}
|
||||
|
||||
// NewEntry returns a new cache entry
|
||||
// it doesn't sets the expiresAt & the response
|
||||
// because these are setting each time on Reset
|
||||
func NewEntry(duration time.Duration) *Entry {
|
||||
// if given duration is not <=0 (which means finds from the headers)
|
||||
// then we should check for the MinimumCacheDuration here
|
||||
if duration >= 0 && duration < cfg.MinimumCacheDuration {
|
||||
duration = cfg.MinimumCacheDuration
|
||||
}
|
||||
|
||||
return &Entry{
|
||||
life: duration,
|
||||
response: &Response{},
|
||||
}
|
||||
// reset called each time a new entry is acquired from the pool.
|
||||
func (e *Entry) reset(lt *memstore.LifeTime, r *Response) {
|
||||
e.response = r
|
||||
e.LastModified = lt.Begun
|
||||
}
|
||||
|
||||
// Response gets the cache response contents
|
||||
// if it's valid returns them with a true value
|
||||
// otherwise returns nil, false
|
||||
func (e *Entry) Response() (*Response, bool) {
|
||||
if !e.valid() {
|
||||
// it has been expired
|
||||
return nil, false
|
||||
}
|
||||
return e.response, true
|
||||
// Response returns the cached response as it's.
|
||||
func (e *Entry) Response() *Response {
|
||||
return e.response
|
||||
}
|
||||
|
||||
// valid returns true if this entry's response is still valid
|
||||
// or false if the expiration time passed
|
||||
func (e *Entry) valid() bool {
|
||||
return !time.Now().After(e.expiresAt)
|
||||
}
|
||||
// // Response gets the cache response contents
|
||||
// // if it's valid returns them with a true value
|
||||
// // otherwise returns nil, false
|
||||
// func (e *Entry) Response() (*Response, bool) {
|
||||
// if !e.isValid() {
|
||||
// // it has been expired
|
||||
// return nil, false
|
||||
// }
|
||||
// return e.response, true
|
||||
// }
|
||||
|
||||
// LifeChanger is the function which returns
|
||||
// a duration which will be compared with the current
|
||||
// entry's (cache life) duration
|
||||
// and execute the LifeChanger func
|
||||
// to set the new life time
|
||||
type LifeChanger func() time.Duration
|
||||
|
||||
// ChangeLifetime modifies the life field
|
||||
// which is the life duration of the cached response
|
||||
// of this cache entry
|
||||
//
|
||||
// useful when we find a max-age header from the handler
|
||||
func (e *Entry) ChangeLifetime(fdur LifeChanger) {
|
||||
if e.life < cfg.MinimumCacheDuration {
|
||||
newLifetime := fdur()
|
||||
if newLifetime > e.life {
|
||||
e.life = newLifetime
|
||||
} else {
|
||||
// if even the new lifetime is less than MinimumCacheDuration
|
||||
// then change set it explicitly here
|
||||
e.life = cfg.MinimumCacheDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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, headers map[string][]string,
|
||||
body []byte, lifeChanger LifeChanger) {
|
||||
if e.response == nil {
|
||||
e.response = &Response{}
|
||||
}
|
||||
if statusCode > 0 {
|
||||
e.response.statusCode = statusCode
|
||||
}
|
||||
|
||||
if len(headers) > 0 {
|
||||
newHeaders := make(map[string][]string, len(headers))
|
||||
CopyHeaders(newHeaders, headers)
|
||||
e.response.headers = newHeaders
|
||||
}
|
||||
|
||||
e.response.body = make([]byte, len(body))
|
||||
copy(e.response.body, body)
|
||||
// check if a given life changer provided
|
||||
// and if it does then execute the change life time
|
||||
if lifeChanger != nil {
|
||||
e.ChangeLifetime(lifeChanger)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
e.expiresAt = now.Add(e.life)
|
||||
e.LastModified = now
|
||||
}
|
||||
// // isValid reports whether this entry's response is still valid or expired.
|
||||
// // If the entry exists in the store then it should be valid anyways.
|
||||
// func (e *Entry) isValid() bool {
|
||||
// return !e.lifeTime.HasExpired()
|
||||
// }
|
||||
|
||||
69
cache/entry/pool.go
vendored
Normal file
69
cache/entry/pool.go
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12/cache/cfg"
|
||||
"github.com/kataras/iris/v12/core/memstore"
|
||||
)
|
||||
|
||||
// Pool is the context pool, it's used inside router and the framework by itself.
|
||||
type Pool struct {
|
||||
pool *sync.Pool
|
||||
}
|
||||
|
||||
// NewPool creates and returns a new context pool.
|
||||
func NewPool() *Pool {
|
||||
return &Pool{pool: &sync.Pool{New: func() any { return &Entry{} }}}
|
||||
}
|
||||
|
||||
// func NewPool(newFunc func() any) *Pool {
|
||||
// return &Pool{pool: &sync.Pool{New: newFunc}}
|
||||
// }
|
||||
|
||||
// Acquire returns an Entry from pool.
|
||||
// See Release.
|
||||
func (c *Pool) Acquire(lifeDuration time.Duration, r *Response, onExpire func()) *Entry {
|
||||
// If the given duration is not <=0 (which means finds from the headers)
|
||||
// then we should check for the MinimumCacheDuration here
|
||||
if lifeDuration >= 0 && lifeDuration < cfg.MinimumCacheDuration {
|
||||
lifeDuration = cfg.MinimumCacheDuration
|
||||
}
|
||||
|
||||
e := c.pool.Get().(*Entry)
|
||||
|
||||
lt := memstore.NewLifeTime()
|
||||
lt.Begin(lifeDuration, func() {
|
||||
onExpire()
|
||||
c.release(e)
|
||||
})
|
||||
|
||||
e.reset(lt, r)
|
||||
return e
|
||||
}
|
||||
|
||||
// Release puts an Entry back to its pull, this function releases its resources.
|
||||
// See Acquire.
|
||||
func (c *Pool) release(e *Entry) {
|
||||
e.response.body = nil
|
||||
e.response.headers = nil
|
||||
e.response.statusCode = 0
|
||||
e.response = nil
|
||||
|
||||
// do not call it, it contains a lock too, release is controlled only inside the Acquire itself when the entry is expired.
|
||||
// if e.lifeTime != nil {
|
||||
// e.lifeTime.ExpireNow() // stop any opening timers if force released.
|
||||
// }
|
||||
|
||||
c.pool.Put(e)
|
||||
}
|
||||
|
||||
// Release can be called by custom stores to release an entry.
|
||||
func (c *Pool) Release(e *Entry) {
|
||||
if e.lifeTime != nil {
|
||||
e.lifeTime.ExpireNow() // stop any opening timers if force released.
|
||||
}
|
||||
|
||||
c.release(e)
|
||||
}
|
||||
55
cache/entry/response.go
vendored
55
cache/entry/response.go
vendored
@@ -1,6 +1,9 @@
|
||||
package entry
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Response is the cached response will be send to the clients
|
||||
// its fields set at runtime on each of the non-cached executions
|
||||
@@ -15,11 +18,28 @@ type Response struct {
|
||||
headers http.Header
|
||||
}
|
||||
|
||||
// NewResponse returns a new cached Response.
|
||||
func NewResponse(statusCode int, headers http.Header, body []byte) *Response {
|
||||
r := new(Response)
|
||||
|
||||
r.SetStatusCode(statusCode)
|
||||
r.SetHeaders(headers)
|
||||
r.SetBody(body)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// SetStatusCode sets a valid status code.
|
||||
func (r *Response) SetStatusCode(statusCode int) {
|
||||
if statusCode <= 0 {
|
||||
statusCode = http.StatusOK
|
||||
}
|
||||
|
||||
r.statusCode = statusCode
|
||||
}
|
||||
|
||||
// StatusCode returns a valid status code.
|
||||
func (r *Response) StatusCode() int {
|
||||
if r.statusCode <= 0 {
|
||||
r.statusCode = 200
|
||||
}
|
||||
return r.statusCode
|
||||
}
|
||||
|
||||
@@ -31,12 +51,39 @@ func (r *Response) StatusCode() int {
|
||||
// return r.contentType
|
||||
// }
|
||||
|
||||
// SetHeaders sets a clone of headers of the cached response.
|
||||
func (r *Response) SetHeaders(h http.Header) {
|
||||
r.headers = h.Clone()
|
||||
}
|
||||
|
||||
// Headers returns the total headers of the cached response.
|
||||
func (r *Response) Headers() http.Header {
|
||||
return r.headers
|
||||
}
|
||||
|
||||
// SetBody consumes "b" and sets the body of the cached response.
|
||||
func (r *Response) SetBody(body []byte) {
|
||||
r.body = make([]byte, len(body))
|
||||
copy(r.body, body)
|
||||
}
|
||||
|
||||
// Body returns contents will be served by the cache handler.
|
||||
func (r *Response) Body() []byte {
|
||||
return r.body
|
||||
}
|
||||
|
||||
// Read implements the io.Reader interface.
|
||||
func (r *Response) Read(b []byte) (int, error) {
|
||||
if len(r.body) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
n := copy(b, r.body)
|
||||
r.body = r.body[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Bytes returns a copy of the cached response body.
|
||||
func (r *Response) Bytes() []byte {
|
||||
return append([]byte(nil), r.body...)
|
||||
}
|
||||
|
||||
52
cache/entry/store.go
vendored
Normal file
52
cache/entry/store.go
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Store is the interface which is responsible to store the cache entries.
|
||||
type Store interface {
|
||||
// Get returns an entry based on its key.
|
||||
Get(key string) *Entry
|
||||
// Set sets an entry based on its key.
|
||||
Set(key string, e *Entry)
|
||||
// Delete deletes an entry based on its key.
|
||||
Delete(key string)
|
||||
}
|
||||
|
||||
// memStore is the default in-memory store for the cache entries.
|
||||
type memStore struct {
|
||||
entries map[string]*Entry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var _ Store = (*memStore)(nil)
|
||||
|
||||
// NewMemStore returns a new in-memory store for the cache entries.
|
||||
func NewMemStore() Store {
|
||||
return &memStore{
|
||||
entries: make(map[string]*Entry),
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns an entry based on its key.
|
||||
func (s *memStore) Get(key string) *Entry {
|
||||
s.mu.RLock()
|
||||
e := s.entries[key]
|
||||
s.mu.RUnlock()
|
||||
return e
|
||||
}
|
||||
|
||||
// Set sets an entry based on its key.
|
||||
func (s *memStore) Set(key string, e *Entry) {
|
||||
s.mu.Lock()
|
||||
s.entries[key] = e
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Delete deletes an entry based on its key.
|
||||
func (s *memStore) Delete(key string) {
|
||||
s.mu.Lock()
|
||||
delete(s.entries, key)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
24
cache/entry/util.go
vendored
24
cache/entry/util.go
vendored
@@ -1,24 +0,0 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var maxAgeExp = regexp.MustCompile(`maxage=(\d+)`)
|
||||
|
||||
// ParseMaxAge parses the max age from the receiver parameter, "cache-control" header
|
||||
// returns seconds as int64
|
||||
// if header not found or parse failed then it returns -1
|
||||
func ParseMaxAge(header string) int64 {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user