-
Notifications
You must be signed in to change notification settings - Fork 13
/
rcache.go
208 lines (172 loc) · 4.71 KB
/
rcache.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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package handler
import (
"fmt"
"net/http"
"sync"
"time"
"github.com/djthorpe/gopi/v3"
)
/////////////////////////////////////////////////////////////////////
// TYPES
type RenderCache struct {
gopi.Unit
gopi.Logger
sync.RWMutex
sync.WaitGroup
r []gopi.HttpRenderer
c map[string]gopi.HttpRenderer
}
/////////////////////////////////////////////////////////////////////
// CONSTANTS
const (
// Maximum number of entries to store for map between request
// to renderer
rcacheMaxSize = 1000
rcacheMinSize = rcacheMaxSize - 100
)
/////////////////////////////////////////////////////////////////////
// LIFECYCLE
func (this *RenderCache) New(gopi.Config) error {
// Set up req->render map
this.c = make(map[string]gopi.HttpRenderer, rcacheMaxSize)
// Return success
return nil
}
// Dispose resources related to the render cache
func (this *RenderCache) Dispose() error {
// Wait for any goroutines to complete
this.WaitGroup.Wait()
// Exclusive lock
this.RWMutex.Lock()
defer this.RWMutex.Unlock()
// Dispose resources
this.c = nil
this.r = nil
// Return success
return nil
}
/////////////////////////////////////////////////////////////////////
// PUBLIC METHODS
// Register appends a renderer to the set of renderers
func (this *RenderCache) Register(renderer gopi.HttpRenderer) error {
this.RWMutex.Lock()
defer this.RWMutex.Unlock()
if renderer == nil {
return gopi.ErrBadParameter.WithPrefix("Register")
} else {
this.r = append(this.r, renderer)
}
// Return success
return nil
}
// Get returns a renderer for an incoming request, or nil if there is
// no renderer which matches the request
func (this *RenderCache) Get(req *http.Request) gopi.HttpRenderer {
this.RWMutex.RLock()
defer this.RWMutex.RUnlock()
// Check incoming parameters
if req == nil || req.URL == nil {
return nil
}
// Check in the existing cache
key := keyForRequest(req)
if r, exists := this.c[key]; exists {
return r
}
// Round robin the renderers to find the correct one
for _, renderer := range this.r {
if renderer.IsModifiedSince(req, time.Time{}) {
// If we have found the right renderer, then cache it
// for this specific request. Also prunes the size of the map
// so we do it in the background
this.WaitGroup.Add(1)
go func() {
this.setr(key, renderer)
this.WaitGroup.Done()
}()
return renderer
}
}
// No renderer found so return nil
return nil
}
// Render will return the render context for a renderer and a request
func (this *RenderCache) Render(renderer gopi.HttpRenderer, req *http.Request) (gopi.HttpRenderContext, error) {
// Check incoming arguments
if renderer == nil || req == nil {
return gopi.HttpRenderContext{}, gopi.ErrNotFound
}
// Get existing content and modified date
key := keyForRequest(req)
if ctx := this.getc(key); ctx.Content != nil {
// Serve from cache if not modified
if renderer.IsModifiedSince(req, ctx.Modified) == false {
return ctx, nil
}
}
// Generate content
if ctx, err := renderer.ServeContent(req); err != nil {
// Remove the content from the cache
this.setc(key, gopi.HttpRenderContext{})
// Return the error
return gopi.HttpRenderContext{}, err
} else {
// Store content if context is cachable
if ctx.Content != nil && ctx.Modified.IsZero() == false {
this.setc(key, ctx)
}
// Return the content
return ctx, nil
}
}
/////////////////////////////////////////////////////////////////////
// STRINGIFY
func (this *RenderCache) String() string {
str := "<http.rendercache"
for _, r := range this.r {
str += " r=" + fmt.Sprint(r)
}
if sz := len(this.c); sz > 0 {
str += fmt.Sprint(" cachesize=", sz)
}
return str + ">"
}
/////////////////////////////////////////////////////////////////////
// PRIVATE METHODS
// Get returns cached content or nil
func (this *RenderCache) getc(key string) gopi.HttpRenderContext {
this.RWMutex.RLock()
defer this.RWMutex.RUnlock()
return gopi.HttpRenderContext{}
}
// Set saves content for a key
func (this *RenderCache) setc(string, gopi.HttpRenderContext) {
this.RWMutex.Lock()
defer this.RWMutex.Unlock()
// NOOP
}
// Setr saves a mapping between request key and renderer, and prunes the
// size of the map
func (this *RenderCache) setr(key string, renderer gopi.HttpRenderer) {
this.RWMutex.Lock()
defer this.RWMutex.Unlock()
// Prune
if len(this.c) >= rcacheMaxSize {
for k := range this.c {
if len(this.c) < rcacheMinSize {
// End loop when the size of the map is below maximum size
break
}
// We assume that the keys returned are in a random order?
this.Debugf("Pruning RenderCache for %q", k)
delete(this.c, k)
}
}
// Set
if renderer != nil {
this.c[key] = renderer
}
}
func keyForRequest(req *http.Request) string {
return req.URL.String()
}