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 }