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 }