Skip to content

Commit b904cb7

Browse files
allow to answer with stale cache entry in case of upstream server error (#45)
1 parent 53f6c12 commit b904cb7

File tree

6 files changed

+51
-14
lines changed

6 files changed

+51
-14
lines changed

cache.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ func getKey(cacheKeyTemplate string, r *http.Request) string {
435435
}
436436

437437
// Get returns the cached response
438-
func (h *HTTPCache) Get(key string, request *http.Request) (*Entry, bool) {
438+
func (h *HTTPCache) Get(key string, request *http.Request, includeStale bool) (*Entry, bool) {
439439
b := h.getBucketIndexForKey(key)
440440
h.entriesLock[b].RLock()
441441
defer h.entriesLock[b].RUnlock()
@@ -447,7 +447,7 @@ func (h *HTTPCache) Get(key string, request *http.Request) (*Entry, bool) {
447447
}
448448

449449
for _, entry := range previousEntries {
450-
if entry.IsFresh() && matchVary(request, entry) {
450+
if (entry.IsFresh() || includeStale) && matchVary(request, entry) {
451451
return entry, true
452452
}
453453
}
@@ -498,14 +498,14 @@ func (h *HTTPCache) Del(key string) error {
498498
}
499499

500500
// Put adds the entry in the cache
501-
func (h *HTTPCache) Put(request *http.Request, entry *Entry) {
501+
func (h *HTTPCache) Put(request *http.Request, entry *Entry, config *Config) {
502502
key := entry.Key()
503503
bucket := h.getBucketIndexForKey(key)
504504

505505
h.entriesLock[bucket].Lock()
506506
defer h.entriesLock[bucket].Unlock()
507507

508-
h.scheduleCleanEntry(entry)
508+
h.scheduleCleanEntry(entry, config.StaleMaxAge)
509509

510510
for i, previousEntry := range h.entries[bucket][key] {
511511
if matchVary(entry.Request, previousEntry) {
@@ -571,9 +571,11 @@ func (h *HTTPCache) cleanEntry(entry *Entry) error {
571571
return nil
572572
}
573573

574-
func (h *HTTPCache) scheduleCleanEntry(entry *Entry) {
574+
func (h *HTTPCache) scheduleCleanEntry(entry *Entry, staleMaxAge time.Duration) {
575575
go func(entry *Entry) {
576-
time.Sleep(entry.expiration.Sub(time.Now()))
576+
expiration := entry.expiration
577+
expiration = expiration.Add(staleMaxAge)
578+
time.Sleep(expiration.Sub(time.Now()))
577579
h.cleanEntry(entry)
578580
}(entry)
579581
}

cache_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func (suite *HTTPCacheTestSuite) SetupSuite() {
216216

217217
func (suite *HTTPCacheTestSuite) TestGetNonExistEntry() {
218218
req := makeRequest("/", http.Header{})
219-
entry, exists := suite.cache.Get("abc", req)
219+
entry, exists := suite.cache.Get("abc", req, false)
220220
suite.Nil(entry)
221221
suite.False(exists)
222222
}
@@ -238,9 +238,9 @@ func (suite *HTTPCacheTestSuite) TestGetExistEntry() {
238238
req := makeRequest("/", http.Header{})
239239
res := makeResponse(200, http.Header{})
240240
entry := NewEntry("hello", req, res, suite.config)
241-
suite.cache.Put(req, entry)
241+
suite.cache.Put(req, entry, suite.config)
242242

243-
prevEntry, exists := suite.cache.Get("hello", req)
243+
prevEntry, exists := suite.cache.Get("hello", req, false)
244244
suite.Equal(prevEntry, entry)
245245
suite.True(exists)
246246
}
@@ -251,7 +251,7 @@ func (suite *HTTPCacheTestSuite) TestCleanEntry() {
251251
key := "friday"
252252

253253
entry := NewEntry(key, req, res, suite.config)
254-
suite.cache.Put(req, entry)
254+
suite.cache.Put(req, entry, suite.config)
255255

256256
keyInKeys := false
257257
keys := suite.cache.Keys()

caddyfile.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ var (
4343
defaultcacheBucketsNum = 256
4444
defaultCacheMaxMemorySize = GB // default is 1 GB
4545
defaultRedisConnectionSetting = "localhost:6379 0"
46+
defaultStaleMaxAge = time.Duration(0)
4647
defaultCacheKeyTemplate = "{http.request.method} {http.request.host}{http.request.uri.path}?{http.request.uri.query}"
4748
// Note: prevent character space in the key
4849
// the key is refereced from github.com/caddyserver/caddy/v2/modules/caddyhttp.addHTTPVarsToReplacer
@@ -66,6 +67,7 @@ const (
6667
// the following are keys for extensions
6768
keyDistributed = "distributed"
6869
keyInfluxLog = "influxlog"
70+
keyStaleMaxAge = "stale_max_age"
6971
)
7072

7173
func init() {
@@ -85,6 +87,7 @@ type Config struct {
8587
Path string `json:"path,omitempty"`
8688
CacheKeyTemplate string `json:"cache_key_template,omitempty"`
8789
RedisConnectionSetting string `json:"redis_connection_setting,omitempty"`
90+
StaleMaxAge time.Duration `json:"stale_max_age,omitempty"`
8891
}
8992

9093
func getDefaultConfig() *Config {
@@ -100,6 +103,7 @@ func getDefaultConfig() *Config {
100103
Type: defaultCacheType,
101104
CacheKeyTemplate: defaultCacheKeyTemplate,
102105
RedisConnectionSetting: defaultRedisConnectionSetting,
106+
StaleMaxAge: defaultStaleMaxAge,
103107
}
104108
}
105109

@@ -250,6 +254,17 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
250254

251255
h.DistributedRaw = caddyconfig.JSONModuleObject(unm, "distributed", "consul", nil)
252256

257+
case keyStaleMaxAge:
258+
if len(args) != 1 {
259+
return d.Err("Invalid usage of stale_max_age in cache config.")
260+
}
261+
262+
duration, err := time.ParseDuration(args[0])
263+
if err != nil {
264+
return d.Err(fmt.Sprintf("%s:%s, %s", keyStaleMaxAge, "Invalid duration ", parameter))
265+
}
266+
config.StaleMaxAge = duration
267+
253268
default:
254269
return d.Err("Unknown cache parameter: " + parameter)
255270
}

endpoint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func (c cachePurge) handleShowCache(w http.ResponseWriter, r *http.Request) erro
146146
key := helper.TrimBy(r.URL.Path, "/", 2)
147147
cache := getHandlerCache()
148148

149-
entry, exists := cache.Get(key, r)
149+
entry, exists := cache.Get(key, r, false)
150150
if exists {
151151
err = entry.WriteBodyTo(w)
152152
}

handler.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
338338
lock := h.URLLocks.Acquire(key)
339339
defer lock.Unlock()
340340

341-
previousEntry, exists := h.Cache.Get(key, r)
341+
previousEntry, exists := h.Cache.Get(key, r, false)
342342

343343
// First case: CACHE HIT
344344
// The response exists in cache and is public
@@ -365,7 +365,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
365365
return caddyhttp.Error(http.StatusInternalServerError, err)
366366
}
367367

368-
h.Cache.Put(r, entry)
368+
h.Cache.Put(r, entry, h.Config)
369369
response.Close()
370370

371371
// NOTE: should set the content-length to the header manually when distributed
@@ -396,6 +396,21 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
396396
entry, err := h.fetchUpstream(r, next)
397397
upstreamDuration = time.Since(t)
398398

399+
if entry.Response.Code >= 500 {
400+
// using stale entry when available
401+
previousEntry, exists := h.Cache.Get(key, r, true)
402+
403+
if exists && previousEntry.isPublic {
404+
if err := h.respond(w, previousEntry, cacheHit); err == nil {
405+
return nil
406+
} else if _, ok := err.(backends.NoPreCollectError); ok {
407+
// if the err is No pre collect, just return nil
408+
w.WriteHeader(previousEntry.Response.Code)
409+
return nil
410+
}
411+
}
412+
}
413+
399414
if err != nil {
400415
return caddyhttp.Error(entry.Response.Code, err)
401416
}
@@ -407,7 +422,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
407422
return caddyhttp.Error(http.StatusInternalServerError, err)
408423
}
409424

410-
h.Cache.Put(r, entry)
425+
h.Cache.Put(r, entry, h.Config)
411426
err = h.respond(w, entry, cacheMiss)
412427
if err != nil {
413428
h.logger.Error("cache handler", zap.Error(err))

readme.org

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@
122122
*** default_max_age
123123
The cache's expiration time.
124124

125+
*** stale_max_age
126+
The duration that a cache entry is kept in the cache, even though it has already expired. The default duration is =0=.
127+
128+
If this duration is > 0 and the upstream server answers with an HTTP status code >= 500 (server error) this plugin checks whether there is still an expired (stale) entry from a previous, successful call in the cache. In that case, this stale entry is used to answer instead of the 5xx response.
129+
125130
*** match_header
126131
only the req's header match the condtions
127132
ex.

0 commit comments

Comments
 (0)