diff --git a/persistence/blockstore/block_store.go b/persistence/blockstore/block_store.go index 647d3e634..9bc4aa5a4 100644 --- a/persistence/blockstore/block_store.go +++ b/persistence/blockstore/block_store.go @@ -93,6 +93,14 @@ func (bs *blockStore) Stop() error { return bs.kv.Stop() } +func (bs *blockStore) Backup(path string) error { + 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) { diff --git a/persistence/kvstore/kvstore.go b/persistence/kvstore/kvstore.go index a352e4257..f4ea132d2 100644 --- a/persistence/kvstore/kvstore.go +++ b/persistence/kvstore/kvstore.go @@ -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" @@ -22,6 +25,8 @@ type KVStore interface { GetAll(prefixKey []byte, descending bool) (keys, values [][]byte, err error) Exists(key []byte) (bool, error) ClearAll() error + + Backup(filepath string) error } const ( @@ -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 { diff --git a/persistence/kvstore/kvstore_test.go b/persistence/kvstore/kvstore_test.go index 402ce4a98..add3753ca 100644 --- a/persistence/kvstore/kvstore_test.go +++ b/persistence/kvstore/kvstore_test.go @@ -2,6 +2,9 @@ package kvstore import ( "encoding/hex" + "io" + "os" + "path/filepath" "strings" "testing" @@ -330,6 +333,57 @@ 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")) @@ -337,3 +391,18 @@ func setupStore(t *testing.T, store KVStore) { err = store.Set([]byte("baz"), []byte("bin")) require.NoError(t, err) } + +func isEmpty(t *testing.T, dir string) (bool, error) { + 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 +} diff --git a/persistence/module.go b/persistence/module.go index 0d6acd17f..98401aad7 100644 --- a/persistence/module.go +++ b/persistence/module.go @@ -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 diff --git a/persistence/trees/atomic_test.go b/persistence/trees/atomic_test.go index 06fdbaf8c..a02f2afd9 100644 --- a/persistence/trees/atomic_test.go +++ b/persistence/trees/atomic_test.go @@ -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" ) @@ -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) @@ -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) } @@ -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 @@ -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) { + 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 } diff --git a/persistence/trees/module.go b/persistence/trees/module.go index 7da8bf20f..fc3f337fb 100644 --- a/persistence/trees/module.go +++ b/persistence/trees/module.go @@ -1,6 +1,8 @@ package trees import ( + "encoding/hex" + "errors" "fmt" "github.com/pokt-network/pocket/persistence/kvstore" @@ -10,6 +12,8 @@ import ( var _ modules.TreeStoreModule = &treeStore{} +// Create returns a TreeStoreSubmodule that has been setup with the provided TreeStoreOptions, started, +// and then registered to the bus. func (*treeStore) Create(bus modules.Bus, options ...modules.TreeStoreOption) (modules.TreeStoreModule, error) { m := &treeStore{} @@ -19,10 +23,6 @@ func (*treeStore) Create(bus modules.Bus, options ...modules.TreeStoreOption) (m bus.RegisterModule(m) - if err := m.setupTrees(); err != nil { - return nil, err - } - return m, nil } @@ -50,8 +50,30 @@ func WithTreeStoreDirectory(path string) modules.TreeStoreOption { } } +// Start loads up the trees from the configured tree store directory. +func (t *treeStore) Start() error { + return t.setupTrees() +} + +// Stop shuts down the database connection to the nodestore for the root tree and then for each merkle tree. +// If Commit has not been called before Stop is called, data will be lost. +func (t *treeStore) Stop() error { + t.logger.Debug().Msgf("🛑 tree store stop initiated at %s 🛑", hex.EncodeToString(t.rootTree.tree.Root())) + errs := []error{} + if err := t.rootTree.nodeStore.Stop(); err != nil { + errs = append(errs, err) + } + for _, st := range t.merkleTrees { + if err := st.nodeStore.Stop(); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + func (t *treeStore) GetModuleName() string { return modules.TreeStoreSubmoduleName } +// setupTrees is called by Start and it loads the treestore at the given directory func (t *treeStore) setupTrees() error { if t.treeStoreDir == ":memory:" { return t.setupInMemory() diff --git a/persistence/trees/module_test.go b/persistence/trees/module_test.go index 91ec5249f..dcd0bc9af 100644 --- a/persistence/trees/module_test.go +++ b/persistence/trees/module_test.go @@ -1,55 +1,52 @@ package trees_test import ( - "fmt" "testing" "github.com/golang/mock/gomock" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/pokt-network/pocket/internal/testutil" "github.com/pokt-network/pocket/p2p/providers/peerstore_provider" "github.com/pokt-network/pocket/persistence/trees" "github.com/pokt-network/pocket/runtime" - "github.com/pokt-network/pocket/runtime/genesis" - "github.com/pokt-network/pocket/runtime/test_artifacts" - coreTypes "github.com/pokt-network/pocket/shared/core/types" - cryptoPocket "github.com/pokt-network/pocket/shared/crypto" "github.com/pokt-network/pocket/shared/modules" mockModules "github.com/pokt-network/pocket/shared/modules/mocks" ) -const ( - serviceURLFormat = "node%d.consensus:42069" -) +var stateTreeNames = []string{ + "root", + "app", + "val", + "fish", + "servicer", + "account", + "pool", + "transactions", + "params", + "flags", + "ibc", +} func TestTreeStore_Create(t *testing.T) { ctrl := gomock.NewController(t) mockRuntimeMgr := mockModules.NewMockRuntimeMgr(ctrl) mockBus := createMockBus(t, mockRuntimeMgr) - genesisStateMock := createMockGenesisState(nil) - persistenceMock := preparePersistenceMock(t, mockBus, genesisStateMock) - - mockBus.EXPECT(). - GetPersistenceModule(). - Return(persistenceMock). - AnyTimes() - persistenceMock.EXPECT(). - GetBus(). - AnyTimes(). - Return(mockBus) - persistenceMock.EXPECT(). - NewRWContext(int64(0)). - AnyTimes() - persistenceMock.EXPECT(). - GetTxIndexer(). - AnyTimes() treemod, err := trees.Create(mockBus, trees.WithTreeStoreDirectory(":memory:")) assert.NoError(t, err) + require.NoError(t, treemod.Start()) + + // Create should setup a value for each tree + for _, v := range stateTreeNames { + root, ns := treemod.GetTree(v) + require.NotEmpty(t, root) + require.NotEmpty(t, ns) + } + got := treemod.GetBus() - assert.Equal(t, got, mockBus) + require.Equal(t, got, mockBus) // root hash should be empty for empty tree root, ns := treemod.GetTree(trees.TransactionsTreeName) @@ -61,81 +58,33 @@ func TestTreeStore_Create(t *testing.T) { require.Empty(t, keys, vals) } -func TestTreeStore_DebugClearAll(t *testing.T) { - // TODO: Write test case for the DebugClearAll method - t.Skip("TODO: Write test case for DebugClearAll method") -} - -// createMockGenesisState configures and returns a mocked GenesisState -func createMockGenesisState(valKeys []cryptoPocket.PrivateKey) *genesis.GenesisState { - genesisState := new(genesis.GenesisState) - validators := make([]*coreTypes.Actor, len(valKeys)) - for i, valKey := range valKeys { - addr := valKey.Address().String() - mockActor := &coreTypes.Actor{ - ActorType: coreTypes.ActorType_ACTOR_TYPE_VAL, - Address: addr, - PublicKey: valKey.PublicKey().String(), - ServiceUrl: validatorId(i + 1), - StakedAmount: test_artifacts.DefaultStakeAmountString, - PausedHeight: int64(0), - UnstakingHeight: int64(0), - Output: addr, - } - validators[i] = mockActor - } - genesisState.Validators = validators - - return genesisState -} - -// Persistence mock - only needed for validatorMap access -func preparePersistenceMock(t *testing.T, busMock *mockModules.MockBus, genesisState *genesis.GenesisState) *mockModules.MockPersistenceModule { +func TestTreeStore_StartAndStop(t *testing.T) { ctrl := gomock.NewController(t) + mockRuntimeMgr := mockModules.NewMockRuntimeMgr(ctrl) + mockBus := createMockBus(t, mockRuntimeMgr) - persistenceModuleMock := mockModules.NewMockPersistenceModule(ctrl) - readCtxMock := mockModules.NewMockPersistenceReadContext(ctrl) - - readCtxMock.EXPECT(). - GetAllValidators(gomock.Any()). - Return(genesisState.GetValidators(), nil).AnyTimes() - readCtxMock.EXPECT(). - GetAllStakedActors(gomock.Any()). - DoAndReturn(func(height int64) ([]*coreTypes.Actor, error) { - return testutil.Concatenate[*coreTypes.Actor]( - genesisState.GetValidators(), - genesisState.GetServicers(), - genesisState.GetFishermen(), - genesisState.GetApplications(), - ), nil - }). - AnyTimes() - persistenceModuleMock.EXPECT(). - NewReadContext(gomock.Any()). - Return(readCtxMock, nil). - AnyTimes() - readCtxMock.EXPECT(). - Release(). - AnyTimes() - persistenceModuleMock.EXPECT(). - GetBus(). - Return(busMock). - AnyTimes() - persistenceModuleMock.EXPECT(). - SetBus(busMock). - AnyTimes() - persistenceModuleMock.EXPECT(). - GetModuleName(). - Return(modules.PersistenceModuleName). - AnyTimes() - busMock. - RegisterModule(persistenceModuleMock) + // Create returns a started TreeStoreSubmodule + treemod, err := trees.Create( + mockBus, + trees.WithTreeStoreDirectory(":memory:"), + trees.WithLogger(&zerolog.Logger{})) + require.NoError(t, err) + require.NoError(t, treemod.Start()) + + // Should stop without error + require.NoError(t, treemod.Stop()) - return persistenceModuleMock + // Should error if node store is called after Stop + for _, treeName := range stateTreeNames { + _, nodestore := treemod.GetTree(treeName) + _, _, err = nodestore.GetAll([]byte(""), false) + require.Error(t, err, "%s tree failed to return an error when expected", treeName) + } } -func validatorId(i int) string { - return fmt.Sprintf(serviceURLFormat, i) +func TestTreeStore_DebugClearAll(t *testing.T) { + // TODO: Write test case for the DebugClearAll method + t.Skip("TODO: Write test case for DebugClearAll method") } // createMockBus returns a mock bus with stubbed out functions for bus registration diff --git a/persistence/trees/trees.go b/persistence/trees/trees.go index 8fdc43676..8c5c936bb 100644 --- a/persistence/trees/trees.go +++ b/persistence/trees/trees.go @@ -15,9 +15,11 @@ package trees import ( "crypto/sha256" "encoding/hex" + "errors" "fmt" "hash" "log" + "path/filepath" "github.com/jackc/pgx/v5" "github.com/pokt-network/pocket/persistence/indexer" @@ -110,17 +112,19 @@ type treeStore struct { type worldState struct { treeStoreDir string rootTree *stateTree + rootHash []byte merkleTrees map[string]*stateTree + merkleRoots map[string][]byte } // GetTree returns the root hash and nodeStore for the matching tree stored in the TreeStore. // This enables the caller to import the SMT without changing the one stored unless they call // `Commit()` to write to the nodestore. func (t *treeStore) GetTree(name string) ([]byte, kvstore.KVStore) { - if name == RootTreeName { + if name == RootTreeName && t.rootTree.tree != nil { return t.rootTree.tree.Root(), t.rootTree.nodeStore } - if tree, ok := t.merkleTrees[name]; ok { + if tree, ok := t.merkleTrees[name]; ok && tree != nil { return tree.tree.Root(), tree.nodeStore } return nil, nil @@ -325,8 +329,42 @@ func (t *treeStore) Rollback() error { return ErrFailedRollback } -// save commits any pending changes to the trees and creates a copy of the current worldState, -// then saves that copy as a rollback point for later use if errors are encountered. +// Load sets the TreeStore trees to the values provided in the worldstate +func (t *treeStore) Load(w *worldState) error { + t.merkleTrees = make(map[string]*stateTree) + + // import root tree + rootTreePath := fmt.Sprintf("%s/%s_nodes", t.treeStoreDir, RootTreeName) + nodeStore, err := kvstore.NewKVStore(rootTreePath) + if err != nil { + return err + } + t.rootTree = &stateTree{ + name: RootTreeName, + tree: smt.ImportSparseMerkleTree(nodeStore, smtTreeHasher, w.rootHash), + nodeStore: nodeStore, + } + + // import merkle trees + for treeName, treeRootHash := range w.merkleRoots { + treePath := fmt.Sprintf("%s/%s_nodes", w.treeStoreDir, treeName) + nodeStore, err := kvstore.NewKVStore(treePath) + if err != nil { + return err + } + + t.merkleTrees[treeName] = &stateTree{ + name: treeName, + nodeStore: nodeStore, + tree: smt.ImportSparseMerkleTree(nodeStore, smtTreeHasher, treeRootHash), + } + } + + return nil +} + +// save commits any pending changes to the trees and creates a copy of the current state of the +// tree store then saves that copy as a rollback point for later use if errors are encountered. // OPTIMIZE: Consider saving only the root hash of each tree and the tree directory here and then // load the trees up in Rollback instead of setting them up here. func (t *treeStore) save() (*worldState, error) { @@ -336,7 +374,10 @@ func (t *treeStore) save() (*worldState, error) { w := &worldState{ treeStoreDir: t.treeStoreDir, - merkleTrees: map[string]*stateTree{}, + merkleRoots: make(map[string][]byte), + merkleTrees: make(map[string]*stateTree), + rootHash: t.rootTree.tree.Root(), + rootTree: t.rootTree, } for treeName := range t.merkleTrees { @@ -360,6 +401,20 @@ func (t *treeStore) save() (*worldState, error) { return w, nil } +// Backup creates a new backup of each tree in the tree store to the provided directory. +// Each tree is backed up in an eponymous file in the provided backupDir. +func (t *treeStore) Backup(backupDir string) error { + errs := []error{} + for _, st := range t.merkleTrees { + treePath := filepath.Join(backupDir, st.name) + if err := st.nodeStore.Backup(treePath); err != nil { + t.logger.Err(err).Msgf("failed to backup %s tree: %+v", st.name, err) + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + //////////////////////// // Actor Tree Helpers // //////////////////////// diff --git a/persistence/trees/trees_test.go b/persistence/trees/trees_test.go index aa8c41ab4..cac314198 100644 --- a/persistence/trees/trees_test.go +++ b/persistence/trees/trees_test.go @@ -50,8 +50,6 @@ func TestTreeStore_Update(t *testing.T) { pmod := newTestPersistenceModule(t, dbUrl) context := newTestPostgresContext(t, 0, pmod) - require.NoError(t, context.SetSavePoint()) - hash1, err := context.ComputeStateHash() require.NoError(t, err) require.NotEmpty(t, hash1) @@ -60,8 +58,6 @@ func TestTreeStore_Update(t *testing.T) { _, err = createAndInsertDefaultTestApp(t, context) require.NoError(t, err) - require.NoError(t, context.SetSavePoint()) - hash2, err := context.ComputeStateHash() require.NoError(t, err) require.NotEmpty(t, hash2) diff --git a/shared/modules/treestore_module.go b/shared/modules/treestore_module.go index a79f7a14f..bddc2451c 100644 --- a/shared/modules/treestore_module.go +++ b/shared/modules/treestore_module.go @@ -19,6 +19,7 @@ type treeStoreFactory = FactoryWithOptions[TreeStoreModule, TreeStoreOption] // merkle trees that compose the state hash of pocket. type TreeStoreModule interface { Submodule + InterruptableModule treeStoreFactory AtomicStore