github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/counters/topics_views.go (about) 1 package counters 2 3 import ( 4 "database/sql" 5 "strconv" 6 "strings" 7 "sync" 8 "sync/atomic" 9 "time" 10 11 c "github.com/Azareal/Gosora/common" 12 qgen "github.com/Azareal/Gosora/query_gen" 13 "github.com/pkg/errors" 14 ) 15 16 var TopicViewCounter *DefaultTopicViewCounter 17 18 // TODO: Use two odd-even maps for now, and move to something more concurrent later, maybe a sharded map? 19 type DefaultTopicViewCounter struct { 20 oddTopics map[int]*RWMutexCounterBucket // map[tid]struct{counter,sync.RWMutex} 21 evenTopics map[int]*RWMutexCounterBucket 22 oddLock sync.RWMutex 23 evenLock sync.RWMutex 24 25 weekState byte 26 27 update *sql.Stmt 28 resetOdd *sql.Stmt 29 resetEven *sql.Stmt 30 resetBoth *sql.Stmt 31 32 insertListBuf []TopicViewInsert 33 saveTick *SavedTick 34 } 35 36 func NewDefaultTopicViewCounter() (*DefaultTopicViewCounter, error) { 37 acc := qgen.NewAcc() 38 t := "topics" 39 co := &DefaultTopicViewCounter{ 40 oddTopics: make(map[int]*RWMutexCounterBucket), 41 evenTopics: make(map[int]*RWMutexCounterBucket), 42 43 //update: acc.Update(t).Set("views=views+?").Where("tid=?").Prepare(), 44 update: acc.Update(t).Set("views=views+?,weekEvenViews=weekEvenViews+?,weekOddViews=weekOddViews+?").Where("tid=?").Prepare(), 45 resetOdd: acc.Update(t).Set("weekOddViews=0").Prepare(), 46 resetEven: acc.Update(t).Set("weekEvenViews=0").Prepare(), 47 resetBoth: acc.Update(t).Set("weekOddViews=0,weekEvenViews=0").Prepare(), 48 49 //insertListBuf: make([]TopicViewInsert, 1024), 50 } 51 e := co.WeekResetInit() 52 if e != nil { 53 return co, e 54 } 55 56 tick := func(f func() error) { 57 c.Tasks.FifteenMin.Add(f) // Who knows how many topics we have queued up, we probably don't want this running too frequently 58 //c.Tasks.Sec.Add(f) 59 c.Tasks.Shutdown.Add(f) 60 } 61 tick(co.Tick) 62 tick(co.WeekResetTick) 63 64 return co, acc.FirstError() 65 } 66 67 type TopicViewInsert struct { 68 Count int 69 TopicID int 70 } 71 72 type SavedTick struct { 73 I int 74 I2 int 75 } 76 77 func (co *DefaultTopicViewCounter) handleInsertListBuf(i, i2 int) error { 78 ilb := co.insertListBuf 79 var lastSuccess int 80 for i3 := i2; i3 < i; i3++ { 81 iitem := ilb[i3] 82 if e := co.insertChunk(iitem.Count, iitem.TopicID); e != nil { 83 co.saveTick = &SavedTick{I: i, I2: lastSuccess + 1} 84 for i3 := i2; i3 < i && i3 <= lastSuccess; i3++ { 85 ilb[i3].Count, ilb[i3].TopicID = 0, 0 86 } 87 return errors.Wrap(errors.WithStack(e), "topicview counter") 88 } 89 lastSuccess = i3 90 } 91 for i3 := i2; i3 < i; i3++ { 92 ilb[i3].Count, ilb[i3].TopicID = 0, 0 93 } 94 return nil 95 } 96 97 func (co *DefaultTopicViewCounter) Tick() error { 98 // TODO: Fold multiple 1 view topics into one query 99 100 /*if co.saveTick != nil { 101 e := co.handleInsertListBuf(co.saveTick.I, co.saveTick.I2) 102 if e != nil { 103 return e 104 } 105 co.saveTick = nil 106 }*/ 107 108 cLoop := func(l *sync.RWMutex, m map[int]*RWMutexCounterBucket) error { 109 //i := 0 110 l.RLock() 111 for topicID, topic := range m { 112 l.RUnlock() 113 var count int 114 topic.RLock() 115 count = topic.counter 116 topic.RUnlock() 117 // TODO: Only delete the bucket when it's zero to avoid hitting popular topics? 118 l.Lock() 119 delete(m, topicID) 120 l.Unlock() 121 /*if len(co.insertListBuf) >= i { 122 co.insertListBuf[i].Count = count 123 co.insertListBuf[i].TopicID = topicID 124 i++ 125 } else if i < 4096 { 126 co.insertListBuf = append(co.insertListBuf, TopicViewInsert{count, topicID}) 127 } else */if e := co.insertChunk(count, topicID); e != nil { 128 return errors.Wrap(errors.WithStack(e), "topicview counter") 129 } 130 l.RLock() 131 } 132 l.RUnlock() 133 return nil //co.handleInsertListBuf(i, 0) 134 } 135 e := cLoop(&co.oddLock, co.oddTopics) 136 if e != nil { 137 return e 138 } 139 return cLoop(&co.evenLock, co.evenTopics) 140 } 141 142 func (co *DefaultTopicViewCounter) WeekResetInit() error { 143 lastWeekResetStr, e := c.Meta.Get("lastWeekReset") 144 if e != nil && e != sql.ErrNoRows { 145 return e 146 } 147 148 spl := strings.Split(lastWeekResetStr, "-") 149 if len(spl) <= 1 { 150 return nil 151 } 152 weekState, e := strconv.Atoi(spl[1]) 153 if e != nil { 154 return e 155 } 156 co.weekState = byte(weekState) 157 158 unixLastWeekReset, e := strconv.ParseInt(spl[0], 10, 64) 159 if e != nil { 160 return e 161 } 162 resetTime := time.Unix(unixLastWeekReset, 0) 163 if time.Since(resetTime).Hours() >= (24 * 7) { 164 _, e = co.resetBoth.Exec() 165 } 166 return e 167 } 168 169 func (co *DefaultTopicViewCounter) WeekResetTick() (e error) { 170 now := time.Now() 171 _, week := now.ISOWeek() 172 if week != int(co.weekState) { 173 if week%2 == 0 { // is even? 174 _, e = co.resetOdd.Exec() 175 } else { 176 _, e = co.resetEven.Exec() 177 } 178 co.weekState = byte(week) 179 } 180 // TODO: Retry? 181 if e != nil { 182 return e 183 } 184 return c.Meta.Set("lastWeekReset", strconv.FormatInt(now.Unix(), 10)+"-"+strconv.Itoa(week)) 185 } 186 187 // TODO: Optimise this further. E.g. Using IN() on every one view topic. Rinse and repeat for two views, three views, four views and five views. 188 func (co *DefaultTopicViewCounter) insertChunk(count, topicID int) (err error) { 189 if count == 0 { 190 return nil 191 } 192 193 c.DebugLogf("Inserting %d views into topic %d", count, topicID) 194 even, odd := 0, 0 195 _, week := time.Now().ISOWeek() 196 if week%2 == 0 { // is even? 197 even += count 198 } else { 199 odd += count 200 } 201 202 if true { 203 _, err = co.update.Exec(count, even, odd, topicID) 204 } else { 205 _, err = co.update.Exec(count, topicID) 206 } 207 if err == sql.ErrNoRows { 208 return nil 209 } else if err != nil { 210 return err 211 } 212 213 // TODO: Add a way to disable this for extra speed ;) 214 tc := c.Topics.GetCache() 215 if tc != nil { 216 t, err := tc.Get(topicID) 217 if err == sql.ErrNoRows { 218 return nil 219 } else if err != nil { 220 return err 221 } 222 atomic.AddInt64(&t.ViewCount, int64(count)) 223 } 224 225 return nil 226 } 227 228 func (co *DefaultTopicViewCounter) Bump(topicID int) { 229 // Is the ID even? 230 if topicID%2 == 0 { 231 co.evenLock.RLock() 232 t, ok := co.evenTopics[topicID] 233 co.evenLock.RUnlock() 234 if ok { 235 t.Lock() 236 t.counter++ 237 t.Unlock() 238 } else { 239 co.evenLock.Lock() 240 co.evenTopics[topicID] = &RWMutexCounterBucket{counter: 1} 241 co.evenLock.Unlock() 242 } 243 return 244 } 245 246 co.oddLock.RLock() 247 t, ok := co.oddTopics[topicID] 248 co.oddLock.RUnlock() 249 if ok { 250 t.Lock() 251 t.counter++ 252 t.Unlock() 253 } else { 254 co.oddLock.Lock() 255 co.oddTopics[topicID] = &RWMutexCounterBucket{counter: 1} 256 co.oddLock.Unlock() 257 } 258 }