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  }