From 2e2492cc5d5f755ad97019b4a2893229044c2bef Mon Sep 17 00:00:00 2001 From: stasatdaglabs <39559713+stasatdaglabs@users.noreply.github.com> Date: Sun, 3 May 2020 12:19:09 +0300 Subject: [PATCH] [NOD-849] Database tests (#695) * [NOD-849] Cover ffldb/transaction with tests. * [NOD-849] Cover cursor.go with tests. * [NOD-849] Cover ldb/transaction with tests. * [NOD-849] Cover location.go with tests. * [NOD-849] Write TestFlatFileMultiFileRollback. * [NOD-849] Fix merge errors. * [NOD-849] Fix a comment. * [NOD-849] Fix a comment. * [NOD-849] Add a test that makes sure that files get deleted on rollback. * [NOD-849] Add a test that makes sure that serializeLocation serialized to an expected value. * [NOD-849] Improve TestFlatFileLocationDeserializationErrors. * [NOD-849] Fix a copy+paste error. * [NOD-849] Explain maxFileSize = 16. * [NOD-849] Remove redundant RollbackUnlessClosed call. * [NOD-849] Extract bucket to a variable in TestCursorSanity. * [NOD-849] Rename TestKeyValueTransactionCommit to TestTransactionCommitForLevelDBMethods. * [NOD-849] Extract prepareXXX into separate functions. * [NOD-849] Simplify function calls in TestTransactionCloseErrors. * [NOD-849] Extract validateCurrentCursorKeyAndValue to a separate function. * [NOD-849] Add a comment over TestCursorSanity. * [NOD-849] Add a comment over function in TestCursorCloseErrors. * [NOD-849] Add a comment over function in TestTransactionCloseErrors. * [NOD-849] Separate TestTransactionCloseErrors to TestTransactionCommitErrors and TestTransactionRollbackErrors. * [NOD-849] Separate TestTransactionCloseErrors to TestTransactionCommitErrors and TestTransactionRollbackErrors. * [NOD-849] Fix copy+paste error in comments. * [NOD-849] Fix merge errors. * [NOD-849] Merge TestTransactionCommitErrors and TestTransactionRollbackErrors into TestTransactionCloseErrors. * [NOD-849] Move prepareDatabaseForTest into ffldb_test.go. * [NOD-849] Add cursorKey to Value error messages in validateCurrentCursorKeyAndValue. --- database/ffldb/ff/flatfile.go | 6 +- database/ffldb/ff/flatfile_test.go | 117 +++++- database/ffldb/ff/location_test.go | 62 +++ database/ffldb/ffldb.go | 5 +- database/ffldb/ffldb_test.go | 22 ++ database/ffldb/ldb/cursor_test.go | 246 ++++++++++++ database/ffldb/ldb/leveldb_test.go | 54 ++- database/ffldb/ldb/transaction_test.go | 146 ++++++++ database/ffldb/transaction.go | 49 ++- database/ffldb/transaction_test.go | 500 +++++++++++++++++++++++++ 10 files changed, 1161 insertions(+), 46 deletions(-) create mode 100644 database/ffldb/ff/location_test.go create mode 100644 database/ffldb/ldb/cursor_test.go create mode 100644 database/ffldb/ldb/transaction_test.go create mode 100644 database/ffldb/transaction_test.go diff --git a/database/ffldb/ff/flatfile.go b/database/ffldb/ff/flatfile.go index 95070046e..f1c81adf2 100644 --- a/database/ffldb/ff/flatfile.go +++ b/database/ffldb/ff/flatfile.go @@ -16,12 +16,14 @@ const ( // cache. Note that this does not include the current/write file, so there // will typically be one more than this value open. maxOpenFiles = 25 +) +var ( // maxFileSize is the maximum size for each file used to store data. // // NOTE: The current code uses uint32 for all offsets, so this value - // must be less than 2^32 (4 GiB). This is also why it's a typed - // constant. + // must be less than 2^32 (4 GiB). + // NOTE: This is a var rather than a const for testing purposes. maxFileSize uint32 = 512 * 1024 * 1024 // 512 MiB ) diff --git a/database/ffldb/ff/flatfile_test.go b/database/ffldb/ff/flatfile_test.go index 3ac8ada80..eadf8be8a 100644 --- a/database/ffldb/ff/flatfile_test.go +++ b/database/ffldb/ff/flatfile_test.go @@ -1,24 +1,40 @@ package ff import ( + "bytes" + "github.com/kaspanet/kaspad/database" "io/ioutil" + "os" "reflect" "testing" ) -func TestFlatFileStoreSanity(t *testing.T) { - // Open a test store - path, err := ioutil.TempDir("", "TestFlatFileStoreSanity") +func prepareStoreForTest(t *testing.T, testName string) (store *flatFileStore, teardownFunc func()) { + // Create a temp db to run tests against + path, err := ioutil.TempDir("", testName) if err != nil { - t.Fatalf("TestFlatFileStoreSanity: TempDir unexpectedly "+ - "failed: %s", err) + t.Fatalf("%s: TempDir unexpectedly "+ + "failed: %s", testName, err) } name := "test" - store, err := openFlatFileStore(path, name) + store, err = openFlatFileStore(path, name) if err != nil { - t.Fatalf("TestFlatFileStoreSanity: openFlatFileStore "+ - "unexpectedly failed: %s", err) + t.Fatalf("%s: openFlatFileStore "+ + "unexpectedly failed: %s", testName, err) } + teardownFunc = func() { + err = store.Close() + if err != nil { + t.Fatalf("%s: Close unexpectedly "+ + "failed: %s", testName, err) + } + } + return store, teardownFunc +} + +func TestFlatFileStoreSanity(t *testing.T) { + store, teardownFunc := prepareStoreForTest(t, "TestFlatFileStoreSanity") + defer teardownFunc() // Write something to the store writeData := []byte("Hello world!") @@ -72,3 +88,88 @@ func TestFlatFilePath(t *testing.T) { } } } + +func TestFlatFileMultiFileRollback(t *testing.T) { + store, teardownFunc := prepareStoreForTest(t, "TestFlatFileMultiFileRollback") + defer teardownFunc() + + // Set the maxFileSize to 16 bytes so that we don't have to write + // an enormous amount of data to disk to get multiple files, all + // for the sake of this test. + currentMaxFileSize := maxFileSize + maxFileSize = 16 + defer func() { + maxFileSize = currentMaxFileSize + }() + + // Write five 8 byte chunks and keep the last location written to + var lastWriteLocation1 *flatFileLocation + for i := byte(0); i < 5; i++ { + writeData := []byte{i, i, i, i, i, i, i, i} + var err error + lastWriteLocation1, err = store.write(writeData) + if err != nil { + t.Fatalf("TestFlatFileMultiFileRollback: write returned "+ + "unexpected error: %s", err) + } + } + + // Grab the current location and the current file number + currentLocation := store.currentLocation() + fileNumberBeforeWriting := store.writeCursor.currentFileNumber + + // Write (2 * maxOpenFiles) more 8 byte chunks and keep the last location written to + var lastWriteLocation2 *flatFileLocation + for i := byte(0); i < byte(2*maxFileSize); i++ { + writeData := []byte{0, 1, 2, 3, 4, 5, 6, 7} + var err error + lastWriteLocation2, err = store.write(writeData) + if err != nil { + t.Fatalf("TestFlatFileMultiFileRollback: write returned "+ + "unexpected error: %s", err) + } + } + + // Grab the file number again to later make sure its file no longer exists + fileNumberAfterWriting := store.writeCursor.currentFileNumber + + // Rollback + err := store.rollback(currentLocation) + if err != nil { + t.Fatalf("TestFlatFileMultiFileRollback: rollback returned "+ + "unexpected error: %s", err) + } + + // Make sure that lastWriteLocation1 still exists + expectedData := []byte{4, 4, 4, 4, 4, 4, 4, 4} + data, err := store.read(lastWriteLocation1) + if err != nil { + t.Fatalf("TestFlatFileMultiFileRollback: read returned "+ + "unexpected error: %s", err) + } + if !bytes.Equal(data, expectedData) { + t.Fatalf("TestFlatFileMultiFileRollback: read returned "+ + "unexpected data. Want: %s, got: %s", string(expectedData), + string(data)) + } + + // Make sure that lastWriteLocation2 does NOT exist + _, err = store.read(lastWriteLocation2) + if err == nil { + t.Fatalf("TestFlatFileMultiFileRollback: read " + + "unexpectedly succeeded") + } + if !database.IsNotFoundError(err) { + t.Fatalf("TestFlatFileMultiFileRollback: read "+ + "returned unexpected error: %s", err) + } + + // Make sure that all the appropriate files have been deleted + for i := fileNumberAfterWriting; i > fileNumberBeforeWriting; i-- { + filePath := flatFilePath(store.basePath, store.storeName, i) + if _, err := os.Stat(filePath); err == nil || !os.IsNotExist(err) { + t.Fatalf("TestFlatFileMultiFileRollback: file "+ + "unexpectedly still exists: %s", filePath) + } + } +} diff --git a/database/ffldb/ff/location_test.go b/database/ffldb/ff/location_test.go new file mode 100644 index 000000000..7d15e4407 --- /dev/null +++ b/database/ffldb/ff/location_test.go @@ -0,0 +1,62 @@ +package ff + +import ( + "bytes" + "encoding/hex" + "reflect" + "strings" + "testing" +) + +func TestFlatFileLocationSerialization(t *testing.T) { + location := &flatFileLocation{ + fileNumber: 1, + fileOffset: 2, + dataLength: 3, + } + + serializedLocation := serializeLocation(location) + expectedSerializedLocation := []byte{1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0} + if !bytes.Equal(serializedLocation, expectedSerializedLocation) { + t.Fatalf("TestFlatFileLocationSerialization: serializeLocation "+ + "returned unexpected bytes. Want: %s, got: %s", + hex.EncodeToString(expectedSerializedLocation), hex.EncodeToString(serializedLocation)) + } + + deserializedLocation, err := deserializeLocation(serializedLocation) + if err != nil { + t.Fatalf("TestFlatFileLocationSerialization: deserializeLocation "+ + "unexpectedly failed: %s", err) + } + if !reflect.DeepEqual(deserializedLocation, location) { + t.Fatalf("TestFlatFileLocationSerialization: original "+ + "location and deserialized location aren't the same. Want: %v, "+ + "got: %v", location, deserializedLocation) + } +} + +func TestFlatFileLocationDeserializationErrors(t *testing.T) { + expectedError := "unexpected serializedLocation length" + + tooShortSerializedLocation := []byte{0, 1, 2, 3, 4, 5} + _, err := deserializeLocation(tooShortSerializedLocation) + if err == nil { + t.Fatalf("TestFlatFileLocationSerialization: deserializeLocation " + + "unexpectedly succeeded") + } + if !strings.Contains(err.Error(), expectedError) { + t.Fatalf("TestFlatFileLocationSerialization: deserializeLocation "+ + "returned unexpected error. Want: %s, got: %s", expectedError, err) + } + + tooLongSerializedLocation := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14} + _, err = deserializeLocation(tooLongSerializedLocation) + if err == nil { + t.Fatalf("TestFlatFileLocationSerialization: deserializeLocation " + + "unexpectedly succeeded") + } + if !strings.Contains(err.Error(), expectedError) { + t.Fatalf("TestFlatFileLocationSerialization: deserializeLocation "+ + "returned unexpected error. Want: %s, got: %s", expectedError, err) + } +} diff --git a/database/ffldb/ffldb.go b/database/ffldb/ffldb.go index 31b4a20f8..7e1c183ca 100644 --- a/database/ffldb/ffldb.go +++ b/database/ffldb/ffldb.go @@ -170,8 +170,9 @@ func (db *ffldb) Begin() (database.Transaction, error) { } transaction := &transaction{ - ldbTx: ldbTx, - ffdb: db.flatFileDB, + ldbTx: ldbTx, + ffdb: db.flatFileDB, + isClosed: false, } return transaction, nil } diff --git a/database/ffldb/ffldb_test.go b/database/ffldb/ffldb_test.go index c58cb4970..a97d27a68 100644 --- a/database/ffldb/ffldb_test.go +++ b/database/ffldb/ffldb_test.go @@ -7,6 +7,28 @@ import ( "testing" ) +func prepareDatabaseForTest(t *testing.T, testName string) (db database.Database, teardownFunc func()) { + // Create a temp db to run tests against + path, err := ioutil.TempDir("", testName) + if err != nil { + t.Fatalf("%s: TempDir unexpectedly "+ + "failed: %s", testName, err) + } + db, err = Open(path) + if err != nil { + t.Fatalf("%s: Open unexpectedly "+ + "failed: %s", testName, err) + } + teardownFunc = func() { + err = db.Close() + if err != nil { + t.Fatalf("%s: Close unexpectedly "+ + "failed: %s", testName, err) + } + } + return db, teardownFunc +} + func TestRepairFlatFiles(t *testing.T) { // Create a temp db to run tests against path, err := ioutil.TempDir("", "TestRepairFlatFiles") diff --git a/database/ffldb/ldb/cursor_test.go b/database/ffldb/ldb/cursor_test.go new file mode 100644 index 000000000..71d580c8c --- /dev/null +++ b/database/ffldb/ldb/cursor_test.go @@ -0,0 +1,246 @@ +package ldb + +import ( + "bytes" + "fmt" + "github.com/kaspanet/kaspad/database" + "reflect" + "strings" + "testing" +) + +func validateCurrentCursorKeyAndValue(t *testing.T, testName string, cursor *LevelDBCursor, + expectedKey *database.Key, expectedValue []byte) { + + cursorKey, err := cursor.Key() + if err != nil { + t.Fatalf("%s: Key "+ + "unexpectedly failed: %s", testName, err) + } + if !reflect.DeepEqual(cursorKey, expectedKey) { + t.Fatalf("%s: Key "+ + "returned wrong key. Want: %s, got: %s", + testName, string(expectedKey.Bytes()), string(cursorKey.Bytes())) + } + cursorValue, err := cursor.Value() + if err != nil { + t.Fatalf("%s: Value "+ + "unexpectedly failed for key %s: %s", + testName, cursorKey, err) + } + if !bytes.Equal(cursorValue, expectedValue) { + t.Fatalf("%s: Value "+ + "returned wrong value for key %s. Want: %s, got: %s", + testName, cursorKey, string(expectedValue), string(cursorValue)) + } +} + +func recoverFromClosedCursorPanic(t *testing.T, testName string) { + panicErr := recover() + if panicErr == nil { + t.Fatalf("%s: cursor unexpectedly "+ + "didn't panic after being closed", testName) + } + expectedPanicErr := "closed cursor" + if !strings.Contains(fmt.Sprintf("%v", panicErr), expectedPanicErr) { + t.Fatalf("%s: cursor panicked "+ + "with wrong message. Want: %v, got: %s", + testName, expectedPanicErr, panicErr) + } +} + +// TestCursorSanity validates typical cursor usage, including +// opening a cursor over some existing data, seeking back +// and forth over that data, and getting some keys/values out +// of the cursor. +func TestCursorSanity(t *testing.T) { + ldb, teardownFunc := prepareDatabaseForTest(t, "TestCursorSanity") + defer teardownFunc() + + // Write some data to the database + bucket := database.MakeBucket([]byte("bucket")) + for i := 0; i < 10; i++ { + key := fmt.Sprintf("key%d", i) + value := fmt.Sprintf("value%d", i) + err := ldb.Put(bucket.Key([]byte(key)), []byte(value)) + if err != nil { + t.Fatalf("TestCursorSanity: Put "+ + "unexpectedly failed: %s", err) + } + } + + // Open a new cursor + cursor := ldb.Cursor(bucket) + defer func() { + err := cursor.Close() + if err != nil { + t.Fatalf("TestCursorSanity: Close "+ + "unexpectedly failed: %s", err) + } + }() + + // Seek to first key and make sure its key and value are correct + hasNext := cursor.First() + if !hasNext { + t.Fatalf("TestCursorSanity: First " + + "unexpectedly returned non-existance") + } + expectedKey := bucket.Key([]byte("key0")) + expectedValue := []byte("value0") + validateCurrentCursorKeyAndValue(t, "TestCursorSanity", cursor, expectedKey, expectedValue) + + // Seek to a non-existant key + err := cursor.Seek(database.MakeBucket().Key([]byte("doesn't exist"))) + if err == nil { + t.Fatalf("TestCursorSanity: Seek " + + "unexpectedly succeeded") + } + if !database.IsNotFoundError(err) { + t.Fatalf("TestCursorSanity: Seek "+ + "returned wrong error: %s", err) + } + + // Seek to the last key + err = cursor.Seek(bucket.Key([]byte("key9"))) + if err != nil { + t.Fatalf("TestCursorSanity: Seek "+ + "unexpectedly failed: %s", err) + } + expectedKey = bucket.Key([]byte("key9")) + expectedValue = []byte("value9") + validateCurrentCursorKeyAndValue(t, "TestCursorSanity", cursor, expectedKey, expectedValue) + + // Call Next to get to the end of the cursor. This should + // return false to signify that there are no items after that. + // Key and Value calls should return ErrNotFound. + hasNext = cursor.Next() + if hasNext { + t.Fatalf("TestCursorSanity: Next " + + "after last value is unexpectedly not done") + } + _, err = cursor.Key() + if err == nil { + t.Fatalf("TestCursorSanity: Key " + + "unexpectedly succeeded") + } + if !database.IsNotFoundError(err) { + t.Fatalf("TestCursorSanity: Key "+ + "returned wrong error: %s", err) + } + _, err = cursor.Value() + if err == nil { + t.Fatalf("TestCursorSanity: Value " + + "unexpectedly succeeded") + } + if !database.IsNotFoundError(err) { + t.Fatalf("TestCursorSanity: Value "+ + "returned wrong error: %s", err) + } +} + +func TestCursorCloseErrors(t *testing.T) { + tests := []struct { + name string + + // function is the LevelDBCursor function that we're + // verifying returns an error after the cursor had + // been closed. + function func(dbTx *LevelDBCursor) error + }{ + { + name: "Seek", + function: func(cursor *LevelDBCursor) error { + return cursor.Seek(database.MakeBucket().Key([]byte{})) + }, + }, + { + name: "Key", + function: func(cursor *LevelDBCursor) error { + _, err := cursor.Key() + return err + }, + }, + { + name: "Value", + function: func(cursor *LevelDBCursor) error { + _, err := cursor.Value() + return err + }, + }, + { + name: "Close", + function: func(cursor *LevelDBCursor) error { + return cursor.Close() + }, + }, + } + + for _, test := range tests { + func() { + ldb, teardownFunc := prepareDatabaseForTest(t, "TestCursorCloseErrors") + defer teardownFunc() + + // Open a new cursor + cursor := ldb.Cursor(database.MakeBucket()) + + // Close the cursor + err := cursor.Close() + if err != nil { + t.Fatalf("TestCursorCloseErrors: Close "+ + "unexpectedly failed: %s", err) + } + + expectedErrContainsString := "closed cursor" + + // Make sure that the test function returns a "closed transaction" error + err = test.function(cursor) + if err == nil { + t.Fatalf("TestCursorCloseErrors: %s "+ + "unexpectedly succeeded", test.name) + } + if !strings.Contains(err.Error(), expectedErrContainsString) { + t.Fatalf("TestCursorCloseErrors: %s "+ + "returned wrong error. Want: %s, got: %s", + test.name, expectedErrContainsString, err) + } + }() + } +} + +func TestCursorCloseFirstAndNext(t *testing.T) { + ldb, teardownFunc := prepareDatabaseForTest(t, "TestCursorCloseFirstAndNext") + defer teardownFunc() + + // Write some data to the database + for i := 0; i < 10; i++ { + key := fmt.Sprintf("key%d", i) + value := fmt.Sprintf("value%d", i) + err := ldb.Put(database.MakeBucket([]byte("bucket")).Key([]byte(key)), []byte(value)) + if err != nil { + t.Fatalf("TestCursorCloseFirstAndNext: Put "+ + "unexpectedly failed: %s", err) + } + } + + // Open a new cursor + cursor := ldb.Cursor(database.MakeBucket([]byte("bucket"))) + + // Close the cursor + err := cursor.Close() + if err != nil { + t.Fatalf("TestCursorCloseFirstAndNext: Close "+ + "unexpectedly failed: %s", err) + } + + // We expect First to panic + func() { + defer recoverFromClosedCursorPanic(t, "TestCursorCloseFirstAndNext") + cursor.First() + }() + + // We expect Next to panic + func() { + defer recoverFromClosedCursorPanic(t, "TestCursorCloseFirstAndNext") + cursor.Next() + }() +} diff --git a/database/ffldb/ldb/leveldb_test.go b/database/ffldb/ldb/leveldb_test.go index 5e44d4b1b..efa00e186 100644 --- a/database/ffldb/ldb/leveldb_test.go +++ b/database/ffldb/ldb/leveldb_test.go @@ -7,30 +7,36 @@ import ( "testing" ) -func TestLevelDBSanity(t *testing.T) { - // Open a test db - path, err := ioutil.TempDir("", "TestLevelDBSanity") +func prepareDatabaseForTest(t *testing.T, testName string) (ldb *LevelDB, teardownFunc func()) { + // Create a temp db to run tests against + path, err := ioutil.TempDir("", testName) if err != nil { - t.Fatalf("TestLevelDBSanity: TempDir unexpectedly "+ - "failed: %s", err) + t.Fatalf("%s: TempDir unexpectedly "+ + "failed: %s", testName, err) } - ldb, err := NewLevelDB(path) + ldb, err = NewLevelDB(path) if err != nil { - t.Fatalf("TestLevelDBSanity: NewLevelDB "+ - "unexpectedly failed: %s", err) + t.Fatalf("%s: NewLevelDB unexpectedly "+ + "failed: %s", testName, err) } - defer func() { - err := ldb.Close() + teardownFunc = func() { + err = ldb.Close() if err != nil { - t.Fatalf("TestLevelDBSanity: Close "+ - "unexpectedly failed: %s", err) + t.Fatalf("%s: Close unexpectedly "+ + "failed: %s", testName, err) } - }() + } + return ldb, teardownFunc +} + +func TestLevelDBSanity(t *testing.T) { + ldb, teardownFunc := prepareDatabaseForTest(t, "TestLevelDBSanity") + defer teardownFunc() // Put something into the db key := database.MakeBucket().Key([]byte("key")) putData := []byte("Hello world!") - err = ldb.Put(key, putData) + err := ldb.Put(key, putData) if err != nil { t.Fatalf("TestLevelDBSanity: Put returned "+ "unexpected error: %s", err) @@ -52,24 +58,8 @@ func TestLevelDBSanity(t *testing.T) { } func TestLevelDBTransactionSanity(t *testing.T) { - // Open a test db - path, err := ioutil.TempDir("", "TestLevelDBTransactionSanity") - if err != nil { - t.Fatalf("TestLevelDBTransactionSanity: TempDir unexpectedly "+ - "failed: %s", err) - } - ldb, err := NewLevelDB(path) - if err != nil { - t.Fatalf("TestLevelDBTransactionSanity: NewLevelDB "+ - "unexpectedly failed: %s", err) - } - defer func() { - err := ldb.Close() - if err != nil { - t.Fatalf("TestLevelDBTransactionSanity: Close "+ - "unexpectedly failed: %s", err) - } - }() + ldb, teardownFunc := prepareDatabaseForTest(t, "TestLevelDBTransactionSanity") + defer teardownFunc() // Case 1. Write in tx and then read directly from the DB // Begin a new transaction diff --git a/database/ffldb/ldb/transaction_test.go b/database/ffldb/ldb/transaction_test.go new file mode 100644 index 000000000..c7dc8416b --- /dev/null +++ b/database/ffldb/ldb/transaction_test.go @@ -0,0 +1,146 @@ +package ldb + +import ( + "github.com/kaspanet/kaspad/database" + "strings" + "testing" +) + +func TestTransactionCloseErrors(t *testing.T) { + tests := []struct { + name string + + // function is the LevelDBTransaction function that + // we're verifying whether it returns an error after + // the transaction had been closed. + function func(dbTx *LevelDBTransaction) error + shouldReturnError bool + }{ + { + name: "Put", + function: func(dbTx *LevelDBTransaction) error { + return dbTx.Put(database.MakeBucket().Key([]byte("key")), []byte("value")) + }, + shouldReturnError: true, + }, + { + name: "Get", + function: func(dbTx *LevelDBTransaction) error { + _, err := dbTx.Get(database.MakeBucket().Key([]byte("key"))) + return err + }, + shouldReturnError: true, + }, + { + name: "Has", + function: func(dbTx *LevelDBTransaction) error { + _, err := dbTx.Has(database.MakeBucket().Key([]byte("key"))) + return err + }, + shouldReturnError: true, + }, + { + name: "Delete", + function: func(dbTx *LevelDBTransaction) error { + return dbTx.Delete(database.MakeBucket().Key([]byte("key"))) + }, + shouldReturnError: true, + }, + { + name: "Cursor", + function: func(dbTx *LevelDBTransaction) error { + _, err := dbTx.Cursor(database.MakeBucket([]byte("bucket"))) + return err + }, + shouldReturnError: true, + }, + { + name: "Rollback", + function: (*LevelDBTransaction).Rollback, + shouldReturnError: true, + }, + { + name: "Commit", + function: (*LevelDBTransaction).Commit, + shouldReturnError: true, + }, + { + name: "RollbackUnlessClosed", + function: (*LevelDBTransaction).RollbackUnlessClosed, + shouldReturnError: false, + }, + } + + for _, test := range tests { + func() { + ldb, teardownFunc := prepareDatabaseForTest(t, "TestTransactionCloseErrors") + defer teardownFunc() + + // Begin a new transaction to test Commit + commitTx, err := ldb.Begin() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: Begin "+ + "unexpectedly failed: %s", err) + } + defer func() { + err := commitTx.RollbackUnlessClosed() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: RollbackUnlessClosed "+ + "unexpectedly failed: %s", err) + } + }() + + // Commit the Commit test transaction + err = commitTx.Commit() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: Commit "+ + "unexpectedly failed: %s", err) + } + + // Begin a new transaction to test Rollback + rollbackTx, err := ldb.Begin() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: Begin "+ + "unexpectedly failed: %s", err) + } + defer func() { + err := rollbackTx.RollbackUnlessClosed() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: RollbackUnlessClosed "+ + "unexpectedly failed: %s", err) + } + }() + + // Rollback the Rollback test transaction + err = rollbackTx.Rollback() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: Rollback "+ + "unexpectedly failed: %s", err) + } + + expectedErrContainsString := "closed transaction" + + // Make sure that the test function returns a "closed transaction" error + // for both the commitTx and the rollbackTx + for _, closedTx := range []*LevelDBTransaction{commitTx, rollbackTx} { + err = test.function(closedTx) + if test.shouldReturnError { + if err == nil { + t.Fatalf("TestTransactionCloseErrors: %s "+ + "unexpectedly succeeded", test.name) + } + if !strings.Contains(err.Error(), expectedErrContainsString) { + t.Fatalf("TestTransactionCloseErrors: %s "+ + "returned wrong error. Want: %s, got: %s", + test.name, expectedErrContainsString, err) + } + } else { + if err != nil { + t.Fatalf("TestTransactionCloseErrors: %s "+ + "unexpectedly failed: %s", test.name, err) + } + } + } + }() + } +} diff --git a/database/ffldb/transaction.go b/database/ffldb/transaction.go index d30ab7b2f..c7652c0ca 100644 --- a/database/ffldb/transaction.go +++ b/database/ffldb/transaction.go @@ -4,6 +4,7 @@ import ( "github.com/kaspanet/kaspad/database" "github.com/kaspanet/kaspad/database/ffldb/ff" "github.com/kaspanet/kaspad/database/ffldb/ldb" + "github.com/pkg/errors" ) // transaction is an ffldb transaction. @@ -13,14 +14,19 @@ import ( // NO guarantee that if one puts data into the transaction then // it will be available to get within the same transaction. type transaction struct { - ldbTx *ldb.LevelDBTransaction - ffdb *ff.FlatFileDB + ldbTx *ldb.LevelDBTransaction + ffdb *ff.FlatFileDB + isClosed bool } // Put sets the value for the given key. It overwrites // any previous value for that key. // This method is part of the DataAccessor interface. func (tx *transaction) Put(key *database.Key, value []byte) error { + if tx.isClosed { + return errors.New("cannot put into a closed transaction") + } + return tx.ldbTx.Put(key, value) } @@ -28,6 +34,10 @@ func (tx *transaction) Put(key *database.Key, value []byte) error { // ErrNotFound if the given key does not exist. // This method is part of the DataAccessor interface. func (tx *transaction) Get(key *database.Key) ([]byte, error) { + if tx.isClosed { + return nil, errors.New("cannot get from a closed transaction") + } + return tx.ldbTx.Get(key) } @@ -35,6 +45,10 @@ func (tx *transaction) Get(key *database.Key) ([]byte, error) { // given key. // This method is part of the DataAccessor interface. func (tx *transaction) Has(key *database.Key) (bool, error) { + if tx.isClosed { + return false, errors.New("cannot has from a closed transaction") + } + return tx.ldbTx.Has(key) } @@ -42,6 +56,10 @@ func (tx *transaction) Has(key *database.Key) (bool, error) { // return an error if the key doesn't exist. // This method is part of the DataAccessor interface. func (tx *transaction) Delete(key *database.Key) error { + if tx.isClosed { + return errors.New("cannot delete from a closed transaction") + } + return tx.ldbTx.Delete(key) } @@ -52,6 +70,10 @@ func (tx *transaction) Delete(key *database.Key) error { // that has just now been inserted. // This method is part of the DataAccessor interface. func (tx *transaction) AppendToStore(storeName string, data []byte) ([]byte, error) { + if tx.isClosed { + return nil, errors.New("cannot append to store on a closed transaction") + } + return appendToStore(tx, tx.ffdb, storeName, data) } @@ -61,12 +83,20 @@ func (tx *transaction) AppendToStore(storeName string, data []byte) ([]byte, err // AppendToStore for further details. // This method is part of the DataAccessor interface. func (tx *transaction) RetrieveFromStore(storeName string, location []byte) ([]byte, error) { + if tx.isClosed { + return nil, errors.New("cannot retrieve from store on a closed transaction") + } + return tx.ffdb.Read(storeName, location) } // Cursor begins a new cursor over the given bucket. // This method is part of the DataAccessor interface. func (tx *transaction) Cursor(bucket *database.Bucket) (database.Cursor, error) { + if tx.isClosed { + return nil, errors.New("cannot open a cursor from a closed transaction") + } + return tx.ldbTx.Cursor(bucket) } @@ -74,6 +104,11 @@ func (tx *transaction) Cursor(bucket *database.Bucket) (database.Cursor, error) // database within this transaction. // This method is part of the Transaction interface. func (tx *transaction) Rollback() error { + if tx.isClosed { + return errors.New("cannot rollback a closed transaction") + } + tx.isClosed = true + return tx.ldbTx.Rollback() } @@ -81,6 +116,11 @@ func (tx *transaction) Rollback() error { // within this transaction. // This method is part of the Transaction interface. func (tx *transaction) Commit() error { + if tx.isClosed { + return errors.New("cannot commit a closed transaction") + } + tx.isClosed = true + return tx.ldbTx.Commit() } @@ -88,5 +128,10 @@ func (tx *transaction) Commit() error { // the database within the transaction, unless the transaction // had already been closed using either Rollback or Commit. func (tx *transaction) RollbackUnlessClosed() error { + if tx.isClosed { + return nil + } + tx.isClosed = true + return tx.ldbTx.RollbackUnlessClosed() } diff --git a/database/ffldb/transaction_test.go b/database/ffldb/transaction_test.go new file mode 100644 index 000000000..c76844858 --- /dev/null +++ b/database/ffldb/transaction_test.go @@ -0,0 +1,500 @@ +package ffldb + +import ( + "bytes" + "github.com/kaspanet/kaspad/database" + "strings" + "testing" +) + +func TestTransactionCommitForLevelDBMethods(t *testing.T) { + db, teardownFunc := prepareDatabaseForTest(t, "TestTransactionCommitForLevelDBMethods") + defer teardownFunc() + + // Put a value into the database + key1 := database.MakeBucket().Key([]byte("key1")) + value1 := []byte("value1") + err := db.Put(key1, value1) + if err != nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Put "+ + "unexpectedly failed: %s", err) + } + + // Begin a new transaction + dbTx, err := db.Begin() + if err != nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Begin "+ + "unexpectedly failed: %s", err) + } + defer func() { + err := dbTx.RollbackUnlessClosed() + if err != nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: RollbackUnlessClosed "+ + "unexpectedly failed: %s", err) + } + }() + + // Make sure that Has returns that the original value exists + exists, err := dbTx.Has(key1) + if err != nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Has "+ + "unexpectedly failed: %s", err) + } + if !exists { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Has " + + "unexpectedly returned that the value does not exist") + } + + // Get the existing value and make sure it's equal to the original + existingValue, err := dbTx.Get(key1) + if err != nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+ + "unexpectedly failed: %s", err) + } + if !bytes.Equal(existingValue, value1) { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+ + "returned unexpected value. Want: %s, got: %s", + string(value1), string(existingValue)) + } + + // Delete the existing value + err = dbTx.Delete(key1) + if err != nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Delete "+ + "unexpectedly failed: %s", err) + } + + // Try to get a value that does not exist and make sure it returns ErrNotFound + _, err = dbTx.Get(database.MakeBucket().Key([]byte("doesn't exist"))) + if err == nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Get " + + "unexpectedly succeeded") + } + if !database.IsNotFoundError(err) { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+ + "returned unexpected error: %s", err) + } + + // Put a new value + key2 := database.MakeBucket().Key([]byte("key2")) + value2 := []byte("value2") + err = dbTx.Put(key2, value2) + if err != nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Put "+ + "unexpectedly failed: %s", err) + } + + // Commit the transaction + err = dbTx.Commit() + if err != nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Commit "+ + "unexpectedly failed: %s", err) + } + + // Make sure that Has returns that the original value does NOT exist + exists, err = db.Has(key1) + if err != nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Has "+ + "unexpectedly failed: %s", err) + } + if exists { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Has " + + "unexpectedly returned that the value exists") + } + + // Try to Get the existing value and make sure an ErrNotFound is returned + _, err = db.Get(key1) + if err == nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Get " + + "unexpectedly succeeded") + } + if !database.IsNotFoundError(err) { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+ + "returned unexpected err: %s", err) + } + + // Make sure that Has returns that the new value exists + exists, err = db.Has(key2) + if err != nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Has "+ + "unexpectedly failed: %s", err) + } + if !exists { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Has " + + "unexpectedly returned that the value does not exist") + } + + // Get the new value and make sure it's equal to the original + existingValue, err = db.Get(key2) + if err != nil { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+ + "unexpectedly failed: %s", err) + } + if !bytes.Equal(existingValue, value2) { + t.Fatalf("TestTransactionCommitForLevelDBMethods: Get "+ + "returned unexpected value. Want: %s, got: %s", + string(value2), string(existingValue)) + } +} + +func TestTransactionRollbackForLevelDBMethods(t *testing.T) { + db, teardownFunc := prepareDatabaseForTest(t, "TestTransactionRollbackForLevelDBMethods") + defer teardownFunc() + + // Put a value into the database + key1 := database.MakeBucket().Key([]byte("key1")) + value1 := []byte("value1") + err := db.Put(key1, value1) + if err != nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Put "+ + "unexpectedly failed: %s", err) + } + + // Begin a new transaction + dbTx, err := db.Begin() + if err != nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Begin "+ + "unexpectedly failed: %s", err) + } + defer func() { + err := dbTx.RollbackUnlessClosed() + if err != nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: RollbackUnlessClosed "+ + "unexpectedly failed: %s", err) + } + }() + + // Make sure that Has returns that the original value exists + exists, err := dbTx.Has(key1) + if err != nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has "+ + "unexpectedly failed: %s", err) + } + if !exists { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has " + + "unexpectedly returned that the value does not exist") + } + + // Get the existing value and make sure it's equal to the original + existingValue, err := dbTx.Get(key1) + if err != nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get "+ + "unexpectedly failed: %s", err) + } + if !bytes.Equal(existingValue, value1) { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get "+ + "returned unexpected value. Want: %s, got: %s", + string(value1), string(existingValue)) + } + + // Delete the existing value + err = dbTx.Delete(key1) + if err != nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Delete "+ + "unexpectedly failed: %s", err) + } + + // Put a new value + key2 := database.MakeBucket().Key([]byte("key2")) + value2 := []byte("value2") + err = dbTx.Put(key2, value2) + if err != nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Put "+ + "unexpectedly failed: %s", err) + } + + // Rollback the transaction + err = dbTx.Rollback() + if err != nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Rollback "+ + "unexpectedly failed: %s", err) + } + + // Make sure that Has returns that the original value still exists + exists, err = db.Has(key1) + if err != nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has "+ + "unexpectedly failed: %s", err) + } + if !exists { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has " + + "unexpectedly returned that the value does not exist") + } + + // Get the existing value and make sure it is still returned + existingValue, err = db.Get(key1) + if err != nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get "+ + "unexpectedly failed: %s", err) + } + if !bytes.Equal(existingValue, value1) { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get "+ + "returned unexpected value. Want: %s, got: %s", + string(value1), string(existingValue)) + } + + // Make sure that Has returns that the new value does NOT exist + exists, err = db.Has(key2) + if err != nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has "+ + "unexpectedly failed: %s", err) + } + if exists { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Has " + + "unexpectedly returned that the value exists") + } + + // Try to Get the new value and make sure it returns an ErrNotFound + _, err = db.Get(key2) + if err == nil { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get " + + "unexpectedly succeeded") + } + if !database.IsNotFoundError(err) { + t.Fatalf("TestTransactionRollbackForLevelDBMethods: Get "+ + "returned unexpected error: %s", err) + } +} + +func TestTransactionCloseErrors(t *testing.T) { + tests := []struct { + name string + function func(dbTx database.Transaction) error + shouldReturnError bool + }{ + { + name: "Put", + function: func(dbTx database.Transaction) error { + return dbTx.Put(database.MakeBucket().Key([]byte("key")), []byte("value")) + }, + shouldReturnError: true, + }, + { + name: "Get", + function: func(dbTx database.Transaction) error { + _, err := dbTx.Get(database.MakeBucket().Key([]byte("key"))) + return err + }, + shouldReturnError: true, + }, + { + name: "Has", + function: func(dbTx database.Transaction) error { + _, err := dbTx.Has(database.MakeBucket().Key([]byte("key"))) + return err + }, + shouldReturnError: true, + }, + { + name: "Delete", + function: func(dbTx database.Transaction) error { + return dbTx.Delete(database.MakeBucket().Key([]byte("key"))) + }, + shouldReturnError: true, + }, + { + name: "Cursor", + function: func(dbTx database.Transaction) error { + _, err := dbTx.Cursor(database.MakeBucket([]byte("bucket"))) + return err + }, + shouldReturnError: true, + }, + { + name: "AppendToStore", + function: func(dbTx database.Transaction) error { + _, err := dbTx.AppendToStore("store", []byte("data")) + return err + }, + shouldReturnError: true, + }, + { + name: "RetrieveFromStore", + function: func(dbTx database.Transaction) error { + _, err := dbTx.RetrieveFromStore("store", []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + return err + }, + shouldReturnError: true, + }, + { + name: "Rollback", + function: func(dbTx database.Transaction) error { + return dbTx.Rollback() + }, + shouldReturnError: true, + }, + { + name: "Commit", + function: func(dbTx database.Transaction) error { + return dbTx.Commit() + }, + shouldReturnError: true, + }, + { + name: "RollbackUnlessClosed", + function: func(dbTx database.Transaction) error { + return dbTx.RollbackUnlessClosed() + }, + shouldReturnError: false, + }, + } + + for _, test := range tests { + func() { + db, teardownFunc := prepareDatabaseForTest(t, "TestTransactionCloseErrors") + defer teardownFunc() + + // Begin a new transaction to test Commit + commitTx, err := db.Begin() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: Begin "+ + "unexpectedly failed: %s", err) + } + defer func() { + err := commitTx.RollbackUnlessClosed() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: RollbackUnlessClosed "+ + "unexpectedly failed: %s", err) + } + }() + + // Commit the Commit test transaction + err = commitTx.Commit() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: Commit "+ + "unexpectedly failed: %s", err) + } + + // Begin a new transaction to test Rollback + rollbackTx, err := db.Begin() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: Begin "+ + "unexpectedly failed: %s", err) + } + defer func() { + err := rollbackTx.RollbackUnlessClosed() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: RollbackUnlessClosed "+ + "unexpectedly failed: %s", err) + } + }() + + // Rollback the Rollback test transaction + err = rollbackTx.Rollback() + if err != nil { + t.Fatalf("TestTransactionCloseErrors: Rollback "+ + "unexpectedly failed: %s", err) + } + + expectedErrContainsString := "closed transaction" + + // Make sure that the test function returns a "closed transaction" error + // for both the commitTx and the rollbackTx + for _, closedTx := range []database.Transaction{commitTx, rollbackTx} { + err = test.function(closedTx) + if test.shouldReturnError { + if err == nil { + t.Fatalf("TestTransactionCloseErrors: %s "+ + "unexpectedly succeeded", test.name) + } + if !strings.Contains(err.Error(), expectedErrContainsString) { + t.Fatalf("TestTransactionCloseErrors: %s "+ + "returned wrong error. Want: %s, got: %s", + test.name, expectedErrContainsString, err) + } + } else { + if err != nil { + t.Fatalf("TestTransactionCloseErrors: %s "+ + "unexpectedly failed: %s", test.name, err) + } + } + } + }() + } +} + +func TestTransactionRollbackUnlessClosed(t *testing.T) { + db, teardownFunc := prepareDatabaseForTest(t, "TestTransactionRollbackUnlessClosed") + defer teardownFunc() + + // Begin a new transaction + dbTx, err := db.Begin() + if err != nil { + t.Fatalf("TestTransactionRollbackUnlessClosed: Begin "+ + "unexpectedly failed: %s", err) + } + + // Roll it back + err = dbTx.RollbackUnlessClosed() + if err != nil { + t.Fatalf("TestTransactionRollbackUnlessClosed: RollbackUnlessClosed "+ + "unexpectedly failed: %s", err) + } +} + +func TestTransactionCommitForFlatFileMethods(t *testing.T) { + db, teardownFunc := prepareDatabaseForTest(t, "TestTransactionCommitForFlatFileMethods") + defer teardownFunc() + + // Put a value into the database + store := "store" + value1 := []byte("value1") + location1, err := db.AppendToStore(store, value1) + if err != nil { + t.Fatalf("TestTransactionCommitForFlatFileMethods: AppendToStore "+ + "unexpectedly failed: %s", err) + } + + // Begin a new transaction + dbTx, err := db.Begin() + if err != nil { + t.Fatalf("TestTransactionCommitForFlatFileMethods: Begin "+ + "unexpectedly failed: %s", err) + } + defer func() { + err := dbTx.RollbackUnlessClosed() + if err != nil { + t.Fatalf("TestTransactionCommitForFlatFileMethods: RollbackUnlessClosed "+ + "unexpectedly failed: %s", err) + } + }() + + // Retrieve the existing value and make sure it's equal to the original + existingValue, err := dbTx.RetrieveFromStore(store, location1) + if err != nil { + t.Fatalf("TestTransactionCommitForFlatFileMethods: RetrieveFromStore "+ + "unexpectedly failed: %s", err) + } + if !bytes.Equal(existingValue, value1) { + t.Fatalf("TestTransactionCommitForFlatFileMethods: RetrieveFromStore "+ + "returned unexpected value. Want: %s, got: %s", + string(value1), string(existingValue)) + } + + // Put a new value + value2 := []byte("value2") + location2, err := dbTx.AppendToStore(store, value2) + if err != nil { + t.Fatalf("TestTransactionCommitForFlatFileMethods: AppendToStore "+ + "unexpectedly failed: %s", err) + } + + // Commit the transaction + err = dbTx.Commit() + if err != nil { + t.Fatalf("TestTransactionCommitForFlatFileMethods: Commit "+ + "unexpectedly failed: %s", err) + } + + // Retrieve the new value and make sure it's equal to the original + newValue, err := db.RetrieveFromStore(store, location2) + if err != nil { + t.Fatalf("TestTransactionCommitForFlatFileMethods: RetrieveFromStore "+ + "unexpectedly failed: %s", err) + } + if !bytes.Equal(newValue, value2) { + t.Fatalf("TestTransactionCommitForFlatFileMethods: RetrieveFromStore "+ + "returned unexpected value. Want: %s, got: %s", + string(value2), string(newValue)) + } +}