diff --git a/.changelog/5876.cfg.md b/.changelog/5876.cfg.md new file mode 100644 index 00000000000..318ec02c943 --- /dev/null +++ b/.changelog/5876.cfg.md @@ -0,0 +1,12 @@ +The pathbadger storage backend is now the default for new nodes + +When a node is provisioned into an empty data directory it will default to +using the `pathbadger` storage backend. + +For existing nodes, the storage backend is automatically detected based on +the data directory. When multiple storage directories exist, the one most +recently modified is used. + +In case one does not want this new behavior, it is still possible to set +the `storage.backend` to `badger`/`pathbadger` to explicitly configure the +desired storage backend and disable autodetection. diff --git a/.changelog/5876.feature.md b/.changelog/5876.feature.md new file mode 100644 index 00000000000..53cb8ec45b5 --- /dev/null +++ b/.changelog/5876.feature.md @@ -0,0 +1,8 @@ +go/storage: Add automatic storage backend detection + +The new default storage backend is "auto" which attempts to detect the +storage backend that should be used based on existing data directories. +When none exist, "pathbadger" is used. When multiple exist, the most +recently modified one is used. + +This should make newly deployed nodes default to pathbadger. diff --git a/go/oasis-test-runner/oasis/oasis.go b/go/oasis-test-runner/oasis/oasis.go index a1cec393f5b..745d944ccc1 100644 --- a/go/oasis-test-runner/oasis/oasis.go +++ b/go/oasis-test-runner/oasis/oasis.go @@ -41,7 +41,7 @@ const ( defaultVRFInterval = 20 defaultVRFSubmissionDelay = 5 - defaultStorageBackend = database.BackendNamePathBadger + defaultStorageBackend = database.BackendNameAuto logNodeFile = "node.log" logConsoleFile = "console.log" diff --git a/go/storage/database/database.go b/go/storage/database/database.go index 6b940b26e79..1d114c2ee34 100644 --- a/go/storage/database/database.go +++ b/go/storage/database/database.go @@ -5,7 +5,10 @@ import ( "context" "fmt" "io" + "os" "path/filepath" + "slices" + "time" "github.com/oasisprotocol/oasis-core/go/storage/api" "github.com/oasisprotocol/oasis-core/go/storage/mkvs/checkpoint" @@ -14,11 +17,17 @@ import ( ) const ( + // BackendNameAuto is the name of the automatic backend detection "backend". + BackendNameAuto = "auto" // BackendNameBadgerDB is the name of the BadgerDB backed database backend. BackendNameBadgerDB = "badger" // BackendNamePathBadger is the name of the PathBadger database backend. BackendNamePathBadger = "pathbadger" + // autoDefaultBackend is the default backend in case automatic backend detection is enabled and + // no previous backend exists. + autoDefaultBackend = BackendNamePathBadger + checkpointDir = "checkpoints" ) @@ -39,6 +48,10 @@ type databaseBackend struct { // New constructs a new database backed storage Backend instance. func New(cfg *api.Config) (api.LocalBackend, error) { + if err := autoDetectBackend(cfg); err != nil { + return nil, err + } + ndb, err := db.New(cfg.Backend, cfg.ToNodeDB()) if err != nil { return nil, fmt.Errorf("storage/database: failed to create node database: %w", err) @@ -164,3 +177,59 @@ func (ba *databaseBackend) Checkpointer() checkpoint.CreateRestorer { func (ba *databaseBackend) NodeDB() dbApi.NodeDB { return ba.ndb } + +// autoDetectBackend attempts automatic backend detection, modifying the configuration in place. +func autoDetectBackend(cfg *api.Config) error { + if cfg.Backend != BackendNameAuto { + return nil + } + + // Make sure that the DefaultFileName was used to derive the subdirectory. Otherwise automatic + // detection cannot be performed. + if filepath.Base(cfg.DB) != DefaultFileName(cfg.Backend) { + return fmt.Errorf("storage/database: 'auto' backend selected using a non-default path") + } + + // Perform automatic database backend detection if selected. Detection will be based on existing + // database directories. If multiple directories are available, the most recently modified is + // selected. + type foundBackend struct { + path string + timestamp time.Time + name string + } + var backends []foundBackend + + for _, b := range db.Backends { + // Generate expected filename for the given backend. + fn := DefaultFileName(b.Name()) + maybeDb := filepath.Join(filepath.Dir(cfg.DB), fn) + fi, err := os.Stat(maybeDb) + if err != nil { + continue + } + + backends = append(backends, foundBackend{ + path: maybeDb, + timestamp: fi.ModTime(), + name: b.Name(), + }) + } + slices.SortFunc(backends, func(a, b foundBackend) int { + return a.timestamp.Compare(b.timestamp) + }) + + // If no existing backends are available, default to "pathbadger". + if len(backends) == 0 { + cfg.Backend = autoDefaultBackend + cfg.DB = filepath.Join(filepath.Dir(cfg.DB), DefaultFileName(cfg.Backend)) + return nil + } + + // Otherwise, use the backend that has been updated most recently. + b := backends[len(backends)-1] + cfg.Backend = b.name + cfg.DB = b.path + + return nil +} diff --git a/go/storage/database/database_test.go b/go/storage/database/database_test.go index 9304138e31e..999faa2e24b 100644 --- a/go/storage/database/database_test.go +++ b/go/storage/database/database_test.go @@ -51,3 +51,92 @@ func doTestImpl(t *testing.T, backend string) { genesisTestHelpers.SetTestChainContext() tests.StorageImplementationTests(t, impl, impl, testNs, 0) } + +func TestAutoBackend(t *testing.T) { + t.Run("NoExistingDir", func(t *testing.T) { + require := require.New(t) + + tmpDir, err := os.MkdirTemp("", "oasis-storage-database-test") + require.NoError(err, "TempDir()") + defer os.RemoveAll(tmpDir) + + // When there is no existing database directory, the default should be used. + cfg := api.Config{ + Backend: "auto", + DB: filepath.Join(tmpDir, DefaultFileName("auto")), + } + impl, err := New(&cfg) + require.NoError(err) + impl.Cleanup() + + require.Equal("pathbadger", cfg.Backend) + require.Equal(filepath.Join(tmpDir, DefaultFileName("pathbadger")), cfg.DB) + }) + + t.Run("OneExistingDir", func(t *testing.T) { + require := require.New(t) + + tmpDir, err := os.MkdirTemp("", "oasis-storage-database-test") + require.NoError(err, "TempDir()") + defer os.RemoveAll(tmpDir) + + // Create a badger database first. + cfg := api.Config{ + Backend: "badger", + DB: filepath.Join(tmpDir, DefaultFileName("badger")), + } + impl, err := New(&cfg) + require.NoError(err) + impl.Cleanup() + + // When there is an existing backend, it should be used. + cfg = api.Config{ + Backend: "auto", + DB: filepath.Join(tmpDir, DefaultFileName("auto")), + } + impl, err = New(&cfg) + require.NoError(err) + impl.Cleanup() + + require.Equal("badger", cfg.Backend) + require.Equal(filepath.Join(tmpDir, DefaultFileName("badger")), cfg.DB) + }) + + t.Run("MultiExistingDirs", func(t *testing.T) { + require := require.New(t) + + tmpDir, err := os.MkdirTemp("", "oasis-storage-database-test") + require.NoError(err, "TempDir()") + defer os.RemoveAll(tmpDir) + + // Create a badger database first. + cfg := api.Config{ + Backend: "badger", + DB: filepath.Join(tmpDir, DefaultFileName("badger")), + } + impl, err := New(&cfg) + require.NoError(err) + impl.Cleanup() + + // Then create a pathbadger database. + cfg = api.Config{ + Backend: "pathbadger", + DB: filepath.Join(tmpDir, DefaultFileName("pathbadger")), + } + impl, err = New(&cfg) + require.NoError(err) + impl.Cleanup() + + // When there are multiple existing backends, the most recent one should be used. + cfg = api.Config{ + Backend: "auto", + DB: filepath.Join(tmpDir, DefaultFileName("auto")), + } + impl, err = New(&cfg) + require.NoError(err) + impl.Cleanup() + + require.Equal("pathbadger", cfg.Backend) + require.Equal(filepath.Join(tmpDir, DefaultFileName("pathbadger")), cfg.DB) + }) +} diff --git a/go/worker/storage/config/config.go b/go/worker/storage/config/config.go index 2f5ca747afd..6145dde8860 100644 --- a/go/worker/storage/config/config.go +++ b/go/worker/storage/config/config.go @@ -35,14 +35,17 @@ type CheckpointerConfig struct { // Validate validates the configuration settings. func (c *Config) Validate() error { - _, err := db.GetBackendByName(c.Backend) - return err + if c.Backend != "auto" { + _, err := db.GetBackendByName(c.Backend) + return err + } + return nil } // DefaultConfig returns the default configuration settings. func DefaultConfig() Config { return Config{ - Backend: "badger", + Backend: "auto", MaxCacheSize: "64mb", FetcherCount: 4, PublicRPCEnabled: false,