github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/pi/statusescache.go (about)

     1  // Copyright (c) 2021-2022 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package pi
     6  
     7  import (
     8  	"container/list"
     9  	"sync"
    10  
    11  	backend "github.com/decred/politeia/politeiad/backendv2"
    12  	"github.com/decred/politeia/politeiad/plugins/pi"
    13  	"github.com/decred/politeia/politeiad/plugins/ticketvote"
    14  )
    15  
    16  // statusesCacheLimit is the cache's default maximum capacity. Note that it's
    17  // a var in order to allow setting different limit values in test files.
    18  var statusesCacheLimit = 1000
    19  
    20  // proposalStatuses is a lazy loaded, memory cache that caches proposal data
    21  // required to determine the proposal status at runtime such as record
    22  // metadata, vote metadata, the vote status and the proposal billing status
    23  // changes. The cache is necessary to improve the performance of determining a
    24  // status of a proposal at runtime by reducing the number of expensive backend
    25  // calls that result in the tlog tree be retrieved, which gets very expensive
    26  // when a tree contains tens of thousands of ticket vote leaves. This is
    27  // helpful when the cached data is not expected to change, which means that
    28  // once we store the data in cache we don't need to fetch it again.
    29  //
    30  // Number of entries stored in cache is limited by statusesCacheLimit. If the
    31  // cache is full and a new entry is being added, the oldest entry is removed
    32  // from the `data` map and the `entries` list.
    33  //
    34  // Currently the limit is set to 1000 as we don't really need more than that
    35  // as the goal of the cache is to speed up fetching the statuses of the
    36  // most recent proposals. Each cache entry size is ~150bytes so the cache total
    37  // size when full is expected to be ~150KB.
    38  type proposalStatuses struct {
    39  	sync.Mutex
    40  	data    map[string]*statusEntry // [token]statusEntry
    41  	entries *list.List              // list of cache records tokens
    42  }
    43  
    44  // statusEntry represents a cached proposal status and proposal data required
    45  // to determine the proposal status.
    46  type statusEntry struct {
    47  	propStatus pi.PropStatusT
    48  
    49  	// The following fields cache data in order to reduce the number of backend
    50  	// calls required to determine the proposal status.
    51  	recordState          backend.StateT
    52  	recordStatus         backend.StatusT
    53  	voteStatus           ticketvote.VoteStatusT
    54  	voteMetadata         *ticketvote.VoteMetadata
    55  	billingStatusesCount int // Number of billing status changes
    56  }
    57  
    58  // get retrieves the data associated with the given token from the
    59  // memory cache.  If data doesn't exist in cache it returns nil.
    60  func (s *proposalStatuses) get(token string) *statusEntry {
    61  	s.Lock()
    62  	defer s.Unlock()
    63  
    64  	return s.data[token]
    65  }
    66  
    67  // set stores the given entry in cache, if a cache entry associated with the
    68  // token already exists it overwrites the old entry. If the cache is full and
    69  // a new entry is being added, the oldest entry is removed from the cache.
    70  func (s *proposalStatuses) set(token string, entry statusEntry) {
    71  	s.Lock()
    72  	defer s.Unlock()
    73  
    74  	// If an entry associated with the proposal already exists in cache
    75  	// overwrite the proposal status.
    76  	if e, ok := s.data[token]; ok {
    77  		if e.propStatus == entry.propStatus {
    78  			// Entry exists, but has not changed. No
    79  			// need to overwrite the existing entry.
    80  			return
    81  		}
    82  		s.data[token] = &entry
    83  		log.Debugf("proposalStatuses: updated entry %v from %v to %v",
    84  			token, e.propStatus, entry.propStatus)
    85  		return
    86  	}
    87  
    88  	// If entry does not exist and cache is full, then remove oldest entry
    89  	if s.entries.Len() == statusesCacheLimit {
    90  		// Remove front - oldest entry from entries list.
    91  		t := s.entries.Remove(s.entries.Back()).(string)
    92  		// Remove oldest status from map.
    93  		delete(s.data, t)
    94  		log.Debugf("proposalStatuses: removed entry %v", t)
    95  	}
    96  
    97  	// Store new status.
    98  	s.entries.PushFront(token)
    99  	s.data[token] = &entry
   100  	log.Debugf("proposalStatuses: added entry %v with status %v",
   101  		token, entry.propStatus)
   102  }