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
LevelDBOptions:
DataDirectoryPath: /chains/privnet
ReadOnly: false
BoltDBOptions:
FilePath: ./chains/privnet.bolt
ReadOnly: false
```
where:
- `Type` is the database type (string value). Supported types: `levelDB` and
`boltDB`.
- `LevelDBOptions` are settings for LevelDB.
- `BoltDBOptions` configures BoltDB.
- `LevelDBOptions` are settings for LevelDB. Includes the DB files path and ReadOnly mode toggle.
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.

View file

@ -2,6 +2,7 @@ package storage
import (
"bytes"
"errors"
"fmt"
"os"
@ -22,16 +23,30 @@ type BoltDBStore struct {
// NewBoltDBStore returns a new ready to use BoltDB storage with created bucket.
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
fileName := cfg.FilePath
if cfg.ReadOnly {
opts.ReadOnly = true
} else {
if err := io.MakeDirForFile(fileName, "BoltDB"); err != nil {
return nil, err
}
}
db, err := bbolt.Open(fileName, fileMode, opts)
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 = tx.CreateBucketIfNotExists(Bucket)
if err != nil {
@ -39,6 +54,7 @@ func NewBoltDBStore(cfg dbconfig.BoltDBOptions) (*BoltDBStore, error) {
}
return nil
})
}
if err != nil {
return nil, fmt.Errorf("failed to initialize BoltDB instance: %w", err)
}

View file

@ -1,11 +1,15 @@
package storage
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/stretchr/testify/require"
"go.etcd.io/bbolt"
)
func newBoltStoreForTesting(t testing.TB) Store {
@ -15,3 +19,42 @@ func newBoltStoreForTesting(t testing.TB) Store {
require.NoError(t, err)
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 struct {
DataDirectoryPath string `yaml:"DataDirectoryPath"`
ReadOnly bool `yaml:"ReadOnly"`
}
// BoltDBOptions configuration for BoltDB.
BoltDBOptions struct {
FilePath string `yaml:"FilePath"`
ReadOnly bool `yaml:"ReadOnly"`
}
)

View file

@ -2,6 +2,7 @@ package storage
import (
"errors"
"fmt"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/syndtr/goleveldb/leveldb"
@ -21,11 +22,14 @@ type LevelDBStore struct {
// initialize the database found at the given path.
func NewLevelDBStore(cfg dbconfig.LevelDBOptions) (*LevelDBStore, error) {
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)
db, err := leveldb.OpenFile(cfg.DataDirectoryPath, opts)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to open LevelDB instance: %w", err)
}
return &LevelDBStore{

View file

@ -5,6 +5,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
"github.com/stretchr/testify/require"
"github.com/syndtr/goleveldb/leveldb"
)
func newLevelDBForTesting(t testing.TB) Store {
@ -16,3 +17,29 @@ func newLevelDBForTesting(t testing.TB) Store {
require.Nil(t, err, "NewLevelDBStore error")
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)
}