Cache blog stats for better performance

This commit is contained in:
Jan-Lukas Else 2021-05-09 09:35:37 +02:00
parent 59e491ee0a
commit 4653e4480c
2 changed files with 53 additions and 31 deletions

View File

@ -3,8 +3,9 @@ package main
import ( import (
"database/sql" "database/sql"
"net/http" "net/http"
"sync"
servertiming "github.com/mitchellh/go-server-timing" "golang.org/x/sync/singleflight"
) )
func serveBlogStats(w http.ResponseWriter, r *http.Request) { func serveBlogStats(w http.ResponseWriter, r *http.Request) {
@ -19,10 +20,33 @@ func serveBlogStats(w http.ResponseWriter, r *http.Request) {
}) })
} }
var blogStatsCacheGroup singleflight.Group
var blogStatsCache = map[string]map[string]interface{}{}
var blogStatsCacheMutex sync.RWMutex
func serveBlogStatsTable(w http.ResponseWriter, r *http.Request) { func serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
blog := r.Context().Value(blogContextKey).(string) blog := r.Context().Value(blogContextKey).(string)
// Start timing data, err, _ := blogStatsCacheGroup.Do(blog, func() (interface{}, error) {
t := servertiming.FromContext(r.Context()).NewMetric("sq").Start() return getBlogStats(blog)
})
if err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
// Render
render(w, r, templateBlogStatsTable, &renderData{
BlogString: blog,
Data: data,
})
}
func getBlogStats(blog string) (data map[string]interface{}, err error) {
blogStatsCacheMutex.RLock()
if data, ok := blogStatsCache[blog]; ok && data != nil {
blogStatsCacheMutex.RUnlock()
return data, nil
}
blogStatsCacheMutex.RUnlock()
// Build query // Build query
prq := &postsRequestConfig{ prq := &postsRequestConfig{
blog: blog, blog: blog,
@ -40,19 +64,16 @@ func serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
// Count total posts // Count total posts
row, err := appDbQueryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+"))", params...) row, err := appDbQueryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+"))", params...)
if err != nil { if err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError) return nil, err
return
} }
total := statsTableType{} total := statsTableType{}
if err = row.Scan(&total.Posts, &total.Chars, &total.Words, &total.WordsPerPost); err != nil { if err = row.Scan(&total.Posts, &total.Chars, &total.Words, &total.WordsPerPost); err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError) return nil, err
return
} }
// Count posts per year // Count posts per year
rows, err := appDbQuery("select *, "+wordsPerPost+" from (select year, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' group by year order by year desc)", params...) rows, err := appDbQuery("select *, "+wordsPerPost+" from (select year, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' group by year order by year desc)", params...)
if err != nil { if err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError) return nil, err
return
} }
var years []statsTableType var years []statsTableType
year := statsTableType{} year := statsTableType{}
@ -60,20 +81,17 @@ func serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
if err = rows.Scan(&year.Name, &year.Posts, &year.Chars, &year.Words, &year.WordsPerPost); err == nil { if err = rows.Scan(&year.Name, &year.Posts, &year.Chars, &year.Words, &year.WordsPerPost); err == nil {
years = append(years, year) years = append(years, year)
} else { } else {
serveError(w, r, err.Error(), http.StatusInternalServerError) return nil, err
return
} }
} }
// Count posts without date // Count posts without date
row, err = appDbQueryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published = '')", params...) row, err = appDbQueryRow("select *, "+wordsPerPost+" from (select "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published = '')", params...)
if err != nil { if err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError) return nil, err
return
} }
noDate := statsTableType{} noDate := statsTableType{}
if err = row.Scan(&noDate.Posts, &noDate.Chars, &noDate.Words, &noDate.WordsPerPost); err != nil { if err = row.Scan(&noDate.Posts, &noDate.Chars, &noDate.Words, &noDate.WordsPerPost); err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError) return nil, err
return
} }
// Count posts per month per year // Count posts per month per year
months := map[string][]statsTableType{} months := map[string][]statsTableType{}
@ -81,28 +99,30 @@ func serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
for _, year := range years { for _, year := range years {
rows, err = appDbQuery("select *, "+wordsPerPost+" from (select month, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' and year = @year group by month order by month desc)", append(params, sql.Named("year", year.Name))...) rows, err = appDbQuery("select *, "+wordsPerPost+" from (select month, "+postCount+", "+charCount+", "+wordCount+" from ("+query+") where published != '' and year = @year group by month order by month desc)", append(params, sql.Named("year", year.Name))...)
if err != nil { if err != nil {
serveError(w, r, err.Error(), http.StatusInternalServerError) return nil, err
return
} }
for rows.Next() { for rows.Next() {
if err = rows.Scan(&month.Name, &month.Posts, &month.Chars, &month.Words, &month.WordsPerPost); err == nil { if err = rows.Scan(&month.Name, &month.Posts, &month.Chars, &month.Words, &month.WordsPerPost); err == nil {
months[year.Name] = append(months[year.Name], month) months[year.Name] = append(months[year.Name], month)
} else { } else {
serveError(w, r, err.Error(), http.StatusInternalServerError) return nil, err
return
} }
} }
} }
// Stop timing blogStatsCacheMutex.Lock()
t.Stop() blogStatsCache[blog] = map[string]interface{}{
// Render "total": total,
render(w, r, templateBlogStatsTable, &renderData{ "years": years,
BlogString: blog, "withoutdate": noDate,
Data: map[string]interface{}{ "months": months,
"total": total, }
"years": years, data = blogStatsCache[blog]
"withoutdate": noDate, blogStatsCacheMutex.Unlock()
"months": months, return data, nil
}, }
})
func clearBlogStatsCache() {
blogStatsCacheMutex.Lock()
blogStatsCache = map[string]map[string]interface{}{}
blogStatsCacheMutex.Unlock()
} }

View File

@ -210,6 +210,8 @@ func getCache(key string, next http.Handler, r *http.Request) (item *cacheItem)
func purgeCache() { func purgeCache() {
cacheR.Clear() cacheR.Clear()
// Clear blog stats as well
clearBlogStatsCache()
} }
func setInternalCacheExpirationHeader(w http.ResponseWriter, r *http.Request, expiration int) { func setInternalCacheExpirationHeader(w http.ResponseWriter, r *http.Request, expiration int) {