github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/counters/forums.go (about) 1 package counters 2 3 import ( 4 "database/sql" 5 "sync" 6 7 c "github.com/Azareal/Gosora/common" 8 qgen "github.com/Azareal/Gosora/query_gen" 9 "github.com/pkg/errors" 10 ) 11 12 var ForumViewCounter *DefaultForumViewCounter 13 14 // 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? 15 // Forums can be reloaded from the database at any time, so we want to keep the counters separate from them 16 type DefaultForumViewCounter struct { 17 oddMap map[int]*RWMutexCounterBucket // map[fid]struct{counter,sync.RWMutex} 18 evenMap map[int]*RWMutexCounterBucket 19 oddLock sync.RWMutex 20 evenLock sync.RWMutex 21 22 insert *sql.Stmt 23 } 24 25 func NewDefaultForumViewCounter() (*DefaultForumViewCounter, error) { 26 acc := qgen.NewAcc() 27 co := &DefaultForumViewCounter{ 28 oddMap: make(map[int]*RWMutexCounterBucket), 29 evenMap: make(map[int]*RWMutexCounterBucket), 30 insert: acc.Insert("viewchunks_forums").Columns("count,createdAt,forum").Fields("?,UTC_TIMESTAMP(),?").Prepare(), 31 } 32 c.Tasks.FifteenMin.Add(co.Tick) // There could be a lot of routes, so we don't want to be running this every second 33 //c.Tasks.Sec.Add(co.Tick) 34 c.Tasks.Shutdown.Add(co.Tick) 35 return co, acc.FirstError() 36 } 37 38 func (co *DefaultForumViewCounter) Tick() error { 39 cLoop := func(l *sync.RWMutex, m map[int]*RWMutexCounterBucket) error { 40 l.RLock() 41 for fid, f := range m { 42 l.RUnlock() 43 var count int 44 f.RLock() 45 count = f.counter 46 f.RUnlock() 47 // TODO: Only delete the bucket when it's zero to avoid hitting popular forums? 48 l.Lock() 49 delete(m, fid) 50 l.Unlock() 51 e := co.insertChunk(count, fid) 52 if e != nil { 53 return errors.Wrap(errors.WithStack(e), "forum counter") 54 } 55 l.RLock() 56 } 57 l.RUnlock() 58 return nil 59 } 60 e := cLoop(&co.oddLock, co.oddMap) 61 if e != nil { 62 return e 63 } 64 return cLoop(&co.evenLock, co.evenMap) 65 } 66 67 func (co *DefaultForumViewCounter) insertChunk(count, forum int) error { 68 if count == 0 { 69 return nil 70 } 71 c.DebugLogf("Inserting a vchunk with a count of %d for forum %d", count, forum) 72 _, e := co.insert.Exec(count, forum) 73 return e 74 } 75 76 func (co *DefaultForumViewCounter) Bump(fid int) { 77 // Is the ID even? 78 if fid%2 == 0 { 79 co.evenLock.RLock() 80 f, ok := co.evenMap[fid] 81 co.evenLock.RUnlock() 82 if ok { 83 f.Lock() 84 f.counter++ 85 f.Unlock() 86 } else { 87 co.evenLock.Lock() 88 co.evenMap[fid] = &RWMutexCounterBucket{counter: 1} 89 co.evenLock.Unlock() 90 } 91 return 92 } 93 94 co.oddLock.RLock() 95 f, ok := co.oddMap[fid] 96 co.oddLock.RUnlock() 97 if ok { 98 f.Lock() 99 f.counter++ 100 f.Unlock() 101 } else { 102 co.oddLock.Lock() 103 co.oddMap[fid] = &RWMutexCounterBucket{counter: 1} 104 co.oddLock.Unlock() 105 } 106 } 107 108 // TODO: Add a forum counter backed by two maps which grow as forums are created but never shrinks