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