gosora/common/counters/forums.go

109 lines
2.7 KiB
Go

package counters
import (
"database/sql"
"sync"
c "git.tuxpa.in/a/gosora/common"
qgen "git.tuxpa.in/a/gosora/query_gen"
"github.com/pkg/errors"
)
var ForumViewCounter *DefaultForumViewCounter
// TODO: Unload forum counters without any views over the past 15 minutes, if the admin has configured the forumstore with a cap and it's been hit?
// Forums can be reloaded from the database at any time, so we want to keep the counters separate from them
type DefaultForumViewCounter struct {
oddMap map[int]*RWMutexCounterBucket // map[fid]struct{counter,sync.RWMutex}
evenMap map[int]*RWMutexCounterBucket
oddLock sync.RWMutex
evenLock sync.RWMutex
insert *sql.Stmt
}
func NewDefaultForumViewCounter() (*DefaultForumViewCounter, error) {
acc := qgen.NewAcc()
co := &DefaultForumViewCounter{
oddMap: make(map[int]*RWMutexCounterBucket),
evenMap: make(map[int]*RWMutexCounterBucket),
insert: acc.Insert("viewchunks_forums").Columns("count,createdAt,forum").Fields("?,UTC_TIMESTAMP(),?").Prepare(),
}
c.Tasks.FifteenMin.Add(co.Tick) // There could be a lot of routes, so we don't want to be running this every second
//c.Tasks.Sec.Add(co.Tick)
c.Tasks.Shutdown.Add(co.Tick)
return co, acc.FirstError()
}
func (co *DefaultForumViewCounter) Tick() error {
cLoop := func(l *sync.RWMutex, m map[int]*RWMutexCounterBucket) error {
l.RLock()
for fid, f := range m {
l.RUnlock()
var count int
f.RLock()
count = f.counter
f.RUnlock()
// TODO: Only delete the bucket when it's zero to avoid hitting popular forums?
l.Lock()
delete(m, fid)
l.Unlock()
e := co.insertChunk(count, fid)
if e != nil {
return errors.Wrap(errors.WithStack(e), "forum counter")
}
l.RLock()
}
l.RUnlock()
return nil
}
e := cLoop(&co.oddLock, co.oddMap)
if e != nil {
return e
}
return cLoop(&co.evenLock, co.evenMap)
}
func (co *DefaultForumViewCounter) insertChunk(count, forum int) error {
if count == 0 {
return nil
}
c.DebugLogf("Inserting a vchunk with a count of %d for forum %d", count, forum)
_, e := co.insert.Exec(count, forum)
return e
}
func (co *DefaultForumViewCounter) Bump(fid int) {
// Is the ID even?
if fid%2 == 0 {
co.evenLock.RLock()
f, ok := co.evenMap[fid]
co.evenLock.RUnlock()
if ok {
f.Lock()
f.counter++
f.Unlock()
} else {
co.evenLock.Lock()
co.evenMap[fid] = &RWMutexCounterBucket{counter: 1}
co.evenLock.Unlock()
}
return
}
co.oddLock.RLock()
f, ok := co.oddMap[fid]
co.oddLock.RUnlock()
if ok {
f.Lock()
f.counter++
f.Unlock()
} else {
co.oddLock.Lock()
co.oddMap[fid] = &RWMutexCounterBucket{counter: 1}
co.oddLock.Unlock()
}
}
// TODO: Add a forum counter backed by two maps which grow as forums are created but never shrinks