From 034190c1c1fd226ac3e9ac5e7a5124d91d157f70 Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Mon, 30 Sep 2024 16:28:38 +0200 Subject: [PATCH] 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. --- .changelog/5876.cfg.md | 12 ++++ .changelog/5876.feature.md | 8 +++ go/oasis-test-runner/oasis/oasis.go | 2 +- go/storage/database/database.go | 69 +++++++++++++++++++++ go/storage/database/database_test.go | 89 ++++++++++++++++++++++++++++ go/worker/storage/config/config.go | 9 ++- 6 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 .changelog/5876.cfg.md create mode 100644 .changelog/5876.feature.md 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,