-
Notifications
You must be signed in to change notification settings - Fork 1
/
cache.go
155 lines (133 loc) · 4.39 KB
/
cache.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package cache
import (
"time"
"github.com/kataras/iris"
"github.com/kataras/iris/context"
)
const (
// Content types used when determining wether to cache the response body
// ContentTypeJSON header value for JSON data.
ContentTypeJSON = context.ContentJSONHeaderValue
// ContentTypeHTML is the string of text/html response header's content type value.
ContentTypeHTML = context.ContentHTMLHeaderValue
// CtxIrisCacheKey the key used in iris.Context.Values().Set(), which holds the given route's cache key
CtxIrisCacheKey = "iris_cache_key"
// CtxIrisSkipCacheKey indicator which a previous handler func can use, to explictly skip the cache middleware
CtxIrisSkipCacheKey = "iris_skip_cache"
)
// Store defines a store which can cache data
type Store interface {
// Stores the data under the given cache key
Store(string, []byte) error
// Retrieves the data with the given cache key
Retrieve(string) (*cacheddata, bool, error)
// Deletes the data under the given cache key
Delete(string) error
// Returns the config for the store
Config() StoreConfig
// Replaces the current config with the given config
SetConfig(config StoreConfig)
}
// KeySupplier defines a function which computes a cache key (string) out of a iris.Context
type KeySupplier func(ctx iris.Context) string
// StoreConfig defines configuration options which are used by the store
type StoreConfig struct {
// The name of the used store
StoreName string
}
// Config defines configuration options which are used by the middleware
type Config struct {
// Wether to automatically remove the data after the cache duration
// or let the middleware only remove it, when a route is called and
// the cache duration is expired.
AutoRemove bool
// Wether iris' gzip compression is used
IrisGzipEnabled bool
// The amount of time data is cached in the store
CacheTimeDuration time.Duration
// The content type which will be cached
ContentType string
// Function which computes the cache key out of an iris.Context
CacheKeyFunc KeySupplier
}
// primitive type representing cached data
type cacheddata struct {
Data []byte `json:"data" bson:"data"`
CreatedOn time.Time `json:"created_on" bson:"created_on"`
}
// NewCacheHF creates a cache handler function with the given cache duration per route.
func NewCacheHF(config Config, store Store) iris.Handler {
return NewCache(config, store).Serve
}
// NewCache creates a new caching middleware with the given cache duration per route
func NewCache(config Config, store Store) *Cache {
c := &Cache{config: config, store: store}
if c.config.CacheKeyFunc == nil {
c.config.CacheKeyFunc = RequestPathToMD5
}
return c
}
// Cache contains the store and the configuration, its `Serve` function is the middleware.
type Cache struct {
store Store
config Config
}
// Serve handles the cache action, should be registered before the main handler.
func (c *Cache) Serve(ctx iris.Context) {
switch t := ctx.Values().Get(CtxIrisSkipCacheKey).(type) {
case bool:
if t {
return
}
}
cacheKey := c.config.CacheKeyFunc(ctx)
cachedJSON, ok, err := c.store.Retrieve(cacheKey)
if err != nil {
panic(err)
}
if ok && time.Now().Before(cachedJSON.CreatedOn.Add(c.config.CacheTimeDuration)) {
ctx.ContentType(c.config.ContentType)
if c.config.IrisGzipEnabled && ctx.ClientSupportsGzip() {
ctx.Header("Content-Encoding", "gzip")
}
ctx.StatusCode(iris.StatusOK)
ctx.Write(cachedJSON.Data)
return
}
// call other routes
ctx.Values().Set(CtxIrisCacheKey, cacheKey)
rec := ctx.Recorder()
ctx.Next()
// check content type
contentType := ctx.GetContentType()
if contentType != c.config.ContentType {
return
}
// get computed response
bytesToCache := rec.Body()
fresh := make([]byte, len(bytesToCache))
copy(fresh, bytesToCache)
c.store.Store(cacheKey, fresh)
// skip auto remove. remove it when the duration is expired and the route is executed
if !c.config.AutoRemove {
return
}
// racy
go func() {
// remove cached data after defined duration
<-time.After(c.config.CacheTimeDuration)
// retrieve cached data as it might be refreshed
cachedData, ok, err := c.store.Retrieve(cacheKey)
if err != nil {
panic(err)
}
if !ok || time.Now().Before(cachedData.CreatedOn.Add(c.config.CacheTimeDuration)) {
return
}
c.store.Delete(cacheKey)
}()
}
// Invalidate invalidates the cached data by the given key
func (c *Cache) Invalidate(cacheKey string) {
c.store.Delete(cacheKey)
}