github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/comments/timestamp.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 comments
     6  
     7  import (
     8  	"encoding/json"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/decred/politeia/politeiad/plugins/comments"
    13  	"github.com/decred/politeia/util"
    14  	"github.com/pkg/errors"
    15  )
    16  
    17  const (
    18  	// timestampKey is the key for a timestamp entry in the key-value store
    19  	// cache.
    20  	timestampKey = "timestamp-{shorttoken}-{commentID}"
    21  )
    22  
    23  // cacheFinalTimestamps accepts a map of comment timestamps, it collects the
    24  // final timestamps then stores them in the key-value store.
    25  func (p *commentsPlugin) cacheFinalTimestamps(token []byte, cts map[uint32]comments.CommentTimestamp) error {
    26  	finalTimestamps, err := finalCommentTimestamps(cts, token)
    27  	if err != nil {
    28  		return err
    29  	}
    30  
    31  	err = p.saveTimestamps(token, finalTimestamps)
    32  	if err != nil {
    33  		return err
    34  	}
    35  
    36  	log.Debugf("Cached final comment timestamps of %v/%v",
    37  		len(finalTimestamps), len(cts))
    38  	return nil
    39  }
    40  
    41  // finalCommentTimestamps accepts a map of comment timestamps, and it returns
    42  // a new map with all final comment timestamps. A timestamp considered final
    43  // if it was successfully timestamped on the DCR chain, meaning it's merkle
    44  // root was included in a confirmed DCR transaction.
    45  func finalCommentTimestamps(ts map[uint32]comments.CommentTimestamp, token []byte) (map[uint32]comments.CommentTimestamp, error) {
    46  	fts := make(map[uint32]comments.CommentTimestamp, len(ts))
    47  	for cid, t := range ts {
    48  		// Search for final comment add timestamps
    49  		for _, at := range t.Adds {
    50  			if timestampIsFinal(at) {
    51  				// Add final comment add to the final timestamps map.
    52  				ct, exists := fts[cid]
    53  				if !exists {
    54  					ct = comments.CommentTimestamp{}
    55  				}
    56  				if len(ct.Adds) == 0 {
    57  					ct.Adds = make([]comments.Timestamp, 0, len(t.Adds))
    58  				}
    59  				ct.Adds = append(ct.Adds, at)
    60  				fts[cid] = ct
    61  			}
    62  		}
    63  
    64  		// Search for final comment del timestamp
    65  		if t.Del != nil && timestampIsFinal(*t.Del) {
    66  			// Add final comment del to final timestamps map.
    67  			ct, exists := fts[cid]
    68  			if !exists {
    69  				ct = comments.CommentTimestamp{}
    70  			}
    71  			ct.Del = t.Del
    72  			fts[cid] = ct
    73  		}
    74  
    75  		// Search for final comment vote timestamps
    76  		for _, vt := range t.Votes {
    77  			if timestampIsFinal(vt) {
    78  				// Add final comment add to the final timestamps map.
    79  				ct, exists := fts[cid]
    80  				if !exists {
    81  					ct = comments.CommentTimestamp{}
    82  				}
    83  				if len(ct.Votes) == 0 {
    84  					ct.Votes = make([]comments.Timestamp, 0, len(t.Votes))
    85  				}
    86  				ct.Votes = append(ct.Votes, vt)
    87  				fts[cid] = ct
    88  			}
    89  		}
    90  	}
    91  
    92  	return fts, nil
    93  }
    94  
    95  // saveTimestamps saves a list of timestamps to the key-value cache.
    96  func (p *commentsPlugin) saveTimestamps(token []byte, ts map[uint32]comments.CommentTimestamp) error {
    97  	// Setup the blob entries
    98  	blobs := make(map[string][]byte, len(ts))
    99  	keys := make([]string, 0, len(ts))
   100  	for cid, v := range ts {
   101  		k, err := getTimestampKey(token, cid)
   102  		if err != nil {
   103  			return err
   104  		}
   105  		b, err := json.Marshal(v)
   106  		if err != nil {
   107  			return err
   108  		}
   109  		blobs[k] = b
   110  		keys = append(keys, k)
   111  	}
   112  
   113  	// Delete exisiting digests
   114  	err := p.tstore.CacheDel(keys)
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	// Save the blob entries
   120  	return p.tstore.CachePut(blobs, false)
   121  }
   122  
   123  // cachedTimestamps returns cached comment timestamps if they exist. An entry
   124  // will not exist in the returned map if a timestamp was not found in the cache
   125  // for a comment ID.
   126  func (p *commentsPlugin) cachedTimestamps(token []byte, commentIDs []uint32) (map[uint32]*comments.CommentTimestamp, error) {
   127  	// Setup the timestamp keys
   128  	keys := make([]string, 0, len(commentIDs))
   129  	for _, cid := range commentIDs {
   130  		k, err := getTimestampKey(token, cid)
   131  		if err != nil {
   132  			return nil, err
   133  		}
   134  		keys = append(keys, k)
   135  	}
   136  
   137  	// Get the timestamp blob entries
   138  	blobs, err := p.tstore.CacheGet(keys)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	// Decode the timestamps
   144  	ts := make(map[uint32]*comments.CommentTimestamp, len(blobs))
   145  	cacheIDs := make([]uint32, 0, len(blobs))
   146  	for k, v := range blobs {
   147  		var t comments.CommentTimestamp
   148  		err := json.Unmarshal(v, &t)
   149  		if err != nil {
   150  			return nil, err
   151  		}
   152  		cid, err := parseTimestampKey(k)
   153  		if err != nil {
   154  			return nil, err
   155  		}
   156  		ts[cid] = &t
   157  		cacheIDs = append(cacheIDs, cid)
   158  	}
   159  
   160  	log.Debugf("Retrieved cached final comment timestamps of %v/%v",
   161  		len(cacheIDs), len(commentIDs))
   162  	return ts, nil
   163  }
   164  
   165  // getTimestampKey returns the key for a timestamp in the key-value store
   166  // cache.
   167  func getTimestampKey(token []byte, commentID uint32) (string, error) {
   168  	t, err := util.ShortTokenEncode(token)
   169  	if err != nil {
   170  		return "", err
   171  	}
   172  	cid := strconv.FormatUint(uint64(commentID), 10)
   173  	key := strings.Replace(timestampKey, "{shorttoken}", t, 1)
   174  	key = strings.Replace(key, "{commentID}", cid, 1)
   175  	return key, nil
   176  }
   177  
   178  // parseTimestampKey parses the comment ID from a timestamp key.
   179  func parseTimestampKey(key string) (uint32, error) {
   180  	s := strings.Split(key, "-")
   181  	if len(s) != 3 {
   182  		return 0, errors.Errorf("invalid comment timestamp key")
   183  	}
   184  	cid, err := strconv.ParseUint(s[2], 10, 64)
   185  	if err != nil {
   186  		return 0, err
   187  	}
   188  	return uint32(cid), nil
   189  }
   190  
   191  // timestampIsFinal returns whether the timestamp is considered to be final and
   192  // will not change in the future. Once the TxID is present then the timestamp
   193  // is considered to be final since it has been included in a DCR transaction.
   194  func timestampIsFinal(t comments.Timestamp) bool {
   195  	return t.TxID != ""
   196  }