Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Persistence] Adds Save and Load functionality to TreeStore #897

Closed
wants to merge 10 commits into from
8 changes: 8 additions & 0 deletions persistence/blockstore/block_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ func (bs *blockStore) Stop() error {
return bs.kv.Stop()
}

func (bs *blockStore) Backup(path string) error {
dylanlott marked this conversation as resolved.
Show resolved Hide resolved
return bs.kv.Backup(path)
}

///////////////
// Accessors //
///////////////

func (bs *blockStore) Delete(key []byte) error { return bs.kv.Delete(key) }
func (bs *blockStore) Exists(key []byte) (bool, error) { return bs.kv.Exists(key) }
func (bs *blockStore) GetAll(prefixKey []byte, descending bool) (keys, values [][]byte, err error) {
Expand Down
29 changes: 29 additions & 0 deletions persistence/kvstore/kvstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ package kvstore

import (
"errors"
"fmt"
"log"
"os"
"path/filepath"

badger "github.com/dgraph-io/badger/v3"
"github.com/pokt-network/smt"
Expand All @@ -22,6 +25,8 @@ type KVStore interface {
GetAll(prefixKey []byte, descending bool) (keys, values [][]byte, err error)
Exists(key []byte) (bool, error)
ClearAll() error

dylanlott marked this conversation as resolved.
Show resolved Hide resolved
Backup(filepath string) error
}

const (
Expand Down Expand Up @@ -141,6 +146,30 @@ func (store *badgerKVStore) Stop() error {
return store.db.Close()
}

// Backup creates a backup for the badgerDB at the provided path.
// It creates a file
func (store *badgerKVStore) Backup(backupPath string) error {
// create backup directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(backupPath), os.ModePerm); err != nil {
return fmt.Errorf("failed to create backup directory: %v", err)
}

// create the backup file itself
backupFile, err := os.Create(backupPath)
if err != nil {
return fmt.Errorf("failed to create backup file: %v", err)
}
defer backupFile.Close()

// dump the database to the backup file
_, err = store.db.Backup(backupFile, 0)
if err != nil {
return err
}

return nil
}

// PrefixEndBytes returns the end byteslice for a noninclusive range
// that would include all byte slices for which the input is the prefix
func prefixEndBytes(prefix []byte) []byte {
Expand Down
69 changes: 69 additions & 0 deletions persistence/kvstore/kvstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package kvstore

import (
"encoding/hex"
"io"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -330,10 +333,76 @@ func TestKVStore_ClearAll(t *testing.T) {
require.NoError(t, err)
}

func TestKVStore_Backup(t *testing.T) {
t.Run("should backup an in-memory database", func(t *testing.T) {
store := NewMemKVStore()
require.NotNil(t, store)

tmpdir := t.TempDir()
path := filepath.Join(tmpdir, "TestKVStore_Backup_InMemory.bak")
err := store.Backup(path)
require.NoError(t, err)

empty, err := isEmpty(t, tmpdir)
require.NoError(t, err)
require.False(t, empty)

// open the directory and assert on individual files
dir, err := os.Open(tmpdir)
require.NoError(t, err)
defer dir.Close()

files, err := dir.Readdir(0) // 0 means read all directory entries
require.NoError(t, err)
require.Equal(t, len(files), 1)
})
t.Run("should backup an on-disk store database", func(t *testing.T) {
tmpdir := t.TempDir()
kvpath := filepath.Join(tmpdir, "TestKVStore_Backup_OnDisk_Source.bak")
store, err := NewKVStore(kvpath)
require.NoError(t, err)
require.NotNil(t, store)

backupDir := t.TempDir()
path := filepath.Join(backupDir, "TestKVStore_Backup_OnDisk_Destination.bak")
err = store.Backup(path)
require.NoError(t, err)

empty, err := isEmpty(t, backupDir)
require.NoError(t, err)
require.False(t, empty)

// open the directory and assert on individual files
dir, err := os.Open(backupDir)
require.NoError(t, err)
defer dir.Close()

files, err := dir.Readdir(0) // 0 means read all directory entries
require.NoError(t, err)
require.NoError(t, err)
require.Equal(t, len(files), 1)
})
}

func setupStore(t *testing.T, store KVStore) {
t.Helper()
err := store.Set([]byte("foo"), []byte("bar"))
require.NoError(t, err)
err = store.Set([]byte("baz"), []byte("bin"))
require.NoError(t, err)
}

func isEmpty(t *testing.T, dir string) (bool, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not go with an approach like this: https://stackoverflow.com/a/24102536/768439 ?

Seems shorter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beacuse the linter complains if ioutil is used since it's officially deprecated.

Copy link
Contributor

@h5law h5law Aug 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use

dirEntries, err := os.ReadDir(dir)
require.NoError(t, err)
if len(dirEntries) == 0 {
	return true
}
return false

https://pkg.go.dev/os#ReadDir

This is what ioutil docs point to as the newer more efficient approach to the deprecated method

t.Helper()
f, err := os.Open(dir)
if err != nil {
return false, err
}
defer f.Close()

_, err = f.Readdirnames(1)
if err == io.EOF {
return true, nil
}
return false, err
}
7 changes: 5 additions & 2 deletions persistence/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,15 @@ func (*persistenceModule) Create(bus modules.Bus, options ...modules.ModuleOptio
return nil, err
}

_, err = trees.Create(
treeMod, err := trees.Create(
bus,
trees.WithTreeStoreDirectory(persistenceCfg.TreesStoreDir),
trees.WithLogger(m.logger))
if err != nil {
return nil, fmt.Errorf("failed to create TreeStoreModule: %w", err)
return nil, fmt.Errorf("failed to create %s: %w", modules.TreeStoreSubmoduleName, err)
}
if err := treeMod.Start(); err != nil {
return nil, fmt.Errorf("failed to start %s: %w", modules.TreeStoreSubmoduleName, err)
}

m.config = persistenceCfg
Expand Down
153 changes: 147 additions & 6 deletions persistence/trees/atomic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package trees

import (
"encoding/hex"
"io"
"os"
"testing"

"github.com/golang/mock/gomock"
"github.com/pokt-network/pocket/logger"
mock_types "github.com/pokt-network/pocket/persistence/types/mocks"
"github.com/pokt-network/pocket/shared/modules"
mockModules "github.com/pokt-network/pocket/shared/modules/mocks"
mock_modules "github.com/pokt-network/pocket/shared/modules/mocks"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)

Expand All @@ -20,12 +22,19 @@ const (
h1 = "7d5712ea1507915c40e295845fa58773baa405b24b87e9d99761125d826ff915"
)

var (
testFoo = []byte("foo")
testBar = []byte("bar")
testKey = []byte("fiz")
testVal = []byte("buz")
)

func TestTreeStore_AtomicUpdatesWithSuccessfulRollback(t *testing.T) {
ctrl := gomock.NewController(t)

mockTxIndexer := mock_types.NewMockTxIndexer(ctrl)
mockBus := mockModules.NewMockBus(ctrl)
mockPersistenceMod := mockModules.NewMockPersistenceModule(ctrl)
mockBus := mock_modules.NewMockBus(ctrl)
mockPersistenceMod := mock_modules.NewMockPersistenceModule(ctrl)

mockBus.EXPECT().GetPersistenceModule().AnyTimes().Return(mockPersistenceMod)
mockPersistenceMod.EXPECT().GetTxIndexer().AnyTimes().Return(mockTxIndexer)
Expand All @@ -45,7 +54,7 @@ func TestTreeStore_AtomicUpdatesWithSuccessfulRollback(t *testing.T) {

// insert test data into every tree
for _, treeName := range stateTreeNames {
err := ts.merkleTrees[treeName].tree.Update([]byte("foo"), []byte("bar"))
err := ts.merkleTrees[treeName].tree.Update(testFoo, testBar)
require.NoError(t, err)
}

Expand Down Expand Up @@ -78,7 +87,7 @@ func TestTreeStore_AtomicUpdatesWithSuccessfulRollback(t *testing.T) {

// insert additional test data into all of the trees
for _, treeName := range stateTreeNames {
require.NoError(t, ts.merkleTrees[treeName].tree.Update([]byte("fiz"), []byte("buz")))
require.NoError(t, ts.merkleTrees[treeName].tree.Update(testKey, testVal))
}

// rollback the changes made to the trees above BEFORE anything was committed
Expand All @@ -89,4 +98,136 @@ func TestTreeStore_AtomicUpdatesWithSuccessfulRollback(t *testing.T) {
hash3 := ts.getStateHash()
require.Equal(t, hash3, hash2)
require.Equal(t, hash3, h1)

err = ts.Rollback()
require.NoError(t, err)

// confirm it's not in the tree
v, err := ts.merkleTrees[TransactionsTreeName].tree.Get(testKey)
require.NoError(t, err)
require.Nil(t, v)
}

func TestTreeStore_SaveAndLoad(t *testing.T) {
t.Parallel()
t.Run("should save a backup in a directory", func(t *testing.T) {
ts := newTestTreeStore(t)
tmpdir := t.TempDir()
// assert that the directory is empty before backup
ok, err := isEmpty(tmpdir)
require.NoError(t, err)
require.True(t, ok)

// Trigger a backup
require.NoError(t, ts.Backup(tmpdir))

// assert that the directory is not empty after Backup has returned
ok, err = isEmpty(tmpdir)
require.NoError(t, err)
require.False(t, ok)
})
t.Run("should load a backup and maintain TreeStore hash integrity", func(t *testing.T) {
ctrl := gomock.NewController(t)
tmpDir := t.TempDir()

mockTxIndexer := mock_types.NewMockTxIndexer(ctrl)
mockBus := mock_modules.NewMockBus(ctrl)
mockPersistenceMod := mock_modules.NewMockPersistenceModule(ctrl)

mockBus.EXPECT().GetPersistenceModule().AnyTimes().Return(mockPersistenceMod)
mockPersistenceMod.EXPECT().GetTxIndexer().AnyTimes().Return(mockTxIndexer)

ts := &treeStore{
logger: logger.Global.CreateLoggerForModule(modules.TreeStoreSubmoduleName),
treeStoreDir: tmpDir,
}
require.NoError(t, ts.Start())
require.NotNil(t, ts.rootTree.tree)

for _, treeName := range stateTreeNames {
err := ts.merkleTrees[treeName].tree.Update([]byte("foo"), []byte("bar"))
require.NoError(t, err)
}

err := ts.Commit()
require.NoError(t, err)

hash1 := ts.getStateHash()
require.NotEmpty(t, hash1)

w, err := ts.save()
require.NoError(t, err)
require.NotNil(t, w)
require.NotNil(t, w.rootHash)
require.NotNil(t, w.merkleRoots)

// Stop the first tree store so that it's databases are no longer used
require.NoError(t, ts.Stop())

// declare a second TreeStore with no trees then load the first worldstate into it
ts2 := &treeStore{
logger: logger.Global.CreateLoggerForModule(modules.TreeStoreSubmoduleName),
treeStoreDir: tmpDir,
}

// Load sets a tree store to the provided worldstate
err = ts2.Load(w)
require.NoError(t, err)

hash2 := ts2.getStateHash()

// Assert that hash is unchanged from save and load
require.Equal(t, hash1, hash2)
})
}

// creates a new tree store with a tmp directory for nodestore persistence
// and then starts the tree store and returns its pointer.
// TECHDEBT(#796) - Organize and dedupe this function into testutil package
func newTestTreeStore(t *testing.T) *treeStore {
t.Helper()
ctrl := gomock.NewController(t)
tmpDir := t.TempDir()

mockTxIndexer := mock_types.NewMockTxIndexer(ctrl)
mockBus := mock_modules.NewMockBus(ctrl)
mockPersistenceMod := mock_modules.NewMockPersistenceModule(ctrl)

mockBus.EXPECT().GetPersistenceModule().AnyTimes().Return(mockPersistenceMod)
mockPersistenceMod.EXPECT().GetTxIndexer().AnyTimes().Return(mockTxIndexer)

ts := &treeStore{
logger: logger.Global.CreateLoggerForModule(modules.TreeStoreSubmoduleName),
treeStoreDir: tmpDir,
}
require.NoError(t, ts.Start())
require.NotNil(t, ts.rootTree.tree)

for _, treeName := range stateTreeNames {
err := ts.merkleTrees[treeName].tree.Update(testFoo, testBar)
require.NoError(t, err)
}

err := ts.Commit()
require.NoError(t, err)

hash1 := ts.getStateHash()
require.NotEmpty(t, hash1)

return ts
}

// TECHDEBT(#796) - Organize and dedupe this function into testutil package
func isEmpty(dir string) (bool, error) {
dylanlott marked this conversation as resolved.
Show resolved Hide resolved
f, err := os.Open(dir)
if err != nil {
return false, err
}
defer f.Close()

_, err = f.Readdirnames(1) // Or f.Readdir(1)
if err == io.EOF {
return true, nil
}
return false, err // Either not empty or error, suits both cases
}
Loading
Loading