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

go/storage: Add automatic storage backend detection #5876

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changelog/5876.cfg.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions .changelog/5876.feature.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion go/oasis-test-runner/oasis/oasis.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const (
defaultVRFInterval = 20
defaultVRFSubmissionDelay = 5

defaultStorageBackend = database.BackendNamePathBadger
defaultStorageBackend = database.BackendNameAuto

logNodeFile = "node.log"
logConsoleFile = "console.log"
Expand Down
69 changes: 69 additions & 0 deletions go/storage/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)

Expand All @@ -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)
Expand Down Expand Up @@ -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
}
89 changes: 89 additions & 0 deletions go/storage/database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
9 changes: 6 additions & 3 deletions go/worker/storage/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading