diff --git a/blockdag/dag.go b/blockdag/dag.go index 9de0b3d35..4a27d9fa4 100644 --- a/blockdag/dag.go +++ b/blockdag/dag.go @@ -777,7 +777,7 @@ func (p *provisionalNode) pastUTXO(virtual *virtualBlock, db database.DB) (pastU // Fetch from the database all the transactions for this block's blue set (besides the selected parent) var blueBlockTransactions []*TxWithBlockHash transactionCount := 0 - err = db.View(func(tx database.Tx) error { + err = db.View(func(dbTx database.Tx) error { // Precalculate the amount of transactions in this block's blue set, besides the selected parent. // This is to avoid an attack in which an attacker fabricates a block that will deliberately cause // a lot of copying, causing a high cost to the whole network. @@ -788,7 +788,7 @@ func (p *provisionalNode) pastUTXO(virtual *virtualBlock, db database.DB) (pastU continue } - blueBlock, err := dbFetchBlockByNode(tx, blueBlockNode) + blueBlock, err := dbFetchBlockByNode(dbTx, blueBlockNode) if err != nil { return err } diff --git a/database/cmd/dbtool/fetchblock.go b/database/cmd/dbtool/fetchblock.go index c9802c23c..e645b8cb8 100644 --- a/database/cmd/dbtool/fetchblock.go +++ b/database/cmd/dbtool/fetchblock.go @@ -43,10 +43,10 @@ func (cmd *fetchBlockCmd) Execute(args []string) error { } defer db.Close() - return db.View(func(tx database.Tx) error { + return db.View(func(dbTx database.Tx) error { log.Infof("Fetching block %s", blockHash) startTime := time.Now() - blockBytes, err := tx.FetchBlock(blockHash) + blockBytes, err := dbTx.FetchBlock(blockHash) if err != nil { return err } diff --git a/database/cmd/dbtool/fetchblockregion.go b/database/cmd/dbtool/fetchblockregion.go index 4c4f245f5..571bb9b9e 100644 --- a/database/cmd/dbtool/fetchblockregion.go +++ b/database/cmd/dbtool/fetchblockregion.go @@ -64,7 +64,7 @@ func (cmd *blockRegionCmd) Execute(args []string) error { } defer db.Close() - return db.View(func(tx database.Tx) error { + return db.View(func(dbTx database.Tx) error { log.Infof("Fetching block region %s<%d:%d>", blockHash, startOffset, startOffset+regionLen-1) region := database.BlockRegion{ @@ -73,7 +73,7 @@ func (cmd *blockRegionCmd) Execute(args []string) error { Len: uint32(regionLen), } startTime := time.Now() - regionBytes, err := tx.FetchBlockRegion(®ion) + regionBytes, err := dbTx.FetchBlockRegion(®ion) if err != nil { return err } diff --git a/database/cmd/dbtool/insecureimport.go b/database/cmd/dbtool/insecureimport.go index 086651bc6..2e0ab3736 100644 --- a/database/cmd/dbtool/insecureimport.go +++ b/database/cmd/dbtool/insecureimport.go @@ -119,8 +119,8 @@ func (bi *blockImporter) processBlock(serializedBlock []byte) (bool, error) { // Skip blocks that already exist. var exists bool - err = bi.db.View(func(tx database.Tx) error { - exists, err = tx.HasBlock(block.Hash()) + err = bi.db.View(func(dbTx database.Tx) error { + exists, err = dbTx.HasBlock(block.Hash()) return err }) if err != nil { @@ -134,8 +134,8 @@ func (bi *blockImporter) processBlock(serializedBlock []byte) (bool, error) { parentHashes := block.MsgBlock().Header.ParentHashes for _, parentHash := range parentHashes { var exists bool - err := bi.db.View(func(tx database.Tx) error { - exists, err = tx.HasBlock(&parentHash) + err := bi.db.View(func(dbTx database.Tx) error { + exists, err = dbTx.HasBlock(&parentHash) return err }) if err != nil { @@ -149,8 +149,8 @@ func (bi *blockImporter) processBlock(serializedBlock []byte) (bool, error) { } // Put the blocks into the database with no checking of chain rules. - err = bi.db.Update(func(tx database.Tx) error { - return tx.StoreBlock(block) + err = bi.db.Update(func(dbTx database.Tx) error { + return dbTx.StoreBlock(block) }) if err != nil { return false, err diff --git a/database/cmd/dbtool/loadheaders.go b/database/cmd/dbtool/loadheaders.go index f348cdb85..338e758a4 100644 --- a/database/cmd/dbtool/loadheaders.go +++ b/database/cmd/dbtool/loadheaders.go @@ -41,9 +41,9 @@ func (cmd *headersCmd) Execute(args []string) error { // the database would keep a metadata index of its own. blockIdxName := []byte("ffldb-blockidx") if !headersCfg.Bulk { - err = db.View(func(tx database.Tx) error { + err = db.View(func(dbTx database.Tx) error { totalHdrs := 0 - blockIdxBucket := tx.Metadata().Bucket(blockIdxName) + blockIdxBucket := dbTx.Metadata().Bucket(blockIdxName) blockIdxBucket.ForEach(func(k, v []byte) error { totalHdrs++ return nil @@ -54,7 +54,7 @@ func (cmd *headersCmd) Execute(args []string) error { blockIdxBucket.ForEach(func(k, v []byte) error { var hash daghash.Hash copy(hash[:], k) - _, err := tx.FetchBlockHeader(&hash) + _, err := dbTx.FetchBlockHeader(&hash) if err != nil { return err } @@ -69,8 +69,8 @@ func (cmd *headersCmd) Execute(args []string) error { } // Bulk load headers. - err = db.View(func(tx database.Tx) error { - blockIdxBucket := tx.Metadata().Bucket(blockIdxName) + err = db.View(func(dbTx database.Tx) error { + blockIdxBucket := dbTx.Metadata().Bucket(blockIdxName) hashes := make([]daghash.Hash, 0, 500000) blockIdxBucket.ForEach(func(k, v []byte) error { var hash daghash.Hash @@ -81,7 +81,7 @@ func (cmd *headersCmd) Execute(args []string) error { log.Infof("Loading headers for %d blocks...", len(hashes)) startTime := time.Now() - hdrs, err := tx.FetchBlockHeaders(hashes) + hdrs, err := dbTx.FetchBlockHeaders(hashes) if err != nil { return err } diff --git a/database/error.go b/database/error.go index 49c250eef..e8de164b3 100644 --- a/database/error.go +++ b/database/error.go @@ -195,3 +195,13 @@ func (e Error) Error() string { func makeError(c ErrorCode, desc string, err error) Error { return Error{ErrorCode: c, Description: desc, Err: err} } + +// IsErrorCode returns whether or not the provided error is a script error with +// the provided error code. +func IsErrorCode(err error, c ErrorCode) bool { + if err, ok := err.(Error); ok { + return err.ErrorCode == c + } + + return false +} diff --git a/database/error_test.go b/database/error_test.go index c4e9f57da..1cdf3acfb 100644 --- a/database/error_test.go +++ b/database/error_test.go @@ -2,48 +2,46 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. -package database_test +package database import ( "errors" "testing" - - "github.com/daglabs/btcd/database" ) // TestErrorCodeStringer tests the stringized output for the ErrorCode type. func TestErrorCodeStringer(t *testing.T) { tests := []struct { - in database.ErrorCode + in ErrorCode want string }{ - {database.ErrDbTypeRegistered, "ErrDbTypeRegistered"}, - {database.ErrDbUnknownType, "ErrDbUnknownType"}, - {database.ErrDbDoesNotExist, "ErrDbDoesNotExist"}, - {database.ErrDbExists, "ErrDbExists"}, - {database.ErrDbNotOpen, "ErrDbNotOpen"}, - {database.ErrDbAlreadyOpen, "ErrDbAlreadyOpen"}, - {database.ErrInvalid, "ErrInvalid"}, - {database.ErrCorruption, "ErrCorruption"}, - {database.ErrTxClosed, "ErrTxClosed"}, - {database.ErrTxNotWritable, "ErrTxNotWritable"}, - {database.ErrBucketNotFound, "ErrBucketNotFound"}, - {database.ErrBucketExists, "ErrBucketExists"}, - {database.ErrBucketNameRequired, "ErrBucketNameRequired"}, - {database.ErrKeyRequired, "ErrKeyRequired"}, - {database.ErrKeyTooLarge, "ErrKeyTooLarge"}, - {database.ErrValueTooLarge, "ErrValueTooLarge"}, - {database.ErrIncompatibleValue, "ErrIncompatibleValue"}, - {database.ErrBlockNotFound, "ErrBlockNotFound"}, - {database.ErrBlockExists, "ErrBlockExists"}, - {database.ErrBlockRegionInvalid, "ErrBlockRegionInvalid"}, - {database.ErrDriverSpecific, "ErrDriverSpecific"}, + {ErrDbTypeRegistered, "ErrDbTypeRegistered"}, + {ErrDbUnknownType, "ErrDbUnknownType"}, + {ErrDbDoesNotExist, "ErrDbDoesNotExist"}, + {ErrDbExists, "ErrDbExists"}, + {ErrDbNotOpen, "ErrDbNotOpen"}, + {ErrDbAlreadyOpen, "ErrDbAlreadyOpen"}, + {ErrInvalid, "ErrInvalid"}, + {ErrCorruption, "ErrCorruption"}, + {ErrTxClosed, "ErrTxClosed"}, + {ErrTxNotWritable, "ErrTxNotWritable"}, + {ErrBucketNotFound, "ErrBucketNotFound"}, + {ErrBucketExists, "ErrBucketExists"}, + {ErrBucketNameRequired, "ErrBucketNameRequired"}, + {ErrKeyRequired, "ErrKeyRequired"}, + {ErrKeyTooLarge, "ErrKeyTooLarge"}, + {ErrValueTooLarge, "ErrValueTooLarge"}, + {ErrIncompatibleValue, "ErrIncompatibleValue"}, + {ErrBlockNotFound, "ErrBlockNotFound"}, + {ErrBlockExists, "ErrBlockExists"}, + {ErrBlockRegionInvalid, "ErrBlockRegionInvalid"}, + {ErrDriverSpecific, "ErrDriverSpecific"}, {0xffff, "Unknown ErrorCode (65535)"}, } // Detect additional error codes that don't have the stringer added. - if len(tests)-1 != int(database.TstNumErrorCodes) { + if len(tests)-1 != int(TstNumErrorCodes) { t.Errorf("It appears an error code was added without adding " + "an associated stringer test") } @@ -64,20 +62,20 @@ func TestError(t *testing.T) { t.Parallel() tests := []struct { - in database.Error + in Error want string }{ { - database.Error{Description: "some error"}, + Error{Description: "some error"}, "some error", }, { - database.Error{Description: "human-readable error"}, + Error{Description: "human-readable error"}, "human-readable error", }, { - database.Error{ - ErrorCode: database.ErrDriverSpecific, + Error{ + ErrorCode: ErrDriverSpecific, Description: "some error", Err: errors.New("driver-specific error"), }, @@ -95,3 +93,26 @@ func TestError(t *testing.T) { } } } + +func TestIsErrorCode(t *testing.T) { + dummyError := errors.New("") + + tests := []struct { + err error + code ErrorCode + expectedResult bool + }{ + {makeError(ErrBucketExists, "", dummyError), ErrBucketExists, true}, + {makeError(ErrBucketExists, "", dummyError), ErrBlockExists, false}, + {dummyError, ErrBlockExists, false}, + {nil, ErrBlockExists, false}, + } + + for i, test := range tests { + actualResult := IsErrorCode(test.err, test.code) + if test.expectedResult != actualResult { + t.Errorf("TestIsErrorCode: %d: Expected: %t, but got: %t", + i, test.expectedResult, actualResult) + } + } +} diff --git a/database/example_test.go b/database/example_test.go index 08d5cfb1e..f2756382f 100644 --- a/database/example_test.go +++ b/database/example_test.go @@ -68,25 +68,25 @@ func Example_basicUsage() { // Use the Update function of the database to perform a managed // read-write transaction. The transaction will automatically be rolled // back if the supplied inner function returns a non-nil error. - err = db.Update(func(tx database.Tx) error { + err = db.Update(func(dbTx database.Tx) error { // Store a key/value pair directly in the metadata bucket. // Typically a nested bucket would be used for a given feature, // but this example is using the metadata bucket directly for // simplicity. key := []byte("mykey") value := []byte("myvalue") - if err := tx.Metadata().Put(key, value); err != nil { + if err := dbTx.Metadata().Put(key, value); err != nil { return err } // Read the key back and ensure it matches. - if !bytes.Equal(tx.Metadata().Get(key), value) { + if !bytes.Equal(dbTx.Metadata().Get(key), value) { return fmt.Errorf("unexpected value for key '%s'", key) } // Create a new nested bucket under the metadata bucket. nestedBucketKey := []byte("mybucket") - nestedBucket, err := tx.Metadata().CreateBucket(nestedBucketKey) + nestedBucket, err := dbTx.Metadata().CreateBucket(nestedBucketKey) if err != nil { return err } @@ -134,9 +134,9 @@ func Example_blockStorageAndRetrieval() { // Use the Update function of the database to perform a managed // read-write transaction and store a genesis block in the database as // and example. - err = db.Update(func(tx database.Tx) error { + err = db.Update(func(dbTx database.Tx) error { genesisBlock := dagconfig.MainNetParams.GenesisBlock - return tx.StoreBlock(util.NewBlock(genesisBlock)) + return dbTx.StoreBlock(util.NewBlock(genesisBlock)) }) if err != nil { fmt.Println(err) @@ -146,9 +146,9 @@ func Example_blockStorageAndRetrieval() { // Use the View function of the database to perform a managed read-only // transaction and fetch the block stored above. var loadedBlockBytes []byte - err = db.Update(func(tx database.Tx) error { + err = db.Update(func(dbTx database.Tx) error { genesisHash := dagconfig.MainNetParams.GenesisHash - blockBytes, err := tx.FetchBlock(genesisHash) + blockBytes, err := dbTx.FetchBlock(genesisHash) if err != nil { return err } diff --git a/database/ffldb/bench_test.go b/database/ffldb/bench_test.go index 6ec58275b..af04e6a55 100644 --- a/database/ffldb/bench_test.go +++ b/database/ffldb/bench_test.go @@ -27,9 +27,9 @@ func BenchmarkBlockHeader(b *testing.B) { } defer os.RemoveAll(dbPath) defer db.Close() - err = db.Update(func(tx database.Tx) error { + err = db.Update(func(dbTx database.Tx) error { block := util.NewBlock(dagconfig.MainNetParams.GenesisBlock) - return tx.StoreBlock(block) + return dbTx.StoreBlock(block) }) if err != nil { b.Fatal(err) @@ -37,10 +37,10 @@ func BenchmarkBlockHeader(b *testing.B) { b.ReportAllocs() b.ResetTimer() - err = db.View(func(tx database.Tx) error { + err = db.View(func(dbTx database.Tx) error { blockHash := dagconfig.MainNetParams.GenesisHash for i := 0; i < b.N; i++ { - _, err := tx.FetchBlockHeader(blockHash) + _, err := dbTx.FetchBlockHeader(blockHash) if err != nil { return err } @@ -68,9 +68,9 @@ func BenchmarkBlock(b *testing.B) { } defer os.RemoveAll(dbPath) defer db.Close() - err = db.Update(func(tx database.Tx) error { + err = db.Update(func(dbTx database.Tx) error { block := util.NewBlock(dagconfig.MainNetParams.GenesisBlock) - return tx.StoreBlock(block) + return dbTx.StoreBlock(block) }) if err != nil { b.Fatal(err) @@ -78,10 +78,10 @@ func BenchmarkBlock(b *testing.B) { b.ReportAllocs() b.ResetTimer() - err = db.View(func(tx database.Tx) error { + err = db.View(func(dbTx database.Tx) error { blockHash := dagconfig.MainNetParams.GenesisHash for i := 0; i < b.N; i++ { - _, err := tx.FetchBlock(blockHash) + _, err := dbTx.FetchBlock(blockHash) if err != nil { return err } diff --git a/database/ffldb/blockio.go b/database/ffldb/blockio.go index 378b00b5e..8811a6d09 100644 --- a/database/ffldb/blockio.go +++ b/database/ffldb/blockio.go @@ -116,6 +116,12 @@ type blockStore struct { // override the value. maxBlockFileSize uint32 + // maxOpenFiles is the max number of open files to maintain in the + // open blocks cache. Note that this does not include the current + // write file, so there will typically be one more than this value open. + // It is defined on the store so the whitebox tests can override the value. + maxOpenFiles int + // The following fields are related to the flat files which hold the // actual blocks. The number of open files is limited by maxOpenFiles. // @@ -272,7 +278,7 @@ func (s *blockStore) openFile(fileNum uint32) (*lockableFile, error) { // therefore should be closed last. s.lruMutex.Lock() lruList := s.openBlocksLRU - if lruList.Len() >= maxOpenFiles { + if lruList.Len() >= s.maxOpenFiles { lruFileNum := lruList.Remove(lruList.Back()).(uint32) oldBlockFile := s.openBlockFiles[lruFileNum] @@ -752,6 +758,7 @@ func newBlockStore(basePath string, network wire.BitcoinNet) *blockStore { network: network, basePath: basePath, maxBlockFileSize: maxBlockFileSize, + maxOpenFiles: maxOpenFiles, openBlockFiles: make(map[uint32]*lockableFile), openBlocksLRU: list.New(), fileNumToLRUElem: make(map[uint32]*list.Element), diff --git a/database/ffldb/blockio_test.go b/database/ffldb/blockio_test.go new file mode 100644 index 000000000..4fec52b77 --- /dev/null +++ b/database/ffldb/blockio_test.go @@ -0,0 +1,116 @@ +package ffldb + +import ( + "errors" + "os" + "testing" + + "bou.ke/monkey" + "github.com/daglabs/btcd/dagconfig/daghash" + "github.com/daglabs/btcd/database" + "github.com/daglabs/btcd/util" + "github.com/daglabs/btcd/wire" +) + +func TestDeleteFile(t *testing.T) { + testBlock := util.NewBlock(wire.NewMsgBlock( + wire.NewBlockHeader(1, []daghash.Hash{}, &daghash.Hash{}, 0, 0))) + + tests := []struct { + fileNum uint32 + expectedErr bool + }{ + {0, false}, + {1, true}, + } + + for _, test := range tests { + func() { + pdb := newTestDb("TestDeleteFile", t) + defer pdb.Close() + + err := pdb.Update(func(dbTx database.Tx) error { + dbTx.StoreBlock(testBlock) + return nil + }) + if err != nil { + t.Fatalf("TestDeleteFile: Error storing block: %s", err) + } + + err = pdb.store.deleteFile(test.fileNum) + if (err != nil) != test.expectedErr { + t.Errorf("TestDeleteFile: %d: Expected error status: %t, but got: %t", + test.fileNum, test.expectedErr, (err != nil)) + } + if err == nil { + filePath := blockFilePath(pdb.store.basePath, test.fileNum) + if _, err := os.Stat(filePath); !os.IsNotExist(err) { + t.Errorf("TestDeleteFile: %d: File %s still exists", test.fileNum, filePath) + } + } + }() + } +} + +// TestHandleRollbackErrors tests all error-cases in *blockStore.handleRollback(). +// The non-error-cases are tested in the more general tests. +// Since handleRollback just logs errors, this test simply causes all error-cases to be hit, +// and makes sure no panic occurs, as well as ensures the writeCursor was updated correctly. +func TestHandleRollbackErrors(t *testing.T) { + testBlock := util.NewBlock(wire.NewMsgBlock( + wire.NewBlockHeader(1, []daghash.Hash{}, &daghash.Hash{}, 0, 0))) + + testBlockSize := uint32(testBlock.MsgBlock().SerializeSize()) + tests := []struct { + name string + fileNum uint32 + offset uint32 + target interface{} + replacement interface{} + }{ + // offset should be size of block + 12 bytes for block network, size and checksum + {"Nothing to rollback", 1, testBlockSize + 12, nil, nil}, + {"deleteFile fails", 0, 0, (*blockStore).deleteFile, + func(*blockStore, uint32) error { return errors.New("error in blockstore.deleteFile") }}, + {"openWriteFile fails", 0, 0, (*blockStore).openWriteFile, + func(*blockStore, uint32) (filer, error) { return nil, errors.New("error in blockstore.openWriteFile") }}, + {"file.Truncate fails", 0, 0, (*os.File).Truncate, + func(*os.File, int64) error { return errors.New("error in file.Truncate") }}, + {"file.Sync fails", 0, 0, (*os.File).Sync, + func(*os.File) error { return errors.New("error in file.Sync") }}, + } + + for _, test := range tests { + func() { + pdb := newTestDb("TestHandleRollbackErrors", t) + defer pdb.Close() + + // Set maxBlockFileSize to testBlockSize so that writeCursor.curFileNum increments + pdb.store.maxBlockFileSize = testBlockSize + + err := pdb.Update(func(dbTx database.Tx) error { + return dbTx.StoreBlock(testBlock) + }) + if err != nil { + t.Fatalf("TestHandleRollbackErrors: %s: Error adding test block to database: %s", test.name, err) + } + + if test.target != nil && test.replacement != nil { + patch := monkey.Patch(test.target, test.replacement) + defer patch.Unpatch() + } + + pdb.store.handleRollback(test.fileNum, test.offset) + + if pdb.store.writeCursor.curFileNum != test.fileNum { + t.Errorf("TestHandleRollbackErrors: %s: Expected fileNum: %d, but got: %d", + test.name, test.fileNum, pdb.store.writeCursor.curFileNum) + } + + if pdb.store.writeCursor.curOffset != test.offset { + t.Errorf("TestHandleRollbackErrors: %s: offset fileNum: %d, but got: %d", + test.name, test.offset, pdb.store.writeCursor.curOffset) + } + }() + } +} diff --git a/database/ffldb/common_test.go b/database/ffldb/common_test.go new file mode 100644 index 000000000..9a070dcb2 --- /dev/null +++ b/database/ffldb/common_test.go @@ -0,0 +1,43 @@ +package ffldb + +import ( + "os" + "path" + "path/filepath" + "testing" + + "github.com/btcsuite/goleveldb/leveldb" + "github.com/btcsuite/goleveldb/leveldb/filter" + "github.com/btcsuite/goleveldb/leveldb/opt" + "github.com/daglabs/btcd/wire" +) + +func newTestDb(testName string, t *testing.T) *db { + dbPath := path.Join(os.TempDir(), "db_test") + err := os.RemoveAll(dbPath) + if err != nil && !os.IsNotExist(err) { + t.Fatalf("%s: Error deleting database folder before starting: %s", testName, err) + } + + network := wire.SimNet + + opts := opt.Options{ + ErrorIfExist: true, + Strict: opt.DefaultStrict, + Compression: opt.NoCompression, + Filter: filter.NewBloomFilter(10), + } + metadataDbPath := filepath.Join(dbPath, metadataDbName) + ldb, err := leveldb.OpenFile(metadataDbPath, &opts) + if err != nil { + t.Errorf("%s: Error opening metadataDbPath: %s", testName, err) + } + err = initDB(ldb) + if err != nil { + t.Errorf("%s: Error initializing metadata Db: %s", testName, err) + } + + store := newBlockStore(dbPath, network) + cache := newDbCache(ldb, store, defaultCacheSize, defaultFlushSecs) + return &db{store: store, cache: cache} +} diff --git a/database/ffldb/db.go b/database/ffldb/db.go index 5ebcf7cc7..ca603e2dd 100644 --- a/database/ffldb/db.go +++ b/database/ffldb/db.go @@ -203,6 +203,12 @@ func (c *cursor) Delete() error { return err } + // Ensure the transaction is writable. + if !c.bucket.tx.writable { + str := "delete requires a writable database transaction" + return makeDbErr(database.ErrTxNotWritable, str, nil) + } + // Error if the cursor is exhausted. if c.currentIter == nil { str := "cursor is exhausted" @@ -652,10 +658,7 @@ func (b *bucket) CreateBucket(key []byte) (database.Bucket, error) { } // Add the new bucket to the bucket index. - if err := b.tx.putKey(bidxKey, childID[:]); err != nil { - str := fmt.Sprintf("failed to create bucket with key %q", key) - return nil, convertErr(str, err) - } + b.tx.putKey(bidxKey, childID[:]) return &bucket{tx: b.tx, id: childID}, nil } @@ -881,7 +884,9 @@ func (b *bucket) Put(key, value []byte) error { return makeDbErr(database.ErrKeyRequired, str, nil) } - return b.tx.putKey(bucketizedKey(b.id, key), value) + b.tx.putKey(bucketizedKey(b.id, key), value) + + return nil } // Get returns the value for the given key. Returns nil if the key does not @@ -931,7 +936,8 @@ func (b *bucket) Delete(key []byte) error { // Nothing to do if there is no key. if len(key) == 0 { - return nil + str := "delete requires a key" + return makeDbErr(database.ErrKeyRequired, str, nil) } b.tx.deleteKey(bucketizedKey(b.id, key), true) @@ -1044,7 +1050,7 @@ func (tx *transaction) hasKey(key []byte) bool { // // NOTE: This function must only be called on a writable transaction. Since it // is an internal helper function, it does not check. -func (tx *transaction) putKey(key, value []byte) error { +func (tx *transaction) putKey(key, value []byte) { // Prevent the key from being deleted if it was previously scheduled // to be deleted on transaction commit. tx.pendingRemove.Delete(key) @@ -1053,7 +1059,6 @@ func (tx *transaction) putKey(key, value []byte) error { // commit. tx.pendingKeys.Put(key, value) tx.notifyActiveIters() - return nil } // fetchKey attempts to fetch the provided key from the database cache (and @@ -1107,9 +1112,9 @@ func (tx *transaction) nextBucketID() ([4]byte, error) { // Increment and update the current bucket ID and return it. var nextBucketID [4]byte binary.BigEndian.PutUint32(nextBucketID[:], curBucketNum+1) - if err := tx.putKey(curBucketIDKeyName, nextBucketID[:]); err != nil { - return [4]byte{}, err - } + + tx.putKey(curBucketIDKeyName, nextBucketID[:]) + return nextBucketID, nil } diff --git a/database/ffldb/db_test.go b/database/ffldb/db_test.go new file mode 100644 index 000000000..32d35e258 --- /dev/null +++ b/database/ffldb/db_test.go @@ -0,0 +1,727 @@ +package ffldb + +import ( + "bytes" + "errors" + "fmt" + "testing" + + "bou.ke/monkey" + "github.com/daglabs/btcd/dagconfig/daghash" + "github.com/daglabs/btcd/database" + "github.com/daglabs/btcd/util" + "github.com/daglabs/btcd/wire" +) + +// TestCursorDeleteErrors tests all error-cases in *cursor.Delete(). +// The non-error-cases are tested in the more general tests. +func TestCursorDeleteErrors(t *testing.T) { + pdb := newTestDb("TestCursorDeleteErrors", t) + + nestedBucket := []byte("nestedBucket") + key := []byte("key") + value := []byte("value") + + err := pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + _, err := metadata.CreateBucket(nestedBucket) + if err != nil { + return err + } + metadata.Put(key, value) + return nil + }) + if err != nil { + t.Fatalf("TestCursorDeleteErrors: Error setting up test-database: %s", err) + } + + // Check for error when attempted to delete a bucket + err = pdb.Update(func(dbTx database.Tx) error { + cursor := dbTx.Metadata().Cursor() + found := false + for ok := cursor.First(); ok; ok = cursor.Next() { + if bytes.Equal(cursor.Key(), nestedBucket) { + found = true + break + } + } + if !found { + t.Errorf("TestCursorDeleteErrors: Key '%s' not found", string(nestedBucket)) + } + + err := cursor.Delete() + if !database.IsErrorCode(err, database.ErrIncompatibleValue) { + t.Errorf("TestCursorDeleteErrors: Expected error of type ErrIncompatibleValue, "+ + "when deleting bucket, but got %v", err) + } + + return nil + }) + if err != nil { + t.Fatalf("TestCursorDeleteErrors: Unexpected error from pdb.Update "+ + "when attempting to delete bucket: %s", err) + } + + // Check for error when transaction is not writable + err = pdb.View(func(dbTx database.Tx) error { + cursor := dbTx.Metadata().Cursor() + if !cursor.First() { + t.Fatal("TestCursorDeleteErrors: Nothing in cursor when testing for delete in " + + "non-writable transaction") + } + + err := cursor.Delete() + if !database.IsErrorCode(err, database.ErrTxNotWritable) { + t.Errorf("TestCursorDeleteErrors: Expected error of type ErrTxNotWritable "+ + "when calling .Delete() on non-writable transaction, but got '%v' instead", err) + } + + return nil + }) + if err != nil { + t.Fatalf("TestCursorDeleteErrors: Unexpected error from pdb.Update "+ + "when attempting to delete on non-writable transaction: %s", err) + } + + // Check for error when cursor was exhausted + err = pdb.Update(func(dbTx database.Tx) error { + cursor := dbTx.Metadata().Cursor() + for ok := cursor.First(); ok; ok = cursor.Next() { + } + + err := cursor.Delete() + if !database.IsErrorCode(err, database.ErrIncompatibleValue) { + t.Errorf("TestCursorDeleteErrors: Expected error of type ErrIncompatibleValue "+ + "when calling .Delete() on exhausted cursor, but got '%v' instead", err) + } + + return nil + }) + if err != nil { + t.Fatalf("TestCursorDeleteErrors: Unexpected error from pdb.Update "+ + "when attempting to delete on exhausted cursor: %s", err) + } + + // Check for error when transaction is closed + tx, err := pdb.Begin(true) + if err != nil { + t.Fatalf("TestCursorDeleteErrors: Error in pdb.Begin(): %s", err) + } + cursor := tx.Metadata().Cursor() + err = tx.Commit() + if err != nil { + t.Fatalf("TestCursorDeleteErrors: Error in tx.Commit(): %s", err) + } + + err = cursor.Delete() + if !database.IsErrorCode(err, database.ErrTxClosed) { + t.Errorf("TestCursorDeleteErrors: Expected error of type ErrTxClosed "+ + "when calling .Delete() on with closed transaction, but got '%s' instead", err) + } +} + +func TestSkipPendingUpdates(t *testing.T) { + pdb := newTestDb("TestSkipPendingUpdates", t) + defer pdb.Close() + + value := []byte("value") + // Add numbered prefixes to keys so that they are in expected order, and before any other keys + firstKey := []byte("1 - first") + toDeleteKey := []byte("2 - toDelete") + toUpdateKey := []byte("3 - toUpdate") + secondKey := []byte("4 - second") + + // create initial metadata for test + err := pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + if err := metadata.Put(firstKey, value); err != nil { + return err + } + if err := metadata.Put(toDeleteKey, value); err != nil { + return err + } + if err := metadata.Put(toUpdateKey, value); err != nil { + return err + } + if err := metadata.Put(secondKey, value); err != nil { + return err + } + + return nil + }) + if err != nil { + t.Fatalf("TestSkipPendingUpdates: Error adding to metadata: %s", err) + } + + // test skips + err = pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + if err := metadata.Delete(toDeleteKey); err != nil { + return err + } + if err := metadata.Put(toUpdateKey, value); err != nil { + return err + } + cursor := metadata.Cursor().(*cursor) + dbIter := cursor.dbIter + + // Check that first is ok + dbIter.First() + expectedKey := bucketizedKey(metadataBucketID, firstKey) + if !bytes.Equal(dbIter.Key(), expectedKey) { + t.Errorf("TestSkipPendingUpdates: 1: key expected to be %v but is %v", expectedKey, dbIter.Key()) + } + + // Go to the next key, which is toDelete + dbIter.Next() + expectedKey = bucketizedKey(metadataBucketID, toDeleteKey) + if !bytes.Equal(dbIter.Key(), expectedKey) { + t.Errorf("TestSkipPendingUpdates: 2: key expected to be %s but is %s", expectedKey, dbIter.Key()) + } + + // at this point toDeleteKey and toUpdateKey should be skipped + cursor.skipPendingUpdates(true) + expectedKey = bucketizedKey(metadataBucketID, secondKey) + if !bytes.Equal(dbIter.Key(), expectedKey) { + t.Errorf("TestSkipPendingUpdates: 3: key expected to be %s but is %s", expectedKey, dbIter.Key()) + } + + // now traverse backwards - should get toUpdate + dbIter.Prev() + expectedKey = bucketizedKey(metadataBucketID, toUpdateKey) + if !bytes.Equal(dbIter.Key(), expectedKey) { + t.Errorf("TestSkipPendingUpdates: 4: key expected to be %s but is %s", expectedKey, dbIter.Key()) + } + + // at this point toUpdateKey and toDeleteKey should be skipped + cursor.skipPendingUpdates(false) + expectedKey = bucketizedKey(metadataBucketID, firstKey) + if !bytes.Equal(dbIter.Key(), expectedKey) { + t.Errorf("TestSkipPendingUpdates: 5: key expected to be %s but is %s", expectedKey, dbIter.Key()) + } + return nil + }) + if err != nil { + t.Fatalf("TestSkipPendingUpdates: Error running main part of test: %s", err) + } +} + +// TestCursor tests various edge-cases in cursor that were not hit by the more general tests +func TestCursor(t *testing.T) { + pdb := newTestDb("TestCursor", t) + defer pdb.Close() + + value := []byte("value") + // Add numbered prefixes to keys so that they are in expected order, and before any other keys + firstKey := []byte("1 - first") + toDeleteKey := []byte("2 - toDelete") + toUpdateKey := []byte("3 - toUpdate") + secondKey := []byte("4 - second") + + // create initial metadata for test + err := pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + if err := metadata.Put(firstKey, value); err != nil { + return err + } + if err := metadata.Put(toDeleteKey, value); err != nil { + return err + } + if err := metadata.Put(toUpdateKey, value); err != nil { + return err + } + if err := metadata.Put(secondKey, value); err != nil { + return err + } + + return nil + }) + if err != nil { + t.Fatalf("Error adding to metadata: %s", err) + } + + // run the actual tests + err = pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + if err := metadata.Delete(toDeleteKey); err != nil { + return err + } + if err := metadata.Put(toUpdateKey, value); err != nil { + return err + } + cursor := metadata.Cursor().(*cursor) + + // Check prev when currentIter == nil + if ok := cursor.Prev(); ok { + t.Error("1: .Prev() should have returned false, but have returned true") + } + // Same thing for .Next() + for ok := cursor.First(); ok; ok = cursor.Next() { + } + if ok := cursor.Next(); ok { + t.Error("2: .Next() should have returned false, but have returned true") + } + + // Check that Key(), rawKey(), Value(), and rawValue() all return nil when currentIter == nil + if key := cursor.Key(); key != nil { + t.Errorf("3: .Key() should have returned nil, but have returned '%s' instead", key) + } + if key := cursor.rawKey(); key != nil { + t.Errorf("4: .rawKey() should have returned nil, but have returned '%s' instead", key) + } + if value := cursor.Value(); value != nil { + t.Errorf("5: .Value() should have returned nil, but have returned '%s' instead", value) + } + if value := cursor.rawValue(); value != nil { + t.Errorf("6: .rawValue() should have returned nil, but have returned '%s' instead", value) + } + + // Check rawValue in normal operation + cursor.First() + if rawValue := cursor.rawValue(); !bytes.Equal(rawValue, value) { + t.Errorf("7: rawValue should have returned '%s' but have returned '%s' instead", value, rawValue) + } + + return nil + }) + if err != nil { + t.Fatalf("Error running the actual tests: %s", err) + } +} + +// TestCreateBucketErrors tests all error-cases in *bucket.CreateBucket(). +// The non-error-cases are tested in the more general tests. +func TestCreateBucketErrors(t *testing.T) { + testKey := []byte("key") + + tests := []struct { + name string + key []byte + target interface{} + replacement interface{} + isWritable bool + isClosed bool + expectedErr database.ErrorCode + }{ + {"empty key", []byte{}, nil, nil, true, false, database.ErrBucketNameRequired}, + {"transaction is closed", testKey, nil, nil, true, true, database.ErrTxClosed}, + {"transaction is not writable", testKey, nil, nil, false, false, database.ErrTxNotWritable}, + {"key already exists", blockIdxBucketName, nil, nil, true, false, database.ErrBucketExists}, + {"nextBucketID error", testKey, (*transaction).nextBucketID, + func(*transaction) ([4]byte, error) { + return [4]byte{}, makeDbErr(database.ErrTxClosed, "error in newBucketID", nil) + }, + true, false, database.ErrTxClosed}, + } + + for _, test := range tests { + func() { + pdb := newTestDb("TestCreateBucketErrors", t) + defer pdb.Close() + + if test.target != nil && test.replacement != nil { + patch := monkey.Patch(test.target, test.replacement) + defer patch.Unpatch() + } + + tx, err := pdb.Begin(test.isWritable) + defer tx.Commit() + if err != nil { + t.Fatalf("TestCreateBucketErrors: %s: error from pdb.Begin: %s", test.name, err) + } + if test.isClosed { + err = tx.Commit() + if err != nil { + t.Fatalf("TestCreateBucketErrors: %s: error from tx.Commit: %s", test.name, err) + } + } + + metadata := tx.Metadata() + + _, err = metadata.CreateBucket(test.key) + + if !database.IsErrorCode(err, test.expectedErr) { + t.Errorf("TestCreateBucketErrors: %s: Expected error of type %d "+ + "but got '%v'", test.name, test.expectedErr, err) + } + + }() + } +} + +// TestPutErrors tests all error-cases in *bucket.Put(). +// The non-error-cases are tested in the more general tests. +func TestPutErrors(t *testing.T) { + testKey := []byte("key") + testValue := []byte("value") + + tests := []struct { + name string + key []byte + isWritable bool + isClosed bool + expectedErr database.ErrorCode + }{ + {"empty key", []byte{}, true, false, database.ErrKeyRequired}, + {"transaction is closed", testKey, true, true, database.ErrTxClosed}, + {"transaction is not writable", testKey, false, false, database.ErrTxNotWritable}, + } + + for _, test := range tests { + func() { + pdb := newTestDb("TestPutErrors", t) + defer pdb.Close() + + tx, err := pdb.Begin(test.isWritable) + defer tx.Commit() + if err != nil { + t.Fatalf("TestPutErrors: %s: error from pdb.Begin: %s", test.name, err) + } + if test.isClosed { + err = tx.Commit() + if err != nil { + t.Fatalf("TestPutErrors: %s: error from tx.Commit: %s", test.name, err) + } + } + + metadata := tx.Metadata() + + err = metadata.Put(test.key, testValue) + + if !database.IsErrorCode(err, test.expectedErr) { + t.Errorf("TestPutErrors: %s: Expected error of type %d "+ + "but got '%v'", test.name, test.expectedErr, err) + } + + }() + } +} + +// TestGetErrors tests all error-cases in *bucket.Get(). +// The non-error-cases are tested in the more general tests. +func TestGetErrors(t *testing.T) { + testKey := []byte("key") + + tests := []struct { + name string + key []byte + isClosed bool + }{ + {"empty key", []byte{}, false}, + {"transaction is closed", testKey, true}, + } + + for _, test := range tests { + func() { + pdb := newTestDb("TestGetErrors", t) + defer pdb.Close() + + tx, err := pdb.Begin(false) + defer tx.Rollback() + if err != nil { + t.Fatalf("TestGetErrors: %s: error from pdb.Begin: %s", test.name, err) + } + if test.isClosed { + err = tx.Rollback() + if err != nil { + t.Fatalf("TestGetErrors: %s: error from tx.Commit: %s", test.name, err) + } + } + + metadata := tx.Metadata() + + if result := metadata.Get(test.key); result != nil { + t.Errorf("TestGetErrors: %s: Expected to return nil, but got %v", test.name, result) + } + }() + } +} + +// TestDeleteErrors tests all error-cases in *bucket.Delete(). +// The non-error-cases are tested in the more general tests. +func TestDeleteErrors(t *testing.T) { + testKey := []byte("key") + + tests := []struct { + name string + key []byte + isWritable bool + isClosed bool + expectedErr database.ErrorCode + }{ + {"empty key", []byte{}, true, false, database.ErrKeyRequired}, + {"transaction is closed", testKey, true, true, database.ErrTxClosed}, + {"transaction is not writable", testKey, false, false, database.ErrTxNotWritable}, + } + + for _, test := range tests { + func() { + pdb := newTestDb("TestDeleteErrors", t) + defer pdb.Close() + + tx, err := pdb.Begin(test.isWritable) + defer tx.Commit() + if err != nil { + t.Fatalf("TestDeleteErrors: %s: error from pdb.Begin: %s", test.name, err) + } + if test.isClosed { + err = tx.Commit() + if err != nil { + t.Fatalf("TestDeleteErrors: %s: error from tx.Commit: %s", test.name, err) + } + } + + metadata := tx.Metadata() + + err = metadata.Delete(test.key) + + if !database.IsErrorCode(err, test.expectedErr) { + t.Errorf("TestDeleteErrors: %s: Expected error of type %d "+ + "but got '%v'", test.name, test.expectedErr, err) + } + }() + } +} + +func TestForEachBucket(t *testing.T) { + pdb := newTestDb("TestForEachBucket", t) + + // set-up test + testKey := []byte("key") + testValue := []byte("value") + bucketKeys := [][]byte{{1}, {2}, {3}} + + err := pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + for _, bucketKey := range bucketKeys { + bucket, err := metadata.CreateBucket(bucketKey) + if err != nil { + return err + } + + err = bucket.Put(testKey, testValue) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + t.Fatalf("TestForEachBucket: Error setting up test-database: %s", err) + } + + // actual test + err = pdb.View(func(dbTx database.Tx) error { + i := 0 + metadata := dbTx.Metadata() + + err := metadata.ForEachBucket(func(bucketKey []byte) error { + if i >= len(bucketKeys) { // in case there are any other buckets in metadata + return nil + } + + expectedBucketKey := bucketKeys[i] + if !bytes.Equal(expectedBucketKey, bucketKey) { + t.Errorf("TestForEachBucket: %d: Expected bucket key: %v, but got: %v", + i, expectedBucketKey, bucketKey) + return nil + } + bucket := metadata.Bucket(bucketKey) + if bucket == nil { + t.Errorf("TestForEachBucket: %d: Bucket is nil", i) + return nil + } + + value := bucket.Get(testKey) + if !bytes.Equal(testValue, value) { + t.Errorf("TestForEachBucket: %d: Expected value: %s, but got: %s", + i, testValue, value) + return nil + } + + i++ + return nil + }) + + return err + }) + if err != nil { + t.Fatalf("TestForEachBucket: Error running actual tests: %s", err) + } +} + +// TestStoreBlockErrors tests all error-cases in *tx.StoreBlock(). +// The non-error-cases are tested in the more general tests. +func TestStoreBlockErrors(t *testing.T) { + testBlock := util.NewBlock(wire.NewMsgBlock(wire.NewBlockHeader(1, []daghash.Hash{}, &daghash.Hash{}, 0, 0))) + + tests := []struct { + name string + target interface{} + replacement interface{} + isWritable bool + isClosed bool + expectedErr database.ErrorCode + }{ + {"transaction is closed", nil, nil, true, true, database.ErrTxClosed}, + {"transaction is not writable", nil, nil, false, false, database.ErrTxNotWritable}, + {"block exists", (*transaction).hasBlock, + func(*transaction, *daghash.Hash) bool { return true }, + true, false, database.ErrBlockExists}, + {"error in block.Bytes", (*util.Block).Bytes, + func(*util.Block) ([]byte, error) { return nil, errors.New("Error in block.Bytes()") }, + true, false, database.ErrDriverSpecific}, + } + + for _, test := range tests { + func() { + pdb := newTestDb("TestStoreBlockErrors", t) + defer pdb.Close() + + if test.target != nil && test.replacement != nil { + patch := monkey.Patch(test.target, test.replacement) + defer patch.Unpatch() + } + + tx, err := pdb.Begin(test.isWritable) + defer tx.Commit() + if err != nil { + t.Fatalf("TestStoreBlockErrors: %s: error from pdb.Begin: %s", test.name, err) + } + if test.isClosed { + err = tx.Commit() + if err != nil { + t.Fatalf("TestStoreBlockErrors: %s: error from tx.Commit: %s", test.name, err) + } + } + + err = tx.StoreBlock(testBlock) + if !database.IsErrorCode(err, test.expectedErr) { + t.Errorf("TestStoreBlockErrors: %s: Expected error of type %d "+ + "but got '%v'", test.name, test.expectedErr, err) + } + + }() + } +} + +// TestDeleteDoubleNestedBucket tests what happens when bucket.DeleteBucket() +// is invoked on a bucket that contains a nested bucket. +func TestDeleteDoubleNestedBucket(t *testing.T) { + pdb := newTestDb("TestDeleteDoubleNestedBucket", t) + defer pdb.Close() + + firstKey := []byte("first") + secondKey := []byte("second") + key := []byte("key") + value := []byte("value") + var rawKey, rawSecondKey []byte + + // Test setup + err := pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + firstBucket, err := metadata.CreateBucket(firstKey) + if err != nil { + return fmt.Errorf("Error creating first bucket: %s", err) + } + secondBucket, err := firstBucket.CreateBucket(secondKey) + if err != nil { + return fmt.Errorf("Error creating second bucket: %s", err) + } + secondBucket.Put(key, value) + + // extract rawKey from cursor and make sure it's in raw database + c := secondBucket.Cursor() + for ok := c.First(); ok && !bytes.Equal(c.Key(), key); ok = c.Next() { + } + if !bytes.Equal(c.Key(), key) { + return fmt.Errorf("Couldn't find key to extract rawKey") + } + rawKey = c.(*cursor).rawKey() + if dbTx.(*transaction).fetchKey(rawKey) == nil { + return fmt.Errorf("rawKey not found") + } + + // extract rawSecondKey from cursor and make sure it's in raw database + c = firstBucket.Cursor() + for ok := c.First(); ok && !bytes.Equal(c.Key(), secondKey); ok = c.Next() { + } + if !bytes.Equal(c.Key(), secondKey) { + return fmt.Errorf("Couldn't find secondKey to extract rawSecondKey") + } + rawSecondKey = c.(*cursor).rawKey() + if dbTx.(*transaction).fetchKey(rawSecondKey) == nil { + return fmt.Errorf("rawSecondKey not found for some reason") + } + + return nil + }) + if err != nil { + t.Fatalf("TestDeleteDoubleNestedBucket: Error in test setup pdb.Update: %s", err) + } + + // Actual test + err = pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + err := metadata.DeleteBucket(firstKey) + if err != nil { + return err + } + + if dbTx.(*transaction).fetchKey(rawSecondKey) != nil { + t.Error("TestDeleteDoubleNestedBucket: secondBucket was not deleted") + } + + if dbTx.(*transaction).fetchKey(rawKey) != nil { + t.Error("TestDeleteDoubleNestedBucket: value inside secondBucket was not deleted") + } + + return nil + }) + if err != nil { + t.Fatalf("TestDeleteDoubleNestedBucket: Error in actual test pdb.Update: %s", err) + } +} + +// TestWritePendingAndCommitErrors tests some error-cases in *tx.writePendingAndCommit(). +// The non-error-cases are tested in the more general tests. +func TestWritePendingAndCommitErrors(t *testing.T) { + putPatch := monkey.Patch((*bucket).Put, + func(_ *bucket, _, _ []byte) error { return errors.New("Error in bucket.Put") }) + defer putPatch.Unpatch() + + rollbackCalled := false + var rollbackPatch *monkey.PatchGuard + rollbackPatch = monkey.Patch((*blockStore).handleRollback, + func(s *blockStore, oldBlockFileNum, oldBlockOffset uint32) { + rollbackPatch.Unpatch() + defer rollbackPatch.Restore() + + rollbackCalled = true + s.handleRollback(oldBlockFileNum, oldBlockOffset) + }) + defer rollbackPatch.Unpatch() + + pdb := newTestDb("TestWritePendingAndCommitErrors", t) + defer pdb.Close() + + err := pdb.Update(func(dbTx database.Tx) error { return nil }) + if err == nil { + t.Errorf("No error returned when metaBucket.Put() should have returned an error") + } + if !rollbackCalled { + t.Errorf("No rollback called when metaBucket.Put() have returned an error") + } + + rollbackCalled = false + err = pdb.Update(func(dbTx database.Tx) error { + return dbTx.StoreBlock(util.NewBlock(wire.NewMsgBlock( + wire.NewBlockHeader(1, []daghash.Hash{}, &daghash.Hash{}, 0, 0)))) + }) + if err == nil { + t.Errorf("No error returned when blockIdx.Put() should have returned an error") + } + if !rollbackCalled { + t.Errorf("No rollback called when blockIdx.Put() have returned an error") + } +} diff --git a/database/ffldb/dbcache_test.go b/database/ffldb/dbcache_test.go new file mode 100644 index 000000000..5d48ef486 --- /dev/null +++ b/database/ffldb/dbcache_test.go @@ -0,0 +1,319 @@ +package ffldb + +import ( + "bytes" + "errors" + "testing" + + "bou.ke/monkey" + "github.com/btcsuite/goleveldb/leveldb" + "github.com/btcsuite/goleveldb/leveldb/opt" + ldbutil "github.com/btcsuite/goleveldb/leveldb/util" + "github.com/daglabs/btcd/database" +) + +// TestDBCacheCloseErrors tests all error-cases in *dbCache.Close(). +// The non-error-cases are tested in the more general tests. +func TestDBCacheCloseErrors(t *testing.T) { + cache := newTestDb("TestDBCacheCloseErrors", t).cache + defer cache.Close() + + closeCalled := false + closePatch := monkey.Patch((*leveldb.DB).Close, func(*leveldb.DB) error { closeCalled = true; return nil }) + defer closePatch.Unpatch() + + expectedErr := errors.New("error on flush") + + flushPatch := monkey.Patch((*dbCache).flush, func(*dbCache) error { return expectedErr }) + defer flushPatch.Unpatch() + + err := cache.Close() + if err != expectedErr { + t.Errorf("TestDBCacheCloseErrors: Expected error on bad flush is %s but got %s", expectedErr, err) + } + if !closeCalled { + t.Errorf("TestDBCacheCloseErrors: ldb.Close was not called when error flushing") + } +} + +// TestUpdateDBErrors tests all error-cases in *dbCache.UpdateDB(). +// The non-error-cases are tested in the more general tests. +func TestUpdateDBErrors(t *testing.T) { + // Test when ldb.OpenTransaction returns error + func() { + cache := newTestDb("TestDBCacheCloseErrors", t).cache + defer cache.Close() + + patch := monkey.Patch((*leveldb.DB).OpenTransaction, + func(*leveldb.DB) (*leveldb.Transaction, error) { return nil, errors.New("error in OpenTransaction") }) + defer patch.Unpatch() + + err := cache.updateDB(func(ldbTx *leveldb.Transaction) error { return nil }) + if err == nil { + t.Errorf("No error in updateDB when ldb.OpenTransaction returns an error") + } + }() + + // Test when ldbTx.Commit returns an error + func() { + cache := newTestDb("TestDBCacheCloseErrors", t).cache + defer cache.Close() + + patch := monkey.Patch((*leveldb.Transaction).Commit, + func(*leveldb.Transaction) error { return errors.New("error in Commit") }) + defer patch.Unpatch() + + err := cache.updateDB(func(ldbTx *leveldb.Transaction) error { return nil }) + if err == nil { + t.Errorf("No error in updateDB when ldbTx.Commit returns an error") + } + }() + + cache := newTestDb("TestDBCacheCloseErrors", t).cache + defer cache.Close() + + // Test when function passed to updateDB returns an error + err := cache.updateDB(func(ldbTx *leveldb.Transaction) error { return errors.New("Error in fn") }) + if err == nil { + t.Errorf("No error in updateDB when passed function returns an error") + } +} + +// TestCommitTxFlushNeeded test the *dbCache.commitTx function when flush is needed, +// including error-cases. +// When flush is not needed is tested in the more general tests. +func TestCommitTxFlushNeeded(t *testing.T) { + tests := []struct { + name string + target interface{} + replacement interface{} + expectedError bool + }{ + {"No errors", nil, nil, false}, + {"Error in flush", (*dbCache).flush, func(*dbCache) error { return errors.New("error") }, true}, + {"Error in commitTreaps", (*dbCache).commitTreaps, + func(*dbCache, TreapForEacher, TreapForEacher) error { return errors.New("error") }, true}, + } + + for _, test := range tests { + func() { + db := newTestDb("TestDBCacheCloseErrors", t) + defer db.Close() + cache := db.cache + + cache.flushInterval = 0 // set flushInterval to 0 so that flush is always required + + if test.target != nil && test.replacement != nil { + patch := monkey.Patch(test.target, test.replacement) + defer patch.Unpatch() + } + + tx, err := db.Begin(true) + if err != nil { + t.Fatalf("Error begining transaction: %s", err) + } + cache.commitTx(tx.(*transaction)) + db.closeLock.RUnlock() + }() + } +} + +func TestExhaustedDbCacheIterator(t *testing.T) { + db := newTestDb("TestExhaustedDbCacheIterator", t) + defer db.Close() + + snapshot, err := db.cache.Snapshot() + if err != nil { + t.Fatalf("TestExhaustedDbCacheIterator: Error creating cache snapshot: %s", err) + } + iterator := snapshot.NewIterator(&ldbutil.Range{}) + + if next := iterator.Next(); next != false { + t.Errorf("TestExhaustedDbCacheIterator: Expected .Next() = false, but got %v", next) + } + + if prev := iterator.Prev(); prev != false { + t.Errorf("TestExhaustedDbCacheIterator: Expected .Prev() = false, but got %v", prev) + } + + if key := iterator.Key(); key != nil { + t.Errorf("TestExhaustedDbCacheIterator: Expected .Key() = nil, but got %v", key) + } + + if value := iterator.Value(); value != nil { + t.Errorf("TestExhaustedDbCacheIterator: Expected .Value() = nil, but got %v", value) + } +} + +// TestLDBIteratorImplPlaceholders hits functions that are there to implement leveldb iterator.Iterator interface, +// but surve no other purpose. +func TestLDBIteratorImplPlaceholders(t *testing.T) { + db := newTestDb("TestIteratorImplPlaceholders", t) + defer db.Close() + + snapshot, err := db.cache.Snapshot() + if err != nil { + t.Fatalf("TestLDBIteratorImplPlaceholders: Error creating cache snapshot: %s", err) + } + iterator := newLdbCacheIter(snapshot, &ldbutil.Range{}) + + if err = iterator.Error(); err != nil { + t.Errorf("TestLDBIteratorImplPlaceholders: Expected .Error() = nil, but got %v", err) + } + + // Call SetReleaser to achieve coverage of it. Actually does nothing + iterator.SetReleaser(nil) +} + +func TestSkipPendingUpdatesCache(t *testing.T) { + pdb := newTestDb("TestSkipPendingUpdatesCache", t) + defer pdb.Close() + + value := []byte("value") + // Add numbered prefixes to keys so that they are in expected order, and before any other keys + firstKey := []byte("1 - first") + toDeleteKey := []byte("2 - toDelete") + toUpdateKey := []byte("3 - toUpdate") + secondKey := []byte("4 - second") + + // create initial metadata for test + err := pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + if err := metadata.Put(firstKey, value); err != nil { + return err + } + if err := metadata.Put(toDeleteKey, value); err != nil { + return err + } + if err := metadata.Put(toUpdateKey, value); err != nil { + return err + } + if err := metadata.Put(secondKey, value); err != nil { + return err + } + + return nil + }) + if err != nil { + t.Fatalf("Error adding to metadata: %s", err) + } + + err = pdb.cache.flush() + if err != nil { + t.Fatalf("Error flushing cache: %s", err) + } + + // test skips + err = pdb.Update(func(dbTx database.Tx) error { + snapshot, err := pdb.cache.Snapshot() + if err != nil { + t.Fatalf("TestSkipPendingUpdatesCache: Error getting snapshot: %s", err) + } + + iterator := snapshot.NewIterator(&ldbutil.Range{}) + snapshot.pendingRemove = snapshot.pendingRemove.Put(bucketizedKey(metadataBucketID, toDeleteKey), value) + snapshot.pendingKeys = snapshot.pendingKeys.Put(bucketizedKey(metadataBucketID, toUpdateKey), value) + + // Check that first is ok + iterator.First() + expectedKey := bucketizedKey(metadataBucketID, firstKey) + actualKey := iterator.Key() + if !bytes.Equal(actualKey, expectedKey) { + t.Errorf("TestSkipPendingUpdatesCache: 1: key expected to be %v but is %v", expectedKey, actualKey) + } + + // Go to the next key, which is second, toDelete and toUpdate will be skipped + iterator.Next() + expectedKey = bucketizedKey(metadataBucketID, secondKey) + actualKey = iterator.Key() + if !bytes.Equal(actualKey, expectedKey) { + t.Errorf("TestSkipPendingUpdatesCache: 2: key expected to be %s but is %s", expectedKey, actualKey) + } + + // now traverse backwards - should get first, toUpdate and toDelete will be skipped + iterator.Prev() + expectedKey = bucketizedKey(metadataBucketID, firstKey) + actualKey = iterator.Key() + if !bytes.Equal(actualKey, expectedKey) { + t.Errorf("TestSkipPendingUpdatesCache: 4: key expected to be %s but is %s", expectedKey, actualKey) + } + + return nil + }) + if err != nil { + t.Fatalf("TestSkipPendingUpdatesCache: Error running main part of test: %s", err) + } +} + +// TestFlushCommitTreapsErrors tests error-cases in *dbCache.flush() when commitTreaps returns error. +// The non-error-cases are tested in the more general tests. +func TestFlushCommitTreapsErrors(t *testing.T) { + pdb := newTestDb("TestFlushCommitTreapsErrors", t) + defer pdb.Close() + + key := []byte("key") + value := []byte("value") + + // Before setting flush interval to zero - put some data so that there's something to flush + err := pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + metadata.Put(key, value) + + return nil + }) + if err != nil { + t.Fatalf("TestFlushCommitTreapsErrors: Error putting some data to flush: %s", err) + } + + cache := pdb.cache + cache.flushInterval = 0 // set flushInterval to 0 so that flush is always required + + // Test for correctness when encountered error on Put + func() { + patch := monkey.Patch((*leveldb.Transaction).Put, + func(*leveldb.Transaction, []byte, []byte, *opt.WriteOptions) error { return errors.New("error") }) + defer patch.Unpatch() + + err := pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + metadata.Put(key, value) + + return nil + }) + + if err == nil { + t.Errorf("TestFlushCommitTreapsErrors: No error from pdb.Update when ldbTx.Put returned error") + } + }() + + // Test for correctness when encountered error on Delete + + // First put some data we can later "fail" to delete + err = pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + metadata.Put(key, value) + + return nil + }) + if err != nil { + t.Fatalf("TestFlushCommitTreapsErrors: Error putting some data to delete: %s", err) + } + + // Now "fail" to delete it + func() { + patch := monkey.Patch((*leveldb.Transaction).Delete, + func(*leveldb.Transaction, []byte, *opt.WriteOptions) error { return errors.New("error") }) + defer patch.Unpatch() + + err := pdb.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() + metadata.Delete(key) + + return nil + }) + + if err == nil { + t.Errorf("TestFlushCommitTreapsErrors: No error from pdb.Update when ldbTx.Delete returned error") + } + }() +} diff --git a/database/ffldb/driver.go b/database/ffldb/driver.go index 24227f135..401ffa083 100644 --- a/database/ffldb/driver.go +++ b/database/ffldb/driver.go @@ -59,16 +59,3 @@ func createDBDriver(args ...interface{}) (database.DB, error) { return openDB(dbPath, network, true) } - -func init() { - // Register the driver. - driver := database.Driver{ - DbType: dbType, - Create: createDBDriver, - Open: openDBDriver, - } - if err := database.RegisterDriver(driver); err != nil { - panic(fmt.Sprintf("Failed to regiser database driver '%s': %v", - dbType, err)) - } -} diff --git a/database/ffldb/driver_test.go b/database/ffldb/driver_test.go index 81706a812..cd79929f7 100644 --- a/database/ffldb/driver_test.go +++ b/database/ffldb/driver_test.go @@ -113,7 +113,7 @@ func TestCreateOpenFail(t *testing.T) { db.Close() wantErrCode = database.ErrDbNotOpen - err = db.View(func(tx database.Tx) error { + err = db.View(func(dbTx database.Tx) error { return nil }) if !checkDbError(t, "View", err, wantErrCode) { @@ -121,7 +121,7 @@ func TestCreateOpenFail(t *testing.T) { } wantErrCode = database.ErrDbNotOpen - err = db.Update(func(tx database.Tx) error { + err = db.Update(func(dbTx database.Tx) error { return nil }) if !checkDbError(t, "Update", err, wantErrCode) { @@ -173,8 +173,8 @@ func TestPersistence(t *testing.T) { } genesisBlock := util.NewBlock(dagconfig.MainNetParams.GenesisBlock) genesisHash := dagconfig.MainNetParams.GenesisHash - err = db.Update(func(tx database.Tx) error { - metadataBucket := tx.Metadata() + err = db.Update(func(dbTx database.Tx) error { + metadataBucket := dbTx.Metadata() if metadataBucket == nil { return fmt.Errorf("Metadata: unexpected nil bucket") } @@ -193,7 +193,7 @@ func TestPersistence(t *testing.T) { } } - if err := tx.StoreBlock(genesisBlock); err != nil { + if err := dbTx.StoreBlock(genesisBlock); err != nil { return fmt.Errorf("StoreBlock: unexpected error: %v", err) } @@ -216,8 +216,8 @@ func TestPersistence(t *testing.T) { // Ensure the values previously stored in the 3rd namespace still exist // and are correct. - err = db.View(func(tx database.Tx) error { - metadataBucket := tx.Metadata() + err = db.View(func(dbTx database.Tx) error { + metadataBucket := dbTx.Metadata() if metadataBucket == nil { return fmt.Errorf("Metadata: unexpected nil bucket") } @@ -237,7 +237,7 @@ func TestPersistence(t *testing.T) { } genesisBlockBytes, _ := genesisBlock.Bytes() - gotBytes, err := tx.FetchBlock(genesisHash) + gotBytes, err := dbTx.FetchBlock(genesisHash) if err != nil { return fmt.Errorf("FetchBlock: unexpected error: %v", err) @@ -282,7 +282,9 @@ func TestInterface(t *testing.T) { // Change the maximum file size to a small value to force multiple flat // files with the test data set. - ffldb.TstRunWithMaxBlockFileSize(db, 2048, func() { + // Change maximum open files to small value to force shifts in the LRU + // mechanism + ffldb.TstRunWithMaxBlockFileSizeAndMaxOpenFiles(db, 2048, 10, func() { testInterface(t, db) }) } diff --git a/database/ffldb/export_test.go b/database/ffldb/export_test.go index caa02b1cd..d833552e4 100644 --- a/database/ffldb/export_test.go +++ b/database/ffldb/export_test.go @@ -16,11 +16,14 @@ import "github.com/daglabs/btcd/database" // TstRunWithMaxBlockFileSize runs the passed function with the maximum allowed // file size for the database set to the provided value. The value will be set // back to the original value upon completion. -func TstRunWithMaxBlockFileSize(idb database.DB, size uint32, fn func()) { +func TstRunWithMaxBlockFileSizeAndMaxOpenFiles(idb database.DB, size uint32, maxOpenFiles int, fn func()) { ffldb := idb.(*db) origSize := ffldb.store.maxBlockFileSize + origMaxOpenFiles := ffldb.store.maxOpenFiles ffldb.store.maxBlockFileSize = size + ffldb.store.maxOpenFiles = maxOpenFiles fn() ffldb.store.maxBlockFileSize = origSize + ffldb.store.maxOpenFiles = origMaxOpenFiles } diff --git a/database/ffldb/init.go b/database/ffldb/init.go new file mode 100644 index 000000000..f54139d8d --- /dev/null +++ b/database/ffldb/init.go @@ -0,0 +1,23 @@ +package ffldb + +import ( + "fmt" + + "github.com/daglabs/btcd/database" +) + +func registerDriver() { + driver := database.Driver{ + DbType: dbType, + Create: createDBDriver, + Open: openDBDriver, + } + if err := database.RegisterDriver(driver); err != nil { + panic(fmt.Sprintf("Failed to regiser database driver '%s': %v", + dbType, err)) + } +} + +func init() { + registerDriver() +} diff --git a/database/ffldb/init_test.go b/database/ffldb/init_test.go new file mode 100644 index 000000000..f5c1571b6 --- /dev/null +++ b/database/ffldb/init_test.go @@ -0,0 +1,26 @@ +package ffldb + +import ( + "errors" + "testing" + + "bou.ke/monkey" + "github.com/daglabs/btcd/database" +) + +// TestRegisterDriverErrors tests all error-cases in registerDriver(). +// The non-error-cases are tested in the more general tests. +func TestInitErrors(t *testing.T) { + patch := monkey.Patch(database.RegisterDriver, + func(driver database.Driver) error { return errors.New("Error in database.RegisterDriver") }) + defer patch.Unpatch() + + defer func() { + err := recover() + if err == nil { + t.Errorf("TestRegisterDriverErrors: No panic on init when database.RegisterDriver returned an error") + } + }() + + registerDriver() +} diff --git a/database/ffldb/interface_test.go b/database/ffldb/interface_test.go index 630fa0a51..08c53182a 100644 --- a/database/ffldb/interface_test.go +++ b/database/ffldb/interface_test.go @@ -626,10 +626,10 @@ func testBucketInterface(tc *testContext, bucket database.Bucket) bool { // would leave any manually created transactions with the database mutex locked // thereby leading to a deadlock and masking the real reason for the panic. It // also logs a test error and repanics so the original panic can be traced. -func rollbackOnPanic(t *testing.T, tx database.Tx) { +func rollbackOnPanic(t *testing.T, dbTx database.Tx) { if err := recover(); err != nil { t.Errorf("Unexpected panic: %v", err) - _ = tx.Rollback() + _ = dbTx.Rollback() panic(err) } } @@ -851,8 +851,8 @@ func testManagedTxPanics(tc *testContext) bool { // Ensure calling Commit on a managed read-only transaction panics. paniced := testPanic(func() { - tc.db.View(func(tx database.Tx) error { - tx.Commit() + tc.db.View(func(dbTx database.Tx) error { + dbTx.Commit() return nil }) }) @@ -863,8 +863,8 @@ func testManagedTxPanics(tc *testContext) bool { // Ensure calling Rollback on a managed read-only transaction panics. paniced = testPanic(func() { - tc.db.View(func(tx database.Tx) error { - tx.Rollback() + tc.db.View(func(dbTx database.Tx) error { + dbTx.Rollback() return nil }) }) @@ -875,8 +875,8 @@ func testManagedTxPanics(tc *testContext) bool { // Ensure calling Commit on a managed read-write transaction panics. paniced = testPanic(func() { - tc.db.Update(func(tx database.Tx) error { - tx.Commit() + tc.db.Update(func(dbTx database.Tx) error { + dbTx.Commit() return nil }) }) @@ -887,8 +887,8 @@ func testManagedTxPanics(tc *testContext) bool { // Ensure calling Rollback on a managed read-write transaction panics. paniced = testPanic(func() { - tc.db.Update(func(tx database.Tx) error { - tx.Rollback() + tc.db.Update(func(dbTx database.Tx) error { + dbTx.Rollback() return nil }) }) @@ -909,8 +909,8 @@ func testMetadataTxInterface(tc *testContext) bool { } bucket1Name := []byte("bucket1") - err := tc.db.Update(func(tx database.Tx) error { - _, err := tx.Metadata().CreateBucket(bucket1Name) + err := tc.db.Update(func(dbTx database.Tx) error { + _, err := dbTx.Metadata().CreateBucket(bucket1Name) return err }) if err != nil { @@ -932,8 +932,8 @@ func testMetadataTxInterface(tc *testContext) bool { } // Test the bucket interface via a managed read-only transaction. - err = tc.db.View(func(tx database.Tx) error { - metadataBucket := tx.Metadata() + err = tc.db.View(func(dbTx database.Tx) error { + metadataBucket := dbTx.Metadata() if metadataBucket == nil { return fmt.Errorf("Metadata: unexpected nil bucket") } @@ -960,7 +960,7 @@ func testMetadataTxInterface(tc *testContext) bool { // Ensure errors returned from the user-supplied View function are // returned. viewError := fmt.Errorf("example view error") - err = tc.db.View(func(tx database.Tx) error { + err = tc.db.View(func(dbTx database.Tx) error { return viewError }) if err != viewError { @@ -973,8 +973,8 @@ func testMetadataTxInterface(tc *testContext) bool { // Also, put a series of values and force a rollback so the following // code can ensure the values were not stored. forceRollbackError := fmt.Errorf("force rollback") - err = tc.db.Update(func(tx database.Tx) error { - metadataBucket := tx.Metadata() + err = tc.db.Update(func(dbTx database.Tx) error { + metadataBucket := dbTx.Metadata() if metadataBucket == nil { return fmt.Errorf("Metadata: unexpected nil bucket") } @@ -1008,8 +1008,8 @@ func testMetadataTxInterface(tc *testContext) bool { // Ensure the values that should not have been stored due to the forced // rollback above were not actually stored. - err = tc.db.View(func(tx database.Tx) error { - metadataBucket := tx.Metadata() + err = tc.db.View(func(dbTx database.Tx) error { + metadataBucket := dbTx.Metadata() if metadataBucket == nil { return fmt.Errorf("Metadata: unexpected nil bucket") } @@ -1028,8 +1028,8 @@ func testMetadataTxInterface(tc *testContext) bool { } // Store a series of values via a managed read-write transaction. - err = tc.db.Update(func(tx database.Tx) error { - metadataBucket := tx.Metadata() + err = tc.db.Update(func(dbTx database.Tx) error { + metadataBucket := dbTx.Metadata() if metadataBucket == nil { return fmt.Errorf("Metadata: unexpected nil bucket") } @@ -1053,8 +1053,8 @@ func testMetadataTxInterface(tc *testContext) bool { } // Ensure the values stored above were committed as expected. - err = tc.db.View(func(tx database.Tx) error { - metadataBucket := tx.Metadata() + err = tc.db.View(func(dbTx database.Tx) error { + metadataBucket := dbTx.Metadata() if metadataBucket == nil { return fmt.Errorf("Metadata: unexpected nil bucket") } @@ -1078,8 +1078,8 @@ func testMetadataTxInterface(tc *testContext) bool { } // Clean up the values stored above in a managed read-write transaction. - err = tc.db.Update(func(tx database.Tx) error { - metadataBucket := tx.Metadata() + err = tc.db.Update(func(dbTx database.Tx) error { + metadataBucket := dbTx.Metadata() if metadataBucket == nil { return fmt.Errorf("Metadata: unexpected nil bucket") } @@ -1107,7 +1107,7 @@ func testMetadataTxInterface(tc *testContext) bool { // testFetchBlockIOMissing ensures that all of the block retrieval API functions // work as expected when requesting blocks that don't exist. -func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool { +func testFetchBlockIOMissing(tc *testContext, dbTx database.Tx) bool { wantErrCode := database.ErrBlockNotFound // --------------------- @@ -1132,7 +1132,7 @@ func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool { // Ensure FetchBlock returns expected error. testName := fmt.Sprintf("FetchBlock #%d on missing block", i) - _, err = tx.FetchBlock(blockHash) + _, err = dbTx.FetchBlock(blockHash) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1140,7 +1140,7 @@ func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool { // Ensure FetchBlockHeader returns expected error. testName = fmt.Sprintf("FetchBlockHeader #%d on missing block", i) - _, err = tx.FetchBlockHeader(blockHash) + _, err = dbTx.FetchBlockHeader(blockHash) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1153,13 +1153,13 @@ func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool { Len: uint32(txLocs[0].TxLen), } allBlockRegions[i] = region - _, err = tx.FetchBlockRegion(®ion) + _, err = dbTx.FetchBlockRegion(®ion) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } // Ensure HasBlock returns false. - hasBlock, err := tx.HasBlock(blockHash) + hasBlock, err := dbTx.HasBlock(blockHash) if err != nil { tc.t.Errorf("HasBlock #%d: unexpected err: %v", i, err) return false @@ -1176,27 +1176,27 @@ func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool { // Ensure FetchBlocks returns expected error. testName := "FetchBlocks on missing blocks" - _, err := tx.FetchBlocks(allBlockHashes) + _, err := dbTx.FetchBlocks(allBlockHashes) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } // Ensure FetchBlockHeaders returns expected error. testName = "FetchBlockHeaders on missing blocks" - _, err = tx.FetchBlockHeaders(allBlockHashes) + _, err = dbTx.FetchBlockHeaders(allBlockHashes) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } // Ensure FetchBlockRegions returns expected error. testName = "FetchBlockRegions on missing blocks" - _, err = tx.FetchBlockRegions(allBlockRegions) + _, err = dbTx.FetchBlockRegions(allBlockRegions) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } // Ensure HasBlocks returns false for all blocks. - hasBlocks, err := tx.HasBlocks(allBlockHashes) + hasBlocks, err := dbTx.HasBlocks(allBlockHashes) if err != nil { tc.t.Errorf("HasBlocks: unexpected err: %v", err) } @@ -1215,7 +1215,7 @@ func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool { // the database, or at least stored into the the passed transaction. It also // tests several error conditions such as ensuring the expected errors are // returned when fetching blocks, headers, and regions that don't exist. -func testFetchBlockIO(tc *testContext, tx database.Tx) bool { +func testFetchBlockIO(tc *testContext, dbTx database.Tx) bool { // --------------------- // Non-bulk Block IO API // --------------------- @@ -1251,7 +1251,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { // Ensure the block data fetched from the database matches the // expected bytes. - gotBlockBytes, err := tx.FetchBlock(blockHash) + gotBlockBytes, err := dbTx.FetchBlock(blockHash) if err != nil { tc.t.Errorf("FetchBlock(%s): unexpected error: %v", blockHash, err) @@ -1264,7 +1264,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { } wantHeaderBytes := blockBytes[0:allBlockHeaderSizes[i]] - gotHeaderBytes, err := tx.FetchBlockHeader(blockHash) + gotHeaderBytes, err := dbTx.FetchBlockHeader(blockHash) if err != nil { tc.t.Errorf("FetchBlockHeader(%s): unexpected error: %v", blockHash, err) @@ -1287,7 +1287,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { allBlockRegions[i] = region endRegionOffset := region.Offset + region.Len wantRegionBytes := blockBytes[region.Offset:endRegionOffset] - gotRegionBytes, err := tx.FetchBlockRegion(®ion) + gotRegionBytes, err := dbTx.FetchBlockRegion(®ion) if err != nil { tc.t.Errorf("FetchBlockRegion(%s): unexpected error: %v", blockHash, err) @@ -1302,7 +1302,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { // Ensure the block header fetched from the database matches the // expected bytes. - hasBlock, err := tx.HasBlock(blockHash) + hasBlock, err := dbTx.HasBlock(blockHash) if err != nil { tc.t.Errorf("HasBlock(%s): unexpected error: %v", blockHash, err) @@ -1324,7 +1324,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { testName := fmt.Sprintf("FetchBlock(%s) invalid block", badBlockHash) wantErrCode := database.ErrBlockNotFound - _, err = tx.FetchBlock(badBlockHash) + _, err = dbTx.FetchBlock(badBlockHash) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1333,7 +1333,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { // the expected error. testName = fmt.Sprintf("FetchBlockHeader(%s) invalid block", badBlockHash) - _, err = tx.FetchBlockHeader(badBlockHash) + _, err = dbTx.FetchBlockHeader(badBlockHash) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1345,7 +1345,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { wantErrCode = database.ErrBlockNotFound region.Hash = badBlockHash region.Offset = ^uint32(0) - _, err = tx.FetchBlockRegion(®ion) + _, err = dbTx.FetchBlockRegion(®ion) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1357,7 +1357,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { wantErrCode = database.ErrBlockRegionInvalid region.Hash = blockHash region.Offset = ^uint32(0) - _, err = tx.FetchBlockRegion(®ion) + _, err = dbTx.FetchBlockRegion(®ion) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1369,7 +1369,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { // Ensure the bulk block data fetched from the database matches the // expected bytes. - blockData, err := tx.FetchBlocks(allBlockHashes) + blockData, err := dbTx.FetchBlocks(allBlockHashes) if err != nil { tc.t.Errorf("FetchBlocks: unexpected error: %v", err) return false @@ -1393,7 +1393,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { // Ensure the bulk block headers fetched from the database match the // expected bytes. - blockHeaderData, err := tx.FetchBlockHeaders(allBlockHashes) + blockHeaderData, err := dbTx.FetchBlockHeaders(allBlockHashes) if err != nil { tc.t.Errorf("FetchBlockHeaders: unexpected error: %v", err) return false @@ -1418,7 +1418,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { // Ensure the first transaction of every block fetched in bulk block // regions from the database matches the expected bytes. - allRegionBytes, err := tx.FetchBlockRegions(allBlockRegions) + allRegionBytes, err := dbTx.FetchBlockRegions(allBlockRegions) if err != nil { tc.t.Errorf("FetchBlockRegions: unexpected error: %v", err) return false @@ -1444,7 +1444,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { // Ensure the bulk determination of whether a set of block hashes are in // the database returns true for all loaded blocks. - hasBlocks, err := tx.HasBlocks(allBlockHashes) + hasBlocks, err := dbTx.HasBlocks(allBlockHashes) if err != nil { tc.t.Errorf("HasBlocks: unexpected error: %v", err) return false @@ -1467,7 +1467,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { copy(badBlockHashes, allBlockHashes) badBlockHashes[len(badBlockHashes)-1] = daghash.Hash{} wantErrCode := database.ErrBlockNotFound - _, err = tx.FetchBlocks(badBlockHashes) + _, err = dbTx.FetchBlocks(badBlockHashes) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1475,7 +1475,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { // Ensure fetching block headers for which one doesn't exist returns the // expected error. testName = "FetchBlockHeaders invalid hash" - _, err = tx.FetchBlockHeaders(badBlockHashes) + _, err = dbTx.FetchBlockHeaders(badBlockHashes) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1487,7 +1487,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { copy(badBlockRegions, allBlockRegions) badBlockRegions[len(badBlockRegions)-1].Hash = &daghash.Hash{} wantErrCode = database.ErrBlockNotFound - _, err = tx.FetchBlockRegions(badBlockRegions) + _, err = dbTx.FetchBlockRegions(badBlockRegions) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1500,7 +1500,7 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { badBlockRegions[i].Offset = ^uint32(0) } wantErrCode = database.ErrBlockRegionInvalid - _, err = tx.FetchBlockRegions(badBlockRegions) + _, err = dbTx.FetchBlockRegions(badBlockRegions) return checkDbError(tc.t, testName, err, wantErrCode) } @@ -1510,11 +1510,11 @@ func testFetchBlockIO(tc *testContext, tx database.Tx) bool { func testBlockIOTxInterface(tc *testContext) bool { // Ensure attempting to store a block with a read-only transaction fails // with the expected error. - err := tc.db.View(func(tx database.Tx) error { + err := tc.db.View(func(dbTx database.Tx) error { wantErrCode := database.ErrTxNotWritable for i, block := range tc.blocks { testName := fmt.Sprintf("StoreBlock(%d) on ro tx", i) - err := tx.StoreBlock(block) + err := dbTx.StoreBlock(block) if !checkDbError(tc.t, testName, err, wantErrCode) { return errSubTestFail } @@ -1534,10 +1534,10 @@ func testBlockIOTxInterface(tc *testContext) bool { // commit or rollback. Then, force a rollback so the code below can // ensure none of the data actually gets stored. forceRollbackError := fmt.Errorf("force rollback") - err = tc.db.Update(func(tx database.Tx) error { + err = tc.db.Update(func(dbTx database.Tx) error { // Store all blocks in the same transaction. for i, block := range tc.blocks { - err := tx.StoreBlock(block) + err := dbTx.StoreBlock(block) if err != nil { tc.t.Errorf("StoreBlock #%d: unexpected error: "+ "%v", i, err) @@ -1551,7 +1551,7 @@ func testBlockIOTxInterface(tc *testContext) bool { for i, block := range tc.blocks { testName := fmt.Sprintf("duplicate block entry #%d "+ "(before commit)", i) - err := tx.StoreBlock(block) + err := dbTx.StoreBlock(block) if !checkDbError(tc.t, testName, err, wantErrCode) { return errSubTestFail } @@ -1559,7 +1559,7 @@ func testBlockIOTxInterface(tc *testContext) bool { // Ensure that all data fetches from the stored blocks before // the transaction has been committed work as expected. - if !testFetchBlockIO(tc, tx) { + if !testFetchBlockIO(tc, dbTx) { return errSubTestFail } @@ -1576,8 +1576,8 @@ func testBlockIOTxInterface(tc *testContext) bool { } // Ensure rollback was successful - err = tc.db.View(func(tx database.Tx) error { - if !testFetchBlockIOMissing(tc, tx) { + err = tc.db.View(func(dbTx database.Tx) error { + if !testFetchBlockIOMissing(tc, dbTx) { return errSubTestFail } return nil @@ -1591,10 +1591,10 @@ func testBlockIOTxInterface(tc *testContext) bool { // Populate the database with loaded blocks and ensure all of the data // fetching APIs work properly. - err = tc.db.Update(func(tx database.Tx) error { + err = tc.db.Update(func(dbTx database.Tx) error { // Store a bunch of blocks in the same transaction. for i, block := range tc.blocks { - err := tx.StoreBlock(block) + err := dbTx.StoreBlock(block) if err != nil { tc.t.Errorf("StoreBlock #%d: unexpected error: "+ "%v", i, err) @@ -1609,7 +1609,7 @@ func testBlockIOTxInterface(tc *testContext) bool { testName := fmt.Sprintf("duplicate block entry #%d "+ "(before commit)", i) wantErrCode := database.ErrBlockExists - err := tx.StoreBlock(block) + err := dbTx.StoreBlock(block) if !checkDbError(tc.t, testName, err, wantErrCode) { return errSubTestFail } @@ -1617,7 +1617,7 @@ func testBlockIOTxInterface(tc *testContext) bool { // Ensure that all data fetches from the stored blocks before // the transaction has been committed work as expected. - if !testFetchBlockIO(tc, tx) { + if !testFetchBlockIO(tc, dbTx) { return errSubTestFail } @@ -1633,8 +1633,8 @@ func testBlockIOTxInterface(tc *testContext) bool { // Ensure all data fetch tests work as expected using a managed // read-only transaction after the data was successfully committed // above. - err = tc.db.View(func(tx database.Tx) error { - if !testFetchBlockIO(tc, tx) { + err = tc.db.View(func(dbTx database.Tx) error { + if !testFetchBlockIO(tc, dbTx) { return errSubTestFail } @@ -1650,8 +1650,8 @@ func testBlockIOTxInterface(tc *testContext) bool { // Ensure all data fetch tests work as expected using a managed // read-write transaction after the data was successfully committed // above. - err = tc.db.Update(func(tx database.Tx) error { - if !testFetchBlockIO(tc, tx) { + err = tc.db.Update(func(dbTx database.Tx) error { + if !testFetchBlockIO(tc, dbTx) { return errSubTestFail } @@ -1663,7 +1663,7 @@ func testBlockIOTxInterface(tc *testContext) bool { for i, block := range tc.blocks { testName := fmt.Sprintf("duplicate block entry #%d "+ "(before commit)", i) - err := tx.StoreBlock(block) + err := dbTx.StoreBlock(block) if !checkDbError(tc.t, testName, err, wantErrCode) { return errSubTestFail } @@ -1683,10 +1683,10 @@ func testBlockIOTxInterface(tc *testContext) bool { // testClosedTxInterface ensures that both the metadata and block IO API // functions behave as expected when attempted against a closed transaction. -func testClosedTxInterface(tc *testContext, tx database.Tx) bool { +func testClosedTxInterface(tc *testContext, dbTx database.Tx) bool { wantErrCode := database.ErrTxClosed - bucket := tx.Metadata() - cursor := tx.Metadata().Cursor() + bucket := dbTx.Metadata() + cursor := dbTx.Metadata().Cursor() bucketName := []byte("closedtxbucket") keyName := []byte("closedtxkey") @@ -1852,21 +1852,21 @@ func testClosedTxInterface(tc *testContext, tx database.Tx) bool { // Ensure StoreBlock returns expected error. testName = "StoreBlock on closed tx" - err = tx.StoreBlock(block) + err = dbTx.StoreBlock(block) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } // Ensure FetchBlock returns expected error. testName = fmt.Sprintf("FetchBlock #%d on closed tx", i) - _, err = tx.FetchBlock(blockHash) + _, err = dbTx.FetchBlock(blockHash) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } // Ensure FetchBlockHeader returns expected error. testName = fmt.Sprintf("FetchBlockHeader #%d on closed tx", i) - _, err = tx.FetchBlockHeader(blockHash) + _, err = dbTx.FetchBlockHeader(blockHash) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1879,14 +1879,14 @@ func testClosedTxInterface(tc *testContext, tx database.Tx) bool { Len: uint32(txLocs[0].TxLen), } allBlockRegions[i] = region - _, err = tx.FetchBlockRegion(®ion) + _, err = dbTx.FetchBlockRegion(®ion) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } // Ensure HasBlock returns expected error. testName = fmt.Sprintf("HasBlock #%d on closed tx", i) - _, err = tx.HasBlock(blockHash) + _, err = dbTx.HasBlock(blockHash) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1898,28 +1898,28 @@ func testClosedTxInterface(tc *testContext, tx database.Tx) bool { // Ensure FetchBlocks returns expected error. testName = "FetchBlocks on closed tx" - _, err = tx.FetchBlocks(allBlockHashes) + _, err = dbTx.FetchBlocks(allBlockHashes) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } // Ensure FetchBlockHeaders returns expected error. testName = "FetchBlockHeaders on closed tx" - _, err = tx.FetchBlockHeaders(allBlockHashes) + _, err = dbTx.FetchBlockHeaders(allBlockHashes) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } // Ensure FetchBlockRegions returns expected error. testName = "FetchBlockRegions on closed tx" - _, err = tx.FetchBlockRegions(allBlockRegions) + _, err = dbTx.FetchBlockRegions(allBlockRegions) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } // Ensure HasBlocks returns expected error. testName = "HasBlocks on closed tx" - _, err = tx.HasBlocks(allBlockHashes) + _, err = dbTx.HasBlocks(allBlockHashes) if !checkDbError(tc.t, testName, err, wantErrCode) { return false } @@ -1930,11 +1930,11 @@ func testClosedTxInterface(tc *testContext, tx database.Tx) bool { // Ensure that attempting to rollback or commit a transaction that is // already closed returns the expected error. - err = tx.Rollback() + err = dbTx.Rollback() if !checkDbError(tc.t, "closed tx rollback", err, wantErrCode) { return false } - err = tx.Commit() + err = dbTx.Commit() return checkDbError(tc.t, "closed tx commit", err, wantErrCode) } @@ -2003,8 +2003,8 @@ func testConcurrecy(tc *testContext) bool { // help prevent durations that are too short which would cause erroneous // test failures on slower systems. startTime := time.Now() - err := tc.db.View(func(tx database.Tx) error { - _, err := tx.FetchBlock(tc.blocks[0].Hash()) + err := tc.db.View(func(dbTx database.Tx) error { + _, err := dbTx.FetchBlock(tc.blocks[0].Hash()) return err }) if err != nil { @@ -2024,9 +2024,9 @@ func testConcurrecy(tc *testContext) bool { numReaders := len(tc.blocks) resultChan := make(chan bool, numReaders) reader := func(blockNum int) { - err := tc.db.View(func(tx database.Tx) error { + err := tc.db.View(func(dbTx database.Tx) error { time.Sleep(sleepTime) - _, err := tx.FetchBlock(tc.blocks[blockNum].Hash()) + _, err := dbTx.FetchBlock(tc.blocks[blockNum].Hash()) return err }) if err != nil { @@ -2092,7 +2092,7 @@ func testConcurrecy(tc *testContext) bool { started := make(chan struct{}) writeComplete := make(chan struct{}) reader = func(blockNum int) { - err := tc.db.View(func(tx database.Tx) error { + err := tc.db.View(func(dbTx database.Tx) error { started <- struct{}{} // Wait for the writer to complete. @@ -2100,7 +2100,7 @@ func testConcurrecy(tc *testContext) bool { // Since this reader was created before the write took // place, the data it added should not be visible. - val := tx.Metadata().Get(concurrentKey) + val := dbTx.Metadata().Get(concurrentKey) if val != nil { return fmt.Errorf("%s should not be visible", concurrentKey) @@ -2124,8 +2124,8 @@ func testConcurrecy(tc *testContext) bool { // All readers are started and waiting for completion of the writer. // Set some data the readers are expecting to not find and signal the // readers the write is done by closing the writeComplete channel. - err = tc.db.Update(func(tx database.Tx) error { - return tx.Metadata().Put(concurrentKey, concurrentVal) + err = tc.db.Update(func(dbTx database.Tx) error { + return dbTx.Metadata().Put(concurrentKey, concurrentVal) }) if err != nil { tc.t.Errorf("Unexpected error in update: %v", err) @@ -2145,7 +2145,7 @@ func testConcurrecy(tc *testContext) bool { // can be active at a time. writeSleepTime := time.Millisecond * 250 writer := func() { - err := tc.db.Update(func(tx database.Tx) error { + err := tc.db.Update(func(dbTx database.Tx) error { time.Sleep(writeSleepTime) return nil }) @@ -2195,7 +2195,7 @@ func testConcurrentClose(tc *testContext) bool { finishReaders := make(chan struct{}) resultChan := make(chan bool, numReaders+1) reader := func() { - err := tc.db.View(func(tx database.Tx) error { + err := tc.db.View(func(dbTx database.Tx) error { atomic.AddInt32(&activeReaders, 1) started <- struct{}{} <-finishReaders diff --git a/database/ffldb/reconcile.go b/database/ffldb/reconcile.go index 2fb3a37b7..67052e887 100644 --- a/database/ffldb/reconcile.go +++ b/database/ffldb/reconcile.go @@ -60,8 +60,8 @@ func reconcileDB(pdb *db, create bool) (database.DB, error) { // Load the current write cursor position from the metadata. var curFileNum, curOffset uint32 - err := pdb.View(func(tx database.Tx) error { - writeRow := tx.Metadata().Get(writeLocKeyName) + err := pdb.View(func(dbTx database.Tx) error { + writeRow := dbTx.Metadata().Get(writeLocKeyName) if writeRow == nil { str := "write cursor does not exist" return makeDbErr(database.ErrCorruption, str, nil) diff --git a/database/ffldb/reconcile_test.go b/database/ffldb/reconcile_test.go new file mode 100644 index 000000000..3e3b49011 --- /dev/null +++ b/database/ffldb/reconcile_test.go @@ -0,0 +1,180 @@ +package ffldb + +import ( + "reflect" + "testing" + + "bou.ke/monkey" +) + +func TestSerializeWriteRow(t *testing.T) { + tests := []struct { + curBlockFileNum uint32 + curFileOffset uint32 + expectedWriteRow []byte + }{ + // WriteRow format: + // First 4 bytes: curBlockFileNum + // Next 4 bytes: curFileOffset + // Next 4 bytes: Castagnoli CRC-32 checksum + // One can easily calculate checksums using the following code: + // https://play.golang.org/p/zoMKT-ORyF9 + {0, 0, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0xB2, 0x28, 0x8C}}, + {10, 11, []byte{0x0A, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0xC1, 0xA6, 0x0D, 0xC8}}, + } + + for i, test := range tests { + actualWriteRow := serializeWriteRow(test.curBlockFileNum, test.curFileOffset) + + if !reflect.DeepEqual(test.expectedWriteRow, actualWriteRow) { + t.Errorf("TestSerializeWriteRow: %d: Expected: %v, but got: %v", + i, test.expectedWriteRow, actualWriteRow) + } + } +} + +func TestDeserializeWriteRow(t *testing.T) { + tests := []struct { + writeRow []byte + expectedCurBlockFileNum uint32 + expectedCurFileOffset uint32 + expectedError bool + }{ + // WriteRow format: + // First 4 bytes: curBlockFileNum + // Next 4 bytes: curFileOffset + // Next 4 bytes: Castagnoli CRC-32 checksum + // One can easily calculate checksums using the following code: + // https://play.golang.org/p/zoMKT-ORyF9 + {[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0xB2, 0x28, 0x8C}, 0, 0, false}, + {[]byte{0x0A, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0xC1, 0xA6, 0x0D, 0xC8}, 10, 11, false}, + {[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0xB2, 0x28, 0x8D}, 0, 0, true}, + {[]byte{0x0A, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0, 0, true}, + } + + for i, test := range tests { + actualCurBlockFileNum, actualCurFileOffset, err := deserializeWriteRow(test.writeRow) + + if (err != nil) != test.expectedError { + t.Errorf("TestDeserializeWriteRow: %d: Expected error status: %t, but got: %t", + i, test.expectedError, err != nil) + } + + if test.expectedCurBlockFileNum != actualCurBlockFileNum { + t.Errorf("TestDeserializeWriteRow: %d: Expected curBlockFileNum: %d, but got: %d", + i, test.expectedCurBlockFileNum, actualCurBlockFileNum) + } + + if test.expectedCurFileOffset != actualCurFileOffset { + t.Errorf("TestDeserializeWriteRow: %d: Expected curFileOffset: %d, but got: %d", + i, test.expectedCurFileOffset, actualCurFileOffset) + } + } +} + +// setWriteRow is a low-level helper method to update the write row in the +// metadata bucket to enable certain test-cases in TestReconcileErrors +// if writeRow = nil deletes the write row altogether +func setWriteRow(pdb *db, writeRow []byte, t *testing.T) { + tx, err := pdb.begin(true) + if err != nil { + t.Fatalf("TestReconcileErrors: Error getting tx for setting "+ + "writeLoc in metadata: %s", err) + } + + if writeRow == nil { + tx.metaBucket.Delete(writeLocKeyName) + if err != nil { + t.Fatalf("TestReconcileErrors: Error deleting writeLoc from metadata: %s", + err) + } + } else { + tx.metaBucket.Put(writeLocKeyName, writeRow) + if err != nil { + t.Fatalf("TestReconcileErrors: Error updating writeLoc in metadata: %s", + err) + } + } + + err = pdb.cache.commitTx(tx) + if err != nil { + t.Fatalf("TestReconcileErrors: Error commiting the update of "+ + "writeLoc in metadata: %s", err) + } + + pdb.writeLock.Unlock() + pdb.closeLock.RUnlock() +} + +// TestReconcileErrors tests all error-cases in reconclieDB. +// The non-error-cases are tested in the more general tests. +func TestReconcileErrors(t *testing.T) { + // Set-up tests + pdb := newTestDb("TestReconcileErrors", t) + + // Test without writeLoc + setWriteRow(pdb, nil, t) + _, err := reconcileDB(pdb, false) + if err == nil { + t.Errorf("TestReconcileErrors: ReconcileDB() didn't error out when " + + "running without a writeRowLoc") + } + + // Test with writeLoc in metadata after the actual cursor position + setWriteRow(pdb, serializeWriteRow(1, 0), t) + _, err = reconcileDB(pdb, false) + if err == nil { + t.Errorf("TestReconcileErrors: ReconcileDB() didn't error out when " + + "curBlockFileNum after the actual cursor position") + } + setWriteRow(pdb, serializeWriteRow(0, 1), t) + _, err = reconcileDB(pdb, false) + if err == nil { + t.Errorf("TestReconcileErrors: ReconcileDB() didn't error out when " + + "curFileOffset after the actual cursor position") + } + + // Restore previous writeRow + setWriteRow(pdb, serializeWriteRow(0, 0), t) + + // Test with writeLoc in metadata before the actual cursor position + handleRollbackCalled := false + patch := monkey.Patch((*blockStore).handleRollback, + func(s *blockStore, oldBlockFileNum, oldBlockOffset uint32) { + handleRollbackCalled = true + }) + defer patch.Unpatch() + + pdb.store.writeCursor.curFileNum = 1 + _, err = reconcileDB(pdb, false) + if err != nil { + t.Errorf("TestReconcileErrors: Error in ReconcileDB() when curFileNum before " + + "the actual cursor position ") + } + if !handleRollbackCalled { + t.Errorf("TestReconcileErrors: handleRollback was not called when curFileNum before " + + "the actual cursor position ") + } + + pdb.store.writeCursor.curFileNum = 0 + pdb.store.writeCursor.curOffset = 1 + _, err = reconcileDB(pdb, false) + if err != nil { + t.Errorf("TestReconcileErrors: Error in ReconcileDB() when curOffset before " + + "the actual cursor position ") + } + if !handleRollbackCalled { + t.Errorf("TestReconcileErrors: handleRollback was not called when curOffset before " + + "the actual cursor position ") + } + + // Restore previous writeCursor location + pdb.store.writeCursor.curFileNum = 0 + pdb.store.writeCursor.curOffset = 0 + // Test create with closed DB to force initDB to fail + pdb.Close() + _, err = reconcileDB(pdb, true) + if err == nil { + t.Errorf("ReconcileDB didn't error out when running with closed db and create = true") + } +} diff --git a/database/ffldb/whitebox_test.go b/database/ffldb/whitebox_test.go index 6b6430f43..fa0048742 100644 --- a/database/ffldb/whitebox_test.go +++ b/database/ffldb/whitebox_test.go @@ -231,7 +231,7 @@ func TestCornerCases(t *testing.T) { // properly. testName = "View: underlying leveldb error" wantErrCode = database.ErrDbNotOpen - err = idb.View(func(tx database.Tx) error { + err = idb.View(func(dbTx database.Tx) error { return nil }) if !checkDbError(t, testName, err, wantErrCode) { @@ -241,7 +241,7 @@ func TestCornerCases(t *testing.T) { // Ensure the Update handles errors in the underlying leveldb database // properly. testName = "Update: underlying leveldb error" - err = idb.Update(func(tx database.Tx) error { + err = idb.Update(func(dbTx database.Tx) error { return nil }) if !checkDbError(t, testName, err, wantErrCode) { @@ -253,13 +253,13 @@ func TestCornerCases(t *testing.T) { // test context including all metadata and the mock files. func resetDatabase(tc *testContext) bool { // Reset the metadata. - err := tc.db.Update(func(tx database.Tx) error { + err := tc.db.Update(func(dbTx database.Tx) error { // Remove all the keys using a cursor while also generating a // list of buckets. It's not safe to remove keys during ForEach // iteration nor is it safe to remove buckets during cursor // iteration, so this dual approach is needed. var bucketNames [][]byte - cursor := tx.Metadata().Cursor() + cursor := dbTx.Metadata().Cursor() for ok := cursor.First(); ok; ok = cursor.Next() { if cursor.Value() != nil { if err := cursor.Delete(); err != nil { @@ -272,12 +272,12 @@ func resetDatabase(tc *testContext) bool { // Remove the buckets. for _, k := range bucketNames { - if err := tx.Metadata().DeleteBucket(k); err != nil { + if err := dbTx.Metadata().DeleteBucket(k); err != nil { return err } } - _, err := tx.Metadata().CreateBucket(blockIdxBucketName) + _, err := dbTx.Metadata().CreateBucket(blockIdxBucketName) return err }) if err != nil { @@ -360,9 +360,9 @@ func testWriteFailures(tc *testContext) bool { // file that fails the write fails when the transaction is // committed, not when the block is stored. tc.maxFileSizes = map[uint32]int64{test.fileNum: test.maxSize} - err := tc.db.Update(func(tx database.Tx) error { + err := tc.db.Update(func(dbTx database.Tx) error { for i, block := range tc.blocks { - err := tx.StoreBlock(block) + err := dbTx.StoreBlock(block) if err != nil { tc.t.Errorf("StoreBlock (%d): unexpected "+ "error: %v", i, err) @@ -423,8 +423,8 @@ func testBlockFileErrors(tc *testContext) bool { } // Insert the first block into the mock file. - err = tc.db.Update(func(tx database.Tx) error { - err := tx.StoreBlock(tc.blocks[0]) + err = tc.db.Update(func(dbTx database.Tx) error { + err := dbTx.StoreBlock(tc.blocks[0]) if err != nil { tc.t.Errorf("StoreBlock: unexpected error: %v", err) return errSubTestFail @@ -464,10 +464,10 @@ func testBlockFileErrors(tc *testContext) bool { // Ensure failures in FetchBlock and FetchBlockRegion(s) since the // underlying file they need to read from has been closed. - err = tc.db.View(func(tx database.Tx) error { + err = tc.db.View(func(dbTx database.Tx) error { testName = "FetchBlock closed file" wantErrCode := database.ErrDriverSpecific - _, err := tx.FetchBlock(block0Hash) + _, err := dbTx.FetchBlock(block0Hash) if !checkDbError(tc.t, testName, err, wantErrCode) { return errSubTestFail } @@ -480,13 +480,13 @@ func testBlockFileErrors(tc *testContext) bool { Offset: 0, }, } - _, err = tx.FetchBlockRegion(®ions[0]) + _, err = dbTx.FetchBlockRegion(®ions[0]) if !checkDbError(tc.t, testName, err, wantErrCode) { return errSubTestFail } testName = "FetchBlockRegions closed file" - _, err = tx.FetchBlockRegions(regions) + _, err = dbTx.FetchBlockRegions(regions) if !checkDbError(tc.t, testName, err, wantErrCode) { return errSubTestFail } @@ -511,8 +511,8 @@ func testCorruption(tc *testContext) bool { } // Insert the first block into the mock file. - err := tc.db.Update(func(tx database.Tx) error { - err := tx.StoreBlock(tc.blocks[0]) + err := tc.db.Update(func(dbTx database.Tx) error { + err := dbTx.StoreBlock(tc.blocks[0]) if err != nil { tc.t.Errorf("StoreBlock: unexpected error: %v", err) return errSubTestFail @@ -556,7 +556,7 @@ func testCorruption(tc *testContext) bool { // Random checksum byte. {uint32(len(block0Bytes)) + 10, false, database.ErrCorruption}, } - err = tc.db.View(func(tx database.Tx) error { + err = tc.db.View(func(dbTx database.Tx) error { data := tc.files[0].file.(*mockFile).data for i, test := range tests { // Corrupt the byte at the offset by a single bit. @@ -574,7 +574,7 @@ func testCorruption(tc *testContext) bool { testName := fmt.Sprintf("FetchBlock (test #%d): "+ "corruption", i) - _, err := tx.FetchBlock(block0Hash) + _, err := dbTx.FetchBlock(block0Hash) if !checkDbError(tc.t, testName, err, test.wantErrCode) { return errSubTestFail } diff --git a/server/p2p/p2p.go b/server/p2p/p2p.go index bb8b25294..fd92e656e 100644 --- a/server/p2p/p2p.go +++ b/server/p2p/p2p.go @@ -2132,8 +2132,8 @@ func (s *Server) Start() { func (s *Server) Stop() error { // Save fee estimator state in the database. - s.db.Update(func(tx database.Tx) error { - metadata := tx.Metadata() + s.db.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() metadata.Put(mempool.EstimateFeeDatabaseKey, s.FeeEstimator.Save()) return nil @@ -2396,8 +2396,8 @@ func NewServer(listenAddrs []string, db database.DB, dagParams *dagconfig.Params // Search for a FeeEstimator state in the database. If none can be found // or if it cannot be loaded, create a new one. - db.Update(func(tx database.Tx) error { - metadata := tx.Metadata() + db.Update(func(dbTx database.Tx) error { + metadata := dbTx.Metadata() feeEstimationData := metadata.Get(mempool.EstimateFeeDatabaseKey) if feeEstimationData != nil { // delete it from the database so that we don't try to restore the diff --git a/txscript/error.go b/txscript/error.go index c03c17452..c0f286e1c 100644 --- a/txscript/error.go +++ b/txscript/error.go @@ -314,6 +314,9 @@ func scriptError(c ErrorCode, desc string) Error { // IsErrorCode returns whether or not the provided error is a script error with // the provided error code. func IsErrorCode(err error, c ErrorCode) bool { - serr, ok := err.(Error) - return ok && serr.ErrorCode == c + if err, ok := err.(Error); ok { + return err.ErrorCode == c + } + + return false }