github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/store/mysql/nonce.go (about)

     1  // Copyright (c) 2020-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 mysql
     6  
     7  import (
     8  	"context"
     9  	"database/sql"
    10  	"strconv"
    11  	"sync"
    12  
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  // nonce returns a new nonce value. This function guarantees that the returned
    17  // nonce will be unique for every invocation.
    18  //
    19  // This function must be called using a transaction.
    20  func (s *mysqlCtx) nonce(ctx context.Context, tx *sql.Tx) (int64, error) {
    21  	// Create and retrieve new nonce value in an atomic database
    22  	// transaction.
    23  	_, err := tx.ExecContext(ctx, "INSERT INTO nonce () VALUES ();")
    24  	if err != nil {
    25  		return 0, errors.WithStack(err)
    26  	}
    27  	rows, err := tx.QueryContext(ctx, "SELECT LAST_INSERT_ID();")
    28  	if err != nil {
    29  		return 0, errors.WithStack(err)
    30  	}
    31  	defer rows.Close()
    32  
    33  	var nonce int64
    34  	for rows.Next() {
    35  		if nonce > 0 {
    36  			// There should only ever be one row returned. Something is
    37  			// wrong if we've already scanned the nonce and its still
    38  			// scanning rows.
    39  			return 0, errors.Errorf("multiple rows returned for nonce")
    40  		}
    41  		err = rows.Scan(&nonce)
    42  		if err != nil {
    43  			return 0, errors.WithStack(err)
    44  		}
    45  	}
    46  	err = rows.Err()
    47  	if err != nil {
    48  		return 0, errors.WithStack(err)
    49  	}
    50  	if nonce == 0 {
    51  		return 0, errors.Errorf("invalid 0 nonce")
    52  	}
    53  
    54  	return nonce, nil
    55  }
    56  
    57  // testNonce is used to verify that nonce races do not occur. This function is
    58  // meant to be run against an actual MySQL/MariaDB instance, not as a unit
    59  // test.
    60  func (s *mysqlCtx) testNonce(ctx context.Context, tx *sql.Tx) error {
    61  	// Get nonce
    62  	nonce, err := s.nonce(ctx, tx)
    63  	if err != nil {
    64  		return err
    65  	}
    66  
    67  	// Save an empty blob to the kv store using the nonce as the key.
    68  	// If a nonce is reused it will cause an error since the key must
    69  	// be unique.
    70  	k := strconv.FormatInt(nonce, 10)
    71  	_, err = tx.ExecContext(ctx,
    72  		"INSERT INTO kv (k, v) VALUES (?, ?);", k, []byte{})
    73  	if err != nil {
    74  		return errors.WithStack(err)
    75  	}
    76  
    77  	return nil
    78  }
    79  
    80  // testNonceIsUnique verifies that nonce races do not occur. This function
    81  // is meant to be run against an actual MySQL/MariaDB instance, not as a unit
    82  // test.
    83  func (s *mysqlCtx) testNonceIsUnique() {
    84  	log.Infof("Starting nonce concurrency test")
    85  
    86  	// Run test
    87  	var (
    88  		wg      sync.WaitGroup
    89  		threads = 1000
    90  	)
    91  	for i := 0; i < threads; i++ {
    92  		// Increment the wait group counter
    93  		wg.Add(1)
    94  
    95  		go func() {
    96  			// Decrement wait group counter on exit
    97  			defer wg.Done()
    98  
    99  			ctx, cancel := ctxWithTimeout()
   100  			defer cancel()
   101  
   102  			// Start transaction
   103  			opts := &sql.TxOptions{
   104  				Isolation: sql.LevelDefault,
   105  			}
   106  			tx, err := s.db.BeginTx(ctx, opts)
   107  			if err != nil {
   108  				log.Errorf("begin tx: %v", err)
   109  				return
   110  			}
   111  
   112  			// Run nonce test
   113  			err = s.testNonce(ctx, tx)
   114  			if err != nil {
   115  				// Attempt to roll back the transaction
   116  				if err2 := tx.Rollback(); err2 != nil {
   117  					// We're in trouble!
   118  					log.Errorf("testNonce: %v, unable to rollback: %v", err, err2)
   119  					return
   120  				}
   121  				log.Errorf("testNonce: %v", err)
   122  				return
   123  			}
   124  
   125  			// Commit transaction
   126  			err = tx.Commit()
   127  			if err != nil {
   128  				log.Errorf("commit tx: %v", err)
   129  				return
   130  			}
   131  		}()
   132  	}
   133  
   134  	log.Infof("Waiting for nonce concurrency test to complete...")
   135  
   136  	// Wait for all tests to complete
   137  	wg.Wait()
   138  
   139  	log.Infof("Nonce concurrency test complete")
   140  }