mirror of https://github.com/jlelse/GoBlog
Cache reactions for 100 posts
This commit is contained in:
parent
1e03474539
commit
69cbe4ba37
5
app.go
5
app.go
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
shutdowner "git.jlel.se/jlelse/go-shutdowner"
|
shutdowner "git.jlel.se/jlelse/go-shutdowner"
|
||||||
ts "git.jlel.se/jlelse/template-strings"
|
ts "git.jlel.se/jlelse/template-strings"
|
||||||
|
"github.com/dgraph-io/ristretto"
|
||||||
ct "github.com/elnormous/contenttype"
|
ct "github.com/elnormous/contenttype"
|
||||||
"github.com/go-fed/httpsig"
|
"github.com/go-fed/httpsig"
|
||||||
"github.com/hacdias/indieauth/v2"
|
"github.com/hacdias/indieauth/v2"
|
||||||
|
@ -73,6 +74,10 @@ type goBlog struct {
|
||||||
mediaStorage mediaStorage
|
mediaStorage mediaStorage
|
||||||
// Minify
|
// Minify
|
||||||
min minify.Minifier
|
min minify.Minifier
|
||||||
|
// Reactions
|
||||||
|
reactionsInit sync.Once
|
||||||
|
reactionsCache *ristretto.Cache
|
||||||
|
reactionsSfg singleflight.Group
|
||||||
// Regex Redirects
|
// Regex Redirects
|
||||||
regexRedirects []*regexRedirect
|
regexRedirects []*regexRedirect
|
||||||
// Sessions
|
// Sessions
|
||||||
|
|
89
reactions.go
89
reactions.go
|
@ -3,8 +3,10 @@ package main
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/dgraph-io/ristretto"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"go.goblog.app/app/pkgs/bufferpool"
|
"go.goblog.app/app/pkgs/bufferpool"
|
||||||
"go.goblog.app/app/pkgs/contenttype"
|
"go.goblog.app/app/pkgs/contenttype"
|
||||||
|
@ -29,6 +31,17 @@ func (a *goBlog) reactionsEnabledForPost(post *post) bool {
|
||||||
return a.reactionsEnabled() && post != nil && post.firstParameter(reactionsPostParam) != "false"
|
return a.reactionsEnabled() && post != nil && post.firstParameter(reactionsPostParam) != "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *goBlog) initReactions() {
|
||||||
|
a.reactionsInit.Do(func() {
|
||||||
|
a.reactionsCache, _ = ristretto.NewCache(&ristretto.Config{
|
||||||
|
NumCounters: 1000,
|
||||||
|
MaxCost: 100, // Cache reactions for 100 posts
|
||||||
|
BufferItems: 64,
|
||||||
|
IgnoreInternalCost: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (a *goBlog) postReaction(w http.ResponseWriter, r *http.Request) {
|
func (a *goBlog) postReaction(w http.ResponseWriter, r *http.Request) {
|
||||||
path := r.FormValue("path")
|
path := r.FormValue("path")
|
||||||
reaction := r.FormValue("reaction")
|
reaction := r.FormValue("reaction")
|
||||||
|
@ -48,6 +61,11 @@ func (a *goBlog) saveReaction(reaction, path string) error {
|
||||||
if !lo.Contains(allowedReactions, reaction) {
|
if !lo.Contains(allowedReactions, reaction) {
|
||||||
return errors.New("reaction not allowed")
|
return errors.New("reaction not allowed")
|
||||||
}
|
}
|
||||||
|
// Init
|
||||||
|
a.initReactions()
|
||||||
|
// Delete from cache
|
||||||
|
defer a.reactionsSfg.Forget(path)
|
||||||
|
defer a.reactionsCache.Del(path)
|
||||||
// Insert reaction
|
// Insert reaction
|
||||||
_, err := a.db.exec("insert into reactions (path, reaction, count) values (?, ?, 1) on conflict (path, reaction) do update set count=count+1", path, reaction)
|
_, err := a.db.exec("insert into reactions (path, reaction, count) values (?, ?, 1) on conflict (path, reaction) do update set count=count+1", path, reaction)
|
||||||
return err
|
return err
|
||||||
|
@ -68,38 +86,57 @@ func (a *goBlog) getReactions(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set(contentType, contenttype.JSONUTF8)
|
w.Header().Set(contentType, contenttype.JSONUTF8)
|
||||||
_ = a.min.Get().Minify(contenttype.JSON, w, buf)
|
_, _ = io.Copy(w, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) getReactionsFromDatabase(path string) (map[string]int, error) {
|
func (a *goBlog) getReactionsFromDatabase(path string) (map[string]int, error) {
|
||||||
sqlBuf := bufferpool.Get()
|
// Init
|
||||||
defer bufferpool.Put(sqlBuf)
|
a.initReactions()
|
||||||
sqlArgs := []any{}
|
// Check cache
|
||||||
sqlBuf.WriteString("select reaction, count from reactions where path=? and reaction in (")
|
if val, cached := a.reactionsCache.Get(path); cached {
|
||||||
sqlArgs = append(sqlArgs, path)
|
// Return from cache
|
||||||
for i, reaction := range allowedReactions {
|
return val.(map[string]int), nil
|
||||||
if i > 0 {
|
}
|
||||||
sqlBuf.WriteString(",")
|
// Get reactions
|
||||||
|
res, err, _ := a.reactionsSfg.Do(path, func() (interface{}, error) {
|
||||||
|
// Build query
|
||||||
|
sqlBuf := bufferpool.Get()
|
||||||
|
defer bufferpool.Put(sqlBuf)
|
||||||
|
sqlArgs := []any{}
|
||||||
|
sqlBuf.WriteString("select reaction, count from reactions where path=? and reaction in (")
|
||||||
|
sqlArgs = append(sqlArgs, path)
|
||||||
|
for i, reaction := range allowedReactions {
|
||||||
|
if i > 0 {
|
||||||
|
sqlBuf.WriteString(",")
|
||||||
|
}
|
||||||
|
sqlBuf.WriteString("?")
|
||||||
|
sqlArgs = append(sqlArgs, reaction)
|
||||||
}
|
}
|
||||||
sqlBuf.WriteString("?")
|
sqlBuf.WriteString(") and path not in (select path from post_parameters where parameter=? and value=?)")
|
||||||
sqlArgs = append(sqlArgs, reaction)
|
sqlArgs = append(sqlArgs, reactionsPostParam, "false")
|
||||||
}
|
// Execute query
|
||||||
sqlBuf.WriteString(") and path not in (select path from post_parameters where parameter=? and value=?)")
|
rows, err := a.db.query(sqlBuf.String(), sqlArgs...)
|
||||||
sqlArgs = append(sqlArgs, reactionsPostParam, "false")
|
|
||||||
rows, err := a.db.query(sqlBuf.String(), sqlArgs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
reactions := map[string]int{}
|
|
||||||
for rows.Next() {
|
|
||||||
var reaction string
|
|
||||||
var count int
|
|
||||||
err = rows.Scan(&reaction, &count)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
reactions[reaction] = count
|
// Build result
|
||||||
|
defer rows.Close()
|
||||||
|
reactions := map[string]int{}
|
||||||
|
for rows.Next() {
|
||||||
|
var reaction string
|
||||||
|
var count int
|
||||||
|
err = rows.Scan(&reaction, &count)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reactions[reaction] = count
|
||||||
|
}
|
||||||
|
// Cache result
|
||||||
|
a.reactionsCache.Set(path, reactions, 1)
|
||||||
|
return reactions, nil
|
||||||
|
})
|
||||||
|
if err != nil || res == nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return reactions, nil
|
return res.(map[string]int), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,13 +136,13 @@ func Test_reactionsHighLevel(t *testing.T) {
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
app.getReactions(rec, req)
|
app.getReactions(rec, req)
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
assert.Equal(t, `{"❤️":1}`, rec.Body.String())
|
assert.Equal(t, "{\"❤️\":1}\n", rec.Body.String())
|
||||||
|
|
||||||
// Get reactions for a non-existing post
|
// Get reactions for a non-existing post
|
||||||
req = httptest.NewRequest(http.MethodGet, "/?path=/non-existing-post", nil)
|
req = httptest.NewRequest(http.MethodGet, "/?path=/non-existing-post", nil)
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
app.getReactions(rec, req)
|
app.getReactions(rec, req)
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
assert.Equal(t, `{}`, rec.Body.String())
|
assert.Equal(t, "{}\n", rec.Body.String())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue