[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:
stasatdaglabs
2020-08-18 10:26:39 +03:00
committed by GitHub
parent 450ff81f86
commit d14809694f
419 changed files with 1989 additions and 2048 deletions

View File

@@ -0,0 +1,38 @@
database
========
[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](https://choosealicense.com/licenses/isc/)
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](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.

View 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
}

View 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
}

View 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()
}()
}

View 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)
}

View 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
}

View 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)
}
}

View 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

View 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)
}

View 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)
}

View 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)
}
}
}

View 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
}

View 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
}

View 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)
}
}

View 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())
}

View File

@@ -0,0 +1,5 @@
package ff
import "github.com/kaspanet/kaspad/infrastructure/logger"
var log, _ = logger.Get(logger.SubsystemTags.KSDB)

View 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
}

View 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))
}

View 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
}
}

View 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
}

View 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)
}
}

View 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)
}

View 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
}

View 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()
}()
}

View 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)
}

View 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)
}
}

View File

@@ -0,0 +1,5 @@
package ldb
import "github.com/kaspanet/kaspad/infrastructure/logger"
var log, _ = logger.Get(logger.SubsystemTags.KSDB)

View 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
}
)

View 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
}

View 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)
}
}
}
}()
}
}

View File

@@ -0,0 +1,5 @@
package ffldb
import "github.com/kaspanet/kaspad/infrastructure/logger"
var log, _ = logger.Get(logger.SubsystemTags.KSDB)

View 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()
}

View 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))
}
}

View 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
}

View 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())
}
}
}

View 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
}

View 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)
}
}