Massively improve cache using singleflight and storing cacheitems in memory

This commit is contained in:
Jan-Lukas Else 2020-10-19 23:02:57 +02:00
parent a2190306da
commit a3c6ba832e
5 changed files with 62 additions and 101 deletions

149
cache.go
View File

@ -1,76 +1,36 @@
package main package main
import ( import (
"database/sql"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"sync" "sync"
"time" "time"
"golang.org/x/sync/singleflight"
) )
var cacheMutexMapMutex *sync.Mutex var cacheMap = map[string]*cacheItem{}
var cacheMutexes map[string]*sync.Mutex var cacheMutex = &sync.RWMutex{}
var cacheDb *sql.DB
var cacheDbWriteMutex = &sync.Mutex{}
func initCache() (err error) { var requestGroup singleflight.Group
cacheMutexMapMutex = &sync.Mutex{}
cacheMutexes = map[string]*sync.Mutex{}
cacheDb, err = sql.Open("sqlite3", ":memory:")
if err != nil {
return err
}
tx, err := cacheDb.Begin()
if err != nil {
return
}
_, err = tx.Exec("CREATE TABLE cache (path text not null primary key, time integer, header blob, body blob);")
if err != nil {
return
}
err = tx.Commit()
if err != nil {
return
}
return
}
func startWritingToCacheDb() {
cacheDbWriteMutex.Lock()
}
func finishWritingToCacheDb() {
cacheDbWriteMutex.Unlock()
}
func cacheMiddleware(next http.Handler) http.Handler { func cacheMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestURL, _ := url.ParseRequestURI(r.RequestURI)
path := slashTrimmedPath(r)
if appConfig.Cache.Enable && if appConfig.Cache.Enable &&
// check bypass query // check bypass query
!(requestURL != nil && requestURL.Query().Get("cache") == "0") { !(r.URL.Query().Get("cache") == "0") &&
// Check cache mutex // check method
cacheMutexMapMutex.Lock() (r.Method == http.MethodGet || r.Method == http.MethodHead) {
if cacheMutexes[path] == nil { // Fix path
cacheMutexes[path] = &sync.Mutex{} path := slashTrimmedPath(r)
} // Get cache or render it
cacheMutexMapMutex.Unlock() cacheInterface, _, _ := requestGroup.Do(path, func() (interface{}, error) {
// Get cache return getCache(path, next, r), nil
cm := cacheMutexes[path] })
cm.Lock() cache := cacheInterface.(*cacheItem)
cacheTime, header, body := getCache(path) // log.Println(string(cache.body))
cm.Unlock() cacheTimeString := time.Unix(cache.creationTime, 0).Format(time.RFC1123)
if cacheTime == 0 { expiresTimeString := time.Unix(cache.creationTime+appConfig.Cache.Expiration, 0).Format(time.RFC1123)
cm.Lock()
// Render cache
renderCache(path, next, w, r)
cm.Unlock()
return
}
cacheTimeString := time.Unix(cacheTime, 0).Format(time.RFC1123)
expiresTimeString := time.Unix(cacheTime+appConfig.Cache.Expiration, 0).Format(time.RFC1123)
// check conditional request // check conditional request
ifModifiedSinceHeader := r.Header.Get("If-Modified-Since") ifModifiedSinceHeader := r.Header.Get("If-Modified-Since")
if ifModifiedSinceHeader != "" && ifModifiedSinceHeader == cacheTimeString { if ifModifiedSinceHeader != "" && ifModifiedSinceHeader == cacheTimeString {
@ -80,13 +40,14 @@ func cacheMiddleware(next http.Handler) http.Handler {
return return
} }
// copy cached headers // copy cached headers
for k, v := range header { for k, v := range cache.header {
w.Header()[k] = v w.Header()[k] = v
} }
setCacheHeaders(w, cacheTimeString, expiresTimeString) setCacheHeaders(w, cacheTimeString, expiresTimeString)
w.Header().Set("GoBlog-Cache", "HIT") // set status code
w.WriteHeader(cache.code)
// write cached body // write cached body
_, _ = w.Write(body) _, _ = w.Write(cache.body)
return return
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
@ -100,46 +61,38 @@ func setCacheHeaders(w http.ResponseWriter, cacheTimeString string, expiresTimeS
w.Header().Set("Expires", expiresTimeString) w.Header().Set("Expires", expiresTimeString)
} }
func renderCache(path string, next http.Handler, w http.ResponseWriter, r *http.Request) { type cacheItem struct {
// No cache available creationTime int64
recorder := httptest.NewRecorder() code int
next.ServeHTTP(recorder, r) header http.Header
// copy values from recorder body []byte
code := recorder.Code
// send response
for k, v := range recorder.Header() {
w.Header()[k] = v
}
now := time.Now()
setCacheHeaders(w, now.Format(time.RFC1123), time.Unix(now.Unix()+appConfig.Cache.Expiration, 0).Format(time.RFC1123))
w.Header().Set("GoBlog-Cache", "MISS")
w.WriteHeader(code)
_, _ = w.Write(recorder.Body.Bytes())
// Save cache
if code == http.StatusOK {
saveCache(path, now, recorder.Header(), recorder.Body.Bytes())
}
} }
func getCache(path string) (creationTime int64, header map[string][]string, body []byte) { func getCache(path string, next http.Handler, r *http.Request) *cacheItem {
var headerBytes []byte cacheMutex.RLock()
allowedTime := time.Now().Unix() - appConfig.Cache.Expiration item, ok := cacheMap[path]
row := cacheDb.QueryRow("select COALESCE(time, 0), header, body from cache where path=? and time>=?", path, allowedTime) cacheMutex.RUnlock()
_ = row.Scan(&creationTime, &headerBytes, &body) if !ok || item.creationTime < time.Now().Unix()-appConfig.Cache.Expiration {
header = make(map[string][]string) item = &cacheItem{}
_ = json.Unmarshal(headerBytes, &header) // No cache available
return recorder := httptest.NewRecorder()
} next.ServeHTTP(recorder, r)
// copy values from recorder
func saveCache(path string, now time.Time, header map[string][]string, body []byte) { now := time.Now()
headerBytes, _ := json.Marshal(header) item.creationTime = now.Unix()
startWritingToCacheDb() item.code = recorder.Code
defer finishWritingToCacheDb() item.header = recorder.Header()
_, _ = cacheDb.Exec("insert or replace into cache (path, time, header, body) values (?, ?, ?, ?);", path, now.Unix(), headerBytes, body) item.body = recorder.Body.Bytes()
// Save cache
cacheMutex.Lock()
cacheMap[path] = item
cacheMutex.Unlock()
}
return item
} }
func purgeCache() { func purgeCache() {
startWritingToCacheDb() cacheMutex.Lock()
defer finishWritingToCacheDb() cacheMap = map[string]*cacheItem{}
_, _ = cacheDb.Exec("delete from cache; vacuum;") cacheMutex.Unlock()
} }

1
go.mod
View File

@ -37,6 +37,7 @@ require (
github.com/yuin/goldmark-emoji v1.0.1 github.com/yuin/goldmark-emoji v1.0.1
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 // indirect golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 // indirect
golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 // indirect golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 // indirect
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect

3
go.sum
View File

@ -352,7 +352,10 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520 h1:Bx6FllMpG4NWDOfhMBz1VR2QYNp/SAOHPIAsaVmxfPo=
golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View File

@ -2,7 +2,9 @@ package main
import ( import (
"compress/flate" "compress/flate"
"log"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -73,7 +75,10 @@ func buildHandler() (http.Handler, error) {
r.Use(middleware.Recoverer) r.Use(middleware.Recoverer)
if appConfig.Server.Logging { if appConfig.Server.Logging {
r.Use(middleware.RealIP) r.Use(middleware.RealIP)
r.Use(middleware.Logger) r.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{
Logger: log.New(os.Stdout, "", log.LstdFlags),
NoColor: true,
}))
} }
r.Use(middleware.Compress(flate.DefaultCompression)) r.Use(middleware.Compress(flate.DefaultCompression))
r.Use(middleware.StripSlashes) r.Use(middleware.StripSlashes)
@ -205,7 +210,7 @@ func buildHandler() (http.Handler, error) {
r.With(cacheMiddleware, minifier.Middleware).Get(sitemapPath, serveSitemap) r.With(cacheMiddleware, minifier.Middleware).Get(sitemapPath, serveSitemap)
// Check redirects, then serve 404 // Check redirects, then serve 404
r.With(checkRegexRedirects, minifier.Middleware).NotFound(serve404) r.With(checkRegexRedirects, cacheMiddleware, minifier.Middleware).NotFound(serve404)
return r, nil return r, nil
} }

View File

@ -29,7 +29,6 @@ func main() {
log.Println("Initialize server components...") log.Println("Initialize server components...")
initMinify() initMinify()
initMarkdown() initMarkdown()
initCache()
err = initTemplateAssets() // Needs minify err = initTemplateAssets() // Needs minify
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)