diff --git a/persistence/blockstore/block_store.go b/persistence/blockstore/block_store.go index 9bc4aa5a4..9eafdaac9 100644 --- a/persistence/blockstore/block_store.go +++ b/persistence/blockstore/block_store.go @@ -4,6 +4,7 @@ package blockstore import ( "fmt" + "path/filepath" "github.com/pokt-network/pocket/persistence/kvstore" "github.com/pokt-network/pocket/shared/codec" @@ -11,6 +12,9 @@ import ( "github.com/pokt-network/pocket/shared/utils" ) +// backupName is the name of the archive file that is created when Backup is called for a BlockStore +const backupName = "blockstore.bak" + // BlockStore is a key-value store that maps block heights to serialized // block structures. // * It manages the atomic state transitions for applying a Unit of Work. @@ -93,8 +97,8 @@ func (bs *blockStore) Stop() error { return bs.kv.Stop() } -func (bs *blockStore) Backup(path string) error { - return bs.kv.Backup(path) +func (bs *blockStore) Backup(dir string) error { + return bs.kv.Backup(filepath.Join(dir, backupName)) } /////////////// diff --git a/persistence/kvstore/kvstore.go b/persistence/kvstore/kvstore.go index f4ea132d2..b25a35a22 100644 --- a/persistence/kvstore/kvstore.go +++ b/persistence/kvstore/kvstore.go @@ -26,7 +26,8 @@ type KVStore interface { Exists(key []byte) (bool, error) ClearAll() error - Backup(filepath string) error + // Backup takes a directory and makes a backup of the KVStore in that directory. + Backup(dir string) error } const ( diff --git a/rpc/handlers_node.go b/rpc/handlers_node.go index f7463adab..581f87cb2 100644 --- a/rpc/handlers_node.go +++ b/rpc/handlers_node.go @@ -1,106 +1,30 @@ package rpc import ( - "fmt" - "net/url" - "os" - "os/exec" - "path/filepath" - "time" - "github.com/labstack/echo/v4" - "github.com/pokt-network/pocket/runtime/configs" ) // PostV1NodeBackup triggers a backup of the TreeStore, the BlockStore, the PostgreSQL database. // TECHDEBT: Run each backup process in a goroutine to as elapsed time will become significant // with the current waterfall approach when even a moderate amount of data resides in each store. func (s *rpcServer) PostV1NodeBackup(ctx echo.Context) error { - dir := os.TempDir() // TODO_IN_THIS_COMMIT give this a sane default and make it configurable + // TECHDEBT: Wire this up to a default config param if dir == "" + // cfg := s.GetBus().GetRuntimeMgr().GetConfig() + + dir := ctx.Param("dir") + s.logger.Info().Msgf("creating backup in %s", dir) // backup the TreeStore - trees := s.GetBus().GetTreeStore() - if err := trees.Backup(dir); err != nil { + if err := s.GetBus().GetTreeStore().Backup(dir); err != nil { return err } // backup the BlockStore - path := fmt.Sprintf("%s-blockstore-backup.sql", time.Now().String()) - if err := s.GetBus().GetPersistenceModule().GetBlockStore().Backup(path); err != nil { - return err - } - - // backup the Postgres database - cfg := s.GetBus().GetRuntimeMgr().GetConfig() - err := postgresBackup(cfg, dir) // TODO_IN_THIS_COMMIT make this point at the right directory per the tests - if err != nil { + if err := s.GetBus().GetPersistenceModule().GetBlockStore().Backup(dir); err != nil { return err } s.logger.Info().Msgf("backup created in %s", dir) return nil } - -func postgresBackup(cfg *configs.Config, dir string) error { - filename := fmt.Sprintf("%s-postgres-backup.sql", time.Now().String()) - file, err := os.Create(filepath.Join(dir, filename)) - if err != nil { - return err - } - defer file.Close() - - // pgurl := cfg.Persistence.PostgresUrl - // credentials, err := parsePostgreSQLConnectionURL(pgurl) - if err != nil { - return err - } - - cmd := exec.Command("which pg_dump") - fmt.Printf("cmd.Stdout: %v\n", cmd.Stdout) - fmt.Printf("cmd.Stderr: %v\n", cmd.Stderr) - - // cmd := exec.Command(fmt.Sprintf("PGPASSWORD=%s", credentials.password), "pg_dump", "-h", credentials.host, "-U", credentials.username, credentials.dbName) - // cmd.Stdout = file - // err = cmd.Run() - // if err != nil { - // return err - // } - - return nil -} - -type credentials struct { - username string - password string - host string - dbName string - sslMode string -} - -// validate a credentials object for connecting to postgres to create a backup -func parsePostgreSQLConnectionURL(connectionURL string) (*credentials, error) { - parsedURL, err := url.Parse(connectionURL) - if err != nil { - return nil, err - } - - if parsedURL.Scheme != "postgres" && parsedURL.Scheme != "postgresql" { - return nil, fmt.Errorf("failed to parse postgres URL") - } - - username := parsedURL.User.Username() - password, _ := parsedURL.User.Password() - host := parsedURL.Host - dbName := parsedURL.Path[1:] // Remove the leading slash - query := parsedURL.Query() - sslMode := query.Get("sslmode") - - return &credentials{ - username: username, - password: password, - host: host, - dbName: dbName, - sslMode: sslMode, - }, nil -} diff --git a/rpc/handlers_test.go b/rpc/handlers_test.go index 5e9922e1d..020e24296 100644 --- a/rpc/handlers_test.go +++ b/rpc/handlers_test.go @@ -1,29 +1,39 @@ package rpc import ( + "io" "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" - "github.com/labstack/echo/v4" "github.com/pokt-network/pocket/internal/testutil" "github.com/pokt-network/pocket/logger" "github.com/pokt-network/pocket/runtime/test_artifacts" "github.com/pokt-network/pocket/shared/modules" + "github.com/stretchr/testify/require" + + "github.com/labstack/echo/v4" ) func Test_RPCPostV1NodeBackup(t *testing.T) { - tests := []struct { + // THIS WORKS BUT ADJUST LATER + type testCase struct { name string setup func(t *testing.T, e echo.Context) *rpcServer + assert func(t *testing.T, tt testCase, e echo.Context, s *rpcServer) wantErr bool - }{ + } + + var testDir = t.TempDir() + + tests := []testCase{ { - name: "should create a backup in the default directory", + name: "should create a backup in the specified directory", setup: func(t *testing.T, e echo.Context) *rpcServer { _, _, url := test_artifacts.SetupPostgresDocker() pmod := testutil.NewTestPersistenceModule(t, url) - // context := testutil.NewTestPostgresContext(t, pmod, 0) s := &rpcServer{ logger: *logger.Global.CreateLoggerForModule(modules.RPCModuleName), @@ -31,16 +41,42 @@ func Test_RPCPostV1NodeBackup(t *testing.T) { s.SetBus(pmod.GetBus()) + e.SetParamNames("dir") + e.SetParamValues(testDir) + return s }, + wantErr: false, + assert: func(t *testing.T, tt testCase, e echo.Context, s *rpcServer) { + empty, err := isEmpty(testDir) + require.NoError(t, err) + require.False(t, empty) + f, err := os.Open(testDir) + require.NoError(t, err) + dirs, err := f.ReadDir(-1) + require.NoError(t, err) + require.True(t, len(dirs) == 12) + + // assert worldstate json was written + _, err = os.Open(filepath.Join(testDir, "worldstate.json")) + require.NoError(t, err) + + // assert blockstore was written + _, err = os.Open(filepath.Join(testDir, "blockstore.bak")) + require.NoError(t, err) + + // cleanup the directory after each test + t.Cleanup(func() { + require.NoError(t, os.RemoveAll(testDir)) + }) + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // create a new echo Context for each test - tempDir := t.TempDir() e := echo.New() - req := httptest.NewRequest(http.MethodPost, tempDir, nil) + req := httptest.NewRequest(http.MethodPost, "/v1/node/backup", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) @@ -51,6 +87,22 @@ func Test_RPCPostV1NodeBackup(t *testing.T) { if err := s.PostV1NodeBackup(c); (err != nil) != tt.wantErr { t.Errorf("rpcServer.PostV1NodeBackup() error = %v, wantErr %v", err, tt.wantErr) } + tt.assert(t, tt, c, s) }) } } + +// 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 +}