diff --git a/go.sum b/go.sum index 2946d39..3f267ec 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,7 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/walletdb/bdb/db.go b/walletdb/bdb/db.go index 9d700bf..cf3fc2e 100644 --- a/walletdb/bdb/db.go +++ b/walletdb/bdb/db.go @@ -342,6 +342,19 @@ func (db *db) Close() error { return convertErr((*bbolt.DB)(db).Close()) } +// Batch is similar to the package-level Update method, but it will attempt to +// optismitcally combine the invocation of several transaction functions into a +// single db write transaction. +// +// This function is part of the walletdb.Db interface implementation. +func (db *db) Batch(f func(tx walletdb.ReadWriteTx) error) error { + return (*bbolt.DB)(db).Batch(func(btx *bbolt.Tx) error { + interfaceTx := transaction{btx} + + return f(&interfaceTx) + }) +} + // filesExists reports whether the named file or directory exists. func fileExists(name string) bool { if _, err := os.Stat(name); err != nil { diff --git a/walletdb/go.mod b/walletdb/go.mod index 5daf526..59f6db3 100644 --- a/walletdb/go.mod +++ b/walletdb/go.mod @@ -7,5 +7,6 @@ require ( github.com/coreos/bbolt v1.3.3 github.com/davecgh/go-spew v1.1.1 go.etcd.io/bbolt v1.3.3 // indirect + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e golang.org/x/sys v0.0.0-20190904154756-749cb33beabd // indirect ) diff --git a/walletdb/go.sum b/walletdb/go.sum index e68003e..ca356ac 100644 --- a/walletdb/go.sum +++ b/walletdb/go.sum @@ -6,5 +6,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/walletdb/interface.go b/walletdb/interface.go index c6c1f87..40fb19e 100644 --- a/walletdb/interface.go +++ b/walletdb/interface.go @@ -7,7 +7,10 @@ package walletdb -import "io" +import ( + "fmt" + "io" +) // ReadTx represents a database transaction that can only be used for reads. If // a database update must occur, use a ReadWriteTx. @@ -201,6 +204,18 @@ type DB interface { Close() error } +// BatchDB is a special version of the main DB interface that allos the caller +// to specify write transactions that should be combine dtoegether if multiple +// goroutines are calling the Batch method. +type BatchDB interface { + DB + + // Batch is similar to the package-level Update method, but it will + // attempt to optismitcally combine the invocation of several + // transaction functions into a single db write transaction. + Batch(func(tx ReadWriteTx) error) error +} + // View opens a database read transaction and executes the function f with the // transaction passed as a parameter. After f exits, the transaction is rolled // back. If f errors, its error is returned, not a rollback error (if any @@ -260,6 +275,23 @@ func Update(db DB, f func(tx ReadWriteTx) error) error { return tx.Commit() } +// Batch opens a database read/write transaction and executes the function f +// with the transaction passed as a parameter. After f exits, if f did not +// error, the transaction is committed. Otherwise, if f did error, the +// transaction is rolled back. If the rollback fails, the original error +// returned by f is still returned. If the commit fails, the commit error is +// returned. +// +// Batch is only useful when there are multiple goroutines calling it. +func Batch(db DB, f func(tx ReadWriteTx) error) error { + batchDB, ok := db.(BatchDB) + if !ok { + return fmt.Errorf("need batch") + } + + return batchDB.Batch(f) +} + // Driver defines a structure for backend drivers to use when they registered // themselves as a backend which implements the Db interface. type Driver struct { diff --git a/walletdb/walletdbtest/interface.go b/walletdb/walletdbtest/interface.go index 663a0fa..fe338a7 100644 --- a/walletdb/walletdbtest/interface.go +++ b/walletdb/walletdbtest/interface.go @@ -5,9 +5,11 @@ package walletdbtest import ( + "bytes" "fmt" "os" "reflect" + "sync" "github.com/btcsuite/btcwallet/walletdb" ) @@ -146,7 +148,7 @@ func testSequence(tc *testContext, testBucket walletdb.ReadWriteBucket) bool { return false } - return false + return true } // testReadWriteBucketInterface ensures the bucket interface is working properly by @@ -726,6 +728,70 @@ func testAdditionalErrors(tc *testContext) bool { return true } +// testBatchInterface tests that if the target database implements the batch +// method, then the method functions as expected. +func testBatchInterface(tc *testContext) bool { + // If the database doesn't support the batch super-set of the + // interface, then we're done here. + batchDB, ok := tc.db.(walletdb.BatchDB) + if !ok { + return true + } + + const numGoroutines = 5 + errChan := make(chan error, numGoroutines) + + var wg sync.WaitGroup + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + err := walletdb.Batch(batchDB, func(tx walletdb.ReadWriteTx) error { + b, err := tx.CreateTopLevelBucket([]byte("test")) + if err != nil { + return err + } + + byteI := []byte{byte(i)} + return b.Put(byteI, byteI) + }) + errChan <- err + }(i) + } + + wg.Wait() + close(errChan) + + for err := range errChan { + if err != nil { + tc.t.Errorf("Batch: unexpected error: %v", err) + return false + } + } + + err := walletdb.View(batchDB, func(tx walletdb.ReadTx) error { + b := tx.ReadBucket([]byte("test")) + + for i := 0; i < numGoroutines; i++ { + byteI := []byte{byte(i)} + if v := b.Get(byteI); v == nil { + return fmt.Errorf("key %v not present", byteI) + } else if !bytes.Equal(v, byteI) { + return fmt.Errorf("key %v not equal to value: "+ + "%v", byteI, v) + } + } + + return nil + }) + if err != nil { + tc.t.Errorf("Batch: unexpected error: %v", err) + return false + } + + return true +} + // TestInterface performs all interfaces tests for this database driver. func TestInterface(t Tester, dbType, dbPath string) { db, err := walletdb.Create(dbType, dbPath, true) @@ -754,4 +820,9 @@ func TestInterface(t Tester, dbType, dbPath string) { if !testAdditionalErrors(&context) { return } + + // If applicable, also test the behavior of the Batch call. + if !testBatchInterface(&context) { + return + } }