mirror of
https://github.com/kaspanet/kaspad.git
synced 2026-02-16 00:23:56 +00:00
[NOD-1223] Reorganize directory structure (#874)
* [NOD-1223] Delete unused files/packages. * [NOD-1223] Move signal and limits to the os package. * [NOD-1223] Put database and dbaccess into the db package. * [NOD-1223] Fold the logs package into the logger package. * [NOD-1223] Rename domainmessage to appmessage. * [NOD-1223] Rename to/from DomainMessage to AppMessage. * [NOD-1223] Move appmessage to the app packge. * [NOD-1223] Move protocol to the app packge. * [NOD-1223] Move the network package to the infrastructure packge. * [NOD-1223] Rename cmd to executables. * [NOD-1223] Fix go.doc in the logger package.
This commit is contained in:
38
infrastructure/db/database/README.md
Normal file
38
infrastructure/db/database/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
database
|
||||
========
|
||||
|
||||
[](https://choosealicense.com/licenses/isc/)
|
||||
[](http://godoc.org/github.com/kaspanet/kaspad/database)
|
||||
|
||||
Package database provides a database for kaspad.
|
||||
|
||||
Overview
|
||||
--------
|
||||
This package provides a database layer to store and retrieve data in a simple
|
||||
and efficient manner.
|
||||
|
||||
The current backend is ffldb, which makes use of leveldb, flat files, and strict
|
||||
checksums in key areas to ensure data integrity.
|
||||
|
||||
Implementors of additional backends are required to implement the following interfaces:
|
||||
|
||||
DataAccessor
|
||||
------------
|
||||
This defines the common interface by which data gets accessed in a generic kaspad
|
||||
database. Both the Database and the Transaction interfaces (see below) implement it.
|
||||
|
||||
Database
|
||||
--------
|
||||
This defines the interface of a database that can begin transactions and close itself.
|
||||
|
||||
Transaction
|
||||
-----------
|
||||
This defines the interface of a generic kaspad database transaction.
|
||||
|
||||
Note: Transactions provide data consistency over the state of the database as it was
|
||||
when the transaction started. There is NO guarantee that if one puts data into the
|
||||
transaction then it will be available to get within the same transaction.
|
||||
|
||||
Cursor
|
||||
------
|
||||
This iterates over database entries given some bucket.
|
||||
84
infrastructure/db/database/common_test.go
Normal file
84
infrastructure/db/database/common_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database/ffldb"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type databasePrepareFunc func(t *testing.T, testName string) (db database.Database, name string, teardownFunc func())
|
||||
|
||||
// databasePrepareFuncs is a set of functions, in which each function
|
||||
// prepares a separate database type for testing.
|
||||
// See testForAllDatabaseTypes for further details.
|
||||
var databasePrepareFuncs = []databasePrepareFunc{
|
||||
prepareFFLDBForTest,
|
||||
}
|
||||
|
||||
func prepareFFLDBForTest(t *testing.T, testName string) (db database.Database, name string, 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 = ffldb.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, "ffldb", teardownFunc
|
||||
}
|
||||
|
||||
// testForAllDatabaseTypes runs the given testFunc for every database
|
||||
// type defined in databasePrepareFuncs. This is to make sure that
|
||||
// all supported database types adhere to the assumptions defined in
|
||||
// the interfaces in this package.
|
||||
func testForAllDatabaseTypes(t *testing.T, testName string,
|
||||
testFunc func(t *testing.T, db database.Database, testName string)) {
|
||||
|
||||
for _, prepareDatabase := range databasePrepareFuncs {
|
||||
func() {
|
||||
db, dbType, teardownFunc := prepareDatabase(t, testName)
|
||||
defer teardownFunc()
|
||||
|
||||
testName := fmt.Sprintf("%s: %s", dbType, testName)
|
||||
testFunc(t, db, testName)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
type keyValuePair struct {
|
||||
key *database.Key
|
||||
value []byte
|
||||
}
|
||||
|
||||
func populateDatabaseForTest(t *testing.T, db database.Database, testName string) []keyValuePair {
|
||||
// Prepare a list of key/value pairs
|
||||
entries := make([]keyValuePair, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
key := database.MakeBucket().Key([]byte(fmt.Sprintf("key%d", i)))
|
||||
value := []byte("value")
|
||||
entries[i] = keyValuePair{key: key, value: value}
|
||||
}
|
||||
|
||||
// Put the pairs into the database
|
||||
for _, entry := range entries {
|
||||
err := db.Put(entry.key, entry.value)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
30
infrastructure/db/database/cursor.go
Normal file
30
infrastructure/db/database/cursor.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package database
|
||||
|
||||
// Cursor iterates over database entries given some bucket.
|
||||
type Cursor interface {
|
||||
// Next moves the iterator to the next key/value pair. It returns whether the
|
||||
// iterator is exhausted. Panics if the cursor is closed.
|
||||
Next() bool
|
||||
|
||||
// First moves the iterator to the first key/value pair. It returns false if
|
||||
// such a pair does not exist. Panics if the cursor is closed.
|
||||
First() bool
|
||||
|
||||
// Seek moves the iterator to the first key/value pair whose key is greater
|
||||
// than or equal to the given key. It returns ErrNotFound if such pair does not
|
||||
// exist.
|
||||
Seek(key *Key) error
|
||||
|
||||
// Key returns the key of the current key/value pair, or ErrNotFound if done.
|
||||
// The caller should not modify the contents of the returned key, and
|
||||
// its contents may change on the next call to Next.
|
||||
Key() (*Key, error)
|
||||
|
||||
// Value returns the value of the current key/value pair, or ErrNotFound if done.
|
||||
// The caller should not modify the contents of the returned slice, and its
|
||||
// contents may change on the next call to Next.
|
||||
Value() ([]byte, error)
|
||||
|
||||
// Close releases associated resources.
|
||||
Close() error
|
||||
}
|
||||
345
infrastructure/db/database/cursor_test.go
Normal file
345
infrastructure/db/database/cursor_test.go
Normal file
@@ -0,0 +1,345 @@
|
||||
// All tests within this file should call testForAllDatabaseTypes
|
||||
// over the actual test. This is to make sure that all supported
|
||||
// database types adhere to the assumptions defined in the
|
||||
// interfaces in this package.
|
||||
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func prepareCursorForTest(t *testing.T, db database.Database, testName string) database.Cursor {
|
||||
cursor, err := db.Cursor(database.MakeBucket())
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Cursor unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
|
||||
return cursor
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCursorNext(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestCursorNext", testCursorNext)
|
||||
}
|
||||
|
||||
func testCursorNext(t *testing.T, db database.Database, testName string) {
|
||||
entries := populateDatabaseForTest(t, db, testName)
|
||||
cursor := prepareCursorForTest(t, db, testName)
|
||||
|
||||
// Make sure that all the entries exist in the cursor, in their
|
||||
// correct order
|
||||
for _, entry := range entries {
|
||||
hasNext := cursor.Next()
|
||||
if !hasNext {
|
||||
t.Fatalf("%s: cursor unexpectedly "+
|
||||
"done", testName)
|
||||
}
|
||||
cursorKey, err := cursor.Key()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Key unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
if !reflect.DeepEqual(cursorKey, entry.key) {
|
||||
t.Fatalf("%s: Cursor returned "+
|
||||
"wrong key. Want: %s, got: %s", testName, entry.key, cursorKey)
|
||||
}
|
||||
cursorValue, err := cursor.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Value unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(cursorValue, entry.value) {
|
||||
t.Fatalf("%s: Cursor returned "+
|
||||
"wrong value. Want: %s, got: %s", testName, entry.value, cursorValue)
|
||||
}
|
||||
}
|
||||
|
||||
// The cursor should now be exhausted. Make sure Next now
|
||||
// returns false
|
||||
hasNext := cursor.Next()
|
||||
if hasNext {
|
||||
t.Fatalf("%s: cursor unexpectedly "+
|
||||
"not done", testName)
|
||||
}
|
||||
|
||||
// Rewind the cursor and close it
|
||||
cursor.First()
|
||||
err := cursor.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Close unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Call Next on the cursor. This time it should panic
|
||||
// because it's closed.
|
||||
func() {
|
||||
defer recoverFromClosedCursorPanic(t, testName)
|
||||
cursor.Next()
|
||||
}()
|
||||
}
|
||||
|
||||
func TestCursorFirst(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestCursorFirst", testCursorFirst)
|
||||
}
|
||||
|
||||
func testCursorFirst(t *testing.T, db database.Database, testName string) {
|
||||
entries := populateDatabaseForTest(t, db, testName)
|
||||
cursor := prepareCursorForTest(t, db, testName)
|
||||
|
||||
// Make sure that First returns true when the cursor is not empty
|
||||
exists := cursor.First()
|
||||
if !exists {
|
||||
t.Fatalf("%s: Cursor unexpectedly "+
|
||||
"returned false", testName)
|
||||
}
|
||||
|
||||
// Make sure that the first key and value are as expected
|
||||
firstEntryKey := entries[0].key
|
||||
firstCursorKey, err := cursor.Key()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Key unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
if !reflect.DeepEqual(firstCursorKey, firstEntryKey) {
|
||||
t.Fatalf("%s: Cursor returned "+
|
||||
"wrong key. Want: %s, got: %s", testName, firstEntryKey, firstCursorKey)
|
||||
}
|
||||
firstEntryValue := entries[0].value
|
||||
firstCursorValue, err := cursor.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Value unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(firstCursorValue, firstEntryValue) {
|
||||
t.Fatalf("%s: Cursor returned "+
|
||||
"wrong value. Want: %s, got: %s", testName, firstEntryValue, firstCursorValue)
|
||||
}
|
||||
|
||||
// Exhaust the cursor
|
||||
for cursor.Next() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
// Call first again and make sure it still returns true
|
||||
exists = cursor.First()
|
||||
if !exists {
|
||||
t.Fatalf("%s: First unexpectedly "+
|
||||
"returned false", testName)
|
||||
}
|
||||
|
||||
// Call next and make sure it returns true as well
|
||||
exists = cursor.Next()
|
||||
if !exists {
|
||||
t.Fatalf("%s: Next unexpectedly "+
|
||||
"returned false", testName)
|
||||
}
|
||||
|
||||
// Remove all the entries from the database
|
||||
for _, entry := range entries {
|
||||
err := db.Delete(entry.key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Delete unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new cursor over an empty dataset
|
||||
cursor = prepareCursorForTest(t, db, testName)
|
||||
|
||||
// Make sure that First returns false when the cursor is empty
|
||||
exists = cursor.First()
|
||||
if exists {
|
||||
t.Fatalf("%s: Cursor unexpectedly "+
|
||||
"returned true", testName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCursorSeek(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestCursorSeek", testCursorSeek)
|
||||
}
|
||||
|
||||
func testCursorSeek(t *testing.T, db database.Database, testName string) {
|
||||
entries := populateDatabaseForTest(t, db, testName)
|
||||
cursor := prepareCursorForTest(t, db, testName)
|
||||
|
||||
// Seek to the fourth entry and make sure it exists
|
||||
fourthEntry := entries[3]
|
||||
err := cursor.Seek(fourthEntry.key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Cursor unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that the key and value are as expected
|
||||
fourthEntryKey := entries[3].key
|
||||
fourthCursorKey, err := cursor.Key()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Key unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
if !reflect.DeepEqual(fourthCursorKey, fourthEntryKey) {
|
||||
t.Fatalf("%s: Cursor returned "+
|
||||
"wrong key. Want: %s, got: %s", testName, fourthEntryKey, fourthCursorKey)
|
||||
}
|
||||
fourthEntryValue := entries[3].value
|
||||
fourthCursorValue, err := cursor.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Value unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(fourthCursorValue, fourthEntryValue) {
|
||||
t.Fatalf("%s: Cursor returned "+
|
||||
"wrong value. Want: %s, got: %s", testName, fourthEntryValue, fourthCursorValue)
|
||||
}
|
||||
|
||||
// Call Next and make sure that we are now on the fifth entry
|
||||
exists := cursor.Next()
|
||||
if !exists {
|
||||
t.Fatalf("%s: Next unexpectedly "+
|
||||
"returned false", testName)
|
||||
}
|
||||
fifthEntryKey := entries[4].key
|
||||
fifthCursorKey, err := cursor.Key()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Key unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
if !reflect.DeepEqual(fifthCursorKey, fifthEntryKey) {
|
||||
t.Fatalf("%s: Cursor returned "+
|
||||
"wrong key. Want: %s, got: %s", testName, fifthEntryKey, fifthCursorKey)
|
||||
}
|
||||
fifthEntryValue := entries[4].value
|
||||
fifthCursorValue, err := cursor.Value()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Value unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(fifthCursorValue, fifthEntryValue) {
|
||||
t.Fatalf("%s: Cursor returned "+
|
||||
"wrong value. Want: %s, got: %s", testName, fifthEntryValue, fifthCursorValue)
|
||||
}
|
||||
|
||||
// Seek to a value that doesn't exist and make sure that
|
||||
// the returned error is ErrNotFound
|
||||
err = cursor.Seek(database.MakeBucket().Key([]byte("doesn't exist")))
|
||||
if err == nil {
|
||||
t.Fatalf("%s: Seek unexpectedly "+
|
||||
"succeeded", testName)
|
||||
}
|
||||
if !database.IsNotFoundError(err) {
|
||||
t.Fatalf("%s: Seek returned "+
|
||||
"wrong error: %s", testName, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCursorCloseErrors(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestCursorCloseErrors", testCursorCloseErrors)
|
||||
}
|
||||
|
||||
func testCursorCloseErrors(t *testing.T, db database.Database, testName string) {
|
||||
populateDatabaseForTest(t, db, testName)
|
||||
cursor := prepareCursorForTest(t, db, testName)
|
||||
|
||||
// Close the cursor
|
||||
err := cursor.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Close "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
function func() error
|
||||
}{
|
||||
{
|
||||
name: "Seek",
|
||||
function: func() error {
|
||||
return cursor.Seek(database.MakeBucket().Key([]byte{}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Key",
|
||||
function: func() error {
|
||||
_, err := cursor.Key()
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Value",
|
||||
function: func() error {
|
||||
_, err := cursor.Value()
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Close",
|
||||
function: func() error {
|
||||
return cursor.Close()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
expectedErrContainsString := "closed cursor"
|
||||
|
||||
// Make sure that the test function returns a "closed cursor" error
|
||||
err = test.function()
|
||||
if err == nil {
|
||||
t.Fatalf("%s: %s "+
|
||||
"unexpectedly succeeded", testName, test.name)
|
||||
}
|
||||
if !strings.Contains(err.Error(), expectedErrContainsString) {
|
||||
t.Fatalf("%s: %s "+
|
||||
"returned wrong error. Want: %s, got: %s",
|
||||
testName, test.name, expectedErrContainsString, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCursorCloseFirstAndNext(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestCursorCloseFirstAndNext", testCursorCloseFirstAndNext)
|
||||
}
|
||||
|
||||
func testCursorCloseFirstAndNext(t *testing.T, db database.Database, testName string) {
|
||||
populateDatabaseForTest(t, db, testName)
|
||||
cursor := prepareCursorForTest(t, db, testName)
|
||||
|
||||
// Close the cursor
|
||||
err := cursor.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Close "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// We expect First to panic
|
||||
func() {
|
||||
defer recoverFromClosedCursorPanic(t, testName)
|
||||
cursor.First()
|
||||
}()
|
||||
|
||||
// We expect Next to panic
|
||||
func() {
|
||||
defer recoverFromClosedCursorPanic(t, testName)
|
||||
cursor.Next()
|
||||
}()
|
||||
}
|
||||
36
infrastructure/db/database/dataaccessor.go
Normal file
36
infrastructure/db/database/dataaccessor.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package database
|
||||
|
||||
// DataAccessor defines the common interface by which data gets
|
||||
// accessed in a generic kaspad database.
|
||||
type DataAccessor interface {
|
||||
// Put sets the value for the given key. It overwrites
|
||||
// any previous value for that key.
|
||||
Put(key *Key, value []byte) error
|
||||
|
||||
// Get gets the value for the given key. It returns
|
||||
// ErrNotFound if the given key does not exist.
|
||||
Get(key *Key) ([]byte, error)
|
||||
|
||||
// Has returns true if the database does contains the
|
||||
// given key.
|
||||
Has(key *Key) (bool, error)
|
||||
|
||||
// Delete deletes the value for the given key. Will not
|
||||
// return an error if the key doesn't exist.
|
||||
Delete(key *Key) error
|
||||
|
||||
// AppendToStore appends the given data to the store
|
||||
// defined by storeName. This function returns a serialized
|
||||
// location handle that's meant to be stored and later used
|
||||
// when querying the data that has just now been inserted.
|
||||
AppendToStore(storeName string, data []byte) ([]byte, error)
|
||||
|
||||
// RetrieveFromStore retrieves data from the store defined by
|
||||
// storeName using the given serialized location handle. It
|
||||
// returns ErrNotFound if the location does not exist. See
|
||||
// AppendToStore for further details.
|
||||
RetrieveFromStore(storeName string, location []byte) ([]byte, error)
|
||||
|
||||
// Cursor begins a new cursor over the given bucket.
|
||||
Cursor(bucket *Bucket) (Cursor, error)
|
||||
}
|
||||
19
infrastructure/db/database/database.go
Normal file
19
infrastructure/db/database/database.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package database
|
||||
|
||||
// Database defines the interface of a database that can begin
|
||||
// transactions and close itself.
|
||||
//
|
||||
// Important: This is not part of the DataAccessor interface
|
||||
// because the Transaction interface includes it. Were we to
|
||||
// merge Database with DataAccessor, implementors of the
|
||||
// Transaction interface would be forced to implement methods
|
||||
// such as Begin and Close, which is undesirable.
|
||||
type Database interface {
|
||||
DataAccessor
|
||||
|
||||
// Begin begins a new database transaction.
|
||||
Begin() (Transaction, error)
|
||||
|
||||
// Close closes the database.
|
||||
Close() error
|
||||
}
|
||||
207
infrastructure/db/database/database_test.go
Normal file
207
infrastructure/db/database/database_test.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// All tests within this file should call testForAllDatabaseTypes
|
||||
// over the actual test. This is to make sure that all supported
|
||||
// database types adhere to the assumptions defined in the
|
||||
// interfaces in this package.
|
||||
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDatabasePut(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestDatabasePut", testDatabasePut)
|
||||
}
|
||||
|
||||
func testDatabasePut(t *testing.T, db database.Database, testName string) {
|
||||
// Put value1 into the database
|
||||
key := database.MakeBucket().Key([]byte("key"))
|
||||
value1 := []byte("value1")
|
||||
err := db.Put(key, value1)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that the returned value is value1
|
||||
returnedValue, err := db.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(returnedValue, value1) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong value. Want: %s, got: %s",
|
||||
testName, string(value1), string(returnedValue))
|
||||
}
|
||||
|
||||
// Put value2 into the database with the same key
|
||||
value2 := []byte("value2")
|
||||
err = db.Put(key, value2)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that the returned value is value2
|
||||
returnedValue, err = db.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(returnedValue, value2) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong value. Want: %s, got: %s",
|
||||
testName, string(value2), string(returnedValue))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseGet(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestDatabaseGet", testDatabaseGet)
|
||||
}
|
||||
|
||||
func testDatabaseGet(t *testing.T, db database.Database, testName string) {
|
||||
// Put a value into the database
|
||||
key := database.MakeBucket().Key([]byte("key"))
|
||||
value := []byte("value")
|
||||
err := db.Put(key, value)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Get the value back and make sure it's the same one
|
||||
returnedValue, err := db.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(returnedValue, value) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong value. Want: %s, got: %s",
|
||||
testName, string(value), string(returnedValue))
|
||||
}
|
||||
|
||||
// Try getting a non-existent value and make sure
|
||||
// the returned error is ErrNotFound
|
||||
_, err = db.Get(database.MakeBucket().Key([]byte("doesn't exist")))
|
||||
if err == nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly succeeded", testName)
|
||||
}
|
||||
if !database.IsNotFoundError(err) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong error: %s", testName, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseHas(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestDatabaseHas", testDatabaseHas)
|
||||
}
|
||||
|
||||
func testDatabaseHas(t *testing.T, db database.Database, testName string) {
|
||||
// Put a value into the database
|
||||
key := database.MakeBucket().Key([]byte("key"))
|
||||
value := []byte("value")
|
||||
err := db.Put(key, value)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that Has returns true for the value we just put
|
||||
exists, err := db.Has(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly returned that the value does not exist", testName)
|
||||
}
|
||||
|
||||
// Make sure that Has returns false for a non-existent value
|
||||
exists, err = db.Has(database.MakeBucket().Key([]byte("doesn't exist")))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if exists {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly returned that the value exists", testName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseDelete(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestDatabaseDelete", testDatabaseDelete)
|
||||
}
|
||||
|
||||
func testDatabaseDelete(t *testing.T, db database.Database, testName string) {
|
||||
// Put a value into the database
|
||||
key := database.MakeBucket().Key([]byte("key"))
|
||||
value := []byte("value")
|
||||
err := db.Put(key, value)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Delete the value
|
||||
err = db.Delete(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Delete "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that Has returns false for the deleted value
|
||||
exists, err := db.Has(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if exists {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly returned that the value exists", testName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseAppendToStoreAndRetrieveFromStore(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestDatabaseAppendToStoreAndRetrieveFromStore", testDatabaseAppendToStoreAndRetrieveFromStore)
|
||||
}
|
||||
|
||||
func testDatabaseAppendToStoreAndRetrieveFromStore(t *testing.T, db database.Database, testName string) {
|
||||
// Append some data into the store
|
||||
storeName := "store"
|
||||
data := []byte("data")
|
||||
location, err := db.AppendToStore(storeName, data)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: AppendToStore "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Retrieve the data and make sure it's equal to what was appended
|
||||
retrievedData, err := db.RetrieveFromStore(storeName, location)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RetrieveFromStore "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(retrievedData, data) {
|
||||
t.Fatalf("%s: RetrieveFromStore "+
|
||||
"returned unexpected data. Want: %s, got: %s",
|
||||
testName, string(data), string(retrievedData))
|
||||
}
|
||||
|
||||
// Make sure that an invalid location returns ErrNotFound
|
||||
fakeLocation := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
|
||||
_, err = db.RetrieveFromStore(storeName, fakeLocation)
|
||||
if err == nil {
|
||||
t.Fatalf("%s: RetrieveFromStore "+
|
||||
"unexpectedly succeeded", testName)
|
||||
}
|
||||
if !database.IsNotFoundError(err) {
|
||||
t.Fatalf("%s: RetrieveFromStore "+
|
||||
"returned wrong error: %s", testName, err)
|
||||
}
|
||||
}
|
||||
34
infrastructure/db/database/doc.go
Normal file
34
infrastructure/db/database/doc.go
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
Package database provides a database for kaspad.
|
||||
|
||||
Overview
|
||||
|
||||
This package provides a database layer to store and retrieve data in a simple
|
||||
and efficient manner.
|
||||
|
||||
The current backend is ffldb, which makes use of leveldb, flat files, and strict
|
||||
checksums in key areas to ensure data integrity.
|
||||
|
||||
Implementors of additional backends are required to implement the following interfaces:
|
||||
|
||||
DataAccessor
|
||||
|
||||
This defines the common interface by which data gets accessed in a generic kaspad
|
||||
database. Both the Database and the Transaction interfaces (see below) implement it.
|
||||
|
||||
Database
|
||||
|
||||
This defines the interface of a database that can begin transactions and close itself.
|
||||
|
||||
Transaction
|
||||
|
||||
This defines the interface of a generic kaspad database transaction.
|
||||
Note: transactions provide data consistency over the state of the database as it was
|
||||
when the transaction started. There is NO guarantee that if one puts data into the
|
||||
transaction then it will be available to get within the same transaction.
|
||||
|
||||
Cursor
|
||||
|
||||
This iterates over database entries given some bucket.
|
||||
*/
|
||||
package database
|
||||
12
infrastructure/db/database/errors.go
Normal file
12
infrastructure/db/database/errors.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package database
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrNotFound denotes that the requested item was not
|
||||
// found in the database.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// IsNotFoundError checks whether an error is an ErrNotFound.
|
||||
func IsNotFoundError(err error) bool {
|
||||
return errors.Is(err, ErrNotFound)
|
||||
}
|
||||
231
infrastructure/db/database/ffldb/ff/flatfile.go
Normal file
231
infrastructure/db/database/ffldb/ff/flatfile.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package ff
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"hash/crc32"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxOpenFiles is the max number of open files to maintain in each store's
|
||||
// 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).
|
||||
// NOTE: This is a var rather than a const for testing purposes.
|
||||
maxFileSize uint32 = 512 * 1024 * 1024 // 512 MiB
|
||||
)
|
||||
|
||||
var (
|
||||
// byteOrder is the preferred byte order used through the flat files.
|
||||
// Sometimes big endian will be used to allow ordered byte sortable
|
||||
// integer values.
|
||||
byteOrder = binary.LittleEndian
|
||||
|
||||
// crc32ByteOrder is the byte order used for CRC-32 checksums.
|
||||
crc32ByteOrder = binary.BigEndian
|
||||
|
||||
// crc32ChecksumLength is the length in bytes of a CRC-32 checksum.
|
||||
crc32ChecksumLength = 4
|
||||
|
||||
// dataLengthLength is the length in bytes of the "data length" section
|
||||
// of a serialized entry in a flat file store.
|
||||
dataLengthLength = 4
|
||||
|
||||
// castagnoli houses the Catagnoli polynomial used for CRC-32 checksums.
|
||||
castagnoli = crc32.MakeTable(crc32.Castagnoli)
|
||||
)
|
||||
|
||||
// flatFileStore houses information used to handle reading and writing data
|
||||
// into flat files with support for multiple concurrent readers.
|
||||
type flatFileStore struct {
|
||||
// basePath is the base path used for the flat files.
|
||||
basePath string
|
||||
|
||||
// storeName is the name of this flat-file store.
|
||||
storeName string
|
||||
|
||||
// The following fields are related to the flat files which hold the
|
||||
// actual data. The number of open files is limited by maxOpenFiles.
|
||||
//
|
||||
// openFilesMutex protects concurrent access to the openFiles map. It
|
||||
// is a RWMutex so multiple readers can simultaneously access open
|
||||
// files.
|
||||
//
|
||||
// openFiles houses the open file handles for existing files which have
|
||||
// been opened read-only along with an individual RWMutex. This scheme
|
||||
// allows multiple concurrent readers to the same file while preventing
|
||||
// the file from being closed out from under them.
|
||||
//
|
||||
// lruMutex protects concurrent access to the least recently used list
|
||||
// and lookup map.
|
||||
//
|
||||
// openFilesLRU tracks how the open files are referenced by pushing the
|
||||
// most recently used files to the front of the list thereby trickling
|
||||
// the least recently used files to end of the list. When a file needs
|
||||
// to be closed due to exceeding the max number of allowed open
|
||||
// files, the one at the end of the list is closed.
|
||||
//
|
||||
// fileNumberToLRUElement is a mapping between a specific file number and
|
||||
// the associated list element on the least recently used list.
|
||||
//
|
||||
// Thus, with the combination of these fields, the database supports
|
||||
// concurrent non-blocking reads across multiple and individual files
|
||||
// along with intelligently limiting the number of open file handles by
|
||||
// closing the least recently used files as needed.
|
||||
//
|
||||
// NOTE: The locking order used throughout is well-defined and MUST be
|
||||
// followed. Failure to do so could lead to deadlocks. In particular,
|
||||
// the locking order is as follows:
|
||||
// 1) openFilesMutex
|
||||
// 2) lruMutex
|
||||
// 3) writeCursor mutex
|
||||
// 4) specific file mutexes
|
||||
//
|
||||
// None of the mutexes are required to be locked at the same time, and
|
||||
// often aren't. However, if they are to be locked simultaneously, they
|
||||
// MUST be locked in the order previously specified.
|
||||
//
|
||||
// Due to the high performance and multi-read concurrency requirements,
|
||||
// write locks should only be held for the minimum time necessary.
|
||||
openFilesMutex sync.RWMutex
|
||||
openFiles map[uint32]*lockableFile
|
||||
lruMutex sync.Mutex
|
||||
openFilesLRU *list.List // Contains uint32 file numbers.
|
||||
fileNumberToLRUElement map[uint32]*list.Element
|
||||
|
||||
// writeCursor houses the state for the current file and location that
|
||||
// new data is written to.
|
||||
writeCursor *writeCursor
|
||||
|
||||
// isClosed is true when the store is closed. Any operations on a closed
|
||||
// store will fail.
|
||||
isClosed bool
|
||||
}
|
||||
|
||||
// writeCursor represents the current file and offset of the flat file on disk
|
||||
// for performing all writes. It also contains a read-write mutex to support
|
||||
// multiple concurrent readers which can reuse the file handle.
|
||||
type writeCursor struct {
|
||||
sync.RWMutex
|
||||
|
||||
// currentFile is the current file that will be appended to when writing
|
||||
// new data.
|
||||
currentFile *lockableFile
|
||||
|
||||
// currentFileNumber is the current file number and is used to allow
|
||||
// readers to use the same open file handle.
|
||||
currentFileNumber uint32
|
||||
|
||||
// currentOffset is the offset in the current file where the next new
|
||||
// data will be written.
|
||||
currentOffset uint32
|
||||
}
|
||||
|
||||
// openFlatFileStore returns a new flat file store with the current file number
|
||||
// and offset set and all fields initialized.
|
||||
func openFlatFileStore(basePath string, storeName string) (*flatFileStore, error) {
|
||||
// Look for the end of the latest file to determine what the write cursor
|
||||
// position is from the viewpoint of the flat files on disk.
|
||||
fileNumber, fileOffset, err := findCurrentLocation(basePath, storeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store := &flatFileStore{
|
||||
basePath: basePath,
|
||||
storeName: storeName,
|
||||
openFiles: make(map[uint32]*lockableFile),
|
||||
openFilesLRU: list.New(),
|
||||
fileNumberToLRUElement: make(map[uint32]*list.Element),
|
||||
writeCursor: &writeCursor{
|
||||
currentFile: &lockableFile{},
|
||||
currentFileNumber: fileNumber,
|
||||
currentOffset: fileOffset,
|
||||
},
|
||||
isClosed: false,
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (s *flatFileStore) Close() error {
|
||||
if s.isClosed {
|
||||
return errors.Errorf("cannot close a closed store %s",
|
||||
s.storeName)
|
||||
}
|
||||
s.isClosed = true
|
||||
|
||||
// Close the write cursor. We lock the write cursor here
|
||||
// to let it finish any undergoing writing.
|
||||
s.writeCursor.Lock()
|
||||
defer s.writeCursor.Unlock()
|
||||
err := s.writeCursor.currentFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Close all open files
|
||||
for _, openFile := range s.openFiles {
|
||||
err := openFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *flatFileStore) currentLocation() *flatFileLocation {
|
||||
return &flatFileLocation{
|
||||
fileNumber: s.writeCursor.currentFileNumber,
|
||||
fileOffset: s.writeCursor.currentOffset,
|
||||
dataLength: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// findCurrentLocation searches the database directory for all flat files for a given
|
||||
// store to find the end of the most recent file. This position is considered
|
||||
// the current write cursor.
|
||||
func findCurrentLocation(dbPath string, storeName string) (fileNumber uint32, fileLength uint32, err error) {
|
||||
currentFileNumber := uint32(0)
|
||||
currentFileLength := uint32(0)
|
||||
for {
|
||||
currentFilePath := flatFilePath(dbPath, storeName, currentFileNumber)
|
||||
stat, err := os.Stat(currentFilePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return 0, 0, errors.WithStack(err)
|
||||
}
|
||||
if currentFileNumber > 0 {
|
||||
fileNumber = currentFileNumber - 1
|
||||
}
|
||||
fileLength = currentFileLength
|
||||
break
|
||||
}
|
||||
currentFileLength = uint32(stat.Size())
|
||||
currentFileNumber++
|
||||
}
|
||||
|
||||
log.Tracef("Scan for store '%s' found latest file #%d with length %d",
|
||||
storeName, fileNumber, fileLength)
|
||||
return fileNumber, fileLength, nil
|
||||
}
|
||||
|
||||
// flatFilePath return the file path for the provided store's flat file number.
|
||||
func flatFilePath(dbPath string, storeName string, fileNumber uint32) string {
|
||||
// Choose 9 digits of precision for the filenames. 9 digits provide
|
||||
// 10^9 files @ 512MiB each a total of ~476.84PiB.
|
||||
|
||||
fileName := fmt.Sprintf("%s-%09d.fdb", storeName, fileNumber)
|
||||
return filepath.Join(dbPath, fileName)
|
||||
}
|
||||
175
infrastructure/db/database/ffldb/ff/flatfile_test.go
Normal file
175
infrastructure/db/database/ffldb/ff/flatfile_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package ff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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("%s: TempDir unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
name := "test"
|
||||
store, err = openFlatFileStore(path, name)
|
||||
if err != nil {
|
||||
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!")
|
||||
location, err := store.write(writeData)
|
||||
if err != nil {
|
||||
t.Fatalf("TestFlatFileStoreSanity: Write returned "+
|
||||
"unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// Read from the location previously written to
|
||||
readData, err := store.read(location)
|
||||
if err != nil {
|
||||
t.Fatalf("TestFlatFileStoreSanity: read returned "+
|
||||
"unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// Make sure that the written data and the read data are equal
|
||||
if !reflect.DeepEqual(readData, writeData) {
|
||||
t.Fatalf("TestFlatFileStoreSanity: read data and "+
|
||||
"write data are not equal. Wrote: %s, read: %s",
|
||||
string(writeData), string(readData))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlatFilePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
dbPath string
|
||||
storeName string
|
||||
fileNumber uint32
|
||||
expectedPath string
|
||||
}{
|
||||
{
|
||||
dbPath: "path",
|
||||
storeName: "store",
|
||||
fileNumber: 0,
|
||||
expectedPath: "path/store-000000000.fdb",
|
||||
},
|
||||
{
|
||||
dbPath: "path/to/database",
|
||||
storeName: "blocks",
|
||||
fileNumber: 123456789,
|
||||
expectedPath: "path/to/database/blocks-123456789.fdb",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
path := flatFilePath(test.dbPath, test.storeName, test.fileNumber)
|
||||
if path != test.expectedPath {
|
||||
t.Errorf("TestFlatFilePath: unexpected path. Want: %s, got: %s",
|
||||
test.expectedPath, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
103
infrastructure/db/database/ffldb/ff/flatfiledb.go
Normal file
103
infrastructure/db/database/ffldb/ff/flatfiledb.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package ff
|
||||
|
||||
// FlatFileDB is a flat-file database. It supports opening
|
||||
// multiple flat-file stores. See flatFileStore for further
|
||||
// details.
|
||||
type FlatFileDB struct {
|
||||
path string
|
||||
flatFileStores map[string]*flatFileStore
|
||||
}
|
||||
|
||||
// NewFlatFileDB opens the flat-file database defined by
|
||||
// the given path.
|
||||
func NewFlatFileDB(path string) *FlatFileDB {
|
||||
return &FlatFileDB{
|
||||
path: path,
|
||||
flatFileStores: make(map[string]*flatFileStore),
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the flat-file database.
|
||||
func (ffdb *FlatFileDB) Close() error {
|
||||
for _, store := range ffdb.flatFileStores {
|
||||
err := store.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write appends the specified data bytes to the specified store.
|
||||
// It returns a serialized location handle that's meant to be
|
||||
// stored and later used when querying the data that has just now
|
||||
// been inserted.
|
||||
// See flatFileStore.write() for further details.
|
||||
func (ffdb *FlatFileDB) Write(storeName string, data []byte) ([]byte, error) {
|
||||
store, err := ffdb.store(storeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
location, err := store.write(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return serializeLocation(location), nil
|
||||
}
|
||||
|
||||
// Read reads data from the specified flat file store at the
|
||||
// location specified by the given serialized location handle.
|
||||
// It returns ErrNotFound if the location does not exist.
|
||||
// See flatFileStore.read() for further details.
|
||||
func (ffdb *FlatFileDB) Read(storeName string, serializedLocation []byte) ([]byte, error) {
|
||||
store, err := ffdb.store(storeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
location, err := deserializeLocation(serializedLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return store.read(location)
|
||||
}
|
||||
|
||||
// CurrentLocation returns the serialized location handle to
|
||||
// the current location within the flat file store defined
|
||||
// storeName. It is mainly to be used to rollback flat-file
|
||||
// stores in case of data incongruency.
|
||||
func (ffdb *FlatFileDB) CurrentLocation(storeName string) ([]byte, error) {
|
||||
store, err := ffdb.store(storeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentLocation := store.currentLocation()
|
||||
return serializeLocation(currentLocation), nil
|
||||
}
|
||||
|
||||
// Rollback truncates the flat-file store defined by the given
|
||||
// storeName to the location defined by the given serialized
|
||||
// location handle.
|
||||
func (ffdb *FlatFileDB) Rollback(storeName string, serializedLocation []byte) error {
|
||||
store, err := ffdb.store(storeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
location, err := deserializeLocation(serializedLocation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return store.rollback(location)
|
||||
}
|
||||
|
||||
func (ffdb *FlatFileDB) store(storeName string) (*flatFileStore, error) {
|
||||
store, ok := ffdb.flatFileStores[storeName]
|
||||
if !ok {
|
||||
var err error
|
||||
store, err = openFlatFileStore(ffdb.path, storeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ffdb.flatFileStores[storeName] = store
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
44
infrastructure/db/database/ffldb/ff/location.go
Normal file
44
infrastructure/db/database/ffldb/ff/location.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package ff
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
// flatFileLocationSerializedSize is the size in bytes of a serialized flat
|
||||
// file location. See serializeLocation for further details.
|
||||
const flatFileLocationSerializedSize = 12
|
||||
|
||||
// flatFileLocation identifies a particular flat file location.
|
||||
type flatFileLocation struct {
|
||||
fileNumber uint32
|
||||
fileOffset uint32
|
||||
dataLength uint32
|
||||
}
|
||||
|
||||
// serializeLocation returns the serialization of the passed flat file location
|
||||
// of certain data. This to later on be used for retrieval of said data.
|
||||
// The serialized location format is:
|
||||
//
|
||||
// [0:4] File Number (4 bytes)
|
||||
// [4:8] File offset (4 bytes)
|
||||
// [8:12] Data length (4 bytes)
|
||||
func serializeLocation(location *flatFileLocation) []byte {
|
||||
var serializedLocation [flatFileLocationSerializedSize]byte
|
||||
byteOrder.PutUint32(serializedLocation[0:4], location.fileNumber)
|
||||
byteOrder.PutUint32(serializedLocation[4:8], location.fileOffset)
|
||||
byteOrder.PutUint32(serializedLocation[8:12], location.dataLength)
|
||||
return serializedLocation[:]
|
||||
}
|
||||
|
||||
// deserializeLocation deserializes the passed serialized flat file location.
|
||||
// See serializeLocation for further details.
|
||||
func deserializeLocation(serializedLocation []byte) (*flatFileLocation, error) {
|
||||
if len(serializedLocation) != flatFileLocationSerializedSize {
|
||||
return nil, errors.Errorf("unexpected serializedLocation length: %d",
|
||||
len(serializedLocation))
|
||||
}
|
||||
location := &flatFileLocation{
|
||||
fileNumber: byteOrder.Uint32(serializedLocation[0:4]),
|
||||
fileOffset: byteOrder.Uint32(serializedLocation[4:8]),
|
||||
dataLength: byteOrder.Uint32(serializedLocation[8:12]),
|
||||
}
|
||||
return location, nil
|
||||
}
|
||||
62
infrastructure/db/database/ffldb/ff/location_test.go
Normal file
62
infrastructure/db/database/ffldb/ff/location_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
44
infrastructure/db/database/ffldb/ff/lockablefile.go
Normal file
44
infrastructure/db/database/ffldb/ff/lockablefile.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package ff
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// lockableFile represents a flat file on disk that has been opened for either
|
||||
// read or read/write access. It also contains a read-write mutex to support
|
||||
// multiple concurrent readers.
|
||||
type lockableFile struct {
|
||||
sync.RWMutex
|
||||
file
|
||||
|
||||
isClosed bool
|
||||
}
|
||||
|
||||
// file is an interface which acts very similar to a *os.File and is typically
|
||||
// implemented by it. It exists so the test code can provide mock files for
|
||||
// properly testing corruption and file system issues.
|
||||
type file interface {
|
||||
io.Closer
|
||||
io.WriterAt
|
||||
io.ReaderAt
|
||||
Truncate(size int64) error
|
||||
Sync() error
|
||||
}
|
||||
|
||||
func (lf *lockableFile) Close() error {
|
||||
if lf.isClosed {
|
||||
return errors.Errorf("cannot close an already closed file")
|
||||
}
|
||||
lf.isClosed = true
|
||||
|
||||
lf.Lock()
|
||||
defer lf.Unlock()
|
||||
|
||||
if lf.file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.WithStack(lf.file.Close())
|
||||
}
|
||||
5
infrastructure/db/database/ffldb/ff/log.go
Normal file
5
infrastructure/db/database/ffldb/ff/log.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package ff
|
||||
|
||||
import "github.com/kaspanet/kaspad/infrastructure/logger"
|
||||
|
||||
var log, _ = logger.Get(logger.SubsystemTags.KSDB)
|
||||
153
infrastructure/db/database/ffldb/ff/read.go
Normal file
153
infrastructure/db/database/ffldb/ff/read.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package ff
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"github.com/pkg/errors"
|
||||
"hash/crc32"
|
||||
"os"
|
||||
)
|
||||
|
||||
// read reads the specified flat file record and returns the data. It ensures
|
||||
// the integrity of the data by comparing the calculated checksum against the
|
||||
// one stored in the flat file. This function also automatically handles all
|
||||
// file management such as opening and closing files as necessary to stay
|
||||
// within the maximum allowed open files limit. It returns ErrNotFound if the
|
||||
// location does not exist.
|
||||
//
|
||||
// Format: <data length><data><checksum>
|
||||
func (s *flatFileStore) read(location *flatFileLocation) ([]byte, error) {
|
||||
if s.isClosed {
|
||||
return nil, errors.Errorf("cannot read from a closed store %s",
|
||||
s.storeName)
|
||||
}
|
||||
|
||||
// Return not-found if the location is greater than or equal to
|
||||
// the current write cursor.
|
||||
if s.writeCursor.currentFileNumber < location.fileNumber ||
|
||||
(s.writeCursor.currentFileNumber == location.fileNumber && s.writeCursor.currentOffset <= location.fileOffset) {
|
||||
return nil, database.ErrNotFound
|
||||
}
|
||||
|
||||
// Get the referenced flat file.
|
||||
flatFile, err := s.flatFile(location.fileNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flatFile.RLock()
|
||||
defer flatFile.RUnlock()
|
||||
|
||||
data := make([]byte, location.dataLength)
|
||||
n, err := flatFile.file.ReadAt(data, int64(location.fileOffset))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read data in store '%s' "+
|
||||
"from file %d, offset %d", s.storeName, location.fileNumber,
|
||||
location.fileOffset)
|
||||
}
|
||||
|
||||
// Calculate the checksum of the read data and ensure it matches the
|
||||
// serialized checksum.
|
||||
serializedChecksum := crc32ByteOrder.Uint32(data[n-crc32ChecksumLength:])
|
||||
calculatedChecksum := crc32.Checksum(data[:n-crc32ChecksumLength], castagnoli)
|
||||
if serializedChecksum != calculatedChecksum {
|
||||
return nil, errors.Errorf("data in store '%s' does not match "+
|
||||
"checksum - got %x, want %x", s.storeName, calculatedChecksum,
|
||||
serializedChecksum)
|
||||
}
|
||||
|
||||
// The data excludes the length of the data and the checksum.
|
||||
return data[dataLengthLength : n-crc32ChecksumLength], nil
|
||||
}
|
||||
|
||||
// flatFile attempts to return an existing file handle for the passed flat file
|
||||
// number if it is already open as well as marking it as most recently used. It
|
||||
// will also open the file when it's not already open subject to the rules
|
||||
// described in openFile. Also handles closing files as needed to avoid going
|
||||
// over the max allowed open files.
|
||||
func (s *flatFileStore) flatFile(fileNumber uint32) (*lockableFile, error) {
|
||||
// When the requested flat file is open for writes, return it.
|
||||
s.writeCursor.RLock()
|
||||
defer s.writeCursor.RUnlock()
|
||||
if fileNumber == s.writeCursor.currentFileNumber && s.writeCursor.currentFile.file != nil {
|
||||
openFile := s.writeCursor.currentFile
|
||||
return openFile, nil
|
||||
}
|
||||
|
||||
// Try to return an open file under the overall files read lock.
|
||||
s.openFilesMutex.RLock()
|
||||
defer s.openFilesMutex.RUnlock()
|
||||
if openFile, ok := s.openFiles[fileNumber]; ok {
|
||||
s.lruMutex.Lock()
|
||||
defer s.lruMutex.Unlock()
|
||||
|
||||
s.openFilesLRU.MoveToFront(s.fileNumberToLRUElement[fileNumber])
|
||||
|
||||
return openFile, nil
|
||||
}
|
||||
|
||||
// Since the file isn't open already, need to check the open files map
|
||||
// again under write lock in case multiple readers got here and a
|
||||
// separate one is already opening the file.
|
||||
if openFlatFile, ok := s.openFiles[fileNumber]; ok {
|
||||
return openFlatFile, nil
|
||||
}
|
||||
|
||||
// The file isn't open, so open it while potentially closing the least
|
||||
// recently used one as needed.
|
||||
openFile, err := s.openFile(fileNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openFile, nil
|
||||
}
|
||||
|
||||
// openFile returns a read-only file handle for the passed flat file number.
|
||||
// The function also keeps track of the open files, performs least recently
|
||||
// used tracking, and limits the number of open files to maxOpenFiles by closing
|
||||
// the least recently used file as needed.
|
||||
//
|
||||
// This function MUST be called with the open files mutex (s.openFilesMutex)
|
||||
// locked for WRITES.
|
||||
func (s *flatFileStore) openFile(fileNumber uint32) (*lockableFile, error) {
|
||||
// Open the appropriate file as read-only.
|
||||
filePath := flatFilePath(s.basePath, s.storeName, fileNumber)
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
flatFile := &lockableFile{file: file}
|
||||
|
||||
// Close the least recently used file if the file exceeds the max
|
||||
// allowed open files. This is not done until after the file open in
|
||||
// case the file fails to open, there is no need to close any files.
|
||||
//
|
||||
// A write lock is required on the LRU list here to protect against
|
||||
// modifications happening as already open files are read from and
|
||||
// shuffled to the front of the list.
|
||||
//
|
||||
// Also, add the file that was just opened to the front of the least
|
||||
// recently used list to indicate it is the most recently used file and
|
||||
// therefore should be closed last.
|
||||
s.lruMutex.Lock()
|
||||
defer s.lruMutex.Unlock()
|
||||
lruList := s.openFilesLRU
|
||||
if lruList.Len() >= maxOpenFiles {
|
||||
lruFileNumber := lruList.Remove(lruList.Back()).(uint32)
|
||||
oldFile := s.openFiles[lruFileNumber]
|
||||
|
||||
// Close the old file under the write lock for the file in case
|
||||
// any readers are currently reading from it so it's not closed
|
||||
// out from under them.
|
||||
oldFile.Lock()
|
||||
defer oldFile.Unlock()
|
||||
_ = oldFile.file.Close()
|
||||
|
||||
delete(s.openFiles, lruFileNumber)
|
||||
delete(s.fileNumberToLRUElement, lruFileNumber)
|
||||
}
|
||||
s.fileNumberToLRUElement[fileNumber] = lruList.PushFront(fileNumber)
|
||||
|
||||
// Store a reference to it in the open files map.
|
||||
s.openFiles[fileNumber] = flatFile
|
||||
|
||||
return flatFile, nil
|
||||
}
|
||||
135
infrastructure/db/database/ffldb/ff/rollback.go
Normal file
135
infrastructure/db/database/ffldb/ff/rollback.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package ff
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
// rollback rolls the flat files on disk back to the provided file number
|
||||
// and offset. This involves potentially deleting and truncating the files that
|
||||
// were partially written.
|
||||
//
|
||||
// There are effectively two scenarios to consider here:
|
||||
// 1) Transient write failures from which recovery is possible
|
||||
// 2) More permanent failures such as hard disk death and/or removal
|
||||
//
|
||||
// In either case, the write cursor will be repositioned to the old flat file
|
||||
// offset regardless of any other errors that occur while attempting to undo
|
||||
// writes.
|
||||
//
|
||||
// For the first scenario, this will lead to any data which failed to be undone
|
||||
// being overwritten and thus behaves as desired as the system continues to run.
|
||||
//
|
||||
// For the second scenario, the metadata which stores the current write cursor
|
||||
// position within the flat files will not have been updated yet and thus if
|
||||
// the system eventually recovers (perhaps the hard drive is reconnected), it
|
||||
// will also lead to any data which failed to be undone being overwritten and
|
||||
// thus behaves as desired.
|
||||
func (s *flatFileStore) rollback(targetLocation *flatFileLocation) error {
|
||||
if s.isClosed {
|
||||
return errors.Errorf("cannot rollback a closed store %s",
|
||||
s.storeName)
|
||||
}
|
||||
|
||||
// Grab the write cursor mutex since it is modified throughout this
|
||||
// function.
|
||||
s.writeCursor.Lock()
|
||||
defer s.writeCursor.Unlock()
|
||||
|
||||
// Nothing to do if the rollback point is the same as the current write
|
||||
// cursor.
|
||||
targetFileNumber := targetLocation.fileNumber
|
||||
targetFileOffset := targetLocation.fileOffset
|
||||
if s.writeCursor.currentFileNumber == targetFileNumber && s.writeCursor.currentOffset == targetFileOffset {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the rollback point is greater than the current write cursor then
|
||||
// something has gone very wrong, e.g. database corruption.
|
||||
if s.writeCursor.currentFileNumber < targetFileNumber ||
|
||||
(s.writeCursor.currentFileNumber == targetFileNumber && s.writeCursor.currentOffset < targetFileOffset) {
|
||||
return errors.Errorf("targetLocation is greater than the " +
|
||||
"current write cursor")
|
||||
}
|
||||
|
||||
// Regardless of any failures that happen below, reposition the write
|
||||
// cursor to the target flat file and offset.
|
||||
defer func() {
|
||||
s.writeCursor.currentFileNumber = targetFileNumber
|
||||
s.writeCursor.currentOffset = targetFileOffset
|
||||
}()
|
||||
|
||||
log.Warnf("ROLLBACK: Rolling back to file %d, offset %d",
|
||||
targetFileNumber, targetFileOffset)
|
||||
|
||||
// Close the current write file if it needs to be deleted.
|
||||
if s.writeCursor.currentFileNumber > targetFileNumber {
|
||||
s.closeCurrentWriteCursorFile()
|
||||
}
|
||||
|
||||
// Delete all files that are newer than the provided rollback file
|
||||
// while also moving the write cursor file backwards accordingly.
|
||||
s.lruMutex.Lock()
|
||||
defer s.lruMutex.Unlock()
|
||||
s.openFilesMutex.Lock()
|
||||
defer s.openFilesMutex.Unlock()
|
||||
for s.writeCursor.currentFileNumber > targetFileNumber {
|
||||
err := s.deleteFile(s.writeCursor.currentFileNumber)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "ROLLBACK: Failed to delete file "+
|
||||
"number %d in store '%s'", s.writeCursor.currentFileNumber,
|
||||
s.storeName)
|
||||
}
|
||||
s.writeCursor.currentFileNumber--
|
||||
}
|
||||
|
||||
// Open the file for the current write cursor if needed.
|
||||
s.writeCursor.currentFile.Lock()
|
||||
defer s.writeCursor.currentFile.Unlock()
|
||||
if s.writeCursor.currentFile.file == nil {
|
||||
openFile, err := s.openWriteFile(s.writeCursor.currentFileNumber)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.writeCursor.currentFile.file = openFile
|
||||
}
|
||||
|
||||
// Truncate the file to the provided target offset.
|
||||
err := s.writeCursor.currentFile.file.Truncate(int64(targetFileOffset))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "ROLLBACK: Failed to truncate file %d "+
|
||||
"in store '%s'", s.writeCursor.currentFileNumber, s.storeName)
|
||||
}
|
||||
|
||||
// Sync the file to disk.
|
||||
err = s.writeCursor.currentFile.file.Sync()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "ROLLBACK: Failed to sync file %d in "+
|
||||
"store '%s'", s.writeCursor.currentFileNumber, s.storeName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteFile removes the file for the passed flat file number.
|
||||
// This function MUST be called with the lruMutex and the openFilesMutex
|
||||
// held for writes.
|
||||
func (s *flatFileStore) deleteFile(fileNumber uint32) error {
|
||||
// Cleanup the file before deleting it
|
||||
if file, ok := s.openFiles[fileNumber]; ok {
|
||||
file.Lock()
|
||||
defer file.Unlock()
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lruElement := s.fileNumberToLRUElement[fileNumber]
|
||||
s.openFilesLRU.Remove(lruElement)
|
||||
delete(s.openFiles, fileNumber)
|
||||
delete(s.fileNumberToLRUElement, fileNumber)
|
||||
}
|
||||
|
||||
// Delete the file from disk
|
||||
filePath := flatFilePath(s.basePath, s.storeName, fileNumber)
|
||||
return errors.WithStack(os.Remove(filePath))
|
||||
}
|
||||
176
infrastructure/db/database/ffldb/ff/write.go
Normal file
176
infrastructure/db/database/ffldb/ff/write.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package ff
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/util/panics"
|
||||
"github.com/pkg/errors"
|
||||
"hash/crc32"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// write appends the specified data bytes to the store's write cursor location
|
||||
// and increments it accordingly. When the data would exceed the max file size
|
||||
// for the current flat file, this function will close the current file, create
|
||||
// the next file, update the write cursor, and write the data to the new file.
|
||||
//
|
||||
// The write cursor will also be advanced the number of bytes actually written
|
||||
// in the event of failure.
|
||||
//
|
||||
// Format: <data length><data><checksum>
|
||||
func (s *flatFileStore) write(data []byte) (*flatFileLocation, error) {
|
||||
if s.isClosed {
|
||||
return nil, errors.Errorf("cannot write to a closed store %s",
|
||||
s.storeName)
|
||||
}
|
||||
|
||||
// Compute how many bytes will be written.
|
||||
// 4 bytes for data length + length of the data + 4 bytes for checksum.
|
||||
dataLength := uint32(len(data))
|
||||
fullLength := uint32(dataLengthLength) + dataLength + uint32(crc32ChecksumLength)
|
||||
|
||||
// Move to the next file if adding the new data would exceed the max
|
||||
// allowed size for the current flat file. Also detect overflow because
|
||||
// even though it isn't possible currently, numbers might change in
|
||||
// the future to make it possible.
|
||||
//
|
||||
// NOTE: The writeCursor.currentOffset field isn't protected by the
|
||||
// mutex since it's only read/changed during this function which can
|
||||
// only be called during a write transaction, of which there can be
|
||||
// only one at a time.
|
||||
cursor := s.writeCursor
|
||||
finalOffset := cursor.currentOffset + fullLength
|
||||
if finalOffset < cursor.currentOffset || finalOffset > maxFileSize {
|
||||
// This is done under the write cursor lock since the curFileNum
|
||||
// field is accessed elsewhere by readers.
|
||||
//
|
||||
// Close the current write file to force a read-only reopen
|
||||
// with LRU tracking. The close is done under the write lock
|
||||
// for the file to prevent it from being closed out from under
|
||||
// any readers currently reading from it.
|
||||
func() {
|
||||
cursor.Lock()
|
||||
defer cursor.Unlock()
|
||||
|
||||
s.closeCurrentWriteCursorFile()
|
||||
|
||||
// Start writes into next file.
|
||||
cursor.currentFileNumber++
|
||||
cursor.currentOffset = 0
|
||||
}()
|
||||
}
|
||||
|
||||
// All writes are done under the write lock for the file to ensure any
|
||||
// readers are finished and blocked first.
|
||||
cursor.currentFile.Lock()
|
||||
defer cursor.currentFile.Unlock()
|
||||
|
||||
// Open the current file if needed. This will typically only be the
|
||||
// case when moving to the next file to write to or on initial database
|
||||
// load. However, it might also be the case if rollbacks happened after
|
||||
// file writes started during a transaction commit.
|
||||
if cursor.currentFile.file == nil {
|
||||
file, err := s.openWriteFile(cursor.currentFileNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cursor.currentFile.file = file
|
||||
}
|
||||
|
||||
originalOffset := cursor.currentOffset
|
||||
hasher := crc32.New(castagnoli)
|
||||
var scratch [4]byte
|
||||
|
||||
// Data length.
|
||||
byteOrder.PutUint32(scratch[:], dataLength)
|
||||
err := s.writeData(scratch[:], "data length")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = hasher.Write(scratch[:])
|
||||
|
||||
// Data.
|
||||
err = s.writeData(data[:], "data")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = hasher.Write(data)
|
||||
|
||||
// Castagnoli CRC-32 as a checksum of all the previous.
|
||||
err = s.writeData(hasher.Sum(nil), "checksum")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sync the file to disk.
|
||||
err = cursor.currentFile.file.Sync()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to sync file %d "+
|
||||
"in store '%s'", cursor.currentFileNumber, s.storeName)
|
||||
}
|
||||
|
||||
location := &flatFileLocation{
|
||||
fileNumber: cursor.currentFileNumber,
|
||||
fileOffset: originalOffset,
|
||||
dataLength: fullLength,
|
||||
}
|
||||
return location, nil
|
||||
}
|
||||
|
||||
// openWriteFile returns a file handle for the passed flat file number in
|
||||
// read/write mode. The file will be created if needed. It is typically used
|
||||
// for the current file that will have all new data appended. Unlike openFile,
|
||||
// this function does not keep track of the open file and it is not subject to
|
||||
// the maxOpenFiles limit.
|
||||
func (s *flatFileStore) openWriteFile(fileNumber uint32) (file, error) {
|
||||
// The current flat file needs to be read-write so it is possible to
|
||||
// append to it. Also, it shouldn't be part of the least recently used
|
||||
// file.
|
||||
filePath := flatFilePath(s.basePath, s.storeName, fileNumber)
|
||||
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to open file %q",
|
||||
filePath)
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// writeData is a helper function for write which writes the provided data at
|
||||
// the current write offset and updates the write cursor accordingly. The field
|
||||
// name parameter is only used when there is an error to provide a nicer error
|
||||
// message.
|
||||
//
|
||||
// The write cursor will be advanced the number of bytes actually written in the
|
||||
// event of failure.
|
||||
//
|
||||
// NOTE: This function MUST be called with the write cursor current file lock
|
||||
// held and must only be called during a write transaction so it is effectively
|
||||
// locked for writes. Also, the write cursor current file must NOT be nil.
|
||||
func (s *flatFileStore) writeData(data []byte, fieldName string) error {
|
||||
cursor := s.writeCursor
|
||||
n, err := cursor.currentFile.file.WriteAt(data, int64(cursor.currentOffset))
|
||||
cursor.currentOffset += uint32(n)
|
||||
if err != nil {
|
||||
var pathErr *os.PathError
|
||||
if ok := errors.As(err, &pathErr); ok && pathErr.Err == syscall.ENOSPC {
|
||||
panics.Exit(log, "No space left on the hard disk.")
|
||||
}
|
||||
return errors.Wrapf(err, "failed to write %s in store %s to file %d "+
|
||||
"at offset %d", fieldName, s.storeName, cursor.currentFileNumber,
|
||||
cursor.currentOffset-uint32(n))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeCurrentWriteCursorFile closes the currently open writeCursor file if
|
||||
// it's open.
|
||||
// This method MUST be called with the writeCursor lock held for writes.
|
||||
func (s *flatFileStore) closeCurrentWriteCursorFile() {
|
||||
s.writeCursor.currentFile.Lock()
|
||||
defer s.writeCursor.currentFile.Unlock()
|
||||
if s.writeCursor.currentFile.file != nil {
|
||||
_ = s.writeCursor.currentFile.file.Close()
|
||||
s.writeCursor.currentFile.file = nil
|
||||
}
|
||||
}
|
||||
178
infrastructure/db/database/ffldb/ffldb.go
Normal file
178
infrastructure/db/database/ffldb/ffldb.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package ffldb
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database/ffldb/ff"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database/ffldb/ldb"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// flatFilesBucket keeps an index flat-file stores and their
|
||||
// current locations. Among other things, it is used to repair
|
||||
// the database in case a corruption occurs.
|
||||
flatFilesBucket = database.MakeBucket([]byte("flat-files"))
|
||||
)
|
||||
|
||||
// ffldb is a database utilizing LevelDB for key-value data and
|
||||
// flat-files for raw data storage.
|
||||
type ffldb struct {
|
||||
flatFileDB *ff.FlatFileDB
|
||||
levelDB *ldb.LevelDB
|
||||
}
|
||||
|
||||
// Open opens a new ffldb with the given path.
|
||||
func Open(path string) (database.Database, error) {
|
||||
flatFileDB := ff.NewFlatFileDB(path)
|
||||
levelDB, err := ldb.NewLevelDB(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := &ffldb{
|
||||
flatFileDB: flatFileDB,
|
||||
levelDB: levelDB,
|
||||
}
|
||||
|
||||
err = db.initialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// Close closes the database.
|
||||
// This method is part of the Database interface.
|
||||
func (db *ffldb) Close() error {
|
||||
err := db.flatFileDB.Close()
|
||||
if err != nil {
|
||||
ldbCloseErr := db.levelDB.Close()
|
||||
if ldbCloseErr != nil {
|
||||
return errors.Wrapf(err, "err occurred during leveldb close: %s", ldbCloseErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return db.levelDB.Close()
|
||||
}
|
||||
|
||||
// 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 (db *ffldb) Put(key *database.Key, value []byte) error {
|
||||
return db.levelDB.Put(key, value)
|
||||
}
|
||||
|
||||
// Get gets the value for the given key. It returns
|
||||
// ErrNotFound if the given key does not exist.
|
||||
// This method is part of the DataAccessor interface.
|
||||
func (db *ffldb) Get(key *database.Key) ([]byte, error) {
|
||||
return db.levelDB.Get(key)
|
||||
}
|
||||
|
||||
// Has returns true if the database does contains the
|
||||
// given key.
|
||||
// This method is part of the DataAccessor interface.
|
||||
func (db *ffldb) Has(key *database.Key) (bool, error) {
|
||||
return db.levelDB.Has(key)
|
||||
}
|
||||
|
||||
// Delete deletes the value for the given key. Will not
|
||||
// return an error if the key doesn't exist.
|
||||
// This method is part of the DataAccessor interface.
|
||||
func (db *ffldb) Delete(key *database.Key) error {
|
||||
return db.levelDB.Delete(key)
|
||||
}
|
||||
|
||||
// AppendToStore appends the given data to the flat
|
||||
// file store defined by storeName. This function
|
||||
// returns a serialized location handle that's meant
|
||||
// to be stored and later used when querying the data
|
||||
// that has just now been inserted.
|
||||
// This method is part of the DataAccessor interface.
|
||||
func (db *ffldb) AppendToStore(storeName string, data []byte) ([]byte, error) {
|
||||
return appendToStore(db, db.flatFileDB, storeName, data)
|
||||
}
|
||||
|
||||
func appendToStore(accessor database.DataAccessor, ffdb *ff.FlatFileDB, storeName string, data []byte) ([]byte, error) {
|
||||
// Save a reference to the current location in case
|
||||
// we fail and need to rollback.
|
||||
previousLocation, err := ffdb.CurrentLocation(storeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rollback := func() error {
|
||||
return ffdb.Rollback(storeName, previousLocation)
|
||||
}
|
||||
|
||||
// Append the data to the store and rollback in case of an error.
|
||||
location, err := ffdb.Write(storeName, data)
|
||||
if err != nil {
|
||||
rollbackErr := rollback()
|
||||
if rollbackErr != nil {
|
||||
return nil, errors.Wrapf(err, "error occurred during rollback: %s", rollbackErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the new location. If this fails we won't be able to update
|
||||
// the current store location, in which case we roll back.
|
||||
currentLocation, err := ffdb.CurrentLocation(storeName)
|
||||
if err != nil {
|
||||
rollbackErr := rollback()
|
||||
if rollbackErr != nil {
|
||||
return nil, errors.Wrapf(err, "error occurred during rollback: %s", rollbackErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set the current store location and roll back in case an error.
|
||||
err = setCurrentStoreLocation(accessor, storeName, currentLocation)
|
||||
if err != nil {
|
||||
rollbackErr := rollback()
|
||||
if rollbackErr != nil {
|
||||
return nil, errors.Wrapf(err, "error occurred during rollback: %s", rollbackErr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return location, err
|
||||
}
|
||||
|
||||
func setCurrentStoreLocation(accessor database.DataAccessor, storeName string, location []byte) error {
|
||||
locationKey := flatFilesBucket.Key([]byte(storeName))
|
||||
return accessor.Put(locationKey, location)
|
||||
}
|
||||
|
||||
// RetrieveFromStore retrieves data from the store defined by
|
||||
// storeName using the given serialized location handle. It
|
||||
// returns ErrNotFound if the location does not exist. See
|
||||
// AppendToStore for further details.
|
||||
// This method is part of the DataAccessor interface.
|
||||
func (db *ffldb) RetrieveFromStore(storeName string, location []byte) ([]byte, error) {
|
||||
return db.flatFileDB.Read(storeName, location)
|
||||
}
|
||||
|
||||
// Cursor begins a new cursor over the given bucket.
|
||||
// This method is part of the DataAccessor interface.
|
||||
func (db *ffldb) Cursor(bucket *database.Bucket) (database.Cursor, error) {
|
||||
ldbCursor := db.levelDB.Cursor(bucket)
|
||||
|
||||
return ldbCursor, nil
|
||||
}
|
||||
|
||||
// Begin begins a new ffldb transaction.
|
||||
// This method is part of the Database interface.
|
||||
func (db *ffldb) Begin() (database.Transaction, error) {
|
||||
ldbTx, err := db.levelDB.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transaction := &transaction{
|
||||
ldbTx: ldbTx,
|
||||
ffdb: db.flatFileDB,
|
||||
isClosed: false,
|
||||
}
|
||||
return transaction, nil
|
||||
}
|
||||
131
infrastructure/db/database/ffldb/ffldb_test.go
Normal file
131
infrastructure/db/database/ffldb/ffldb_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package ffldb
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"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")
|
||||
if err != nil {
|
||||
t.Fatalf("TestRepairFlatFiles: TempDir unexpectedly "+
|
||||
"failed: %s", err)
|
||||
}
|
||||
db, err := Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("TestRepairFlatFiles: Open unexpectedly "+
|
||||
"failed: %s", err)
|
||||
}
|
||||
isOpen := true
|
||||
defer func() {
|
||||
if isOpen {
|
||||
err := db.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("TestRepairFlatFiles: Close unexpectedly "+
|
||||
"failed: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Cast to ffldb since we're going to be messing with its internals
|
||||
ffldbInstance, ok := db.(*ffldb)
|
||||
if !ok {
|
||||
t.Fatalf("TestRepairFlatFiles: unexpectedly can't cast " +
|
||||
"db to ffldb")
|
||||
}
|
||||
|
||||
// Append data to the same store
|
||||
storeName := "test"
|
||||
_, err = ffldbInstance.AppendToStore(storeName, []byte("data1"))
|
||||
if err != nil {
|
||||
t.Fatalf("TestRepairFlatFiles: AppendToStore unexpectedly "+
|
||||
"failed: %s", err)
|
||||
}
|
||||
|
||||
// Grab the current location to test against later
|
||||
oldCurrentLocation, err := ffldbInstance.flatFileDB.CurrentLocation(storeName)
|
||||
if err != nil {
|
||||
t.Fatalf("TestRepairFlatFiles: CurrentStoreLocation "+
|
||||
"unexpectedly failed: %s", err)
|
||||
}
|
||||
|
||||
// Append more data to the same store. We expect this to disappear later.
|
||||
location2, err := ffldbInstance.AppendToStore(storeName, []byte("data2"))
|
||||
if err != nil {
|
||||
t.Fatalf("TestRepairFlatFiles: AppendToStore unexpectedly "+
|
||||
"failed: %s", err)
|
||||
}
|
||||
|
||||
// Manually update the current location to point to the first piece of data
|
||||
err = setCurrentStoreLocation(ffldbInstance, storeName, oldCurrentLocation)
|
||||
if err != nil {
|
||||
t.Fatalf("TestRepairFlatFiles: setCurrentStoreLocation "+
|
||||
"unexpectedly failed: %s", err)
|
||||
}
|
||||
|
||||
// Reopen the database
|
||||
err = ffldbInstance.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("TestRepairFlatFiles: Close unexpectedly "+
|
||||
"failed: %s", err)
|
||||
}
|
||||
isOpen = false
|
||||
db, err = Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("TestRepairFlatFiles: Open unexpectedly "+
|
||||
"failed: %s", err)
|
||||
}
|
||||
isOpen = true
|
||||
ffldbInstance, ok = db.(*ffldb)
|
||||
if !ok {
|
||||
t.Fatalf("TestRepairFlatFiles: unexpectedly can't cast " +
|
||||
"db to ffldb")
|
||||
}
|
||||
|
||||
// Make sure that the current location rolled back as expected
|
||||
currentLocation, err := ffldbInstance.flatFileDB.CurrentLocation(storeName)
|
||||
if err != nil {
|
||||
t.Fatalf("TestRepairFlatFiles: CurrentStoreLocation "+
|
||||
"unexpectedly failed: %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(oldCurrentLocation, currentLocation) {
|
||||
t.Fatalf("TestRepairFlatFiles: currentLocation did " +
|
||||
"not roll back")
|
||||
}
|
||||
|
||||
// Make sure that we can't get data that no longer exists
|
||||
_, err = ffldbInstance.RetrieveFromStore(storeName, location2)
|
||||
if err == nil {
|
||||
t.Fatalf("TestRepairFlatFiles: RetrieveFromStore " +
|
||||
"unexpectedly succeeded")
|
||||
}
|
||||
if !database.IsNotFoundError(err) {
|
||||
t.Fatalf("TestRepairFlatFiles: RetrieveFromStore "+
|
||||
"returned wrong error: %s", err)
|
||||
}
|
||||
}
|
||||
55
infrastructure/db/database/ffldb/initialize.go
Normal file
55
infrastructure/db/database/ffldb/initialize.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package ffldb
|
||||
|
||||
// initialize initializes the database. If this function fails then the
|
||||
// database is irrecoverably corrupted.
|
||||
func (db *ffldb) initialize() error {
|
||||
flatFiles, err := db.flatFiles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for storeName, currentLocation := range flatFiles {
|
||||
err := db.tryRepair(storeName, currentLocation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *ffldb) flatFiles() (map[string][]byte, error) {
|
||||
flatFilesCursor := db.levelDB.Cursor(flatFilesBucket)
|
||||
defer func() {
|
||||
err := flatFilesCursor.Close()
|
||||
if err != nil {
|
||||
log.Warnf("cursor failed to close")
|
||||
}
|
||||
}()
|
||||
|
||||
flatFiles := make(map[string][]byte)
|
||||
for flatFilesCursor.Next() {
|
||||
storeNameKey, err := flatFilesCursor.Key()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storeName := string(storeNameKey.Suffix())
|
||||
|
||||
currentLocation, err := flatFilesCursor.Value()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flatFiles[storeName] = currentLocation
|
||||
}
|
||||
return flatFiles, nil
|
||||
}
|
||||
|
||||
// tryRepair attempts to sync the store with the current location value.
|
||||
// Possible scenarios:
|
||||
// a. currentLocation and the store are synced. Rollback does nothing.
|
||||
// b. currentLocation is smaller than the store's location. Rollback truncates
|
||||
// the store.
|
||||
// c. currentLocation is greater than the store's location. Rollback returns an
|
||||
// error. This indicates definite database corruption and is irrecoverable.
|
||||
func (db *ffldb) tryRepair(storeName string, currentLocation []byte) error {
|
||||
return db.flatFileDB.Rollback(storeName, currentLocation)
|
||||
}
|
||||
111
infrastructure/db/database/ffldb/ldb/cursor.go
Normal file
111
infrastructure/db/database/ffldb/ldb/cursor.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package ldb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
// LevelDBCursor is a thin wrapper around native leveldb iterators.
|
||||
type LevelDBCursor struct {
|
||||
ldbIterator iterator.Iterator
|
||||
bucket *database.Bucket
|
||||
|
||||
isClosed bool
|
||||
}
|
||||
|
||||
// Cursor begins a new cursor over the given prefix.
|
||||
func (db *LevelDB) Cursor(bucket *database.Bucket) *LevelDBCursor {
|
||||
ldbIterator := db.ldb.NewIterator(util.BytesPrefix(bucket.Path()), nil)
|
||||
return &LevelDBCursor{
|
||||
ldbIterator: ldbIterator,
|
||||
bucket: bucket,
|
||||
isClosed: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Next moves the iterator to the next key/value pair. It returns whether the
|
||||
// iterator is exhausted. Panics if the cursor is closed.
|
||||
func (c *LevelDBCursor) Next() bool {
|
||||
if c.isClosed {
|
||||
panic("cannot call next on a closed cursor")
|
||||
}
|
||||
return c.ldbIterator.Next()
|
||||
}
|
||||
|
||||
// First moves the iterator to the first key/value pair. It returns false if
|
||||
// such a pair does not exist. Panics if the cursor is closed.
|
||||
func (c *LevelDBCursor) First() bool {
|
||||
if c.isClosed {
|
||||
panic("cannot call first on a closed cursor")
|
||||
}
|
||||
return c.ldbIterator.First()
|
||||
}
|
||||
|
||||
// Seek moves the iterator to the first key/value pair whose key is greater
|
||||
// than or equal to the given key. It returns ErrNotFound if such pair does not
|
||||
// exist.
|
||||
func (c *LevelDBCursor) Seek(key *database.Key) error {
|
||||
if c.isClosed {
|
||||
return errors.New("cannot seek a closed cursor")
|
||||
}
|
||||
|
||||
found := c.ldbIterator.Seek(key.Bytes())
|
||||
if !found {
|
||||
return errors.Wrapf(database.ErrNotFound, "key %s not found", key)
|
||||
}
|
||||
|
||||
// Use c.ldbIterator.Key because c.Key removes the prefix from the key
|
||||
currentKey := c.ldbIterator.Key()
|
||||
if currentKey == nil || !bytes.Equal(currentKey, key.Bytes()) {
|
||||
return errors.Wrapf(database.ErrNotFound, "key %s not found", key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Key returns the key of the current key/value pair, or ErrNotFound if done.
|
||||
// Note that the key is trimmed to not include the prefix the cursor was opened
|
||||
// with. The caller should not modify the contents of the returned slice, and
|
||||
// its contents may change on the next call to Next.
|
||||
func (c *LevelDBCursor) Key() (*database.Key, error) {
|
||||
if c.isClosed {
|
||||
return nil, errors.New("cannot get the key of a closed cursor")
|
||||
}
|
||||
fullKeyPath := c.ldbIterator.Key()
|
||||
if fullKeyPath == nil {
|
||||
return nil, errors.Wrapf(database.ErrNotFound, "cannot get the "+
|
||||
"key of an exhausted cursor")
|
||||
}
|
||||
suffix := bytes.TrimPrefix(fullKeyPath, c.bucket.Path())
|
||||
return c.bucket.Key(suffix), nil
|
||||
}
|
||||
|
||||
// Value returns the value of the current key/value pair, or ErrNotFound if done.
|
||||
// The caller should not modify the contents of the returned slice, and its
|
||||
// contents may change on the next call to Next.
|
||||
func (c *LevelDBCursor) Value() ([]byte, error) {
|
||||
if c.isClosed {
|
||||
return nil, errors.New("cannot get the value of a closed cursor")
|
||||
}
|
||||
value := c.ldbIterator.Value()
|
||||
if value == nil {
|
||||
return nil, errors.Wrapf(database.ErrNotFound, "cannot get the "+
|
||||
"value of an exhausted cursor")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Close releases associated resources.
|
||||
func (c *LevelDBCursor) Close() error {
|
||||
if c.isClosed {
|
||||
return errors.New("cannot close an already closed cursor")
|
||||
}
|
||||
c.isClosed = true
|
||||
|
||||
c.ldbIterator.Release()
|
||||
return nil
|
||||
}
|
||||
246
infrastructure/db/database/ffldb/ldb/cursor_test.go
Normal file
246
infrastructure/db/database/ffldb/ldb/cursor_test.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package ldb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/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()
|
||||
}()
|
||||
}
|
||||
88
infrastructure/db/database/ffldb/ldb/leveldb.go
Normal file
88
infrastructure/db/database/ffldb/ldb/leveldb.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package ldb
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
ldbErrors "github.com/syndtr/goleveldb/leveldb/errors"
|
||||
)
|
||||
|
||||
// LevelDB defines a thin wrapper around leveldb.
|
||||
type LevelDB struct {
|
||||
ldb *leveldb.DB
|
||||
}
|
||||
|
||||
// NewLevelDB opens a leveldb instance defined by the given path.
|
||||
func NewLevelDB(path string) (*LevelDB, error) {
|
||||
// Open leveldb. If it doesn't exist, create it.
|
||||
ldb, err := leveldb.OpenFile(path, Options())
|
||||
|
||||
// If the database is corrupted, attempt to recover.
|
||||
if _, corrupted := err.(*ldbErrors.ErrCorrupted); corrupted {
|
||||
log.Warnf("LevelDB corruption detected for path %s: %s",
|
||||
path, err)
|
||||
var recoverErr error
|
||||
ldb, recoverErr = leveldb.RecoverFile(path, nil)
|
||||
if recoverErr != nil {
|
||||
return nil, errors.Wrapf(err, "failed recovering from "+
|
||||
"database corruption: %s", recoverErr)
|
||||
}
|
||||
log.Warnf("LevelDB recovered from corruption for path %s",
|
||||
path)
|
||||
}
|
||||
|
||||
// If the database cannot be opened for any other
|
||||
// reason, return the error as-is.
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
db := &LevelDB{
|
||||
ldb: ldb,
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// Close closes the leveldb instance.
|
||||
func (db *LevelDB) Close() error {
|
||||
err := db.ldb.Close()
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Put sets the value for the given key. It overwrites
|
||||
// any previous value for that key.
|
||||
func (db *LevelDB) Put(key *database.Key, value []byte) error {
|
||||
err := db.ldb.Put(key.Bytes(), value, nil)
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Get gets the value for the given key. It returns
|
||||
// ErrNotFound if the given key does not exist.
|
||||
func (db *LevelDB) Get(key *database.Key) ([]byte, error) {
|
||||
data, err := db.ldb.Get(key.Bytes(), nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, leveldb.ErrNotFound) {
|
||||
return nil, errors.Wrapf(database.ErrNotFound,
|
||||
"key %s not found", key)
|
||||
}
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Has returns true if the database does contains the
|
||||
// given key.
|
||||
func (db *LevelDB) Has(key *database.Key) (bool, error) {
|
||||
exists, err := db.ldb.Has(key.Bytes(), nil)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
// Delete deletes the value for the given key. Will not
|
||||
// return an error if the key doesn't exist.
|
||||
func (db *LevelDB) Delete(key *database.Key) error {
|
||||
err := db.ldb.Delete(key.Bytes(), nil)
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
152
infrastructure/db/database/ffldb/ldb/leveldb_test.go
Normal file
152
infrastructure/db/database/ffldb/ldb/leveldb_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package ldb
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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("%s: TempDir unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
ldb, err = NewLevelDB(path)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: NewLevelDB unexpectedly "+
|
||||
"failed: %s", testName, err)
|
||||
}
|
||||
teardownFunc = func() {
|
||||
err = ldb.Close()
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatalf("TestLevelDBSanity: Put returned "+
|
||||
"unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// Get from the key previously put to
|
||||
getData, err := ldb.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("TestLevelDBSanity: Get returned "+
|
||||
"unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// Make sure that the put data and the get data are equal
|
||||
if !reflect.DeepEqual(getData, putData) {
|
||||
t.Fatalf("TestLevelDBSanity: get data and "+
|
||||
"put data are not equal. Put: %s, got: %s",
|
||||
string(putData), string(getData))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelDBTransactionSanity(t *testing.T) {
|
||||
ldb, teardownFunc := prepareDatabaseForTest(t, "TestLevelDBTransactionSanity")
|
||||
defer teardownFunc()
|
||||
|
||||
// Case 1. Write in tx and then read directly from the DB
|
||||
// Begin a new transaction
|
||||
tx, err := ldb.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: Begin "+
|
||||
"unexpectedly failed: %s", err)
|
||||
}
|
||||
|
||||
// Put something into the transaction
|
||||
key := database.MakeBucket().Key([]byte("key"))
|
||||
putData := []byte("Hello world!")
|
||||
err = tx.Put(key, putData)
|
||||
if err != nil {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: Put "+
|
||||
"returned unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// Get from the key previously put to. Since the tx is not
|
||||
// yet committed, this should return ErrNotFound.
|
||||
getData, err := ldb.Get(key)
|
||||
if err == nil {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: Get " +
|
||||
"unexpectedly succeeded")
|
||||
}
|
||||
if !database.IsNotFoundError(err) {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: Get "+
|
||||
"returned wrong error: %s", err)
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: Commit "+
|
||||
"returned unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// Get from the key previously put to. Now that the tx was
|
||||
// committed, this should succeed.
|
||||
getData, err = ldb.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: Get "+
|
||||
"returned unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// Make sure that the put data and the get data are equal
|
||||
if !reflect.DeepEqual(getData, putData) {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: get "+
|
||||
"data and put data are not equal. Put: %s, got: %s",
|
||||
string(putData), string(getData))
|
||||
}
|
||||
|
||||
// Case 2. Write directly to the DB and then read from a tx
|
||||
// Put something into the db
|
||||
key = database.MakeBucket().Key([]byte("key2"))
|
||||
putData = []byte("Goodbye world!")
|
||||
err = ldb.Put(key, putData)
|
||||
if err != nil {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: Put "+
|
||||
"returned unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// Begin a new transaction
|
||||
tx, err = ldb.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: Begin "+
|
||||
"unexpectedly failed: %s", err)
|
||||
}
|
||||
|
||||
// Get from the key previously put to
|
||||
getData, err = tx.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: Get "+
|
||||
"returned unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// Make sure that the put data and the get data are equal
|
||||
if !reflect.DeepEqual(getData, putData) {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: get "+
|
||||
"data and put data are not equal. Put: %s, got: %s",
|
||||
string(putData), string(getData))
|
||||
}
|
||||
|
||||
// Rollback the transaction
|
||||
err = tx.Rollback()
|
||||
if err != nil {
|
||||
t.Fatalf("TestLevelDBTransactionSanity: rollback "+
|
||||
"returned unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
5
infrastructure/db/database/ffldb/ldb/log.go
Normal file
5
infrastructure/db/database/ffldb/ldb/log.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package ldb
|
||||
|
||||
import "github.com/kaspanet/kaspad/infrastructure/logger"
|
||||
|
||||
var log, _ = logger.Get(logger.SubsystemTags.KSDB)
|
||||
19
infrastructure/db/database/ffldb/ldb/options.go
Normal file
19
infrastructure/db/database/ffldb/ldb/options.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package ldb
|
||||
|
||||
import "github.com/syndtr/goleveldb/leveldb/opt"
|
||||
|
||||
var (
|
||||
defaultOptions = opt.Options{
|
||||
Compression: opt.NoCompression,
|
||||
BlockCacheCapacity: 256 * opt.MiB,
|
||||
WriteBuffer: 128 * opt.MiB,
|
||||
DisableSeeksCompaction: true,
|
||||
}
|
||||
|
||||
// Options is a function that returns a leveldb
|
||||
// opt.Options struct for opening a database.
|
||||
// It's defined as a variable for the sake of testing.
|
||||
Options = func() *opt.Options {
|
||||
return &defaultOptions
|
||||
}
|
||||
)
|
||||
140
infrastructure/db/database/ffldb/ldb/transaction.go
Normal file
140
infrastructure/db/database/ffldb/ldb/transaction.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package ldb
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
// LevelDBTransaction is a thin wrapper around native leveldb
|
||||
// batches and snapshots. It supports both get and put.
|
||||
//
|
||||
// Snapshots provide a frozen view of the database at the moment
|
||||
// the transaction begins. On the other hand, batches provide a
|
||||
// mechanism to combine several database writes into one write,
|
||||
// which seamlessly rolls back the database in case any individual
|
||||
// write fails. Together the two forms a logic unit similar
|
||||
// to what one might expect from a classic database transaction.
|
||||
//
|
||||
// Note: Transactions provide data consistency over the state of
|
||||
// the database as it was when the transaction started. As it's
|
||||
// currently implemented, if one puts data into the transaction
|
||||
// then it will not be available to get within the same transaction.
|
||||
type LevelDBTransaction struct {
|
||||
db *LevelDB
|
||||
snapshot *leveldb.Snapshot
|
||||
batch *leveldb.Batch
|
||||
isClosed bool
|
||||
}
|
||||
|
||||
// Begin begins a new transaction.
|
||||
func (db *LevelDB) Begin() (*LevelDBTransaction, error) {
|
||||
snapshot, err := db.ldb.GetSnapshot()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
batch := new(leveldb.Batch)
|
||||
|
||||
transaction := &LevelDBTransaction{
|
||||
db: db,
|
||||
snapshot: snapshot,
|
||||
batch: batch,
|
||||
isClosed: false,
|
||||
}
|
||||
return transaction, nil
|
||||
}
|
||||
|
||||
// Commit commits whatever changes were made to the database
|
||||
// within this transaction.
|
||||
func (tx *LevelDBTransaction) Commit() error {
|
||||
if tx.isClosed {
|
||||
return errors.New("cannot commit a closed transaction")
|
||||
}
|
||||
|
||||
tx.isClosed = true
|
||||
tx.snapshot.Release()
|
||||
return errors.WithStack(tx.db.ldb.Write(tx.batch, nil))
|
||||
}
|
||||
|
||||
// Rollback rolls back whatever changes were made to the
|
||||
// database within this transaction.
|
||||
func (tx *LevelDBTransaction) Rollback() error {
|
||||
if tx.isClosed {
|
||||
return errors.New("cannot rollback a closed transaction")
|
||||
}
|
||||
|
||||
tx.isClosed = true
|
||||
tx.snapshot.Release()
|
||||
tx.batch.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RollbackUnlessClosed rolls back changes that were made to
|
||||
// the database within the transaction, unless the transaction
|
||||
// had already been closed using either Rollback or Commit.
|
||||
func (tx *LevelDBTransaction) RollbackUnlessClosed() error {
|
||||
if tx.isClosed {
|
||||
return nil
|
||||
}
|
||||
return tx.Rollback()
|
||||
}
|
||||
|
||||
// Put sets the value for the given key. It overwrites
|
||||
// any previous value for that key.
|
||||
func (tx *LevelDBTransaction) Put(key *database.Key, value []byte) error {
|
||||
if tx.isClosed {
|
||||
return errors.New("cannot put into a closed transaction")
|
||||
}
|
||||
|
||||
tx.batch.Put(key.Bytes(), value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get gets the value for the given key. It returns
|
||||
// ErrNotFound if the given key does not exist.
|
||||
func (tx *LevelDBTransaction) Get(key *database.Key) ([]byte, error) {
|
||||
if tx.isClosed {
|
||||
return nil, errors.New("cannot get from a closed transaction")
|
||||
}
|
||||
|
||||
data, err := tx.snapshot.Get(key.Bytes(), nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, leveldb.ErrNotFound) {
|
||||
return nil, errors.Wrapf(database.ErrNotFound,
|
||||
"key %s not found", key)
|
||||
}
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Has returns true if the database does contains the
|
||||
// given key.
|
||||
func (tx *LevelDBTransaction) Has(key *database.Key) (bool, error) {
|
||||
if tx.isClosed {
|
||||
return false, errors.New("cannot has from a closed transaction")
|
||||
}
|
||||
|
||||
res, err := tx.snapshot.Has(key.Bytes(), nil)
|
||||
return res, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Delete deletes the value for the given key. Will not
|
||||
// return an error if the key doesn't exist.
|
||||
func (tx *LevelDBTransaction) Delete(key *database.Key) error {
|
||||
if tx.isClosed {
|
||||
return errors.New("cannot delete from a closed transaction")
|
||||
}
|
||||
|
||||
tx.batch.Delete(key.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cursor begins a new cursor over the given bucket.
|
||||
func (tx *LevelDBTransaction) Cursor(bucket *database.Bucket) (*LevelDBCursor, error) {
|
||||
if tx.isClosed {
|
||||
return nil, errors.New("cannot open a cursor from a closed transaction")
|
||||
}
|
||||
|
||||
return tx.db.Cursor(bucket), nil
|
||||
}
|
||||
146
infrastructure/db/database/ffldb/ldb/transaction_test.go
Normal file
146
infrastructure/db/database/ffldb/ldb/transaction_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package ldb
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
5
infrastructure/db/database/ffldb/log.go
Normal file
5
infrastructure/db/database/ffldb/log.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package ffldb
|
||||
|
||||
import "github.com/kaspanet/kaspad/infrastructure/logger"
|
||||
|
||||
var log, _ = logger.Get(logger.SubsystemTags.KSDB)
|
||||
137
infrastructure/db/database/ffldb/transaction.go
Normal file
137
infrastructure/db/database/ffldb/transaction.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package ffldb
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database/ffldb/ff"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database/ffldb/ldb"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// transaction is an ffldb transaction.
|
||||
//
|
||||
// Note: Transactions provide data consistency over the state of
|
||||
// the database as it was when the transaction started. There is
|
||||
// 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
|
||||
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)
|
||||
}
|
||||
|
||||
// Get gets the value for the given key. It returns
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Has returns true if the database does contains the
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Delete deletes the value for the given key. Will not
|
||||
// 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)
|
||||
}
|
||||
|
||||
// AppendToStore appends the given data to the flat
|
||||
// file store defined by storeName. This function
|
||||
// returns a serialized location handle that's meant
|
||||
// to be stored and later used when querying the data
|
||||
// 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)
|
||||
}
|
||||
|
||||
// RetrieveFromStore retrieves data from the store defined by
|
||||
// storeName using the given serialized location handle. It
|
||||
// returns ErrNotFound if the location does not exist. See
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Rollback rolls back whatever changes were made to the
|
||||
// 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()
|
||||
}
|
||||
|
||||
// Commit commits whatever changes were made to the database
|
||||
// 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()
|
||||
}
|
||||
|
||||
// RollbackUnlessClosed rolls back changes that were made to
|
||||
// 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()
|
||||
}
|
||||
500
infrastructure/db/database/ffldb/transaction_test.go
Normal file
500
infrastructure/db/database/ffldb/transaction_test.go
Normal file
@@ -0,0 +1,500 @@
|
||||
package ffldb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/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))
|
||||
}
|
||||
}
|
||||
85
infrastructure/db/database/keys.go
Normal file
85
infrastructure/db/database/keys.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
var bucketSeparator = []byte("/")
|
||||
|
||||
// Key is a helper type meant to combine prefix
|
||||
// and suffix into a single database key.
|
||||
type Key struct {
|
||||
bucket *Bucket
|
||||
suffix []byte
|
||||
}
|
||||
|
||||
// Bytes returns the full key bytes that are consisted
|
||||
// from the bucket path concatenated to the suffix.
|
||||
func (k *Key) Bytes() []byte {
|
||||
bucketPath := k.bucket.Path()
|
||||
keyBytes := make([]byte, len(bucketPath)+len(k.suffix))
|
||||
copy(keyBytes, bucketPath)
|
||||
copy(keyBytes[len(bucketPath):], k.suffix)
|
||||
return keyBytes
|
||||
}
|
||||
|
||||
func (k *Key) String() string {
|
||||
return hex.EncodeToString(k.Bytes())
|
||||
}
|
||||
|
||||
// Bucket returns the key bucket.
|
||||
func (k *Key) Bucket() *Bucket {
|
||||
return k.bucket
|
||||
}
|
||||
|
||||
// Suffix returns the key suffix.
|
||||
func (k *Key) Suffix() []byte {
|
||||
return k.suffix
|
||||
}
|
||||
|
||||
// newKey returns a new key composed
|
||||
// of the given bucket and suffix
|
||||
func newKey(bucket *Bucket, suffix []byte) *Key {
|
||||
return &Key{bucket: bucket, suffix: suffix}
|
||||
}
|
||||
|
||||
// Bucket is a helper type meant to combine buckets
|
||||
// and sub-buckets that can be used to create database
|
||||
// keys and prefix-based cursors.
|
||||
type Bucket struct {
|
||||
path [][]byte
|
||||
}
|
||||
|
||||
// MakeBucket creates a new Bucket using the given path
|
||||
// of buckets.
|
||||
func MakeBucket(path ...[]byte) *Bucket {
|
||||
return &Bucket{path: path}
|
||||
}
|
||||
|
||||
// Bucket returns the sub-bucket of the current bucket
|
||||
// defined by bucketBytes.
|
||||
func (b *Bucket) Bucket(bucketBytes []byte) *Bucket {
|
||||
newPath := make([][]byte, len(b.path)+1)
|
||||
copy(newPath, b.path)
|
||||
copy(newPath[len(b.path):], [][]byte{bucketBytes})
|
||||
|
||||
return MakeBucket(newPath...)
|
||||
}
|
||||
|
||||
// Key returns a key in the current bucket with the
|
||||
// given suffix.
|
||||
func (b *Bucket) Key(suffix []byte) *Key {
|
||||
return newKey(b, suffix)
|
||||
}
|
||||
|
||||
// Path returns the full path of the current bucket.
|
||||
func (b *Bucket) Path() []byte {
|
||||
bucketPath := bytes.Join(b.path, bucketSeparator)
|
||||
|
||||
bucketPathWithFinalSeparator := make([]byte, len(bucketPath)+len(bucketSeparator))
|
||||
copy(bucketPathWithFinalSeparator, bucketPath)
|
||||
copy(bucketPathWithFinalSeparator[len(bucketPath):], bucketSeparator)
|
||||
|
||||
return bucketPathWithFinalSeparator
|
||||
}
|
||||
83
infrastructure/db/database/keys_test.go
Normal file
83
infrastructure/db/database/keys_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBucketPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
bucketByteSlices [][]byte
|
||||
expectedPath []byte
|
||||
}{
|
||||
{
|
||||
bucketByteSlices: [][]byte{[]byte("hello")},
|
||||
expectedPath: []byte("hello/"),
|
||||
},
|
||||
{
|
||||
bucketByteSlices: [][]byte{[]byte("hello"), []byte("world")},
|
||||
expectedPath: []byte("hello/world/"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
// Build a result using the MakeBucket function alone
|
||||
resultKey := MakeBucket(test.bucketByteSlices...).Path()
|
||||
if !reflect.DeepEqual(resultKey, test.expectedPath) {
|
||||
t.Errorf("TestBucketPath: got wrong path using MakeBucket. "+
|
||||
"Want: %s, got: %s", string(test.expectedPath), string(resultKey))
|
||||
}
|
||||
|
||||
// Build a result using sub-Bucket calls
|
||||
bucket := MakeBucket()
|
||||
for _, bucketBytes := range test.bucketByteSlices {
|
||||
bucket = bucket.Bucket(bucketBytes)
|
||||
}
|
||||
resultKey = bucket.Path()
|
||||
if !reflect.DeepEqual(resultKey, test.expectedPath) {
|
||||
t.Errorf("TestBucketPath: got wrong path using sub-Bucket "+
|
||||
"calls. Want: %s, got: %s", string(test.expectedPath), string(resultKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBucketKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
bucketByteSlices [][]byte
|
||||
key []byte
|
||||
expectedKeyBytes []byte
|
||||
expectedKey *Key
|
||||
}{
|
||||
{
|
||||
bucketByteSlices: [][]byte{[]byte("hello")},
|
||||
key: []byte("test"),
|
||||
expectedKeyBytes: []byte("hello/test"),
|
||||
expectedKey: &Key{
|
||||
bucket: MakeBucket([]byte("hello")),
|
||||
suffix: []byte("test"),
|
||||
},
|
||||
},
|
||||
{
|
||||
bucketByteSlices: [][]byte{[]byte("hello"), []byte("world")},
|
||||
key: []byte("test"),
|
||||
expectedKeyBytes: []byte("hello/world/test"),
|
||||
expectedKey: &Key{
|
||||
bucket: MakeBucket([]byte("hello"), []byte("world")),
|
||||
suffix: []byte("test"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
resultKey := MakeBucket(test.bucketByteSlices...).Key(test.key)
|
||||
if !reflect.DeepEqual(resultKey, test.expectedKey) {
|
||||
t.Errorf("TestBucketKey: got wrong key. Want: %s, got: %s",
|
||||
test.expectedKeyBytes, resultKey)
|
||||
}
|
||||
if !bytes.Equal(resultKey.Bytes(), test.expectedKeyBytes) {
|
||||
t.Errorf("TestBucketKey: got wrong key bytes. Want: %s, got: %s",
|
||||
test.expectedKeyBytes, resultKey.Bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
25
infrastructure/db/database/transaction.go
Normal file
25
infrastructure/db/database/transaction.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package database
|
||||
|
||||
// Transaction defines the interface of a generic kaspad database
|
||||
// transaction.
|
||||
//
|
||||
// Note: Transactions provide data consistency over the state of
|
||||
// the database as it was when the transaction started. There is
|
||||
// NO guarantee that if one puts data into the transaction then
|
||||
// it will be available to get within the same transaction.
|
||||
type Transaction interface {
|
||||
DataAccessor
|
||||
|
||||
// Rollback rolls back whatever changes were made to the
|
||||
// database within this transaction.
|
||||
Rollback() error
|
||||
|
||||
// Commit commits whatever changes were made to the database
|
||||
// within this transaction.
|
||||
Commit() error
|
||||
|
||||
// RollbackUnlessClosed rolls back changes that were made to
|
||||
// the database within the transaction, unless the transaction
|
||||
// had already been closed using either Rollback or Commit.
|
||||
RollbackUnlessClosed() error
|
||||
}
|
||||
549
infrastructure/db/database/transaction_test.go
Normal file
549
infrastructure/db/database/transaction_test.go
Normal file
@@ -0,0 +1,549 @@
|
||||
// All tests within this file should call testForAllDatabaseTypes
|
||||
// over the actual test. This is to make sure that all supported
|
||||
// database types adhere to the assumptions defined in the
|
||||
// interfaces in this package.
|
||||
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/kaspanet/kaspad/infrastructure/db/database"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTransactionPut(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestTransactionPut", testTransactionPut)
|
||||
}
|
||||
|
||||
func testTransactionPut(t *testing.T, db database.Database, testName string) {
|
||||
// Begin a new transaction
|
||||
dbTx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Begin "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
defer func() {
|
||||
err := dbTx.RollbackUnlessClosed()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RollbackUnlessClosed "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Put value1 into the transaction
|
||||
key := database.MakeBucket().Key([]byte("key"))
|
||||
value1 := []byte("value1")
|
||||
err = dbTx.Put(key, value1)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Put value2 into the transaction with the same key
|
||||
value2 := []byte("value2")
|
||||
err = dbTx.Put(key, value2)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
err = dbTx.Commit()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Commit "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that the returned value is value2
|
||||
returnedValue, err := db.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(returnedValue, value2) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong value. Want: %s, got: %s",
|
||||
testName, string(value2), string(returnedValue))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionGet(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestTransactionGet", testTransactionGet)
|
||||
}
|
||||
|
||||
func testTransactionGet(t *testing.T, db database.Database, testName string) {
|
||||
// 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("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Begin a new transaction
|
||||
dbTx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Begin "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
defer func() {
|
||||
err := dbTx.RollbackUnlessClosed()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RollbackUnlessClosed "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Get the value back and make sure it's the same one
|
||||
returnedValue, err := dbTx.Get(key1)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(returnedValue, value1) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong value. Want: %s, got: %s",
|
||||
testName, string(value1), string(returnedValue))
|
||||
}
|
||||
|
||||
// Try getting a non-existent value and make sure
|
||||
// the returned error is ErrNotFound
|
||||
_, err = dbTx.Get(database.MakeBucket().Key([]byte("doesn't exist")))
|
||||
if err == nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly succeeded", testName)
|
||||
}
|
||||
if !database.IsNotFoundError(err) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong error: %s", testName, err)
|
||||
}
|
||||
|
||||
// Put a new value into the database outside of the transaction
|
||||
key2 := database.MakeBucket().Key([]byte("key2"))
|
||||
value2 := []byte("value2")
|
||||
err = db.Put(key2, value2)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that the new value doesn't exist inside the transaction
|
||||
_, err = dbTx.Get(key2)
|
||||
if err == nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly succeeded", testName)
|
||||
}
|
||||
if !database.IsNotFoundError(err) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong error: %s", testName, err)
|
||||
}
|
||||
|
||||
// Put a new value into the transaction
|
||||
key3 := database.MakeBucket().Key([]byte("key3"))
|
||||
value3 := []byte("value3")
|
||||
err = dbTx.Put(key3, value3)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that the new value doesn't exist outside the transaction
|
||||
_, err = db.Get(key3)
|
||||
if err == nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly succeeded", testName)
|
||||
}
|
||||
if !database.IsNotFoundError(err) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong error: %s", testName, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionHas(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestTransactionHas", testTransactionHas)
|
||||
}
|
||||
|
||||
func testTransactionHas(t *testing.T, db database.Database, testName string) {
|
||||
// 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("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Begin a new transaction
|
||||
dbTx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Begin "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
defer func() {
|
||||
err := dbTx.RollbackUnlessClosed()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RollbackUnlessClosed "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Make sure that Has returns true for the value we just put
|
||||
exists, err := dbTx.Has(key1)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly returned that the value does not exist", testName)
|
||||
}
|
||||
|
||||
// Make sure that Has returns false for a non-existent value
|
||||
exists, err = dbTx.Has(database.MakeBucket().Key([]byte("doesn't exist")))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if exists {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly returned that the value exists", testName)
|
||||
}
|
||||
|
||||
// Put a new value into the database outside of the transaction
|
||||
key2 := database.MakeBucket().Key([]byte("key2"))
|
||||
value2 := []byte("value2")
|
||||
err = db.Put(key2, value2)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that the new value doesn't exist inside the transaction
|
||||
exists, err = dbTx.Has(key2)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if exists {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly returned that the value exists", testName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionDelete(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestTransactionDelete", testTransactionDelete)
|
||||
}
|
||||
|
||||
func testTransactionDelete(t *testing.T, db database.Database, testName string) {
|
||||
// Put a value into the database
|
||||
key := database.MakeBucket().Key([]byte("key"))
|
||||
value := []byte("value")
|
||||
err := db.Put(key, value)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Begin two new transactions
|
||||
dbTx1, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Begin "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
dbTx2, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Begin "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
defer func() {
|
||||
err := dbTx1.RollbackUnlessClosed()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RollbackUnlessClosed "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
err = dbTx2.RollbackUnlessClosed()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RollbackUnlessClosed "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Delete the value in the first transaction
|
||||
err = dbTx1.Delete(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Delete "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Commit the first transaction
|
||||
err = dbTx1.Commit()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Commit "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that Has returns false for the deleted value
|
||||
exists, err := db.Has(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if exists {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly returned that the value exists", testName)
|
||||
}
|
||||
|
||||
// Make sure that the second transaction was no affected
|
||||
exists, err = dbTx2.Has(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("%s: Has "+
|
||||
"unexpectedly returned that the value does not exist", testName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionAppendToStoreAndRetrieveFromStore(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestTransactionAppendToStoreAndRetrieveFromStore", testTransactionAppendToStoreAndRetrieveFromStore)
|
||||
}
|
||||
|
||||
func testTransactionAppendToStoreAndRetrieveFromStore(t *testing.T, db database.Database, testName string) {
|
||||
// Begin a new transaction
|
||||
dbTx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Begin "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
defer func() {
|
||||
err := dbTx.RollbackUnlessClosed()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RollbackUnlessClosed "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Append some data into the store
|
||||
storeName := "store"
|
||||
data := []byte("data")
|
||||
location, err := dbTx.AppendToStore(storeName, data)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: AppendToStore "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Retrieve the data and make sure it's equal to what was appended
|
||||
retrievedData, err := dbTx.RetrieveFromStore(storeName, location)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RetrieveFromStore "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(retrievedData, data) {
|
||||
t.Fatalf("%s: RetrieveFromStore "+
|
||||
"returned unexpected data. Want: %s, got: %s",
|
||||
testName, string(data), string(retrievedData))
|
||||
}
|
||||
|
||||
// Make sure that an invalid location returns ErrNotFound
|
||||
fakeLocation := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
|
||||
_, err = dbTx.RetrieveFromStore(storeName, fakeLocation)
|
||||
if err == nil {
|
||||
t.Fatalf("%s: RetrieveFromStore "+
|
||||
"unexpectedly succeeded", testName)
|
||||
}
|
||||
if !database.IsNotFoundError(err) {
|
||||
t.Fatalf("%s: RetrieveFromStore "+
|
||||
"returned wrong error: %s", testName, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionCommit(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestTransactionCommit", testTransactionCommit)
|
||||
}
|
||||
|
||||
func testTransactionCommit(t *testing.T, db database.Database, testName string) {
|
||||
// Begin a new transaction
|
||||
dbTx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Begin "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
defer func() {
|
||||
err := dbTx.RollbackUnlessClosed()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RollbackUnlessClosed "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Put a value into the transaction
|
||||
key := database.MakeBucket().Key([]byte("key"))
|
||||
value := []byte("value")
|
||||
err = dbTx.Put(key, value)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
err = dbTx.Commit()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Commit "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that the returned value exists and is as expected
|
||||
returnedValue, err := db.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
if !bytes.Equal(returnedValue, value) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong value. Want: %s, got: %s",
|
||||
testName, string(value), string(returnedValue))
|
||||
}
|
||||
|
||||
// Make sure that further operations on the transaction return an error
|
||||
_, err = dbTx.Get(key)
|
||||
if err == nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly succeeded", testName)
|
||||
}
|
||||
expectedError := "closed transaction"
|
||||
if !strings.Contains(err.Error(), expectedError) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong error. Want: %s, got: %s",
|
||||
testName, expectedError, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionRollback(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestTransactionRollback", testTransactionRollback)
|
||||
}
|
||||
|
||||
func testTransactionRollback(t *testing.T, db database.Database, testName string) {
|
||||
// Begin a new transaction
|
||||
dbTx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Begin "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
defer func() {
|
||||
err := dbTx.RollbackUnlessClosed()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RollbackUnlessClosed "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Put a value into the transaction
|
||||
key := database.MakeBucket().Key([]byte("key"))
|
||||
value := []byte("value")
|
||||
err = dbTx.Put(key, value)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Rollback the transaction
|
||||
err = dbTx.Rollback()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Rollback "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that the returned value did not get added to the database
|
||||
_, err = db.Get(key)
|
||||
if err == nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly succeeded", testName)
|
||||
}
|
||||
if !database.IsNotFoundError(err) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong error", testName)
|
||||
}
|
||||
|
||||
// Make sure that further operations on the transaction return an error
|
||||
_, err = dbTx.Get(key)
|
||||
if err == nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly succeeded", testName)
|
||||
}
|
||||
expectedError := "closed transaction"
|
||||
if !strings.Contains(err.Error(), expectedError) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong error. Want: %s, got: %s",
|
||||
testName, expectedError, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransactionRollbackUnlessClosed(t *testing.T) {
|
||||
testForAllDatabaseTypes(t, "TestTransactionRollbackUnlessClosed", testTransactionRollbackUnlessClosed)
|
||||
}
|
||||
|
||||
func testTransactionRollbackUnlessClosed(t *testing.T, db database.Database, testName string) {
|
||||
// Begin a new transaction
|
||||
dbTx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Begin "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
defer func() {
|
||||
err := dbTx.RollbackUnlessClosed()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RollbackUnlessClosed "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Put a value into the transaction
|
||||
key := database.MakeBucket().Key([]byte("key"))
|
||||
value := []byte("value")
|
||||
err = dbTx.Put(key, value)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Put "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// RollbackUnlessClosed the transaction
|
||||
err = dbTx.RollbackUnlessClosed()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RollbackUnlessClosed "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
|
||||
// Make sure that the returned value did not get added to the database
|
||||
_, err = db.Get(key)
|
||||
if err == nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly succeeded", testName)
|
||||
}
|
||||
if !database.IsNotFoundError(err) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong error", testName)
|
||||
}
|
||||
|
||||
// Make sure that further operations on the transaction return an error
|
||||
_, err = dbTx.Get(key)
|
||||
if err == nil {
|
||||
t.Fatalf("%s: Get "+
|
||||
"unexpectedly succeeded", testName)
|
||||
}
|
||||
expectedError := "closed transaction"
|
||||
if !strings.Contains(err.Error(), expectedError) {
|
||||
t.Fatalf("%s: Get "+
|
||||
"returned wrong error. Want: %s, got: %s",
|
||||
testName, expectedError, err)
|
||||
}
|
||||
|
||||
// Make sure that further calls to RollbackUnlessClosed don't return an error
|
||||
err = dbTx.RollbackUnlessClosed()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: RollbackUnlessClosed "+
|
||||
"unexpectedly failed: %s", testName, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user