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
|
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.
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
@ -22,23 +23,38 @@ 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 err := io.MakeDirForFile(fileName, "BoltDB"); err != nil {
|
if cfg.ReadOnly {
|
||||||
return nil, err
|
opts.ReadOnly = true
|
||||||
|
} else {
|
||||||
|
if err := io.MakeDirForFile(fileName, "BoltDB"); err != nil {
|
||||||
|
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 = 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 {
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"))
|
||||||
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue