core: allow RO mode for Bolt and Level
This commit is contained in:
parent
cbdd45cc96
commit
2f5137e9b7
6 changed files with 113 additions and 15 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package storage
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
|
@ -22,23 +23,38 @@ 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 err := io.MakeDirForFile(fileName, "BoltDB"); err != nil {
|
||||
return nil, err
|
||||
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 {
|
||||
return fmt.Errorf("could not create root bucket: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
err = db.Update(func(tx *bbolt.Tx) error {
|
||||
_, err = tx.CreateBucketIfNotExists(Bucket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create root bucket: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize BoltDB instance: %w", err)
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue