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 }