github.com/decred/dcrlnd@v0.7.6/kvdb/etcd/db.go (about)

     1  //go:build kvdb_etcd
     2  // +build kvdb_etcd
     3  
     4  package etcd
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"runtime"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/btcsuite/btcwallet/walletdb"
    15  	"go.etcd.io/etcd/client/pkg/v3/transport"
    16  	clientv3 "go.etcd.io/etcd/client/v3"
    17  	"go.etcd.io/etcd/client/v3/namespace"
    18  )
    19  
    20  const (
    21  	// etcdConnectionTimeout is the timeout until successful connection to
    22  	// the etcd instance.
    23  	etcdConnectionTimeout = 10 * time.Second
    24  
    25  	// etcdLongTimeout is a timeout for longer taking etcd operatons.
    26  	etcdLongTimeout = 30 * time.Second
    27  
    28  	// etcdDefaultRootBucketId is used as the root bucket key. Note that
    29  	// the actual key is not visible, since all bucket keys are hashed.
    30  	etcdDefaultRootBucketId = "@"
    31  )
    32  
    33  // callerStats holds commit stats for a specific caller. Currently it only
    34  // holds the max stat, meaning that for a particular caller the largest
    35  // commit set is recorded.
    36  type callerStats struct {
    37  	count       int
    38  	commitStats CommitStats
    39  }
    40  
    41  func (s callerStats) String() string {
    42  	return fmt.Sprintf("count: %d, retries: %d, rset: %d, wset: %d",
    43  		s.count, s.commitStats.Retries, s.commitStats.Rset,
    44  		s.commitStats.Wset)
    45  }
    46  
    47  // commitStatsCollector collects commit stats for commits succeeding
    48  // and also for commits failing.
    49  type commitStatsCollector struct {
    50  	sync.RWMutex
    51  	succ map[string]*callerStats
    52  	fail map[string]*callerStats
    53  }
    54  
    55  // newCommitStatsColletor creates a new commitStatsCollector instance.
    56  func newCommitStatsColletor() *commitStatsCollector {
    57  	return &commitStatsCollector{
    58  		succ: make(map[string]*callerStats),
    59  		fail: make(map[string]*callerStats),
    60  	}
    61  }
    62  
    63  // PrintStats returns collected stats pretty printed into a string.
    64  func (c *commitStatsCollector) PrintStats() string {
    65  	c.RLock()
    66  	defer c.RUnlock()
    67  
    68  	s := "\nFailure:\n"
    69  	for k, v := range c.fail {
    70  		s += fmt.Sprintf("%s\t%s\n", k, v)
    71  	}
    72  
    73  	s += "\nSuccess:\n"
    74  	for k, v := range c.succ {
    75  		s += fmt.Sprintf("%s\t%s\n", k, v)
    76  	}
    77  
    78  	return s
    79  }
    80  
    81  // updateStatsMap updatess commit stats map for a caller.
    82  func updateStatMap(
    83  	caller string, stats CommitStats, m map[string]*callerStats) {
    84  
    85  	if _, ok := m[caller]; !ok {
    86  		m[caller] = &callerStats{}
    87  	}
    88  
    89  	curr := m[caller]
    90  	curr.count++
    91  
    92  	// Update only if the total commit set is greater or equal.
    93  	currTotal := curr.commitStats.Rset + curr.commitStats.Wset
    94  	if currTotal <= (stats.Rset + stats.Wset) {
    95  		curr.commitStats = stats
    96  	}
    97  }
    98  
    99  // callback is an STM commit stats callback passed which can be passed
   100  // using a WithCommitStatsCallback to the STM upon construction.
   101  func (c *commitStatsCollector) callback(succ bool, stats CommitStats) {
   102  	caller := "unknown"
   103  
   104  	// Get the caller. As this callback is called from
   105  	// the backend interface that means we need to ascend
   106  	// 4 frames in the callstack.
   107  	_, file, no, ok := runtime.Caller(4)
   108  	if ok {
   109  		caller = fmt.Sprintf("%s#%d", file, no)
   110  	}
   111  
   112  	c.Lock()
   113  	defer c.Unlock()
   114  
   115  	if succ {
   116  		updateStatMap(caller, stats, c.succ)
   117  	} else {
   118  		updateStatMap(caller, stats, c.fail)
   119  	}
   120  }
   121  
   122  // db holds a reference to the etcd client connection.
   123  type db struct {
   124  	cfg                  Config
   125  	ctx                  context.Context
   126  	cancel               func()
   127  	cli                  *clientv3.Client
   128  	commitStatsCollector *commitStatsCollector
   129  	txQueue              *commitQueue
   130  	txMutex              sync.RWMutex
   131  }
   132  
   133  // Enforce db implements the walletdb.DB interface.
   134  var _ walletdb.DB = (*db)(nil)
   135  
   136  // newEtcdBackend returns a db object initialized with the passed backend
   137  // config. If etcd connection cannot be established, then returns error.
   138  func newEtcdBackend(ctx context.Context, cfg Config) (*db, error) {
   139  	clientCfg := clientv3.Config{
   140  		Endpoints:          []string{cfg.Host},
   141  		DialTimeout:        etcdConnectionTimeout,
   142  		Username:           cfg.User,
   143  		Password:           cfg.Pass,
   144  		MaxCallSendMsgSize: cfg.MaxMsgSize,
   145  	}
   146  
   147  	if !cfg.DisableTLS {
   148  		tlsInfo := transport.TLSInfo{
   149  			CertFile:           cfg.CertFile,
   150  			KeyFile:            cfg.KeyFile,
   151  			InsecureSkipVerify: cfg.InsecureSkipVerify,
   152  		}
   153  
   154  		tlsConfig, err := tlsInfo.ClientConfig()
   155  		if err != nil {
   156  			return nil, err
   157  		}
   158  
   159  		clientCfg.TLS = tlsConfig
   160  	}
   161  
   162  	ctx, cancel := context.WithCancel(ctx)
   163  	clientCfg.Context = ctx
   164  	cli, err := clientv3.New(clientCfg)
   165  	if err != nil {
   166  		cancel()
   167  		return nil, err
   168  	}
   169  
   170  	// Apply the namespace.
   171  	cli.KV = namespace.NewKV(cli.KV, cfg.Namespace)
   172  	cli.Watcher = namespace.NewWatcher(cli.Watcher, cfg.Namespace)
   173  	cli.Lease = namespace.NewLease(cli.Lease, cfg.Namespace)
   174  
   175  	backend := &db{
   176  		cfg:     cfg,
   177  		ctx:     ctx,
   178  		cancel:  cancel,
   179  		cli:     cli,
   180  		txQueue: NewCommitQueue(ctx),
   181  	}
   182  
   183  	if cfg.CollectStats {
   184  		backend.commitStatsCollector = newCommitStatsColletor()
   185  	}
   186  
   187  	return backend, nil
   188  }
   189  
   190  // getSTMOptions creates all STM options based on the backend config.
   191  func (db *db) getSTMOptions() []STMOptionFunc {
   192  	opts := []STMOptionFunc{
   193  		WithAbortContext(db.ctx),
   194  	}
   195  
   196  	if db.cfg.CollectStats {
   197  		opts = append(opts,
   198  			WithCommitStatsCallback(db.commitStatsCollector.callback),
   199  		)
   200  	}
   201  
   202  	return opts
   203  }
   204  
   205  // View opens a database read transaction and executes the function f with the
   206  // transaction passed as a parameter. After f exits, the transaction is rolled
   207  // back. If f errors, its error is returned, not a rollback error (if any
   208  // occur). The passed reset function is called before the start of the
   209  // transaction and can be used to reset intermediate state. As callers may
   210  // expect retries of the f closure (depending on the database backend used), the
   211  // reset function will be called before each retry respectively.
   212  func (db *db) View(f func(tx walletdb.ReadTx) error, reset func()) error {
   213  	if db.cfg.SingleWriter {
   214  		db.txMutex.RLock()
   215  		defer db.txMutex.RUnlock()
   216  	}
   217  
   218  	apply := func(stm STM) error {
   219  		reset()
   220  		return f(newReadWriteTx(stm, etcdDefaultRootBucketId, nil))
   221  	}
   222  
   223  	_, err := RunSTM(db.cli, apply, db.txQueue, db.getSTMOptions()...)
   224  	return err
   225  }
   226  
   227  // Update opens a database read/write transaction and executes the function f
   228  // with the transaction passed as a parameter. After f exits, if f did not
   229  // error, the transaction is committed. Otherwise, if f did error, the
   230  // transaction is rolled back. If the rollback fails, the original error
   231  // returned by f is still returned. If the commit fails, the commit error is
   232  // returned. As callers may expect retries of the f closure, the reset function
   233  // will be called before each retry respectively.
   234  func (db *db) Update(f func(tx walletdb.ReadWriteTx) error, reset func()) error {
   235  	if db.cfg.SingleWriter {
   236  		db.txMutex.Lock()
   237  		defer db.txMutex.Unlock()
   238  	}
   239  
   240  	apply := func(stm STM) error {
   241  		reset()
   242  		return f(newReadWriteTx(stm, etcdDefaultRootBucketId, nil))
   243  	}
   244  
   245  	_, err := RunSTM(db.cli, apply, db.txQueue, db.getSTMOptions()...)
   246  	return err
   247  }
   248  
   249  // PrintStats returns all collected stats pretty printed into a string.
   250  func (db *db) PrintStats() string {
   251  	if db.commitStatsCollector != nil {
   252  		return db.commitStatsCollector.PrintStats()
   253  	}
   254  
   255  	return ""
   256  }
   257  
   258  // BeginReadWriteTx opens a database read+write transaction.
   259  func (db *db) BeginReadWriteTx() (walletdb.ReadWriteTx, error) {
   260  	var locker sync.Locker
   261  	if db.cfg.SingleWriter {
   262  		db.txMutex.Lock()
   263  		locker = &db.txMutex
   264  	}
   265  
   266  	return newReadWriteTx(
   267  		NewSTM(db.cli, db.txQueue, db.getSTMOptions()...),
   268  		etcdDefaultRootBucketId, locker,
   269  	), nil
   270  }
   271  
   272  // BeginReadTx opens a database read transaction.
   273  func (db *db) BeginReadTx() (walletdb.ReadTx, error) {
   274  	var locker sync.Locker
   275  	if db.cfg.SingleWriter {
   276  		db.txMutex.RLock()
   277  		locker = db.txMutex.RLocker()
   278  	}
   279  
   280  	return newReadWriteTx(
   281  		NewSTM(db.cli, db.txQueue, db.getSTMOptions()...),
   282  		etcdDefaultRootBucketId, locker,
   283  	), nil
   284  }
   285  
   286  // Copy writes a copy of the database to the provided writer.  This call will
   287  // start a read-only transaction to perform all operations.
   288  // This function is part of the walletdb.Db interface implementation.
   289  func (db *db) Copy(w io.Writer) error {
   290  	ctx, cancel := context.WithTimeout(db.ctx, etcdLongTimeout)
   291  	defer cancel()
   292  
   293  	readCloser, err := db.cli.Snapshot(ctx)
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	_, err = io.Copy(w, readCloser)
   299  
   300  	return err
   301  }
   302  
   303  // Close cleanly shuts down the database and syncs all data.
   304  // This function is part of the walletdb.Db interface implementation.
   305  func (db *db) Close() error {
   306  	err := db.cli.Close()
   307  	db.cancel()
   308  	db.txQueue.Stop()
   309  	return err
   310  }