github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/tickloop.go (about) 1 package common 2 3 import ( 4 "fmt" 5 "log" 6 "strconv" 7 "strings" 8 "sync/atomic" 9 "time" 10 11 qgen "github.com/Azareal/Gosora/query_gen" 12 "github.com/Azareal/Gosora/uutils" 13 "github.com/pkg/errors" 14 ) 15 16 var CTickLoop *TickLoop 17 18 type TickLoop struct { 19 HalfSec *time.Ticker 20 Sec *time.Ticker 21 FifteenMin *time.Ticker 22 Hour *time.Ticker 23 Day *time.Ticker 24 25 HalfSecf func() error 26 Secf func() error 27 FifteenMinf func() error 28 Hourf func() error 29 Dayf func() error 30 } 31 32 func NewTickLoop() *TickLoop { 33 return &TickLoop{ 34 // TODO: Write tests for these 35 // Run this goroutine once every half second 36 HalfSec: time.NewTicker(time.Second / 2), 37 Sec: time.NewTicker(time.Second), 38 FifteenMin: time.NewTicker(15 * time.Minute), 39 Hour: time.NewTicker(time.Hour), 40 Day: time.NewTicker(time.Hour * 24), 41 } 42 } 43 44 func (l *TickLoop) Loop() { 45 r := func(e error) { 46 if e != nil { 47 LogError(e) 48 } 49 } 50 defer EatPanics() 51 for { 52 select { 53 case <-l.HalfSec.C: 54 r(l.HalfSecf()) 55 case <-l.Sec.C: 56 r(l.Secf()) 57 case <-l.FifteenMin.C: 58 r(l.FifteenMinf()) 59 case <-l.Hour.C: 60 r(l.Hourf()) 61 // TODO: Handle the instance going down a lot better 62 case <-l.Day.C: 63 r(l.Dayf()) 64 } 65 } 66 } 67 68 var ErrDBDown = errors.New("The database is down") 69 70 func DBTimeout() time.Duration { 71 if Dev.HourDBTimeout { 72 return time.Hour 73 } 74 //db.SetConnMaxLifetime(time.Second * 60 * 5) // Make this infinite as the temporary lifetime change will purge the stale connections? 75 return -1 76 } 77 78 var pint int64 79 80 func StartTick() (abort bool) { 81 opint := pint 82 db := qgen.Builder.GetConn() 83 isDBDown := atomic.LoadInt32(&IsDBDown) 84 if e := db.Ping(); e != nil { 85 // TODO: There's a bit of a race here, but it doesn't matter if this error appears multiple times in the logs as it's capped at three times, we just want to cut it down 99% of the time 86 if isDBDown == 0 { 87 db.SetConnMaxLifetime(time.Second / 4) // Drop all the connections and start over 88 LogWarning(e, ErrDBDown.Error()) 89 } 90 atomic.StoreInt32(&IsDBDown, 1) 91 return true 92 } 93 if isDBDown == 1 { 94 log.Print("The database is back") 95 } 96 db.SetConnMaxLifetime(DBTimeout()) 97 atomic.StoreInt32(&IsDBDown, 0) 98 return opint != pint 99 } 100 101 // TODO: Move these into DailyTick() methods? 102 func asmMatches() error { 103 // TODO: Find a more efficient way of doing this 104 return qgen.NewAcc().Select("activity_stream").Cols("asid").EachInt(func(asid int) error { 105 if ActivityMatches.CountAsid(asid) > 0 { 106 return nil 107 } 108 return Activity.Delete(asid) 109 }) 110 } 111 112 // TODO: Name the tasks so we can figure out which one it was when something goes wrong? Or maybe toss it up WithStack down there? 113 func RunTasks(tasks []func() error) error { 114 for _, task := range tasks { 115 if e := task(); e != nil { 116 return e 117 } 118 } 119 return nil 120 } 121 122 /*func init() { 123 DbInits.Add(func(acc *qgen.Accumulator) error { 124 replyStmts = ReplyStmts{ 125 isLiked: acc.Select("likes").Columns("targetItem").Where("sentBy=? and targetItem=? and targetType='replies'").Prepare(), 126 } 127 return acc.FirstError() 128 }) 129 }*/ 130 131 func StartupTasks() (e error) { 132 r := func(ee error) { 133 if e == nil { 134 e = ee 135 } 136 } 137 if Config.DisableRegLog { 138 r(RegLogs.Purge()) 139 } 140 if Config.DisableLoginLog { 141 r(LoginLogs.Purge()) 142 } 143 if Config.DisablePostIP { 144 // TODO: Clear the caches? 145 r(Topics.ClearIPs()) 146 r(Rstore.ClearIPs()) 147 r(Prstore.ClearIPs()) 148 } 149 if Config.DisablePollIP { 150 r(Polls.ClearIPs()) 151 } 152 if Config.DisableLastIP { 153 r(Users.ClearLastIPs()) 154 } 155 return e 156 } 157 158 func Dailies() (e error) { 159 if e = asmMatches(); e != nil { 160 return e 161 } 162 newAcc := func() *qgen.Accumulator { 163 return qgen.NewAcc() 164 } 165 exec := func(ac qgen.AccExec) { 166 if e != nil { 167 return 168 } 169 _, ee := ac.Exec() 170 e = ee 171 } 172 r := func(ee error) { 173 if e == nil { 174 e = ee 175 } 176 } 177 178 if Config.LogPruneCutoff > -1 { 179 // TODO: Clear the caches? 180 if !Config.DisableLoginLog { 181 r(LoginLogs.DeleteOlderThanDays(Config.LogPruneCutoff)) 182 } 183 if !Config.DisableRegLog { 184 r(RegLogs.DeleteOlderThanDays(Config.LogPruneCutoff)) 185 } 186 } 187 188 if !Config.DisablePostIP && Config.PostIPCutoff > -1 { 189 // TODO: Use unixtime to remove this MySQLesque logic? 190 f := func(tbl string) { 191 exec(newAcc().Update(tbl).Set("ip=''").DateOlderThan("createdAt", Config.PostIPCutoff, "day").Where("ip!=''")) 192 } 193 f("topics") 194 f("replies") 195 f("users_replies") 196 } 197 198 if !Config.DisablePollIP && Config.PollIPCutoff > -1 { 199 // TODO: Use unixtime to remove this MySQLesque logic? 200 exec(newAcc().Update("polls_votes").Set("ip=''").DateOlderThan("castAt", Config.PollIPCutoff, "day").Where("ip!=''")) 201 202 // TODO: Find some way of purging the ip data in polls_votes without breaking any anti-cheat measures which might be running... maybe hash it instead? 203 } 204 205 // TODO: lastActiveAt isn't currently set, so we can't rely on this to purge last_ips of users who haven't been on in a while 206 if !Config.DisableLastIP && Config.LastIPCutoff > 0 { 207 //exec(newAcc().Update("users").Set("last_ip='0'").DateOlderThan("lastActiveAt",c.Config.PostIPCutoff,"day").Where("last_ip!='0'")) 208 mon := time.Now().Month() 209 exec(newAcc().Update("users").Set("last_ip=''").Where("last_ip!='' AND last_ip NOT LIKE '" + strconv.Itoa(int(mon)) + "-%'")) 210 } 211 212 if e != nil { 213 return e 214 } 215 if e = Tasks.Day.Run(); e != nil { 216 return e 217 } 218 if e = ForumActionStore.DailyTick(); e != nil { 219 return e 220 } 221 222 { 223 e := Meta.SetInt64("lastDaily", time.Now().Unix()) 224 if e != nil { 225 return e 226 } 227 } 228 229 return nil 230 } 231 232 type TickWatch struct { 233 Name string 234 Start int64 235 DBCheck int64 236 StartHook int64 237 Tasks int64 238 EndHook int64 239 240 Ticker *time.Ticker 241 Deadline *time.Ticker 242 EndChan chan bool 243 OutEndChan chan bool 244 } 245 246 func NewTickWatch() *TickWatch { 247 return &TickWatch{ 248 Ticker: time.NewTicker(time.Second * 5), 249 Deadline: time.NewTicker(time.Hour), 250 } 251 } 252 253 func (w *TickWatch) DumpElapsed() { 254 var sb strings.Builder 255 f := func(str string) { 256 sb.WriteString(str) 257 } 258 ff := func(str string, args ...interface{}) { 259 f(fmt.Sprintf(str, args...)) 260 } 261 elapse := func(name string, bef, v int64) { 262 if bef == 0 || v == 0 { 263 ff("%s: %d\n", v) 264 return 265 } 266 dur := time.Duration(v - bef) 267 milli := dur.Milliseconds() 268 if milli < 1000 { 269 ff("%s: %d - %d ms\n", name, v, milli) 270 } else if milli > 60000 { 271 secs := milli / 1000 272 mins := secs / 60 273 ff("%s: %d - m%d s%.2f\n", name, v, mins, float64(milli-(mins*60*1000))/1000) 274 } else { 275 ff("%s: %d - %.2f secs\n", name, v, dur.Seconds()) 276 } 277 } 278 279 f("Name: " + w.Name + "\n") 280 ff("Start: %d\n", w.Start) 281 elapse("DBCheck", w.Start, w.DBCheck) 282 if w.DBCheck == 0 { 283 Log(sb.String()) 284 return 285 } 286 elapse("StartHook", w.DBCheck, w.StartHook) 287 if w.StartHook == 0 { 288 Log(sb.String()) 289 return 290 } 291 elapse("Tasks", w.StartHook, w.Tasks) 292 if w.Tasks == 0 { 293 Log(sb.String()) 294 return 295 } 296 elapse("EndHook", w.Tasks, w.EndHook) 297 298 Log(sb.String()) 299 } 300 301 func (w *TickWatch) Run() { 302 w.EndChan = make(chan bool) 303 // Use a goroutine to circumvent ticks which never end 304 // TODO: Reuse goroutines across multiple *TickWatch? 305 go func() { 306 defer w.Ticker.Stop() 307 defer close(w.EndChan) 308 defer EatPanics() 309 var n int 310 var downOnce bool 311 for { 312 select { 313 case <-w.Ticker.C: 314 // Less noisy logs 315 if n > 20 && n%5 == 0 { 316 Logf("%d seconds elapsed since tick %s started", 5*n, w.Name) 317 } else if n > 2 && n%2 != 0 { 318 Logf("%d seconds elapsed since tick %s started", 5*n, w.Name) 319 } 320 if !downOnce && w.DBCheck == 0 { 321 qgen.Builder.GetConn().SetConnMaxLifetime(time.Second / 4) // Drop all the connections and start over 322 LogWarning(ErrDBDown) 323 atomic.StoreInt32(&IsDBDown, 1) 324 downOnce = true 325 } 326 n++ 327 case <-w.Deadline.C: 328 Log("Hit TickWatch deadline") 329 dur := time.Duration(uutils.Nanotime() - w.Start) 330 if dur.Seconds() > 5 { 331 Log("tick " + w.Name + " has run for " + dur.String()) 332 w.DumpElapsed() 333 } 334 return 335 case <-w.EndChan: 336 dur := time.Duration(uutils.Nanotime() - w.Start) 337 if dur.Seconds() > 5 { 338 Log("tick " + w.Name + " completed in " + dur.String()) 339 w.DumpElapsed() 340 } 341 if w.OutEndChan != nil { 342 w.OutEndChan <- true 343 close(w.OutEndChan) 344 } 345 return 346 } 347 } 348 }() 349 } 350 351 func (w *TickWatch) Stop() { 352 w.EndChan <- true 353 } 354 355 func (w *TickWatch) Set(a *int64, v int64) { 356 atomic.StoreInt64(a, v) 357 } 358 359 func (w *TickWatch) Clear() { 360 w.Start = 0 361 w.DBCheck = 0 362 w.StartHook = 0 363 w.Tasks = 0 364 w.EndHook = 0 365 }