Merge pull request #5547 from bhandras/etcd_backend_rwlock

etcd: optionally reduce concurrency to single writer for legacy code
This commit is contained in:
Oliver Gugger 2021-07-21 16:28:20 +02:00 committed by GitHub
commit fb17bf2bb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 76 additions and 17 deletions

@ -18,6 +18,12 @@ the release notes folder that at leasts links to PR being added.
in place](https://github.com/lightningnetwork/lnd/pull/5545). in place](https://github.com/lightningnetwork/lnd/pull/5545).
* [Added minor fixes to contribution guidelines](https://github.com/lightningnetwork/lnd/pull/5503). * [Added minor fixes to contribution guidelines](https://github.com/lightningnetwork/lnd/pull/5503).
## Database
* [Ensure single writer for legacy
code](https://github.com/lightningnetwork/lnd/pull/5547) when using etcd
backend.
# Contributors (Alphabetical Order) # Contributors (Alphabetical Order)
* ErikEk * ErikEk
* Zero-1729 * Zero-1729

@ -25,4 +25,8 @@ type Config struct {
InsecureSkipVerify bool `long:"insecure_skip_verify" description:"Whether we intend to skip TLS verification"` InsecureSkipVerify bool `long:"insecure_skip_verify" description:"Whether we intend to skip TLS verification"`
CollectStats bool `long:"collect_stats" description:"Whether to collect etcd commit stats."` CollectStats bool `long:"collect_stats" description:"Whether to collect etcd commit stats."`
// SingleWriter should be set to true if we intend to only allow a
// single writer to the database at a time.
SingleWriter bool
} }

@ -125,6 +125,7 @@ type db struct {
cli *clientv3.Client cli *clientv3.Client
commitStatsCollector *commitStatsCollector commitStatsCollector *commitStatsCollector
txQueue *commitQueue txQueue *commitQueue
txMutex sync.RWMutex
} }
// Enforce db implements the walletdb.DB interface. // Enforce db implements the walletdb.DB interface.
@ -204,9 +205,14 @@ func (db *db) getSTMOptions() []STMOptionFunc {
// expect retries of the f closure (depending on the database backend used), the // expect retries of the f closure (depending on the database backend used), the
// reset function will be called before each retry respectively. // reset function will be called before each retry respectively.
func (db *db) View(f func(tx walletdb.ReadTx) error, reset func()) error { func (db *db) View(f func(tx walletdb.ReadTx) error, reset func()) error {
if db.cfg.SingleWriter {
db.txMutex.RLock()
defer db.txMutex.RUnlock()
}
apply := func(stm STM) error { apply := func(stm STM) error {
reset() reset()
return f(newReadWriteTx(stm, etcdDefaultRootBucketId)) return f(newReadWriteTx(stm, etcdDefaultRootBucketId, nil))
} }
return RunSTM(db.cli, apply, db.txQueue, db.getSTMOptions()...) return RunSTM(db.cli, apply, db.txQueue, db.getSTMOptions()...)
@ -220,9 +226,14 @@ func (db *db) View(f func(tx walletdb.ReadTx) error, reset func()) error {
// returned. As callers may expect retries of the f closure, the reset function // returned. As callers may expect retries of the f closure, the reset function
// will be called before each retry respectively. // will be called before each retry respectively.
func (db *db) Update(f func(tx walletdb.ReadWriteTx) error, reset func()) error { func (db *db) Update(f func(tx walletdb.ReadWriteTx) error, reset func()) error {
if db.cfg.SingleWriter {
db.txMutex.Lock()
defer db.txMutex.Unlock()
}
apply := func(stm STM) error { apply := func(stm STM) error {
reset() reset()
return f(newReadWriteTx(stm, etcdDefaultRootBucketId)) return f(newReadWriteTx(stm, etcdDefaultRootBucketId, nil))
} }
return RunSTM(db.cli, apply, db.txQueue, db.getSTMOptions()...) return RunSTM(db.cli, apply, db.txQueue, db.getSTMOptions()...)
@ -239,17 +250,29 @@ func (db *db) PrintStats() string {
// BeginReadWriteTx opens a database read+write transaction. // BeginReadWriteTx opens a database read+write transaction.
func (db *db) BeginReadWriteTx() (walletdb.ReadWriteTx, error) { func (db *db) BeginReadWriteTx() (walletdb.ReadWriteTx, error) {
var locker sync.Locker
if db.cfg.SingleWriter {
db.txMutex.Lock()
locker = &db.txMutex
}
return newReadWriteTx( return newReadWriteTx(
NewSTM(db.cli, db.txQueue, db.getSTMOptions()...), NewSTM(db.cli, db.txQueue, db.getSTMOptions()...),
etcdDefaultRootBucketId, etcdDefaultRootBucketId, locker,
), nil ), nil
} }
// BeginReadTx opens a database read transaction. // BeginReadTx opens a database read transaction.
func (db *db) BeginReadTx() (walletdb.ReadTx, error) { func (db *db) BeginReadTx() (walletdb.ReadTx, error) {
var locker sync.Locker
if db.cfg.SingleWriter {
db.txMutex.RLock()
locker = db.txMutex.RLocker()
}
return newReadWriteTx( return newReadWriteTx(
NewSTM(db.cli, db.txQueue, db.getSTMOptions()...), NewSTM(db.cli, db.txQueue, db.getSTMOptions()...),
etcdDefaultRootBucketId, etcdDefaultRootBucketId, locker,
), nil ), nil
} }

@ -78,8 +78,13 @@ func NewEtcdTestFixture(t *testing.T) *EtcdTestFixture {
} }
} }
func (f *EtcdTestFixture) NewBackend() walletdb.DB { func (f *EtcdTestFixture) NewBackend(singleWriter bool) walletdb.DB {
db, err := newEtcdBackend(context.TODO(), f.BackendConfig()) cfg := f.BackendConfig()
if singleWriter {
cfg.SingleWriter = true
}
db, err := newEtcdBackend(context.TODO(), cfg)
require.NoError(f.t, err) require.NoError(f.t, err)
return db return db

@ -3,6 +3,8 @@
package etcd package etcd
import ( import (
"sync"
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
) )
@ -17,13 +19,18 @@ type readWriteTx struct {
// active is true if the transaction hasn't been committed yet. // active is true if the transaction hasn't been committed yet.
active bool active bool
// lock is passed on for manual txns when the backend is instantiated
// such that we read/write lock transactions to ensure a single writer.
lock sync.Locker
} }
// newReadWriteTx creates an rw transaction with the passed STM. // newReadWriteTx creates an rw transaction with the passed STM.
func newReadWriteTx(stm STM, prefix string) *readWriteTx { func newReadWriteTx(stm STM, prefix string, lock sync.Locker) *readWriteTx {
return &readWriteTx{ return &readWriteTx{
stm: stm, stm: stm,
active: true, active: true,
lock: lock,
rootBucketID: makeBucketID([]byte(prefix)), rootBucketID: makeBucketID([]byte(prefix)),
} }
} }
@ -65,6 +72,10 @@ func (tx *readWriteTx) Rollback() error {
return walletdb.ErrTxClosed return walletdb.ErrTxClosed
} }
if tx.lock != nil {
defer tx.lock.Unlock()
}
// Rollback the STM and set the tx to inactive. // Rollback the STM and set the tx to inactive.
tx.stm.Rollback() tx.stm.Rollback()
tx.active = false tx.active = false
@ -99,6 +110,10 @@ func (tx *readWriteTx) Commit() error {
return walletdb.ErrTxClosed return walletdb.ErrTxClosed
} }
if tx.lock != nil {
defer tx.lock.Unlock()
}
// Try committing the transaction. // Try committing the transaction.
if err := tx.stm.Commit(); err != nil { if err := tx.stm.Commit(); err != nil {
return err return err

@ -3,6 +3,7 @@
package kvdb package kvdb
import ( import (
"fmt"
"testing" "testing"
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
@ -143,18 +144,23 @@ func TestEtcd(t *testing.T) {
continue continue
} }
t.Run(test.name, func(t *testing.T) { rwLock := []bool{false, true}
t.Parallel() for _, doRwLock := range rwLock {
name := fmt.Sprintf("%v/RWLock=%v", test.name, doRwLock)
f := etcd.NewEtcdTestFixture(t) t.Run(name, func(t *testing.T) {
defer f.Cleanup() t.Parallel()
test.test(t, f.NewBackend()) f := etcd.NewEtcdTestFixture(t)
defer f.Cleanup()
if test.expectedDb != nil { test.test(t, f.NewBackend(doRwLock))
dump := f.Dump()
require.Equal(t, test.expectedDb, dump) if test.expectedDb != nil {
} dump := f.Dump()
}) require.Equal(t, test.expectedDb, dump)
}
})
}
} }
} }