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

     1  // Copyright (c) 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 ticketvote
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/plugins"
    14  	"github.com/decred/politeia/util"
    15  )
    16  
    17  // subs contains the list of runoff vote submissions for a parent record. Every
    18  // parent record with submissions will have a subs entry in the plugin cache.
    19  //
    20  // When a parent record hosts a runoff vote, child records can declare their
    21  // intent to participate in the runoff vote using the VoteMetadata LinkTo
    22  // field. The cached submissions list for the parent record is updated when the
    23  // child record is made public.
    24  //
    25  // The submissions list will only contain public records. If a public record
    26  // is part of the submissions list and then is updated to a non-public status,
    27  // it's removed from the submissions list.
    28  //
    29  // See the subsClient structure for more information on how caching is handled.
    30  type subs struct {
    31  	Tokens map[string]struct{} `json:"tokens"`
    32  }
    33  
    34  // newSubs returns a new subs.
    35  func newSubs() *subs {
    36  	return &subs{
    37  		Tokens: make(map[string]struct{}),
    38  	}
    39  }
    40  
    41  // Add adds a record token to the submissions list.
    42  func (s *subs) Add(token string) {
    43  	s.Tokens[token] = struct{}{}
    44  }
    45  
    46  // Del deletes a record token from the submissions list.
    47  func (s *subs) Del(token string) {
    48  	delete(s.Tokens, token)
    49  }
    50  
    51  // subsClient provides an API for interacting with the runoff vote submissions
    52  // cache. The data is savedd to the TstoreClient provided plugin cache.
    53  //
    54  // A mutex is required because tstore does not provide plugins with a sql
    55  // transaction that can be used to execute multiple database requests
    56  // atomically. Concurrent access to the subs cache during updates must be
    57  // control locally using a mutex for now.
    58  type subsClient struct {
    59  	sync.Mutex
    60  	tstore plugins.TstoreClient
    61  }
    62  
    63  // newSubsClient returns a new subsClient.
    64  func newSubsClient(tstore plugins.TstoreClient) *subsClient {
    65  	return &subsClient{
    66  		tstore: tstore,
    67  	}
    68  }
    69  
    70  // Add adds a runoff vote submission to the cached submissions list for the
    71  // parent record.
    72  //
    73  // Plugin writes are not currently executed using a sql transaction, which
    74  // means that there is no way to unwind previous writes if this cache update
    75  // fails. For this reason, we panic instead of returning an error so that the
    76  // sysadmin is alerted that the cache is incoherent and needs to be rebuilt.
    77  //
    78  // This function is concurrency safe.
    79  func (c *subsClient) Add(parent, sub string) error {
    80  	c.Lock()
    81  	defer c.Unlock()
    82  
    83  	err := c.add(parent, sub)
    84  	if err != nil {
    85  		e := fmt.Sprintf("%v %v: %v", parent, sub, err)
    86  		panic(e)
    87  	}
    88  
    89  	log.Debugf("Sub %v added to runoff vote subs list %v", sub, parent)
    90  
    91  	return nil
    92  }
    93  
    94  // Del deletes a runoff vote submission from the cached submissions list for
    95  // the parent record.
    96  //
    97  // Plugin writes are not currently executed using a sql transaction, which
    98  // means that there is no way to unwind previous writes if this cache update
    99  // fails. For this reason, we panic instead of returning an error so that the
   100  // sysadmin is alerted that the cache is incoherent and needs to be rebuilt.
   101  //
   102  // This function is concurrency safe.
   103  func (c *subsClient) Del(parent, sub string) error {
   104  	c.Lock()
   105  	defer c.Unlock()
   106  
   107  	err := c.del(parent, sub)
   108  	if err != nil {
   109  		e := fmt.Sprintf("%v %v: %v", parent, sub, err)
   110  		panic(e)
   111  	}
   112  
   113  	log.Debugf("Sub %v deleted from runoff vote subs list %v", sub, parent)
   114  
   115  	return nil
   116  }
   117  
   118  // DelEntry deletes a runoff vote submissions list from the cache. The full
   119  // cache entry is deleted.
   120  //
   121  // This function is concurrency safe.
   122  func (c *subsClient) DelEntry(parent string) error {
   123  	c.Lock()
   124  	defer c.Unlock()
   125  
   126  	key, err := buildSubsKey(parent)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	err = c.tstore.CacheDel([]string{key})
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	log.Debugf("Vote subs cache entry deleted %v", parent)
   136  
   137  	return nil
   138  }
   139  
   140  // Get retrieves a runoff vote submissions list from the cache.
   141  //
   142  // A new subs is returned if a cache entry is not found for the record.
   143  //
   144  // This function is concurrency safe.
   145  func (c *subsClient) Get(parent string) (*subs, error) {
   146  	key, err := buildSubsKey(parent)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	entries, err := c.tstore.CacheGet([]string{key})
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  	b, ok := entries[key]
   155  	if !ok {
   156  		return newSubs(), nil
   157  	}
   158  	var s subs
   159  	err = json.Unmarshal(b, &s)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  	return &s, nil
   164  }
   165  
   166  // add adds a runoff vote submission to the cached submissions list for the
   167  // parent record.
   168  //
   169  // This function is not concurrency safe. It must be called with the mutex
   170  // locked.
   171  func (c *subsClient) add(parent, sub string) error {
   172  	s, err := c.Get(parent)
   173  	if err != nil {
   174  		return err
   175  	}
   176  	s.Add(sub)
   177  	return c.save(parent, *s)
   178  }
   179  
   180  // del deletes a runoff vote submission from the cached submissions list for
   181  // the parent record.
   182  //
   183  // This function is not concurrency safe. It must be called with the mutex
   184  // locked.
   185  func (c *subsClient) del(parent, sub string) error {
   186  	s, err := c.Get(parent)
   187  	if err != nil {
   188  		return err
   189  	}
   190  	s.Del(sub)
   191  	return c.save(parent, *s)
   192  }
   193  
   194  // save saves a sub to the tstore provided plugin cache.
   195  func (c *subsClient) save(parent string, s subs) error {
   196  	key, err := buildSubsKey(parent)
   197  	if err != nil {
   198  		return err
   199  	}
   200  	b, err := json.Marshal(s)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	return c.tstore.CachePut(map[string][]byte{key: b}, false)
   205  }
   206  
   207  const (
   208  	// subsKey is the key-value store key for an entry in the submissions cache.
   209  	subsKey = "subs-{shorttoken}"
   210  )
   211  
   212  // buildSubsKey returns the submissions cache key for a record.
   213  //
   214  // The short token is used in the file path so that the submissions list can be
   215  // retrieved using either the full token or the short token.
   216  func buildSubsKey(parent string) (string, error) {
   217  	t, err := util.ShortTokenString(parent)
   218  	if err != nil {
   219  		return "", err
   220  	}
   221  	return strings.Replace(subsKey, "{shorttoken}", t, 1), nil
   222  }