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  }