GoBlog/cache.go

217 lines
5.3 KiB
Go
Raw Permalink Normal View History

2020-07-29 20:45:26 +00:00
package main
import (
2021-07-25 08:53:12 +00:00
"context"
2020-07-29 20:45:26 +00:00
"net/http"
2022-02-21 17:47:41 +00:00
"net/url"
"sort"
2020-07-29 20:45:26 +00:00
"time"
"github.com/dgraph-io/ristretto"
2022-02-21 17:47:41 +00:00
"go.goblog.app/app/pkgs/bufferpool"
"golang.org/x/sync/singleflight"
2020-07-29 20:45:26 +00:00
)
2021-07-25 08:53:12 +00:00
const (
cacheLoggedInKey contextKey = "cacheLoggedIn"
cacheExpirationKey contextKey = "cacheExpiration"
2022-02-01 14:58:12 +00:00
cacheControl = "Cache-Control"
2021-07-25 08:53:12 +00:00
)
2020-11-20 14:33:20 +00:00
type cache struct {
g singleflight.Group
c *ristretto.Cache
}
2020-09-22 14:42:36 +00:00
func (a *goBlog) initCache() (err error) {
a.cache = &cache{}
if a.cfg.Cache != nil && !a.cfg.Cache.Enable {
// Cache disabled
return nil
}
a.cache.c, err = ristretto.NewCache(&ristretto.Config{
NumCounters: 40 * 1000, // 4000 items when full with 5 KB items -> x10 = 40.000
MaxCost: 20 * 1000 * 1000, // 20 MB
BufferItems: 64, // recommended
Metrics: true,
})
go func() {
ticker := time.NewTicker(15 * time.Minute)
for range ticker.C {
met := a.cache.c.Metrics
2023-12-27 10:37:58 +00:00
a.info("Cache metrics", "metrics", met.String())
}
}()
2020-11-20 14:33:20 +00:00
return
}
2020-09-22 14:42:36 +00:00
2021-07-25 08:53:12 +00:00
func cacheLoggedIn(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), cacheLoggedInKey, true)))
})
}
func (a *goBlog) cacheMiddleware(next http.Handler) http.Handler {
2020-07-29 20:45:26 +00:00
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Do checks
if a.cache.c == nil || !cacheable(r) {
2021-02-20 22:35:16 +00:00
next.ServeHTTP(w, r)
return
}
2021-07-25 08:53:12 +00:00
// Check login
if cli, ok := r.Context().Value(cacheLoggedInKey).(bool); ok && cli {
// Continue caching, but remove login
setLoggedIn(r, false)
2022-02-26 19:38:52 +00:00
} else if a.isLoggedIn(r) {
// Don't cache logged in requests
next.ServeHTTP(w, r)
return
2021-02-20 22:35:16 +00:00
}
// Search and serve cache
key := cacheKey(r)
// Get cache or render it
2022-03-16 07:28:03 +00:00
cacheInterface, _, _ := a.cache.g.Do(key, func() (any, error) {
2021-07-25 08:53:12 +00:00
return a.cache.getCache(key, next, r), nil
2021-02-20 22:35:16 +00:00
})
ci := cacheInterface.(*cacheItem)
2021-12-30 11:40:21 +00:00
// copy and set headers
a.setCacheHeaders(w, ci)
2021-02-20 22:35:16 +00:00
// check conditional request
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == ci.eTag {
2021-02-20 22:35:16 +00:00
// send 304
w.WriteHeader(http.StatusNotModified)
return
}
// set status code
w.WriteHeader(ci.code)
2021-02-20 22:35:16 +00:00
// write cached body
_, _ = w.Write(ci.body)
2020-07-29 20:45:26 +00:00
})
}
2021-12-30 11:40:21 +00:00
func cacheable(r *http.Request) bool {
2022-01-31 10:31:16 +00:00
if r.Method != http.MethodGet && r.Method != http.MethodHead {
2021-12-30 11:40:21 +00:00
return false
}
if r.URL.Query().Get("cache") == "0" || r.URL.Query().Get("cache") == "false" {
return false
}
return true
}
2022-02-21 17:47:41 +00:00
func cacheKey(r *http.Request) (key string) {
buf := bufferpool.Get()
2021-02-24 12:16:33 +00:00
// Special cases
if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest {
2022-02-21 17:47:41 +00:00
_, _ = buf.WriteString("as-")
2021-02-24 12:16:33 +00:00
}
2021-05-09 12:42:53 +00:00
if torUsed, ok := r.Context().Value(torUsedKey).(bool); ok && torUsed {
2022-02-21 17:47:41 +00:00
_, _ = buf.WriteString("tor-")
2021-05-09 12:42:53 +00:00
}
// Add cache URL
_, _ = buf.WriteString(r.URL.EscapedPath())
2022-02-21 17:47:41 +00:00
if query := r.URL.Query(); len(query) > 0 {
2021-02-24 12:16:33 +00:00
_ = buf.WriteByte('?')
2022-02-21 17:47:41 +00:00
keys := make([]string, 0, len(query))
for k := range query {
keys = append(keys, k)
}
sort.Strings(keys)
for i, k := range keys {
keyEscaped := url.QueryEscape(k)
for j, val := range query[k] {
if i > 0 || j > 0 {
buf.WriteByte('&')
}
buf.WriteString(keyEscaped)
buf.WriteByte('=')
buf.WriteString(url.QueryEscape(val))
}
}
2021-02-24 12:16:33 +00:00
}
2022-02-21 17:47:41 +00:00
// Get key as string
key = buf.String()
// Return buffer to pool
bufferpool.Put(buf)
return
}
func (a *goBlog) setCacheHeaders(w http.ResponseWriter, cache *cacheItem) {
2021-12-30 11:40:21 +00:00
// Copy headers
for k, v := range cache.header.Clone() {
w.Header()[k] = v
}
// Set cache headers
w.Header().Set("ETag", cache.eTag)
2023-04-11 14:43:14 +00:00
w.Header().Set(cacheControl, "public,no-cache")
2020-07-30 19:08:41 +00:00
}
type cacheItem struct {
2023-04-11 14:43:14 +00:00
expiration int
eTag string
code int
header http.Header
body []byte
2020-10-07 15:35:52 +00:00
}
2022-02-21 17:47:41 +00:00
// Calculate byte size of cache item using size of header, body and etag
func (ci *cacheItem) cost() int {
headerBuf := bufferpool.Get()
_ = ci.header.Write(headerBuf)
headerSize := len(headerBuf.Bytes())
bufferpool.Put(headerBuf)
return headerSize + len(ci.body) + len(ci.eTag)
}
2022-02-21 17:47:41 +00:00
func (c *cache) getCache(key string, next http.Handler, r *http.Request) *cacheItem {
if rItem, ok := c.c.Get(key); ok {
2022-02-21 17:47:41 +00:00
return rItem.(*cacheItem)
2020-11-20 14:33:20 +00:00
}
2022-02-21 17:47:41 +00:00
// No cache available
// Make and use copy of r
2023-12-25 12:44:35 +00:00
//nolint:contextcheck
2022-02-21 17:47:41 +00:00
cr := r.Clone(valueOnlyContext{r.Context()})
// Remove problematic headers
cr.Header.Del("If-Modified-Since")
cr.Header.Del("If-Unmodified-Since")
cr.Header.Del("If-None-Match")
cr.Header.Del("If-Match")
cr.Header.Del("If-Range")
cr.Header.Del("Range")
// Record request
rec := newCacheRecorder()
next.ServeHTTP(rec, cr)
item := rec.finish()
// Set expiration
item.expiration, _ = cr.Context().Value(cacheExpirationKey).(int)
// Remove problematic headers
item.header.Del("Accept-Ranges")
item.header.Del("ETag")
2023-04-11 14:43:14 +00:00
item.header.Del("Last-Modified")
2022-02-21 17:47:41 +00:00
// Save cache
if cch := item.header.Get(cacheControl); !containsStrings(cch, "no-store", "private", "no-cache") {
cost := int64(item.cost())
if item.expiration == 0 {
c.c.Set(key, item, cost)
} else {
c.c.SetWithTTL(key, item, cost, time.Duration(item.expiration)*time.Second)
}
}
return item
2020-07-29 20:45:26 +00:00
}
func (c *cache) purge() {
if c == nil {
return
}
c.c.Clear()
2020-11-20 14:33:20 +00:00
}
2021-07-25 08:53:12 +00:00
func (a *goBlog) defaultCacheExpiration() int {
if a.cfg.Cache != nil {
return a.cfg.Cache.Expiration
2021-05-08 19:22:48 +00:00
}
2021-07-25 08:53:12 +00:00
return 0
}