core: allow RO mode for Bolt and Level

This commit is contained in:
Anna Shaleva 2022-10-03 15:05:44 +03:00
parent cbdd45cc96
commit 2f5137e9b7
6 changed files with 113 additions and 15 deletions

View file

@ -47,14 +47,20 @@ DBConfiguration:
Type: leveldb Type: leveldb
LevelDBOptions: LevelDBOptions:
DataDirectoryPath: /chains/privnet DataDirectoryPath: /chains/privnet
ReadOnly: false
BoltDBOptions: BoltDBOptions:
FilePath: ./chains/privnet.bolt FilePath: ./chains/privnet.bolt
ReadOnly: false
``` ```
where: where:
- `Type` is the database type (string value). Supported types: `levelDB` and - `Type` is the database type (string value). Supported types: `levelDB` and
`boltDB`. `boltDB`.
- `LevelDBOptions` are settings for LevelDB. - `LevelDBOptions` are settings for LevelDB. Includes the DB files path and ReadOnly mode toggle.
- `BoltDBOptions` configures BoltDB. If ReadOnly mode is on, then an error will be returned on attempt to connect to unexisting or empty
database. Database doesn't allow changes in this mode, a warning will be logged on DB persist attempts.
- `BoltDBOptions` configures BoltDB. Includes the DB files path and ReadOnly mode toggle. If ReadOnly
mode is on, then an error will be returned on attempt to connect with unexisting or empty database.
Database doesn't allow changes in this mode, a warning will be logged on DB persist attempts.
Only options for the specified database type will be used. Only options for the specified database type will be used.

View file

@ -2,6 +2,7 @@ package storage
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
@ -22,16 +23,30 @@ type BoltDBStore struct {
// NewBoltDBStore returns a new ready to use BoltDB storage with created bucket. // NewBoltDBStore returns a new ready to use BoltDB storage with created bucket.
func NewBoltDBStore(cfg dbconfig.BoltDBOptions) (*BoltDBStore, error) { func NewBoltDBStore(cfg dbconfig.BoltDBOptions) (*BoltDBStore, error) {
var opts *bbolt.Options // should be exposed via BoltDBOptions if anything needed cp := *bbolt.DefaultOptions // Do not change bbolt's global variable.
opts := &cp
fileMode := os.FileMode(0600) // should be exposed via BoltDBOptions if anything needed fileMode := os.FileMode(0600) // should be exposed via BoltDBOptions if anything needed
fileName := cfg.FilePath fileName := cfg.FilePath
if cfg.ReadOnly {
opts.ReadOnly = true
} else {
if err := io.MakeDirForFile(fileName, "BoltDB"); err != nil { if err := io.MakeDirForFile(fileName, "BoltDB"); err != nil {
return nil, err return nil, err
} }
}
db, err := bbolt.Open(fileName, fileMode, opts) db, err := bbolt.Open(fileName, fileMode, opts)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to open BoltDB instance: %w", err)
} }
if opts.ReadOnly {
err = db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket(Bucket)
if b == nil {
return errors.New("root bucket does not exist")
}
return nil
})
} else {
err = db.Update(func(tx *bbolt.Tx) error { err = db.Update(func(tx *bbolt.Tx) error {
_, err = tx.CreateBucketIfNotExists(Bucket) _, err = tx.CreateBucketIfNotExists(Bucket)
if err != nil { if err != nil {
@ -39,6 +54,7 @@ func NewBoltDBStore(cfg dbconfig.BoltDBOptions) (*BoltDBStore, error) {
} }
return nil return nil
}) })
}
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize BoltDB instance: %w", err) return nil, fmt.Errorf("failed to initialize BoltDB instance: %w", err)
} }

View file

@ -1,11 +1,15 @@
package storage package storage
import ( import (
"os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.etcd.io/bbolt"
) )
func newBoltStoreForTesting(t testing.TB) Store { func newBoltStoreForTesting(t testing.TB) Store {
@ -15,3 +19,42 @@ func newBoltStoreForTesting(t testing.TB) Store {
require.NoError(t, err) require.NoError(t, err)
return boltDBStore return boltDBStore
} }
func TestROBoltDB(t *testing.T) {
d := t.TempDir()
testFileName := filepath.Join(d, "test_ro_bolt_db")
cfg := dbconfig.BoltDBOptions{
FilePath: testFileName,
ReadOnly: true,
}
// If DB doesn't exist, then error should be returned.
_, err := NewBoltDBStore(cfg)
require.Error(t, err)
// Create the DB and try to open it in RO mode.
cfg.ReadOnly = false
store, err := NewBoltDBStore(cfg)
require.NoError(t, err)
require.NoError(t, store.Close())
cfg.ReadOnly = true
store, err = NewBoltDBStore(cfg)
require.NoError(t, err)
// Changes must be prohibited.
putErr := store.PutChangeSet(map[string][]byte{"one": []byte("one")}, nil)
require.ErrorIs(t, putErr, bbolt.ErrDatabaseReadOnly)
require.NoError(t, store.Close())
// Create the DB without buckets and try to open it in RO mode, an error is expected.
fileMode := os.FileMode(0600)
cfg.FilePath = filepath.Join(d, "clean_ro_bolt_db")
require.NoError(t, io.MakeDirForFile(cfg.FilePath, "BoltDB"))
db, err := bbolt.Open(cfg.FilePath, fileMode, nil)
require.NoError(t, err)
require.NoError(t, db.Close())
_, err = NewBoltDBStore(cfg)
require.Error(t, err)
require.True(t, strings.Contains(err.Error(), "root bucket does not exist"))
}

View file

@ -13,9 +13,11 @@ type (
// LevelDBOptions configuration for LevelDB. // LevelDBOptions configuration for LevelDB.
LevelDBOptions struct { LevelDBOptions struct {
DataDirectoryPath string `yaml:"DataDirectoryPath"` DataDirectoryPath string `yaml:"DataDirectoryPath"`
ReadOnly bool `yaml:"ReadOnly"`
} }
// BoltDBOptions configuration for BoltDB. // BoltDBOptions configuration for BoltDB.
BoltDBOptions struct { BoltDBOptions struct {
FilePath string `yaml:"FilePath"` FilePath string `yaml:"FilePath"`
ReadOnly bool `yaml:"ReadOnly"`
} }
) )

View file

@ -2,6 +2,7 @@ package storage
import ( import (
"errors" "errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
@ -21,11 +22,14 @@ type LevelDBStore struct {
// initialize the database found at the given path. // initialize the database found at the given path.
func NewLevelDBStore(cfg dbconfig.LevelDBOptions) (*LevelDBStore, error) { func NewLevelDBStore(cfg dbconfig.LevelDBOptions) (*LevelDBStore, error) {
var opts = new(opt.Options) // should be exposed via LevelDBOptions if anything needed var opts = new(opt.Options) // should be exposed via LevelDBOptions if anything needed
if cfg.ReadOnly {
opts.ReadOnly = true
opts.ErrorIfMissing = true
}
opts.Filter = filter.NewBloomFilter(10) opts.Filter = filter.NewBloomFilter(10)
db, err := leveldb.OpenFile(cfg.DataDirectoryPath, opts) db, err := leveldb.OpenFile(cfg.DataDirectoryPath, opts)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to open LevelDB instance: %w", err)
} }
return &LevelDBStore{ return &LevelDBStore{

View file

@ -5,6 +5,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/syndtr/goleveldb/leveldb"
) )
func newLevelDBForTesting(t testing.TB) Store { func newLevelDBForTesting(t testing.TB) Store {
@ -16,3 +17,29 @@ func newLevelDBForTesting(t testing.TB) Store {
require.Nil(t, err, "NewLevelDBStore error") require.Nil(t, err, "NewLevelDBStore error")
return newLevelStore return newLevelStore
} }
func TestROLevelDB(t *testing.T) {
ldbDir := t.TempDir()
opts := dbconfig.LevelDBOptions{
DataDirectoryPath: ldbDir,
ReadOnly: true,
}
// If DB doesn't exist, then error should be returned.
_, err := NewLevelDBStore(opts)
require.Error(t, err)
// Create the DB and try to open it in RO mode.
opts.ReadOnly = false
store, err := NewLevelDBStore(opts)
require.NoError(t, err)
require.NoError(t, store.Close())
opts.ReadOnly = true
store, err = NewLevelDBStore(opts)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, store.Close()) })
// Changes must be prohibited.
putErr := store.PutChangeSet(map[string][]byte{"one": []byte("one")}, nil)
require.ErrorIs(t, putErr, leveldb.ErrReadOnly)
}