Remove outdated code of metabase and localstore

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2020-12-08 10:51:34 +03:00 committed by Alex Vanin
parent 869d9e571c
commit a875d80491
41 changed files with 1725 additions and 3123 deletions

View file

@ -22,7 +22,7 @@ import (
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/bucket" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/bucket"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/bucket/fsbucket" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/bucket/fsbucket"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine"
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase/v2" meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/shard" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/shard"
"github.com/nspcc-dev/neofs-node/pkg/morph/client" "github.com/nspcc-dev/neofs-node/pkg/morph/client"
"github.com/nspcc-dev/neofs-node/pkg/morph/client/container/wrapper" "github.com/nspcc-dev/neofs-node/pkg/morph/client/container/wrapper"

View file

@ -1,81 +0,0 @@
package localstore
import (
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/pkg/errors"
"go.uber.org/zap"
)
func addressBytes(a *objectSDK.Address) ([]byte, error) {
return a.Marshal()
}
func objectBytes(o *object.Object) ([]byte, error) {
return o.Marshal()
}
func (s *Storage) Put(obj *object.Object) error {
addrBytes, err := addressBytes(obj.Address())
if err != nil {
return errors.Wrap(err, "could not marshal object address")
}
objBytes, err := objectBytes(obj)
if err != nil {
return errors.Wrap(err, "could not marshal the object")
}
if err := s.blobBucket.Set(addrBytes, objBytes); err != nil {
return errors.Wrap(err, "could no save object in BLOB storage")
}
if err := s.metaBase.Put(object.NewRawFromObject(obj).CutPayload().Object()); err != nil {
return errors.Wrap(err, "could not save object in meta storage")
}
return nil
}
func (s *Storage) Delete(addr *objectSDK.Address) error {
addrBytes, err := addressBytes(addr)
if err != nil {
return errors.Wrap(err, "could not marshal object address")
}
if err := s.blobBucket.Del(addrBytes); err != nil {
s.log.Warn("could not remove object from BLOB storage",
zap.Error(err),
)
}
if err := s.metaBase.Delete(addr); err != nil {
return errors.Wrap(err, "could not remove object from meta storage")
}
return nil
}
func (s *Storage) Get(addr *objectSDK.Address) (*object.Object, error) {
addrBytes, err := addressBytes(addr)
if err != nil {
return nil, errors.Wrap(err, "could not marshal object address")
}
objBytes, err := s.blobBucket.Get(addrBytes)
if err != nil {
return nil, errors.Wrap(err, "could not get object from BLOB storage")
}
obj := object.New()
return obj, obj.Unmarshal(objBytes)
}
func (s *Storage) Head(addr *objectSDK.Address) (*object.Object, error) {
return s.metaBase.Get(addr)
}
func (s *Storage) Select(fs objectSDK.SearchFilters) ([]*objectSDK.Address, error) {
return s.metaBase.Select(fs)
}

View file

@ -1,54 +0,0 @@
package localstore
import (
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/bucket"
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase"
"github.com/nspcc-dev/neofs-node/pkg/util/logger"
"go.uber.org/zap"
)
// Storage represents NeoFS local object storage.
type Storage struct {
log *logger.Logger
metaBase *meta.DB
blobBucket bucket.Bucket
}
// Option is an option of Storage constructor.
type Option func(*cfg)
type cfg struct {
logger *logger.Logger
}
func defaultCfg() *cfg {
return &cfg{
logger: zap.L(),
}
}
// New is a local object storage constructor.
func New(blob bucket.Bucket, meta *meta.DB, opts ...Option) *Storage {
cfg := defaultCfg()
for i := range opts {
opts[i](cfg)
}
return &Storage{
log: cfg.logger,
metaBase: meta,
blobBucket: blob,
}
}
// WithLogger returns Storage option of used logger.
func WithLogger(l *logger.Logger) Option {
return func(c *cfg) {
if l != nil {
c.logger = l
}
}
}

View file

@ -1,6 +1,11 @@
package meta package meta
import ( import (
"encoding/binary"
"encoding/hex"
"os"
"strconv"
"github.com/nspcc-dev/neofs-api-go/pkg/object" "github.com/nspcc-dev/neofs-api-go/pkg/object"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object" v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-node/pkg/util/logger" "github.com/nspcc-dev/neofs-node/pkg/util/logger"
@ -10,30 +15,36 @@ import (
// DB represents local metabase of storage node. // DB represents local metabase of storage node.
type DB struct { type DB struct {
info Info
*cfg *cfg
matchers map[object.SearchMatchType]func(string, string, string) bool matchers map[object.SearchMatchType]func(string, []byte, string) bool
boltDB *bbolt.DB
} }
// Option is an option of DB constructor. // Option is an option of DB constructor.
type Option func(*cfg) type Option func(*cfg)
type cfg struct { type cfg struct {
boltDB *bbolt.DB boltOptions *bbolt.Options // optional
info Info
log *logger.Logger log *logger.Logger
} }
func defaultCfg() *cfg { func defaultCfg() *cfg {
return &cfg{ return &cfg{
info: Info{
Permission: os.ModePerm, // 0777
},
log: zap.L(), log: zap.L(),
} }
} }
// NewDB creates, initializes and returns DB instance. // New creates and returns new Metabase instance.
func NewDB(opts ...Option) *DB { func New(opts ...Option) *DB {
c := defaultCfg() c := defaultCfg()
for i := range opts { for i := range opts {
@ -41,43 +52,51 @@ func NewDB(opts ...Option) *DB {
} }
return &DB{ return &DB{
info: Info{
Path: c.boltDB.Path(),
},
cfg: c, cfg: c,
matchers: map[object.SearchMatchType]func(string, string, string) bool{ matchers: map[object.SearchMatchType]func(string, []byte, string) bool{
object.MatchUnknown: unknownMatcher, object.MatchUnknown: unknownMatcher,
object.MatchStringEqual: stringEqualMatcher, object.MatchStringEqual: stringEqualMatcher,
}, },
} }
} }
func (db *DB) Close() error { func stringEqualMatcher(key string, objVal []byte, filterVal string) bool {
return db.boltDB.Close()
}
func stringEqualMatcher(key, objVal, filterVal string) bool {
switch key { switch key {
default: default:
return objVal == filterVal return string(objVal) == filterVal
case v2object.FilterPropertyPhy, v2object.FilterPropertyRoot: case v2object.FilterHeaderPayloadHash, v2object.FilterHeaderHomomorphicHash:
return true return hex.EncodeToString(objVal) == filterVal
case v2object.FilterHeaderCreationEpoch, v2object.FilterHeaderPayloadLength:
return strconv.FormatUint(binary.LittleEndian.Uint64(objVal), 10) == filterVal
} }
} }
func unknownMatcher(key, _, _ string) bool { func unknownMatcher(_ string, _ []byte, _ string) bool {
switch key { return false
default:
return false
case v2object.FilterPropertyPhy, v2object.FilterPropertyRoot:
return true
}
} }
// FromBoltDB returns option to construct DB from BoltDB instance. // bucketKeyHelper returns byte representation of val that is used as a key
func FromBoltDB(db *bbolt.DB) Option { // in boltDB. Useful for getting filter values from unique and list indexes.
return func(c *cfg) { func bucketKeyHelper(hdr string, val string) []byte {
c.boltDB = db switch hdr {
case v2object.FilterHeaderPayloadHash:
v, err := hex.DecodeString(val)
if err != nil {
return nil
}
return v
case v2object.FilterHeaderSplitID:
s := object.NewSplitID()
err := s.Parse(val)
if err != nil {
return nil
}
return s.ToV2()
default:
return []byte(val)
} }
} }
@ -87,3 +106,25 @@ func WithLogger(l *logger.Logger) Option {
c.log = l c.log = l
} }
} }
// WithBoltDBOptions returns option to specify BoltDB options.
func WithBoltDBOptions(opts *bbolt.Options) Option {
return func(c *cfg) {
c.boltOptions = opts
}
}
// WithPath returns option to set system path to Metabase.
func WithPath(path string) Option {
return func(c *cfg) {
c.info.Path = path
}
}
// WithPermissions returns option to specify permission bits
// of Metabase system path.
func WithPermissions(perm os.FileMode) Option {
return func(c *cfg) {
c.info.Permission = perm
}
}

View file

@ -1,4 +1,4 @@
package meta package meta_test
import ( import (
"crypto/rand" "crypto/rand"
@ -11,13 +11,13 @@ import (
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object" objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-api-go/pkg/owner" "github.com/nspcc-dev/neofs-api-go/pkg/owner"
"github.com/nspcc-dev/neofs-node/pkg/core/object" "github.com/nspcc-dev/neofs-node/pkg/core/object"
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase"
"github.com/nspcc-dev/neofs-node/pkg/util/test" "github.com/nspcc-dev/neofs-node/pkg/util/test"
"github.com/pkg/errors" "github.com/nspcc-dev/tzhash/tz"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.etcd.io/bbolt"
) )
func testSelect(t *testing.T, db *DB, fs objectSDK.SearchFilters, exp ...*objectSDK.Address) { func testSelect(t *testing.T, db *meta.DB, fs objectSDK.SearchFilters, exp ...*objectSDK.Address) {
res, err := db.Select(fs) res, err := db.Select(fs)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, res, len(exp)) require.Len(t, res, len(exp))
@ -29,7 +29,7 @@ func testSelect(t *testing.T, db *DB, fs objectSDK.SearchFilters, exp ...*object
func testCID() *container.ID { func testCID() *container.ID {
cs := [sha256.Size]byte{} cs := [sha256.Size]byte{}
rand.Read(cs[:]) _, _ = rand.Read(cs[:])
id := container.NewID() id := container.NewID()
id.SetSHA256(cs) id.SetSHA256(cs)
@ -39,7 +39,7 @@ func testCID() *container.ID {
func testOID() *objectSDK.ID { func testOID() *objectSDK.ID {
cs := [sha256.Size]byte{} cs := [sha256.Size]byte{}
rand.Read(cs[:]) _, _ = rand.Read(cs[:])
id := objectSDK.NewID() id := objectSDK.NewID()
id.SetSHA256(cs) id.SetSHA256(cs)
@ -47,221 +47,67 @@ func testOID() *objectSDK.ID {
return id return id
} }
func TestDB(t *testing.T) { func newDB(t testing.TB) *meta.DB {
path := t.Name()
bdb := meta.New(meta.WithPath(path), meta.WithPermissions(0600))
require.NoError(t, bdb.Open())
return bdb
}
func releaseDB(db *meta.DB) {
db.Close()
os.Remove(db.DumpInfo().Path)
}
func generateRawObject(t *testing.T) *object.RawObject {
return generateRawObjectWithCID(t, testCID())
}
func generateRawObjectWithCID(t *testing.T, cid *container.ID) *object.RawObject {
version := pkg.NewVersion() version := pkg.NewVersion()
version.SetMajor(2) version.SetMajor(2)
version.SetMinor(1) version.SetMinor(1)
cid := testCID()
w, err := owner.NEO3WalletFromPublicKey(&test.DecodeKey(-1).PublicKey) w, err := owner.NEO3WalletFromPublicKey(&test.DecodeKey(-1).PublicKey)
require.NoError(t, err) require.NoError(t, err)
ownerID := owner.NewID() ownerID := owner.NewID()
ownerID.SetNeo3Wallet(w) ownerID.SetNeo3Wallet(w)
oid := testOID() csum := new(pkg.Checksum)
csum.SetSHA256(sha256.Sum256(w.Bytes()))
csumTZ := new(pkg.Checksum)
csumTZ.SetTillichZemor(tz.Sum(csum.Sum()))
obj := object.NewRaw() obj := object.NewRaw()
obj.SetID(oid) obj.SetID(testOID())
obj.SetOwnerID(ownerID) obj.SetOwnerID(ownerID)
obj.SetContainerID(cid) obj.SetContainerID(cid)
obj.SetVersion(version) obj.SetVersion(version)
obj.SetPayloadChecksum(csum)
obj.SetPayloadHomomorphicHash(csumTZ)
k, v := "key", "value" return obj
a := objectSDK.NewAttribute()
a.SetKey(k)
a.SetValue(v)
obj.SetAttributes(a)
db := newDB(t)
defer releaseDB(db)
o := obj.Object()
require.NoError(t, db.Put(o))
o2, err := db.Get(o.Address())
require.NoError(t, err)
require.Equal(t, o, o2)
fs := objectSDK.SearchFilters{}
// filter container ID
fs.AddObjectContainerIDFilter(objectSDK.MatchStringEqual, cid)
testSelect(t, db, fs, o.Address())
// filter owner ID
fs.AddObjectOwnerIDFilter(objectSDK.MatchStringEqual, ownerID)
testSelect(t, db, fs, o.Address())
// filter attribute
fs.AddFilter(k, v, objectSDK.MatchStringEqual)
testSelect(t, db, fs, o.Address())
// filter mismatch
fs.AddFilter(k, v+"1", objectSDK.MatchStringEqual)
testSelect(t, db, fs)
} }
func TestDB_Delete(t *testing.T) { func generateAddress() *objectSDK.Address {
db := newDB(t) addr := objectSDK.NewAddress()
addr.SetContainerID(testCID())
addr.SetObjectID(testOID())
defer releaseDB(db) return addr
obj := object.NewRaw()
obj.SetContainerID(testCID())
obj.SetID(testOID())
o := obj.Object()
require.NoError(t, db.Put(o))
addr := o.Address()
_, err := db.Get(addr)
require.NoError(t, err)
fs := objectSDK.SearchFilters{}
fs.AddObjectContainerIDFilter(objectSDK.MatchStringEqual, o.ContainerID())
testSelect(t, db, fs, o.Address())
require.NoError(t, db.Delete(addr))
_, err = db.Get(addr)
require.Error(t, err)
testSelect(t, db, fs)
} }
func TestDB_SelectProperties(t *testing.T) { func addAttribute(obj *object.RawObject, key, val string) {
db := newDB(t) attr := objectSDK.NewAttribute()
attr.SetKey(key)
attr.SetValue(val)
defer releaseDB(db) attrs := obj.Attributes()
attrs = append(attrs, attr)
parent := object.NewRaw() obj.SetAttributes(attrs...)
parent.SetContainerID(testCID())
parent.SetID(testOID())
child := object.NewRaw()
child.SetContainerID(testCID())
child.SetID(testOID())
child.SetParent(parent.Object().SDK())
parAddr := parent.Object().Address()
childAddr := child.Object().Address()
require.NoError(t, db.Put(child.Object()))
// root filter
fs := objectSDK.SearchFilters{}
fs.AddRootFilter()
testSelect(t, db, fs, parAddr)
// phy filter
fs = fs[:0]
fs.AddPhyFilter()
testSelect(t, db, fs, childAddr)
lnk := object.NewRaw()
lnk.SetContainerID(testCID())
lnk.SetID(testOID())
lnk.SetChildren(testOID())
require.NoError(t, db.Put(lnk.Object()))
}
func TestDB_Path(t *testing.T) {
path := t.Name()
bdb, err := bbolt.Open(path, 0600, nil)
require.NoError(t, err)
db := NewDB(FromBoltDB(bdb))
defer releaseDB(db)
require.Equal(t, path, db.DumpInfo().Path)
}
func newDB(t testing.TB) *DB {
path := t.Name()
bdb, err := bbolt.Open(path, 0600, nil)
require.NoError(t, err)
return NewDB(FromBoltDB(bdb))
}
func releaseDB(db *DB) {
db.Close()
os.Remove(db.DumpInfo().Path)
}
func TestSelectNonExistentAttributes(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
obj := object.NewRaw()
obj.SetID(testOID())
obj.SetContainerID(testCID())
require.NoError(t, db.Put(obj.Object()))
fs := objectSDK.SearchFilters{}
// add filter by non-existent attribute
fs.AddFilter("key", "value", objectSDK.MatchStringEqual)
res, err := db.Select(fs)
require.NoError(t, err)
require.Empty(t, res)
}
func TestVirtualObject(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
// create object with parent
obj := generateObject(t, testPrm{
withParent: true,
})
require.NoError(t, db.Put(obj))
childAddr := obj.Address()
parAddr := obj.GetParent().Address()
// child object must be readable
_, err := db.Get(childAddr)
require.NoError(t, err)
// parent object must not be readable
_, err = db.Get(parAddr)
require.True(t, errors.Is(err, object.ErrNotFound))
fs := objectSDK.SearchFilters{}
// both objects should appear in selection
testSelect(t, db, fs, childAddr, parAddr)
// filter leaves
fs.AddPhyFilter()
// only child object should appear
testSelect(t, db, fs, childAddr)
fs = fs[:0]
// filter non-leaf objects
fs.AddRootFilter()
// only parent object should appear
testSelect(t, db, fs, parAddr)
} }

View file

@ -1,47 +1,217 @@
package meta package meta
import ( import (
"github.com/nspcc-dev/neofs-api-go/pkg/object" "bytes"
"github.com/pkg/errors" "errors"
"fmt"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"go.etcd.io/bbolt" "go.etcd.io/bbolt"
"go.uber.org/zap"
) )
var tombstoneBucket = []byte("tombstones") var ErrVirtualObject = errors.New("do not remove virtual object directly")
// Delete marks object as deleted.
func (db *DB) Delete(addr *object.Address) error {
return db.delete(addr)
}
func objectRemoved(tx *bbolt.Tx, addr []byte) bool {
tombstoneBucket := tx.Bucket(tombstoneBucket)
return tombstoneBucket != nil && tombstoneBucket.Get(addr) != nil
}
// DeleteObjects marks list of objects as deleted. // DeleteObjects marks list of objects as deleted.
func (db *DB) DeleteObjects(list ...*object.Address) { func (db *DB) Delete(lst ...*objectSDK.Address) error {
if err := db.delete(list...); err != nil {
db.log.Error("could not delete object list",
zap.String("error", err.Error()),
)
}
}
func (db *DB) delete(list ...*object.Address) error {
return db.boltDB.Update(func(tx *bbolt.Tx) error { return db.boltDB.Update(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(tombstoneBucket) for i := range lst {
if err != nil { err := db.delete(tx, lst[i], false)
return errors.Wrapf(err, "(%T) could not create tombstone bucket", db) if err != nil {
} return err // maybe log and continue?
for i := range list {
if err := bucket.Put(addressKey(list[i]), nil); err != nil {
return errors.Wrapf(err, "(%T) could not put to tombstone bucket", db)
} }
} }
return nil return nil
}) })
} }
func (db *DB) delete(tx *bbolt.Tx, addr *objectSDK.Address, isParent bool) error {
pl := parentLength(tx, addr) // parentLength of address, for virtual objects it is > 0
// do not remove virtual objects directly
if !isParent && pl > 0 {
return ErrVirtualObject
}
// unmarshal object
obj, err := db.get(tx, addr, false)
if err != nil {
return err
}
// if object is an only link to a parent, then remove parent
if parent := obj.GetParent(); parent != nil {
if parentLength(tx, parent.Address()) == 1 {
err = db.deleteObject(tx, obj.GetParent(), true)
if err != nil {
return err
}
}
}
// remove object
return db.deleteObject(tx, obj, isParent)
}
func (db *DB) deleteObject(
tx *bbolt.Tx,
obj *object.Object,
isParent bool,
) error {
uniqueIndexes, err := delUniqueIndexes(obj, isParent)
if err != nil {
return fmt.Errorf("can' build unique indexes: %w", err)
}
// delete unique indexes
for i := range uniqueIndexes {
delUniqueIndexItem(tx, uniqueIndexes[i])
}
// build list indexes
listIndexes, err := listIndexes(obj)
if err != nil {
return fmt.Errorf("can' build list indexes: %w", err)
}
// delete list indexes
for i := range listIndexes {
delListIndexItem(tx, listIndexes[i])
}
// build fake bucket tree indexes
fkbtIndexes, err := fkbtIndexes(obj)
if err != nil {
return fmt.Errorf("can' build fake bucket tree indexes: %w", err)
}
// delete fkbt indexes
for i := range fkbtIndexes {
delFKBTIndexItem(tx, fkbtIndexes[i])
}
return nil
}
// parentLength returns amount of available children from parentid index.
func parentLength(tx *bbolt.Tx, addr *objectSDK.Address) int {
bkt := tx.Bucket(parentBucketName(addr.ContainerID()))
if bkt == nil {
return 0
}
lst, err := decodeList(bkt.Get(objectKey(addr.ObjectID())))
if err != nil {
return 0
}
return len(lst)
}
func delUniqueIndexItem(tx *bbolt.Tx, item namedBucketItem) {
bkt := tx.Bucket(item.name)
if bkt != nil {
_ = bkt.Delete(item.key) // ignore error, best effort there
}
}
func delFKBTIndexItem(tx *bbolt.Tx, item namedBucketItem) {
bkt := tx.Bucket(item.name)
if bkt == nil {
return
}
fkbtRoot := bkt.Bucket(item.key)
if fkbtRoot == nil {
return
}
_ = fkbtRoot.Delete(item.val) // ignore error, best effort there
}
func delListIndexItem(tx *bbolt.Tx, item namedBucketItem) {
bkt := tx.Bucket(item.name)
if bkt == nil {
return
}
lst, err := decodeList(bkt.Get(item.key))
if err != nil || len(lst) == 0 {
return
}
// remove element from the list
newLst := make([][]byte, 0, len(lst))
for i := range lst {
if !bytes.Equal(item.val, lst[i]) {
newLst = append(newLst, lst[i])
}
}
// if list empty, remove the key from <list> bucket
if len(newLst) == 0 {
_ = bkt.Delete(item.key) // ignore error, best effort there
return
}
// if list is not empty, then update it
encodedLst, err := encodeList(lst)
if err != nil {
return // ignore error, best effort there
}
_ = bkt.Put(item.key, encodedLst) // ignore error, best effort there
}
func delUniqueIndexes(obj *object.Object, isParent bool) ([]namedBucketItem, error) {
addr := obj.Address()
objKey := objectKey(addr.ObjectID())
addrKey := addressKey(addr)
result := make([]namedBucketItem, 0, 5)
// add value to primary unique bucket
if !isParent {
var bucketName []byte
switch obj.Type() {
case objectSDK.TypeRegular:
bucketName = primaryBucketName(addr.ContainerID())
case objectSDK.TypeTombstone:
bucketName = tombstoneBucketName(addr.ContainerID())
case objectSDK.TypeStorageGroup:
bucketName = storageGroupBucketName(addr.ContainerID())
default:
return nil, ErrUnknownObjectType
}
result = append(result, namedBucketItem{
name: bucketName,
key: objKey,
})
}
result = append(result,
namedBucketItem{ // remove from small blobovnicza id index
name: smallBucketName(addr.ContainerID()),
key: objKey,
},
namedBucketItem{ // remove from root index
name: rootBucketName(addr.ContainerID()),
key: objKey,
},
namedBucketItem{ // remove from graveyard index
name: graveyardBucketName,
key: addrKey,
},
namedBucketItem{ // remove from ToMoveIt index
name: toMoveItBucketName,
key: addrKey,
},
)
return result, nil
}

View file

@ -1,71 +1,60 @@
package meta package meta_test
import ( import (
"testing" "testing"
"github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func BenchmarkDB_Delete(b *testing.B) { func TestDB_Delete(t *testing.T) {
db := newDB(b)
defer releaseDB(db)
var existingAddr *object.Address
for i := 0; i < 10; i++ {
obj := generateObject(b, testPrm{})
existingAddr = obj.Address()
require.NoError(b, db.Put(obj))
}
b.Run("existing address", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := db.Delete(existingAddr)
b.StopTimer()
require.NoError(b, err)
b.StartTimer()
}
})
b.Run("non-existing address", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
addr := object.NewAddress()
addr.SetContainerID(testCID())
addr.SetObjectID(testOID())
b.StartTimer()
err := db.Delete(addr)
b.StopTimer()
require.NoError(b, err)
b.StartTimer()
}
})
}
func TestDB_DeleteObjects(t *testing.T) {
db := newDB(t) db := newDB(t)
defer releaseDB(db) defer releaseDB(db)
o1 := generateObject(t, testPrm{}) cid := testCID()
o2 := generateObject(t, testPrm{}) parent := generateRawObjectWithCID(t, cid)
addAttribute(parent, "foo", "bar")
require.NoError(t, db.Put(o1)) child := generateRawObjectWithCID(t, cid)
require.NoError(t, db.Put(o2)) child.SetParent(parent.Object().SDK())
child.SetParentID(parent.ID())
db.DeleteObjects(o1.Address(), o2.Address()) // put object with parent
err := db.Put(child.Object(), nil)
require.NoError(t, err)
testSelect(t, db, object.SearchFilters{}) // fill ToMoveIt index
err = db.ToMoveIt(child.Object().Address())
require.NoError(t, err)
// check if Movable list is not empty
l, err := db.Movable()
require.NoError(t, err)
require.Len(t, l, 1)
// inhume parent and child so they will be on graveyard
ts := generateRawObjectWithCID(t, cid)
err = db.Inhume(child.Object().Address(), ts.Object().Address())
require.NoError(t, err)
err = db.Inhume(child.Object().Address(), ts.Object().Address())
require.NoError(t, err)
// delete object
err = db.Delete(child.Object().Address())
require.NoError(t, err)
// check if there is no data in Movable index
l, err = db.Movable()
require.NoError(t, err)
require.Len(t, l, 0)
// check if they removed from graveyard
ok, err := db.Exists(child.Object().Address())
require.NoError(t, err)
require.False(t, ok)
ok, err = db.Exists(parent.Object().Address())
require.NoError(t, err)
require.False(t, ok)
} }

View file

@ -1,22 +1,88 @@
package meta package meta
import ( import (
"errors"
"fmt"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object" objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object" "github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/pkg/errors" "go.etcd.io/bbolt"
) )
// Exists checks if object is presented in metabase. var ErrLackSplitInfo = errors.New("no split info on parent object")
func (db *DB) Exists(addr *objectSDK.Address) (bool, error) {
// FIXME: temp solution, avoid direct Get usage
_, err := db.Get(addr)
if err != nil {
if errors.Is(err, object.ErrNotFound) {
return false, nil
}
return false, err // Exists returns ErrAlreadyRemoved if addr was marked as removed. Otherwise it
// returns true if addr is in primary index or false if it is not.
func (db *DB) Exists(addr *objectSDK.Address) (exists bool, err error) {
err = db.boltDB.View(func(tx *bbolt.Tx) error {
exists, err = db.exists(tx, addr)
return err
})
return exists, err
}
func (db *DB) exists(tx *bbolt.Tx, addr *objectSDK.Address) (exists bool, err error) {
// check graveyard first
if inGraveyard(tx, addr) {
return false, object.ErrAlreadyRemoved
} }
return true, nil objKey := objectKey(addr.ObjectID())
// if graveyard is empty, then check if object exists in primary bucket
if inBucket(tx, primaryBucketName(addr.ContainerID()), objKey) {
return true, nil
}
// if primary bucket is empty, then check if object exists in parent bucket
if inBucket(tx, parentBucketName(addr.ContainerID()), objKey) {
rawSplitInfo := getFromBucket(tx, rootBucketName(addr.ContainerID()), objKey)
if len(rawSplitInfo) == 0 {
return false, ErrLackSplitInfo
}
splitInfo := objectSDK.NewSplitInfo()
err := splitInfo.Unmarshal(rawSplitInfo)
if err != nil {
return false, fmt.Errorf("can't unmarshal split info from root index: %w", err)
}
return false, objectSDK.NewSplitInfoError(splitInfo)
}
// if parent bucket is empty, then check if object exists in tombstone bucket
if inBucket(tx, tombstoneBucketName(addr.ContainerID()), objKey) {
return true, nil
}
// if parent bucket is empty, then check if object exists in storage group bucket
return inBucket(tx, storageGroupBucketName(addr.ContainerID()), objKey), nil
}
// inGraveyard returns true if object was marked as removed.
func inGraveyard(tx *bbolt.Tx, addr *objectSDK.Address) bool {
graveyard := tx.Bucket(graveyardBucketName)
if graveyard == nil {
return false
}
tombstone := graveyard.Get(addressKey(addr))
return len(tombstone) != 0
}
// inBucket checks if key <key> is present in bucket <name>.
func inBucket(tx *bbolt.Tx, name, key []byte) bool {
bkt := tx.Bucket(name)
if bkt == nil {
return false
}
// using `get` as `exists`: https://github.com/boltdb/bolt/issues/321
val := bkt.Get(key)
return len(val) != 0
} }

View file

@ -1,43 +1,96 @@
package meta package meta
import ( import (
"fmt"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object" objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object" "github.com/nspcc-dev/neofs-node/pkg/core/object"
"go.etcd.io/bbolt" "go.etcd.io/bbolt"
) )
// Get returns object header for specified address. // Get returns object header for specified address.
func (db *DB) Get(addr *objectSDK.Address) (*object.Object, error) { func (db *DB) Get(addr *objectSDK.Address) (obj *object.Object, err error) {
var obj *object.Object err = db.boltDB.View(func(tx *bbolt.Tx) error {
obj, err = db.get(tx, addr, true)
if err := db.boltDB.View(func(tx *bbolt.Tx) error {
addrKey := addressKey(addr)
// check if object marked as deleted
if objectRemoved(tx, addrKey) {
return object.ErrNotFound
}
primaryBucket := tx.Bucket(primaryBucket)
if primaryBucket == nil {
return object.ErrNotFound
}
data := primaryBucket.Get(addrKey)
if data == nil {
return object.ErrNotFound
}
var err error
obj = object.New()
err = obj.Unmarshal(data)
return err return err
}); err != nil { })
return obj, err
}
func (db *DB) get(tx *bbolt.Tx, addr *objectSDK.Address, checkGraveyard bool) (*object.Object, error) {
obj := object.New()
key := objectKey(addr.ObjectID())
cid := addr.ContainerID()
if checkGraveyard && inGraveyard(tx, addr) {
return nil, object.ErrAlreadyRemoved
}
// check in primary index
data := getFromBucket(tx, primaryBucketName(cid), key)
if len(data) != 0 {
return obj, obj.Unmarshal(data)
}
// if not found then check in tombstone index
data = getFromBucket(tx, tombstoneBucketName(cid), key)
if len(data) != 0 {
return obj, obj.Unmarshal(data)
}
// if not found then check in storage group index
data = getFromBucket(tx, storageGroupBucketName(cid), key)
if len(data) != 0 {
return obj, obj.Unmarshal(data)
}
// if not found then check if object is a virtual
return getVirtualObject(tx, cid, key)
}
func getFromBucket(tx *bbolt.Tx, name, key []byte) []byte {
bkt := tx.Bucket(name)
if bkt == nil {
return nil
}
return bkt.Get(key)
}
func getVirtualObject(tx *bbolt.Tx, cid *container.ID, key []byte) (*object.Object, error) {
parentBucket := tx.Bucket(parentBucketName(cid))
if parentBucket == nil {
return nil, object.ErrNotFound
}
relativeLst, err := decodeList(parentBucket.Get(key))
if err != nil {
return nil, err return nil, err
} }
return obj, nil if len(relativeLst) == 0 { // this should never happen though
return nil, object.ErrNotFound
}
// pick last item, for now there is not difference which address to pick
// but later list might be sorted so first or last value can be more
// prioritized to choose
virtualOID := relativeLst[len(relativeLst)-1]
data := getFromBucket(tx, primaryBucketName(cid), virtualOID)
child := object.New()
err = child.Unmarshal(data)
if err != nil {
return nil, fmt.Errorf("can't unmarshal child with parent: %w", err)
}
if child.GetParent() == nil { // this should never happen though
return nil, object.ErrNotFound
}
return child.GetParent(), nil
} }

View file

@ -1,56 +1,96 @@
package meta package meta_test
import ( import (
"bytes"
"testing" "testing"
"github.com/nspcc-dev/neofs-api-go/pkg/object" objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func BenchmarkDB_Get(b *testing.B) { func TestDB_Get(t *testing.T) {
db := newDB(b) db := newDB(t)
defer releaseDB(db) defer releaseDB(db)
var existingAddr *object.Address raw := generateRawObject(t)
for i := 0; i < 10; i++ { // equal fails on diff of <nil> attributes and <{}> attributes,
obj := generateObject(b, testPrm{}) /* so we make non empty attribute slice in parent*/
addAttribute(raw, "foo", "bar")
existingAddr = obj.Address() t.Run("object not found", func(t *testing.T) {
_, err := db.Get(raw.Object().Address())
require.NoError(b, db.Put(obj)) require.Error(t, err)
}
b.Run("existing address", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := db.Get(existingAddr)
b.StopTimer()
require.NoError(b, err)
b.StartTimer()
}
}) })
b.Run("non-existing address", func(b *testing.B) { t.Run("put regular object", func(t *testing.T) {
b.ReportAllocs() err := db.Put(raw.Object(), nil)
b.ResetTimer() require.NoError(t, err)
for i := 0; i < b.N; i++ { newObj, err := db.Get(raw.Object().Address())
b.StopTimer() require.NoError(t, err)
addr := object.NewAddress() require.Equal(t, raw.Object(), newObj)
addr.SetContainerID(testCID()) })
addr.SetObjectID(testOID())
b.StartTimer()
_, err := db.Get(addr) t.Run("put tombstone object", func(t *testing.T) {
raw.SetType(objectSDK.TypeTombstone)
raw.SetID(testOID())
b.StopTimer() err := db.Put(raw.Object(), nil)
require.Error(b, err) require.NoError(t, err)
b.StartTimer()
} newObj, err := db.Get(raw.Object().Address())
require.NoError(t, err)
require.Equal(t, raw.Object(), newObj)
})
t.Run("put storage group object", func(t *testing.T) {
raw.SetType(objectSDK.TypeStorageGroup)
raw.SetID(testOID())
err := db.Put(raw.Object(), nil)
require.NoError(t, err)
newObj, err := db.Get(raw.Object().Address())
require.NoError(t, err)
require.Equal(t, raw.Object(), newObj)
})
t.Run("put virtual object", func(t *testing.T) {
cid := testCID()
parent := generateRawObjectWithCID(t, cid)
addAttribute(parent, "foo", "bar")
child := generateRawObjectWithCID(t, cid)
child.SetParent(parent.Object().SDK())
child.SetParentID(parent.ID())
err := db.Put(child.Object(), nil)
require.NoError(t, err)
newParent, err := db.Get(parent.Object().Address())
require.NoError(t, err)
require.True(t, binaryEqual(parent.Object(), newParent))
newChild, err := db.Get(child.Object().Address())
require.NoError(t, err)
require.True(t, binaryEqual(child.Object(), newChild))
}) })
} }
// binary equal is used when object contains empty lists in the structure and
// requre.Equal fails on comparing <nil> and []{} lists.
func binaryEqual(a, b *object.Object) bool {
binaryA, err := a.Marshal()
if err != nil {
return false
}
binaryB, err := b.Marshal()
if err != nil {
return false
}
return bytes.Equal(binaryA, binaryB)
}

View file

@ -1,9 +1,16 @@
package meta package meta
import (
"os"
)
// Info groups the information about DB. // Info groups the information about DB.
type Info struct { type Info struct {
// Full path to the metabase. // Full path to the metabase.
Path string Path string
// Permission of database file.
Permission os.FileMode
} }
// DumpInfo returns information about the DB. // DumpInfo returns information about the DB.

View file

@ -1,145 +1,424 @@
package meta package meta
import ( import (
"bytes"
"encoding/gob"
"errors"
"fmt"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object" objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object" "github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/pkg/errors" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobovnicza"
"go.etcd.io/bbolt" "go.etcd.io/bbolt"
) )
type bucketItem struct { type (
key, val string namedBucketItem struct {
} name, key, val []byte
}
var (
primaryBucket = []byte("objects")
indexBucket = []byte("index")
) )
// Put saves object in DB. var (
// ErrUnknownObjectType = errors.New("unknown object type")
// Object payload expected to be cut. ErrIncorrectBlobovniczaUpdate = errors.New("updating blobovnicza id on object without it")
func (db *DB) Put(obj *object.Object) error { ErrIncorrectSplitInfoUpdate = errors.New("updating split info on object without it")
ErrIncorrectRootObject = errors.New("invalid root object")
)
// Put saves object header in metabase. Object payload expected to be cut.
// Big objects have nil blobovniczaID.
func (db *DB) Put(obj *object.Object, id *blobovnicza.ID) error {
return db.boltDB.Update(func(tx *bbolt.Tx) error { return db.boltDB.Update(func(tx *bbolt.Tx) error {
par := false return db.put(tx, obj, id, nil)
for ; obj != nil; obj, par = obj.GetParent(), true {
// create primary bucket (addr: header)
primaryBucket, err := tx.CreateBucketIfNotExists(primaryBucket)
if err != nil {
return errors.Wrapf(err, "(%T) could not create primary bucket", db)
}
data, err := obj.Marshal()
if err != nil {
return errors.Wrapf(err, "(%T) could not marshal the object", db)
}
addrKey := addressKey(obj.Address())
if !par {
// put header to primary bucket
if err := primaryBucket.Put(addrKey, data); err != nil {
return errors.Wrapf(err, "(%T) could not put item to primary bucket", db)
}
}
// create bucket for indices
indexBucket, err := tx.CreateBucketIfNotExists(indexBucket)
if err != nil {
return errors.Wrapf(err, "(%T) could not create index bucket", db)
}
// calculate indexed values for object
indices := objectIndices(obj, par)
for i := range indices {
// create index bucket
keyBucket, err := indexBucket.CreateBucketIfNotExists([]byte(indices[i].key))
if err != nil {
return errors.Wrapf(err, "(%T) could not create bucket for header key", db)
}
v := []byte(indices[i].val)
// create address bucket for the value
valBucket, err := keyBucket.CreateBucketIfNotExists(nonEmptyKeyBytes(v))
if err != nil {
return errors.Wrapf(err, "(%T) could not create bucket for header value", db)
}
// put object address to value bucket
if err := valBucket.Put(addrKey, nil); err != nil {
return errors.Wrapf(err, "(%T) could not put item to header bucket", db)
}
}
}
return nil
}) })
} }
func nonEmptyKeyBytes(key []byte) []byte { func (db *DB) put(tx *bbolt.Tx, obj *object.Object, id *blobovnicza.ID, si *objectSDK.SplitInfo) error {
return append([]byte{0}, key...) isParent := si != nil
exists, err := db.exists(tx, obj.Address())
if errors.As(err, &splitInfoError) {
exists = true // object exists, however it is virtual
} else if err != nil {
return err // return any error besides SplitInfoError
}
// most right child and split header overlap parent so we have to
// check if object exists to not overwrite it twice
if exists {
// when storage engine moves small objects from one blobovniczaID
// to another, then it calls metabase.Put method with new blobovniczaID
// and this code should be triggered
if !isParent && id != nil {
return updateBlobovniczaID(tx, obj.Address(), id)
}
// when storage already has last object in split hierarchy and there is
// a linking object to put (or vice versa), we should update split info
// with object ids of these objects
if isParent {
return updateSplitInfo(tx, obj.Address(), si)
}
return nil
}
if obj.GetParent() != nil && !isParent { // limit depth by two
parentSI, err := splitInfoFromObject(obj)
if err != nil {
return err
}
err = db.put(tx, obj.GetParent(), id, parentSI)
if err != nil {
return err
}
}
// build unique indexes
uniqueIndexes, err := uniqueIndexes(obj, si, id)
if err != nil {
return fmt.Errorf("can' build unique indexes: %w", err)
}
// put unique indexes
for i := range uniqueIndexes {
err := putUniqueIndexItem(tx, uniqueIndexes[i])
if err != nil {
return err
}
}
// build list indexes
listIndexes, err := listIndexes(obj)
if err != nil {
return fmt.Errorf("can' build list indexes: %w", err)
}
// put list indexes
for i := range listIndexes {
err := putListIndexItem(tx, listIndexes[i])
if err != nil {
return err
}
}
// build fake bucket tree indexes
fkbtIndexes, err := fkbtIndexes(obj)
if err != nil {
return fmt.Errorf("can' build fake bucket tree indexes: %w", err)
}
// put fake bucket tree indexes
for i := range fkbtIndexes {
err := putFKBTIndexItem(tx, fkbtIndexes[i])
if err != nil {
return err
}
}
return nil
} }
func cutKeyBytes(key []byte) []byte { // builds list of <unique> indexes from the object.
return key[1:] func uniqueIndexes(obj *object.Object, si *objectSDK.SplitInfo, id *blobovnicza.ID) ([]namedBucketItem, error) {
} isParent := si != nil
addr := obj.Address()
objKey := objectKey(addr.ObjectID())
result := make([]namedBucketItem, 0, 3)
func addressKey(addr *objectSDK.Address) []byte { // add value to primary unique bucket
return []byte(addr.String()) if !isParent {
} var bucketName []byte
func objectIndices(obj *object.Object, parent bool) []bucketItem { switch obj.Type() {
as := obj.Attributes() case objectSDK.TypeRegular:
bucketName = primaryBucketName(addr.ContainerID())
case objectSDK.TypeTombstone:
bucketName = tombstoneBucketName(addr.ContainerID())
case objectSDK.TypeStorageGroup:
bucketName = storageGroupBucketName(addr.ContainerID())
default:
return nil, ErrUnknownObjectType
}
res := make([]bucketItem, 0, 6+len(as)) // 6 predefined buckets and object attributes rawObject, err := obj.Marshal()
if err != nil {
return nil, fmt.Errorf("can't marshal object header: %w", err)
}
res = append(res, result = append(result, namedBucketItem{
bucketItem{ name: bucketName,
key: v2object.FilterHeaderVersion, key: objKey,
val: obj.Version().String(), val: rawObject,
}, })
bucketItem{
key: v2object.FilterHeaderContainerID,
val: obj.ContainerID().String(),
},
bucketItem{
key: v2object.FilterHeaderOwnerID,
val: obj.OwnerID().String(),
},
bucketItem{
key: v2object.FilterHeaderParent,
val: obj.ParentID().String(),
},
bucketItem{
key: v2object.FilterHeaderObjectID,
val: obj.ID().String(),
},
// TODO: add remaining fields after neofs-api#72
)
// index blobovniczaID if it is present
if id != nil {
result = append(result, namedBucketItem{
name: smallBucketName(addr.ContainerID()),
key: objKey,
val: *id,
})
}
}
// index root object
if obj.Type() == objectSDK.TypeRegular && !obj.HasParent() { if obj.Type() == objectSDK.TypeRegular && !obj.HasParent() {
res = append(res, bucketItem{ var (
key: v2object.FilterPropertyRoot, err error
splitInfo []byte
)
if isParent {
splitInfo, err = si.Marshal()
if err != nil {
return nil, fmt.Errorf("can't marshal split info: %w", err)
}
}
result = append(result, namedBucketItem{
name: rootBucketName(addr.ContainerID()),
key: objKey,
val: splitInfo,
}) })
} }
if !parent { return result, nil
res = append(res, bucketItem{ }
key: v2object.FilterPropertyPhy,
}) // builds list of <list> indexes from the object.
} func listIndexes(obj *object.Object) ([]namedBucketItem, error) {
result := make([]namedBucketItem, 0, 3)
for _, a := range as { addr := obj.Address()
res = append(res, bucketItem{ objKey := objectKey(addr.ObjectID())
key: a.Key(),
val: a.Value(), // index payload hashes
}) result = append(result, namedBucketItem{
} name: payloadHashBucketName(addr.ContainerID()),
key: obj.PayloadChecksum().Sum(),
return res val: objKey,
})
// index parent ids
if obj.ParentID() != nil {
result = append(result, namedBucketItem{
name: parentBucketName(addr.ContainerID()),
key: objectKey(obj.ParentID()),
val: objKey,
})
}
// index split ids
if obj.SplitID() != nil {
result = append(result, namedBucketItem{
name: splitBucketName(addr.ContainerID()),
key: obj.SplitID().ToV2(),
val: objKey,
})
}
return result, nil
}
// builds list of <fake bucket tree> indexes from the object.
func fkbtIndexes(obj *object.Object) ([]namedBucketItem, error) {
addr := obj.Address()
objKey := []byte(addr.ObjectID().String())
attrs := obj.Attributes()
result := make([]namedBucketItem, 0, 1+len(attrs))
// owner
result = append(result, namedBucketItem{
name: ownerBucketName(addr.ContainerID()),
key: []byte(obj.OwnerID().String()),
val: objKey,
})
// user specified attributes
for i := range attrs {
result = append(result, namedBucketItem{
name: attributeBucketName(addr.ContainerID(), attrs[i].Key()),
key: []byte(attrs[i].Value()),
val: objKey,
})
}
return result, nil
}
func putUniqueIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
bkt, err := tx.CreateBucketIfNotExists(item.name)
if err != nil {
return fmt.Errorf("can't create index %v: %w", item.name, err)
}
return bkt.Put(item.key, item.val)
}
func putFKBTIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
bkt, err := tx.CreateBucketIfNotExists(item.name)
if err != nil {
return fmt.Errorf("can't create index %v: %w", item.name, err)
}
fkbtRoot, err := bkt.CreateBucketIfNotExists(item.key)
if err != nil {
return fmt.Errorf("can't create fake bucket tree index %v: %w", item.key, err)
}
return fkbtRoot.Put(item.val, zeroValue)
}
func putListIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
bkt, err := tx.CreateBucketIfNotExists(item.name)
if err != nil {
return fmt.Errorf("can't create index %v: %w", item.name, err)
}
lst, err := decodeList(bkt.Get(item.key))
if err != nil {
return fmt.Errorf("can't decode leaf list %v: %w", item.key, err)
}
lst = append(lst, item.val)
encodedLst, err := encodeList(lst)
if err != nil {
return fmt.Errorf("can't encode leaf list %v: %w", item.key, err)
}
return bkt.Put(item.key, encodedLst)
}
// encodeList decodes list of bytes into a single blog for list bucket indexes.
func encodeList(lst [][]byte) ([]byte, error) {
buf := bytes.NewBuffer(nil)
encoder := gob.NewEncoder(buf)
// consider using protobuf encoding instead of glob
if err := encoder.Encode(lst); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// decodeList decodes blob into the list of bytes from list bucket index.
func decodeList(data []byte) (lst [][]byte, err error) {
if len(data) == 0 {
return nil, nil
}
decoder := gob.NewDecoder(bytes.NewReader(data))
if err := decoder.Decode(&lst); err != nil {
return nil, err
}
return lst, nil
}
// updateBlobovniczaID for existing objects if they were moved from from
// one blobovnicza to another.
func updateBlobovniczaID(tx *bbolt.Tx, addr *objectSDK.Address, id *blobovnicza.ID) error {
bkt := tx.Bucket(smallBucketName(addr.ContainerID()))
if bkt == nil {
// if object exists, don't have blobovniczaID and we want to update it
// then ignore, this should never happen
return ErrIncorrectBlobovniczaUpdate
}
objectKey := objectKey(addr.ObjectID())
if len(bkt.Get(objectKey)) == 0 {
return ErrIncorrectBlobovniczaUpdate
}
return bkt.Put(objectKey, *id)
}
// updateSpliInfo for existing objects if storage filled with extra information
// about last object in split hierarchy or linking object.
func updateSplitInfo(tx *bbolt.Tx, addr *objectSDK.Address, from *objectSDK.SplitInfo) error {
bkt := tx.Bucket(rootBucketName(addr.ContainerID()))
if bkt == nil {
// if object doesn't exists and we want to update split info on it
// then ignore, this should never happen
return ErrIncorrectSplitInfoUpdate
}
objectKey := objectKey(addr.ObjectID())
rawSplitInfo := bkt.Get(objectKey)
if len(rawSplitInfo) == 0 {
return ErrIncorrectSplitInfoUpdate
}
to := objectSDK.NewSplitInfo()
err := to.Unmarshal(rawSplitInfo)
if err != nil {
return fmt.Errorf("can't unmarshal split info from root index: %w", err)
}
result := mergeSplitInfo(from, to)
rawSplitInfo, err = result.Marshal()
if err != nil {
return fmt.Errorf("can't marhsal merged split info: %w", err)
}
return bkt.Put(objectKey, rawSplitInfo)
}
// splitInfoFromObject returns split info based on last or linkin object.
// Otherwise returns nil, nil.
func splitInfoFromObject(obj *object.Object) (*objectSDK.SplitInfo, error) {
if obj.Parent() == nil {
return nil, nil
}
info := objectSDK.NewSplitInfo()
info.SetSplitID(obj.SplitID())
switch {
case isLinkObject(obj):
info.SetLink(obj.ID())
case isLastObject(obj):
info.SetLastPart(obj.ID())
default:
return nil, ErrIncorrectRootObject // should never happen
}
return info, nil
}
// mergeSplitInfo ignores conflicts and rewrites `to` with non empty values
// from `from`.
func mergeSplitInfo(from, to *objectSDK.SplitInfo) *objectSDK.SplitInfo {
to.SetSplitID(from.SplitID()) // overwrite SplitID and ignore conflicts
if lp := from.LastPart(); lp != nil {
to.SetLastPart(lp)
}
if link := from.Link(); link != nil {
to.SetLink(link)
}
return to
}
// isLinkObject returns true if object contains parent header and list
// of children.
func isLinkObject(obj *object.Object) bool {
return len(obj.Children()) > 0 && obj.Parent() != nil
}
// isLastObject returns true if object contains only parent header without list
// of children.
func isLastObject(obj *object.Object) bool {
return len(obj.Children()) == 0 && obj.Parent() != nil
} }

View file

@ -1,136 +1,48 @@
package meta package meta_test
import ( import (
"crypto/rand"
"crypto/sha256"
"fmt"
"testing" "testing"
"github.com/nspcc-dev/neofs-api-go/pkg" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobovnicza"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-api-go/pkg/owner"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/nspcc-dev/neofs-node/pkg/util/test"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type testPrm struct { func TestDB_PutBlobovnicaUpdate(t *testing.T) {
withParent bool db := newDB(t)
attrNum int
}
func (p testPrm) String() string {
return fmt.Sprintf("[with_parent:%t, attributes:%d]",
p.withParent,
p.attrNum,
)
}
func generateChecksum() *pkg.Checksum {
cs := pkg.NewChecksum()
sh := [sha256.Size]byte{}
rand.Read(sh[:])
cs.SetSHA256(sh)
return cs
}
func generateObject(t require.TestingT, prm testPrm) *object.Object {
obj := object.NewRaw()
obj.SetID(testOID())
obj.SetVersion(pkg.SDKVersion())
obj.SetContainerID(testCID())
obj.SetPayloadChecksum(generateChecksum())
obj.SetPayloadHomomorphicHash(generateChecksum())
obj.SetType(objectSDK.TypeRegular)
as := make([]*objectSDK.Attribute, 0, prm.attrNum)
for i := 0; i < prm.attrNum; i++ {
a := objectSDK.NewAttribute()
k := make([]byte, 32)
rand.Read(k)
a.SetKey(string(k))
v := make([]byte, 32)
rand.Read(v)
a.SetValue(string(v))
as = append(as, a)
}
obj.SetAttributes(as...)
wallet, err := owner.NEO3WalletFromPublicKey(&test.DecodeKey(-1).PublicKey)
require.NoError(t, err)
ownerID := owner.NewID()
ownerID.SetNeo3Wallet(wallet)
obj.SetOwnerID(ownerID)
if prm.withParent {
prm.withParent = false
obj.SetChildren(testOID())
obj.SetPreviousID(testOID())
obj.SetParentID(testOID())
obj.SetParent(generateObject(t, prm).SDK())
}
return obj.Object()
}
func BenchmarkDB_Put(b *testing.B) {
db := newDB(b)
defer releaseDB(db) defer releaseDB(db)
for _, prm := range []testPrm{ raw1 := generateRawObject(t)
{ blobovniczaID := blobovnicza.ID{1, 2, 3, 4}
withParent: false,
attrNum: 0,
},
{
withParent: true,
attrNum: 0,
},
{
withParent: false,
attrNum: 100,
},
{
withParent: true,
attrNum: 100,
},
{
withParent: false,
attrNum: 1000,
},
{
withParent: true,
attrNum: 1000,
},
} {
b.Run(prm.String(), func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ { // put one object with blobovniczaID
b.StopTimer() err := db.Put(raw1.Object(), &blobovniczaID)
obj := generateObject(b, prm) require.NoError(t, err)
b.StartTimer()
err := db.Put(obj) fetchedBlobovniczaID, err := db.IsSmall(raw1.Object().Address())
require.NoError(t, err)
require.Equal(t, &blobovniczaID, fetchedBlobovniczaID)
b.StopTimer() t.Run("update blobovniczaID", func(t *testing.T) {
require.NoError(b, err) newID := blobovnicza.ID{5, 6, 7, 8}
b.StartTimer()
} err := db.Put(raw1.Object(), &newID)
}) require.NoError(t, err)
}
fetchedBlobovniczaID, err := db.IsSmall(raw1.Object().Address())
require.NoError(t, err)
require.Equal(t, &newID, fetchedBlobovniczaID)
})
t.Run("update blobovniczaID on bad object", func(t *testing.T) {
raw2 := generateRawObject(t)
err := db.Put(raw2.Object(), nil)
require.NoError(t, err)
fetchedBlobovniczaID, err := db.IsSmall(raw2.Object().Address())
require.NoError(t, err)
require.Nil(t, fetchedBlobovniczaID)
err = db.Put(raw2.Object(), &blobovniczaID)
require.Error(t, err)
})
} }

View file

@ -1,87 +1,73 @@
package meta package meta
import ( import (
"encoding/binary"
"fmt"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
"github.com/nspcc-dev/neofs-api-go/pkg/object" "github.com/nspcc-dev/neofs-api-go/pkg/object"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object" v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.etcd.io/bbolt" "go.etcd.io/bbolt"
"go.uber.org/zap"
) )
type (
// filterGroup is a structure that have search filters grouped by access
// method. We have fast filters that looks for indexes and do not unmarshal
// objects, and slow filters, that applied after fast filters created
// smaller set of objects to check.
filterGroup struct {
cid *container.ID
fastFilters object.SearchFilters
slowFilters object.SearchFilters
}
)
var ErrContainerNotInQuery = errors.New("search query does not contain container id filter")
// Select returns list of addresses of objects that match search filters. // Select returns list of addresses of objects that match search filters.
func (db *DB) Select(fs object.SearchFilters) (res []*object.Address, err error) { func (db *DB) Select(fs object.SearchFilters) (res []*object.Address, err error) {
err = db.boltDB.View(func(tx *bbolt.Tx) error { err = db.boltDB.View(func(tx *bbolt.Tx) error {
res, err = db.selectObjects(tx, fs) res, err = db.selectObjects(tx, fs)
return err return err
}) })
return return res, err
} }
func (db *DB) selectObjects(tx *bbolt.Tx, fs object.SearchFilters) ([]*object.Address, error) { func (db *DB) selectObjects(tx *bbolt.Tx, fs object.SearchFilters) ([]*object.Address, error) {
if len(fs) == 0 { group, err := groupFilters(fs)
return db.selectAll(tx) if err != nil {
return nil, err
} }
// get indexed bucket if group.cid == nil {
indexBucket := tx.Bucket(indexBucket) return nil, ErrContainerNotInQuery
if indexBucket == nil {
// empty storage
return nil, nil
} }
// keep processed addresses // keep matched addresses in this cache
// value equal to number (index+1) of latest matched filter // value equal to number (index+1) of latest matched filter
mAddr := make(map[string]int) mAddr := make(map[string]int)
for fNum := range fs { expLen := len(group.fastFilters) // expected value of matched filters in mAddr
matchFunc, ok := db.matchers[fs[fNum].Operation()]
if !ok {
return nil, errors.Errorf("no function for matcher %v", fs[fNum].Operation())
}
key := fs[fNum].Header() if len(group.fastFilters) == 0 {
expLen = 1
// get bucket with values db.selectAll(tx, group.cid, mAddr)
keyBucket := indexBucket.Bucket([]byte(key)) } else {
if keyBucket == nil { for i := range group.fastFilters {
// no object has this attribute => empty result db.selectFastFilter(tx, group.cid, group.fastFilters[i], mAddr, i)
return nil, nil
}
fVal := fs[fNum].Value()
// iterate over all existing values for the key
if err := keyBucket.ForEach(func(k, v []byte) error {
include := matchFunc(key, string(cutKeyBytes(k)), fVal)
if include {
return keyBucket.Bucket(k).ForEach(func(k, _ []byte) error {
if num := mAddr[string(k)]; num == fNum {
// otherwise object does not match current or some previous filter
mAddr[string(k)] = fNum + 1
}
return nil
})
}
return nil
}); err != nil {
return nil, errors.Wrapf(err, "(%T) could not iterate bucket %s", db, key)
} }
} }
fLen := len(fs)
res := make([]*object.Address, 0, len(mAddr)) res := make([]*object.Address, 0, len(mAddr))
for a, ind := range mAddr { for a, ind := range mAddr {
if ind != fLen { if ind != expLen {
continue continue // ignore objects with unmatched fast filters
}
// check if object marked as deleted
if objectRemoved(tx, []byte(a)) {
continue
} }
addr := object.NewAddress() addr := object.NewAddress()
@ -90,60 +76,300 @@ func (db *DB) selectObjects(tx *bbolt.Tx, fs object.SearchFilters) ([]*object.Ad
return nil, err return nil, err
} }
if inGraveyard(tx, addr) {
continue // ignore removed objects
}
if !db.matchSlowFilters(tx, addr, group.slowFilters) {
continue // ignore objects with unmatched slow filters
}
res = append(res, addr) res = append(res, addr)
} }
return res, nil return res, nil
} }
func (db *DB) selectAll(tx *bbolt.Tx) ([]*object.Address, error) { // selectAll adds to resulting cache all available objects in metabase.
result := map[string]struct{}{} func (db *DB) selectAll(tx *bbolt.Tx, cid *container.ID, to map[string]int) {
prefix := cid.String() + "/"
primaryBucket := tx.Bucket(primaryBucket) selectAllFromBucket(tx, primaryBucketName(cid), prefix, to, 0)
indexBucket := tx.Bucket(indexBucket) selectAllFromBucket(tx, tombstoneBucketName(cid), prefix, to, 0)
selectAllFromBucket(tx, storageGroupBucketName(cid), prefix, to, 0)
selectAllFromBucket(tx, parentBucketName(cid), prefix, to, 0)
}
if primaryBucket == nil || indexBucket == nil { // selectAllFromBucket goes through all keys in bucket and adds them in a
return nil, nil // resulting cache. Keys should be stringed object ids.
func selectAllFromBucket(tx *bbolt.Tx, name []byte, prefix string, to map[string]int, fNum int) {
bkt := tx.Bucket(name)
if bkt == nil {
return
} }
if err := primaryBucket.ForEach(func(k, _ []byte) error { _ = bkt.ForEach(func(k, v []byte) error {
result[string(k)] = struct{}{} key := prefix + string(k) // consider using string builders from sync.Pool
markAddressInCache(to, fNum, key)
return nil return nil
}); err != nil { })
return nil, errors.Wrapf(err, "(%T) could not iterate primary bucket", db) }
}
// selectFastFilter makes fast optimized checks for well known buckets or
rootBucket := indexBucket.Bucket([]byte(v2object.FilterPropertyRoot)) // looking through user attribute buckets otherwise.
if rootBucket != nil { func (db *DB) selectFastFilter(
rootBucket = rootBucket.Bucket(nonEmptyKeyBytes(nil)) tx *bbolt.Tx,
} cid *container.ID, // container we search on
f object.SearchFilter, // fast filter
if rootBucket != nil { to map[string]int, // resulting cache
if err := rootBucket.ForEach(func(k, v []byte) error { fNum int, // index of filter
result[string(k)] = struct{}{} ) {
prefix := cid.String() + "/"
return nil
}); err != nil { switch f.Header() {
return nil, errors.Wrapf(err, "(%T) could not iterate root bucket", db) case v2object.FilterHeaderObjectID:
} db.selectObjectID(tx, f, prefix, to, fNum)
} case v2object.FilterHeaderOwnerID:
bucketName := ownerBucketName(cid)
list := make([]*object.Address, 0, len(result)) db.selectFromFKBT(tx, bucketName, f, prefix, to, fNum)
case v2object.FilterHeaderPayloadHash:
for k := range result { bucketName := payloadHashBucketName(cid)
// check if object marked as deleted db.selectFromList(tx, bucketName, f, prefix, to, fNum)
if objectRemoved(tx, []byte(k)) { case v2object.FilterHeaderObjectType:
continue var bucketName []byte
}
switch f.Value() { // do it better after https://github.com/nspcc-dev/neofs-api/issues/84
addr := object.NewAddress() case "Regular":
if err := addr.Parse(k); err != nil { bucketName = primaryBucketName(cid)
return nil, err // TODO: storage was broken, so we need to handle it
} selectAllFromBucket(tx, bucketName, prefix, to, fNum)
list = append(list, addr) bucketName = parentBucketName(cid)
} case "Tombstone":
bucketName = tombstoneBucketName(cid)
return list, nil case "StorageGroup":
bucketName = storageGroupBucketName(cid)
default:
db.log.Debug("unknown object type", zap.String("type", f.Value()))
return
}
selectAllFromBucket(tx, bucketName, prefix, to, fNum)
case v2object.FilterHeaderParent:
bucketName := parentBucketName(cid)
db.selectFromList(tx, bucketName, f, prefix, to, fNum)
case v2object.FilterHeaderSplitID:
bucketName := splitBucketName(cid)
db.selectFromList(tx, bucketName, f, prefix, to, fNum)
case v2object.FilterPropertyRoot:
selectAllFromBucket(tx, rootBucketName(cid), prefix, to, fNum)
case v2object.FilterPropertyPhy:
selectAllFromBucket(tx, primaryBucketName(cid), prefix, to, fNum)
selectAllFromBucket(tx, tombstoneBucketName(cid), prefix, to, fNum)
selectAllFromBucket(tx, storageGroupBucketName(cid), prefix, to, fNum)
default: // user attribute
bucketName := attributeBucketName(cid, f.Header())
db.selectFromFKBT(tx, bucketName, f, prefix, to, fNum)
}
}
// selectFromList looks into <fkbt> index to find list of addresses to add in
// resulting cache.
func (db *DB) selectFromFKBT(
tx *bbolt.Tx,
name []byte, // fkbt root bucket name
f object.SearchFilter, // filter for operation and value
prefix string, // prefix to create addr from oid in index
to map[string]int, // resulting cache
fNum int, // index of filter
) { //
matchFunc, ok := db.matchers[f.Operation()]
if !ok {
db.log.Debug("missing matcher", zap.Uint32("operation", uint32(f.Operation())))
return
}
fkbtRoot := tx.Bucket(name)
if fkbtRoot == nil {
return
}
err := fkbtRoot.ForEach(func(k, _ []byte) error {
if matchFunc(f.Header(), k, f.Value()) {
fkbtLeaf := fkbtRoot.Bucket(k)
if fkbtLeaf == nil {
return nil
}
return fkbtLeaf.ForEach(func(k, _ []byte) error {
addr := prefix + string(k)
markAddressInCache(to, fNum, addr)
return nil
})
}
return nil
})
if err != nil {
db.log.Debug("error in FKBT selection", zap.String("error", err.Error()))
}
}
// selectFromList looks into <list> index to find list of addresses to add in
// resulting cache.
func (db *DB) selectFromList(
tx *bbolt.Tx,
name []byte, // list root bucket name
f object.SearchFilter, // filter for operation and value
prefix string, // prefix to create addr from oid in index
to map[string]int, // resulting cache
fNum int, // index of filter
) { //
bkt := tx.Bucket(name)
if bkt == nil {
return
}
switch f.Operation() {
case object.MatchStringEqual:
default:
db.log.Debug("unknown operation", zap.Uint32("operation", uint32(f.Operation())))
return
}
// warning: it works only for MatchStringEQ, for NotEQ you should iterate over
// bkt and apply matchFunc, don't forget to implement this when it will be
// needed. Right now it is not efficient to iterate over bucket
// when there is only MatchStringEQ.
lst, err := decodeList(bkt.Get(bucketKeyHelper(f.Header(), f.Value())))
if err != nil {
db.log.Debug("can't decode list bucket leaf", zap.String("error", err.Error()))
}
for i := range lst {
addr := prefix + string(lst[i])
markAddressInCache(to, fNum, addr)
}
}
// selectObjectID processes objectID filter with in-place optimizations.
func (db *DB) selectObjectID(
tx *bbolt.Tx,
f object.SearchFilter,
prefix string,
to map[string]int, // resulting cache
fNum int, // index of filter
) {
switch f.Operation() {
case object.MatchStringEqual:
default:
db.log.Debug("unknown operation", zap.Uint32("operation", uint32(f.Operation())))
return
}
// warning: it is in-place optimization and works only for MatchStringEQ,
// for NotEQ you should iterate over bkt and apply matchFunc
addrStr := prefix + f.Value()
addr := object.NewAddress()
err := addr.Parse(addrStr)
if err != nil {
db.log.Debug("can't decode object id address",
zap.String("addr", addrStr),
zap.String("error", err.Error()))
return
}
ok, err := db.exists(tx, addr)
if (err == nil && ok) || errors.As(err, &splitInfoError) {
markAddressInCache(to, fNum, addrStr)
}
}
// matchSlowFilters return true if object header is matched by all slow filters.
func (db *DB) matchSlowFilters(tx *bbolt.Tx, addr *object.Address, f object.SearchFilters) bool {
if len(f) == 0 {
return true
}
obj, err := db.get(tx, addr, true)
if err != nil {
return false
}
for i := range f {
matchFunc, ok := db.matchers[f[i].Operation()]
if !ok {
return false
}
var data []byte
switch f[i].Header() {
case v2object.FilterHeaderVersion:
data = []byte(obj.Version().String())
case v2object.FilterHeaderHomomorphicHash:
data = obj.PayloadHomomorphicHash().Sum()
case v2object.FilterHeaderCreationEpoch:
data = make([]byte, 8)
binary.LittleEndian.PutUint64(data, obj.CreationEpoch())
case v2object.FilterHeaderPayloadLength:
data = make([]byte, 8)
binary.LittleEndian.PutUint64(data, obj.PayloadSize())
default:
continue // ignore unknown search attributes
}
if !matchFunc(f[i].Header(), data, f[i].Value()) {
return false
}
}
return true
}
// groupFilters divides filters in two groups: fast and slow. Fast filters
// processed by indexes and slow filters processed after by unmarshaling
// object headers.
func groupFilters(filters object.SearchFilters) (*filterGroup, error) {
res := &filterGroup{
fastFilters: make(object.SearchFilters, 0, len(filters)),
slowFilters: make(object.SearchFilters, 0, len(filters)),
}
for i := range filters {
switch filters[i].Header() {
case v2object.FilterHeaderContainerID:
res.cid = container.NewID()
err := res.cid.Parse(filters[i].Value())
if err != nil {
return nil, fmt.Errorf("can't parse container id: %w", err)
}
case // slow filters
v2object.FilterHeaderVersion,
v2object.FilterHeaderCreationEpoch,
v2object.FilterHeaderPayloadLength,
v2object.FilterHeaderHomomorphicHash:
res.slowFilters = append(res.slowFilters, filters[i])
default: // fast filters or user attributes if unknown
res.fastFilters = append(res.fastFilters, filters[i])
}
}
return res, nil
}
func markAddressInCache(cache map[string]int, fNum int, addr string) {
if num := cache[addr]; num == fNum {
cache[addr] = num + 1
}
} }

View file

@ -1,226 +1,409 @@
package meta package meta_test
import ( import (
"crypto/rand" "encoding/hex"
"testing" "testing"
"github.com/nspcc-dev/neofs-api-go/pkg"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object" objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object" v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func addNFilters(fs *objectSDK.SearchFilters, n int) { func TestDB_SelectUserAttributes(t *testing.T) {
for i := 0; i < n; i++ {
key := make([]byte, 32)
rand.Read(key)
val := make([]byte, 32)
rand.Read(val)
fs.AddFilter(string(key), string(val), objectSDK.MatchStringEqual)
}
}
func BenchmarkDB_Select(b *testing.B) {
db := newDB(b)
defer releaseDB(db)
for i := 0; i < 100; i++ {
obj := generateObject(b, testPrm{
withParent: true,
attrNum: 100,
})
require.NoError(b, db.Put(obj))
}
for _, item := range []struct {
name string
filters func(*objectSDK.SearchFilters)
}{
{
name: "empty",
filters: func(*objectSDK.SearchFilters) {
return
},
},
{
name: "1 filter",
filters: func(fs *objectSDK.SearchFilters) {
addNFilters(fs, 1)
},
},
{
name: "10 filters",
filters: func(fs *objectSDK.SearchFilters) {
addNFilters(fs, 10)
},
},
{
name: "100 filters",
filters: func(fs *objectSDK.SearchFilters) {
addNFilters(fs, 100)
},
},
{
name: "1000 filters",
filters: func(fs *objectSDK.SearchFilters) {
addNFilters(fs, 1000)
},
},
} {
b.Run(item.name, func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
fs := new(objectSDK.SearchFilters)
item.filters(fs)
b.StartTimer()
_, err := db.Select(*fs)
b.StopTimer()
require.NoError(b, err)
b.StartTimer()
}
})
}
}
func TestMismatchAfterMatch(t *testing.T) {
db := newDB(t) db := newDB(t)
defer releaseDB(db) defer releaseDB(db)
obj := generateObject(t, testPrm{ cid := testCID()
attrNum: 1,
})
require.NoError(t, db.Put(obj)) raw1 := generateRawObjectWithCID(t, cid)
addAttribute(raw1, "foo", "bar")
addAttribute(raw1, "x", "y")
a := obj.Attributes()[0] err := db.Put(raw1.Object(), nil)
require.NoError(t, err)
fs := objectSDK.SearchFilters{} raw2 := generateRawObjectWithCID(t, cid)
addAttribute(raw2, "foo", "bar")
addAttribute(raw2, "x", "z")
// 1st - mismatching filter err = db.Put(raw2.Object(), nil)
fs.AddFilter(a.Key(), a.Value()+"1", objectSDK.MatchStringEqual) require.NoError(t, err)
// 2nd - matching filter raw3 := generateRawObjectWithCID(t, cid)
fs.AddFilter(a.Key(), a.Value(), objectSDK.MatchStringEqual) addAttribute(raw3, "a", "b")
err = db.Put(raw3.Object(), nil)
require.NoError(t, err)
fs := generateSearchFilter(cid)
fs.AddFilter("foo", "bar", objectSDK.MatchStringEqual)
testSelect(t, db, fs,
raw1.Object().Address(),
raw2.Object().Address(),
)
fs = generateSearchFilter(cid)
fs.AddFilter("x", "y", objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw1.Object().Address())
fs = generateSearchFilter(cid)
fs.AddFilter("a", "b", objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw3.Object().Address())
fs = generateSearchFilter(cid)
fs.AddFilter("c", "d", objectSDK.MatchStringEqual)
testSelect(t, db, fs) testSelect(t, db, fs)
fs = generateSearchFilter(cid)
testSelect(t, db, fs,
raw1.Object().Address(),
raw2.Object().Address(),
raw3.Object().Address(),
)
} }
func addCommonAttribute(objs ...*object.Object) *objectSDK.Attribute { func TestDB_SelectRootPhyParent(t *testing.T) {
aCommon := objectSDK.NewAttribute() db := newDB(t)
aCommon.SetKey("common key") defer releaseDB(db)
aCommon.SetValue("common value")
for _, o := range objs { cid := testCID()
object.NewRawFromObject(o).SetAttributes(
append(o.Attributes(), aCommon)..., // prepare
small := generateRawObjectWithCID(t, cid)
err := db.Put(small.Object(), nil)
require.NoError(t, err)
ts := generateRawObjectWithCID(t, cid)
ts.SetType(objectSDK.TypeTombstone)
err = db.Put(ts.Object(), nil)
require.NoError(t, err)
sg := generateRawObjectWithCID(t, cid)
sg.SetType(objectSDK.TypeStorageGroup)
err = db.Put(sg.Object(), nil)
require.NoError(t, err)
leftChild := generateRawObjectWithCID(t, cid)
leftChild.InitRelations()
err = db.Put(leftChild.Object(), nil)
require.NoError(t, err)
parent := generateRawObjectWithCID(t, cid)
rightChild := generateRawObjectWithCID(t, cid)
rightChild.SetParent(parent.Object().SDK())
rightChild.SetParentID(parent.ID())
err = db.Put(rightChild.Object(), nil)
require.NoError(t, err)
link := generateRawObjectWithCID(t, cid)
link.SetParent(parent.Object().SDK())
link.SetParentID(parent.ID())
link.SetChildren(leftChild.ID(), rightChild.ID())
err = db.Put(link.Object(), nil)
require.NoError(t, err)
t.Run("root objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddRootFilter()
testSelect(t, db, fs,
small.Object().Address(),
parent.Object().Address(),
) )
}
return aCommon
}
func TestSelectRemoved(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
// create 2 objects
obj1 := generateObject(t, testPrm{})
obj2 := generateObject(t, testPrm{})
// add common attribute
a := addCommonAttribute(obj1, obj2)
// add to DB
require.NoError(t, db.Put(obj1))
require.NoError(t, db.Put(obj2))
fs := objectSDK.SearchFilters{}
fs.AddFilter(a.Key(), a.Value(), objectSDK.MatchStringEqual)
testSelect(t, db, fs, obj1.Address(), obj2.Address())
// remote 1st object
require.NoError(t, db.Delete(obj1.Address()))
testSelect(t, db, fs, obj2.Address())
}
func TestMissingObjectAttribute(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
// add object w/o attribute
obj1 := generateObject(t, testPrm{
attrNum: 1,
}) })
// add object w/o attribute t.Run("phy objects", func(t *testing.T) {
obj2 := generateObject(t, testPrm{}) fs := generateSearchFilter(cid)
fs.AddPhyFilter()
testSelect(t, db, fs,
small.Object().Address(),
ts.Object().Address(),
sg.Object().Address(),
leftChild.Object().Address(),
rightChild.Object().Address(),
link.Object().Address(),
)
})
a1 := obj1.Attributes()[0] t.Run("regular objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderObjectType, "Regular", objectSDK.MatchStringEqual)
testSelect(t, db, fs,
small.Object().Address(),
leftChild.Object().Address(),
rightChild.Object().Address(),
link.Object().Address(),
parent.Object().Address(),
)
})
// add common attribute t.Run("tombstone objects", func(t *testing.T) {
aCommon := addCommonAttribute(obj1, obj2) fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderObjectType, "Tombstone", objectSDK.MatchStringEqual)
testSelect(t, db, fs, ts.Object().Address())
})
// write to DB t.Run("storage group objects", func(t *testing.T) {
require.NoError(t, db.Put(obj1)) fs := generateSearchFilter(cid)
require.NoError(t, db.Put(obj2)) fs.AddFilter(v2object.FilterHeaderObjectType, "StorageGroup", objectSDK.MatchStringEqual)
testSelect(t, db, fs, sg.Object().Address())
})
fs := objectSDK.SearchFilters{} t.Run("objects with parent", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderParent,
parent.ID().String(),
objectSDK.MatchStringEqual)
// 1st filter by common attribute testSelect(t, db, fs,
fs.AddFilter(aCommon.Key(), aCommon.Value(), objectSDK.MatchStringEqual) rightChild.Object().Address(),
link.Object().Address(),
)
})
// next filter by attribute from 1st object only t.Run("all objects", func(t *testing.T) {
fs.AddFilter(a1.Key(), a1.Value(), objectSDK.MatchStringEqual) fs := generateSearchFilter(cid)
testSelect(t, db, fs,
testSelect(t, db, fs, obj1.Address()) small.Object().Address(),
ts.Object().Address(),
sg.Object().Address(),
leftChild.Object().Address(),
rightChild.Object().Address(),
link.Object().Address(),
parent.Object().Address(),
)
})
} }
func TestSelectParentID(t *testing.T) { func TestDB_SelectInhume(t *testing.T) {
db := newDB(t) db := newDB(t)
defer releaseDB(db) defer releaseDB(db)
// generate 2 objects cid := testCID()
obj1 := generateObject(t, testPrm{})
obj2 := generateObject(t, testPrm{})
// set parent ID of 1st object raw1 := generateRawObjectWithCID(t, cid)
par := testOID() err := db.Put(raw1.Object(), nil)
object.NewRawFromObject(obj1).SetParentID(par) require.NoError(t, err)
// store objects raw2 := generateRawObjectWithCID(t, cid)
require.NoError(t, db.Put(obj1)) err = db.Put(raw2.Object(), nil)
require.NoError(t, db.Put(obj2)) require.NoError(t, err)
// filter by parent ID fs := generateSearchFilter(cid)
fs := objectSDK.SearchFilters{} testSelect(t, db, fs,
fs.AddParentIDFilter(objectSDK.MatchStringEqual, par) raw1.Object().Address(),
raw2.Object().Address(),
)
testSelect(t, db, fs, obj1.Address()) tombstone := objectSDK.NewAddress()
tombstone.SetContainerID(cid)
tombstone.SetObjectID(testOID())
err = db.Inhume(raw2.Object().Address(), tombstone)
require.NoError(t, err)
fs = generateSearchFilter(cid)
testSelect(t, db, fs,
raw1.Object().Address(),
)
} }
func TestSelectObjectID(t *testing.T) { func TestDB_SelectPayloadHash(t *testing.T) {
db := newDB(t) db := newDB(t)
defer releaseDB(db) defer releaseDB(db)
// generate object cid := testCID()
obj := generateObject(t, testPrm{})
// store objects raw1 := generateRawObjectWithCID(t, cid)
require.NoError(t, db.Put(obj)) err := db.Put(raw1.Object(), nil)
require.NoError(t, err)
// filter by object ID raw2 := generateRawObjectWithCID(t, cid)
fs := objectSDK.SearchFilters{} err = db.Put(raw2.Object(), nil)
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, obj.ID()) require.NoError(t, err)
testSelect(t, db, fs, obj.Address()) fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderPayloadHash,
hex.EncodeToString(raw1.PayloadChecksum().Sum()),
objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw1.Object().Address())
}
func TestDB_SelectWithSlowFilters(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
cid := testCID()
v20 := new(pkg.Version)
v20.SetMajor(2)
v21 := new(pkg.Version)
v21.SetMajor(2)
v21.SetMinor(1)
raw1 := generateRawObjectWithCID(t, cid)
raw1.SetPayloadSize(10)
raw1.SetCreationEpoch(11)
raw1.SetVersion(v20)
err := db.Put(raw1.Object(), nil)
require.NoError(t, err)
raw2 := generateRawObjectWithCID(t, cid)
raw2.SetPayloadSize(20)
raw2.SetCreationEpoch(21)
raw2.SetVersion(v21)
err = db.Put(raw2.Object(), nil)
require.NoError(t, err)
t.Run("object with TZHash", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderHomomorphicHash,
hex.EncodeToString(raw1.PayloadHomomorphicHash().Sum()),
objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw1.Object().Address())
})
t.Run("object with payload length", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderPayloadLength, "20", objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw2.Object().Address())
})
t.Run("object with creation epoch", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderCreationEpoch, "11", objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw1.Object().Address())
})
t.Run("object with version", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddObjectVersionFilter(objectSDK.MatchStringEqual, v21)
testSelect(t, db, fs, raw2.Object().Address())
})
}
func generateSearchFilter(cid *container.ID) objectSDK.SearchFilters {
fs := objectSDK.SearchFilters{}
fs.AddObjectContainerIDFilter(objectSDK.MatchStringEqual, cid)
return fs
}
func TestDB_SelectObjectID(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
cid := testCID()
// prepare
parent := generateRawObjectWithCID(t, cid)
regular := generateRawObjectWithCID(t, cid)
regular.SetParentID(parent.ID())
regular.SetParent(parent.Object().SDK())
err := db.Put(regular.Object(), nil)
require.NoError(t, err)
ts := generateRawObjectWithCID(t, cid)
ts.SetType(objectSDK.TypeTombstone)
err = db.Put(ts.Object(), nil)
require.NoError(t, err)
sg := generateRawObjectWithCID(t, cid)
sg.SetType(objectSDK.TypeStorageGroup)
err = db.Put(sg.Object(), nil)
require.NoError(t, err)
t.Run("not found objects", func(t *testing.T) {
raw := generateRawObjectWithCID(t, cid)
fs := generateSearchFilter(cid)
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, raw.ID())
testSelect(t, db, fs)
})
t.Run("regular objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, regular.ID())
testSelect(t, db, fs, regular.Object().Address())
})
t.Run("tombstone objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, ts.ID())
testSelect(t, db, fs, ts.Object().Address())
})
t.Run("storage group objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, sg.ID())
testSelect(t, db, fs, sg.Object().Address())
})
t.Run("storage group objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, parent.ID())
testSelect(t, db, fs, parent.Object().Address())
})
}
func TestDB_SelectSplitID(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
cid := testCID()
child1 := generateRawObjectWithCID(t, cid)
child2 := generateRawObjectWithCID(t, cid)
child3 := generateRawObjectWithCID(t, cid)
split1 := objectSDK.NewSplitID()
split2 := objectSDK.NewSplitID()
child1.SetSplitID(split1)
child2.SetSplitID(split1)
child3.SetSplitID(split2)
require.NoError(t, db.Put(child1.Object(), nil))
require.NoError(t, db.Put(child2.Object(), nil))
require.NoError(t, db.Put(child3.Object(), nil))
t.Run("split id", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderSplitID, split1.String(), objectSDK.MatchStringEqual)
testSelect(t, db, fs,
child1.Object().Address(),
child2.Object().Address(),
)
fs = generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderSplitID, split2.String(), objectSDK.MatchStringEqual)
testSelect(t, db, fs, child3.Object().Address())
})
t.Run("empty split", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderSplitID, "", objectSDK.MatchStringEqual)
testSelect(t, db, fs)
})
t.Run("unknown split id", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderSplitID,
objectSDK.NewSplitID().String(),
objectSDK.MatchStringEqual)
testSelect(t, db, fs)
})
} }

View file

@ -1,130 +0,0 @@
package meta
import (
"encoding/binary"
"encoding/hex"
"os"
"strconv"
"github.com/nspcc-dev/neofs-api-go/pkg/object"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-node/pkg/util/logger"
"go.etcd.io/bbolt"
"go.uber.org/zap"
)
// DB represents local metabase of storage node.
type DB struct {
*cfg
matchers map[object.SearchMatchType]func(string, []byte, string) bool
boltDB *bbolt.DB
}
// Option is an option of DB constructor.
type Option func(*cfg)
type cfg struct {
boltOptions *bbolt.Options // optional
info Info
log *logger.Logger
}
func defaultCfg() *cfg {
return &cfg{
info: Info{
Permission: os.ModePerm, // 0777
},
log: zap.L(),
}
}
// New creates and returns new Metabase instance.
func New(opts ...Option) *DB {
c := defaultCfg()
for i := range opts {
opts[i](c)
}
return &DB{
cfg: c,
matchers: map[object.SearchMatchType]func(string, []byte, string) bool{
object.MatchUnknown: unknownMatcher,
object.MatchStringEqual: stringEqualMatcher,
},
}
}
func stringEqualMatcher(key string, objVal []byte, filterVal string) bool {
switch key {
default:
return string(objVal) == filterVal
case v2object.FilterHeaderPayloadHash, v2object.FilterHeaderHomomorphicHash:
return hex.EncodeToString(objVal) == filterVal
case v2object.FilterHeaderCreationEpoch, v2object.FilterHeaderPayloadLength:
return strconv.FormatUint(binary.LittleEndian.Uint64(objVal), 10) == filterVal
}
}
func unknownMatcher(_ string, _ []byte, _ string) bool {
return false
}
// bucketKeyHelper returns byte representation of val that is used as a key
// in boltDB. Useful for getting filter values from unique and list indexes.
func bucketKeyHelper(hdr string, val string) []byte {
switch hdr {
case v2object.FilterHeaderPayloadHash:
v, err := hex.DecodeString(val)
if err != nil {
return nil
}
return v
case v2object.FilterHeaderSplitID:
s := object.NewSplitID()
err := s.Parse(val)
if err != nil {
return nil
}
return s.ToV2()
default:
return []byte(val)
}
}
// WithLogger returns option to set logger of DB.
func WithLogger(l *logger.Logger) Option {
return func(c *cfg) {
c.log = l
}
}
// WithBoltDBOptions returns option to specify BoltDB options.
func WithBoltDBOptions(opts *bbolt.Options) Option {
return func(c *cfg) {
c.boltOptions = opts
}
}
// WithPath returns option to set system path to Metabase.
func WithPath(path string) Option {
return func(c *cfg) {
c.info.Path = path
}
}
// WithPermissions returns option to specify permission bits
// of Metabase system path.
func WithPermissions(perm os.FileMode) Option {
return func(c *cfg) {
c.info.Permission = perm
}
}

View file

@ -1,113 +0,0 @@
package meta_test
import (
"crypto/rand"
"crypto/sha256"
"os"
"testing"
"github.com/nspcc-dev/neofs-api-go/pkg"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-api-go/pkg/owner"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase/v2"
"github.com/nspcc-dev/neofs-node/pkg/util/test"
"github.com/nspcc-dev/tzhash/tz"
"github.com/stretchr/testify/require"
)
func testSelect(t *testing.T, db *meta.DB, fs objectSDK.SearchFilters, exp ...*objectSDK.Address) {
res, err := db.Select(fs)
require.NoError(t, err)
require.Len(t, res, len(exp))
for i := range exp {
require.Contains(t, res, exp[i])
}
}
func testCID() *container.ID {
cs := [sha256.Size]byte{}
_, _ = rand.Read(cs[:])
id := container.NewID()
id.SetSHA256(cs)
return id
}
func testOID() *objectSDK.ID {
cs := [sha256.Size]byte{}
_, _ = rand.Read(cs[:])
id := objectSDK.NewID()
id.SetSHA256(cs)
return id
}
func newDB(t testing.TB) *meta.DB {
path := t.Name()
bdb := meta.New(meta.WithPath(path), meta.WithPermissions(0600))
require.NoError(t, bdb.Open())
return bdb
}
func releaseDB(db *meta.DB) {
db.Close()
os.Remove(db.DumpInfo().Path)
}
func generateRawObject(t *testing.T) *object.RawObject {
return generateRawObjectWithCID(t, testCID())
}
func generateRawObjectWithCID(t *testing.T, cid *container.ID) *object.RawObject {
version := pkg.NewVersion()
version.SetMajor(2)
version.SetMinor(1)
w, err := owner.NEO3WalletFromPublicKey(&test.DecodeKey(-1).PublicKey)
require.NoError(t, err)
ownerID := owner.NewID()
ownerID.SetNeo3Wallet(w)
csum := new(pkg.Checksum)
csum.SetSHA256(sha256.Sum256(w.Bytes()))
csumTZ := new(pkg.Checksum)
csumTZ.SetTillichZemor(tz.Sum(csum.Sum()))
obj := object.NewRaw()
obj.SetID(testOID())
obj.SetOwnerID(ownerID)
obj.SetContainerID(cid)
obj.SetVersion(version)
obj.SetPayloadChecksum(csum)
obj.SetPayloadHomomorphicHash(csumTZ)
return obj
}
func generateAddress() *objectSDK.Address {
addr := objectSDK.NewAddress()
addr.SetContainerID(testCID())
addr.SetObjectID(testOID())
return addr
}
func addAttribute(obj *object.RawObject, key, val string) {
attr := objectSDK.NewAttribute()
attr.SetKey(key)
attr.SetValue(val)
attrs := obj.Attributes()
attrs = append(attrs, attr)
obj.SetAttributes(attrs...)
}

View file

@ -1,217 +0,0 @@
package meta
import (
"bytes"
"errors"
"fmt"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"go.etcd.io/bbolt"
)
var ErrVirtualObject = errors.New("do not remove virtual object directly")
// DeleteObjects marks list of objects as deleted.
func (db *DB) Delete(lst ...*objectSDK.Address) error {
return db.boltDB.Update(func(tx *bbolt.Tx) error {
for i := range lst {
err := db.delete(tx, lst[i], false)
if err != nil {
return err // maybe log and continue?
}
}
return nil
})
}
func (db *DB) delete(tx *bbolt.Tx, addr *objectSDK.Address, isParent bool) error {
pl := parentLength(tx, addr) // parentLength of address, for virtual objects it is > 0
// do not remove virtual objects directly
if !isParent && pl > 0 {
return ErrVirtualObject
}
// unmarshal object
obj, err := db.get(tx, addr, false)
if err != nil {
return err
}
// if object is an only link to a parent, then remove parent
if parent := obj.GetParent(); parent != nil {
if parentLength(tx, parent.Address()) == 1 {
err = db.deleteObject(tx, obj.GetParent(), true)
if err != nil {
return err
}
}
}
// remove object
return db.deleteObject(tx, obj, isParent)
}
func (db *DB) deleteObject(
tx *bbolt.Tx,
obj *object.Object,
isParent bool,
) error {
uniqueIndexes, err := delUniqueIndexes(obj, isParent)
if err != nil {
return fmt.Errorf("can' build unique indexes: %w", err)
}
// delete unique indexes
for i := range uniqueIndexes {
delUniqueIndexItem(tx, uniqueIndexes[i])
}
// build list indexes
listIndexes, err := listIndexes(obj)
if err != nil {
return fmt.Errorf("can' build list indexes: %w", err)
}
// delete list indexes
for i := range listIndexes {
delListIndexItem(tx, listIndexes[i])
}
// build fake bucket tree indexes
fkbtIndexes, err := fkbtIndexes(obj)
if err != nil {
return fmt.Errorf("can' build fake bucket tree indexes: %w", err)
}
// delete fkbt indexes
for i := range fkbtIndexes {
delFKBTIndexItem(tx, fkbtIndexes[i])
}
return nil
}
// parentLength returns amount of available children from parentid index.
func parentLength(tx *bbolt.Tx, addr *objectSDK.Address) int {
bkt := tx.Bucket(parentBucketName(addr.ContainerID()))
if bkt == nil {
return 0
}
lst, err := decodeList(bkt.Get(objectKey(addr.ObjectID())))
if err != nil {
return 0
}
return len(lst)
}
func delUniqueIndexItem(tx *bbolt.Tx, item namedBucketItem) {
bkt := tx.Bucket(item.name)
if bkt != nil {
_ = bkt.Delete(item.key) // ignore error, best effort there
}
}
func delFKBTIndexItem(tx *bbolt.Tx, item namedBucketItem) {
bkt := tx.Bucket(item.name)
if bkt == nil {
return
}
fkbtRoot := bkt.Bucket(item.key)
if fkbtRoot == nil {
return
}
_ = fkbtRoot.Delete(item.val) // ignore error, best effort there
}
func delListIndexItem(tx *bbolt.Tx, item namedBucketItem) {
bkt := tx.Bucket(item.name)
if bkt == nil {
return
}
lst, err := decodeList(bkt.Get(item.key))
if err != nil || len(lst) == 0 {
return
}
// remove element from the list
newLst := make([][]byte, 0, len(lst))
for i := range lst {
if !bytes.Equal(item.val, lst[i]) {
newLst = append(newLst, lst[i])
}
}
// if list empty, remove the key from <list> bucket
if len(newLst) == 0 {
_ = bkt.Delete(item.key) // ignore error, best effort there
return
}
// if list is not empty, then update it
encodedLst, err := encodeList(lst)
if err != nil {
return // ignore error, best effort there
}
_ = bkt.Put(item.key, encodedLst) // ignore error, best effort there
}
func delUniqueIndexes(obj *object.Object, isParent bool) ([]namedBucketItem, error) {
addr := obj.Address()
objKey := objectKey(addr.ObjectID())
addrKey := addressKey(addr)
result := make([]namedBucketItem, 0, 5)
// add value to primary unique bucket
if !isParent {
var bucketName []byte
switch obj.Type() {
case objectSDK.TypeRegular:
bucketName = primaryBucketName(addr.ContainerID())
case objectSDK.TypeTombstone:
bucketName = tombstoneBucketName(addr.ContainerID())
case objectSDK.TypeStorageGroup:
bucketName = storageGroupBucketName(addr.ContainerID())
default:
return nil, ErrUnknownObjectType
}
result = append(result, namedBucketItem{
name: bucketName,
key: objKey,
})
}
result = append(result,
namedBucketItem{ // remove from small blobovnicza id index
name: smallBucketName(addr.ContainerID()),
key: objKey,
},
namedBucketItem{ // remove from root index
name: rootBucketName(addr.ContainerID()),
key: objKey,
},
namedBucketItem{ // remove from graveyard index
name: graveyardBucketName,
key: addrKey,
},
namedBucketItem{ // remove from ToMoveIt index
name: toMoveItBucketName,
key: addrKey,
},
)
return result, nil
}

View file

@ -1,60 +0,0 @@
package meta_test
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestDB_Delete(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
cid := testCID()
parent := generateRawObjectWithCID(t, cid)
addAttribute(parent, "foo", "bar")
child := generateRawObjectWithCID(t, cid)
child.SetParent(parent.Object().SDK())
child.SetParentID(parent.ID())
// put object with parent
err := db.Put(child.Object(), nil)
require.NoError(t, err)
// fill ToMoveIt index
err = db.ToMoveIt(child.Object().Address())
require.NoError(t, err)
// check if Movable list is not empty
l, err := db.Movable()
require.NoError(t, err)
require.Len(t, l, 1)
// inhume parent and child so they will be on graveyard
ts := generateRawObjectWithCID(t, cid)
err = db.Inhume(child.Object().Address(), ts.Object().Address())
require.NoError(t, err)
err = db.Inhume(child.Object().Address(), ts.Object().Address())
require.NoError(t, err)
// delete object
err = db.Delete(child.Object().Address())
require.NoError(t, err)
// check if there is no data in Movable index
l, err = db.Movable()
require.NoError(t, err)
require.Len(t, l, 0)
// check if they removed from graveyard
ok, err := db.Exists(child.Object().Address())
require.NoError(t, err)
require.False(t, ok)
ok, err = db.Exists(parent.Object().Address())
require.NoError(t, err)
require.False(t, ok)
}

View file

@ -1,88 +0,0 @@
package meta
import (
"errors"
"fmt"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"go.etcd.io/bbolt"
)
var ErrLackSplitInfo = errors.New("no split info on parent object")
// Exists returns ErrAlreadyRemoved if addr was marked as removed. Otherwise it
// returns true if addr is in primary index or false if it is not.
func (db *DB) Exists(addr *objectSDK.Address) (exists bool, err error) {
err = db.boltDB.View(func(tx *bbolt.Tx) error {
exists, err = db.exists(tx, addr)
return err
})
return exists, err
}
func (db *DB) exists(tx *bbolt.Tx, addr *objectSDK.Address) (exists bool, err error) {
// check graveyard first
if inGraveyard(tx, addr) {
return false, object.ErrAlreadyRemoved
}
objKey := objectKey(addr.ObjectID())
// if graveyard is empty, then check if object exists in primary bucket
if inBucket(tx, primaryBucketName(addr.ContainerID()), objKey) {
return true, nil
}
// if primary bucket is empty, then check if object exists in parent bucket
if inBucket(tx, parentBucketName(addr.ContainerID()), objKey) {
rawSplitInfo := getFromBucket(tx, rootBucketName(addr.ContainerID()), objKey)
if len(rawSplitInfo) == 0 {
return false, ErrLackSplitInfo
}
splitInfo := objectSDK.NewSplitInfo()
err := splitInfo.Unmarshal(rawSplitInfo)
if err != nil {
return false, fmt.Errorf("can't unmarshal split info from root index: %w", err)
}
return false, objectSDK.NewSplitInfoError(splitInfo)
}
// if parent bucket is empty, then check if object exists in tombstone bucket
if inBucket(tx, tombstoneBucketName(addr.ContainerID()), objKey) {
return true, nil
}
// if parent bucket is empty, then check if object exists in storage group bucket
return inBucket(tx, storageGroupBucketName(addr.ContainerID()), objKey), nil
}
// inGraveyard returns true if object was marked as removed.
func inGraveyard(tx *bbolt.Tx, addr *objectSDK.Address) bool {
graveyard := tx.Bucket(graveyardBucketName)
if graveyard == nil {
return false
}
tombstone := graveyard.Get(addressKey(addr))
return len(tombstone) != 0
}
// inBucket checks if key <key> is present in bucket <name>.
func inBucket(tx *bbolt.Tx, name, key []byte) bool {
bkt := tx.Bucket(name)
if bkt == nil {
return false
}
// using `get` as `exists`: https://github.com/boltdb/bolt/issues/321
val := bkt.Get(key)
return len(val) != 0
}

View file

@ -1,96 +0,0 @@
package meta
import (
"fmt"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"go.etcd.io/bbolt"
)
// Get returns object header for specified address.
func (db *DB) Get(addr *objectSDK.Address) (obj *object.Object, err error) {
err = db.boltDB.View(func(tx *bbolt.Tx) error {
obj, err = db.get(tx, addr, true)
return err
})
return obj, err
}
func (db *DB) get(tx *bbolt.Tx, addr *objectSDK.Address, checkGraveyard bool) (*object.Object, error) {
obj := object.New()
key := objectKey(addr.ObjectID())
cid := addr.ContainerID()
if checkGraveyard && inGraveyard(tx, addr) {
return nil, object.ErrAlreadyRemoved
}
// check in primary index
data := getFromBucket(tx, primaryBucketName(cid), key)
if len(data) != 0 {
return obj, obj.Unmarshal(data)
}
// if not found then check in tombstone index
data = getFromBucket(tx, tombstoneBucketName(cid), key)
if len(data) != 0 {
return obj, obj.Unmarshal(data)
}
// if not found then check in storage group index
data = getFromBucket(tx, storageGroupBucketName(cid), key)
if len(data) != 0 {
return obj, obj.Unmarshal(data)
}
// if not found then check if object is a virtual
return getVirtualObject(tx, cid, key)
}
func getFromBucket(tx *bbolt.Tx, name, key []byte) []byte {
bkt := tx.Bucket(name)
if bkt == nil {
return nil
}
return bkt.Get(key)
}
func getVirtualObject(tx *bbolt.Tx, cid *container.ID, key []byte) (*object.Object, error) {
parentBucket := tx.Bucket(parentBucketName(cid))
if parentBucket == nil {
return nil, object.ErrNotFound
}
relativeLst, err := decodeList(parentBucket.Get(key))
if err != nil {
return nil, err
}
if len(relativeLst) == 0 { // this should never happen though
return nil, object.ErrNotFound
}
// pick last item, for now there is not difference which address to pick
// but later list might be sorted so first or last value can be more
// prioritized to choose
virtualOID := relativeLst[len(relativeLst)-1]
data := getFromBucket(tx, primaryBucketName(cid), virtualOID)
child := object.New()
err = child.Unmarshal(data)
if err != nil {
return nil, fmt.Errorf("can't unmarshal child with parent: %w", err)
}
if child.GetParent() == nil { // this should never happen though
return nil, object.ErrNotFound
}
return child.GetParent(), nil
}

View file

@ -1,96 +0,0 @@
package meta_test
import (
"bytes"
"testing"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/stretchr/testify/require"
)
func TestDB_Get(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
raw := generateRawObject(t)
// equal fails on diff of <nil> attributes and <{}> attributes,
/* so we make non empty attribute slice in parent*/
addAttribute(raw, "foo", "bar")
t.Run("object not found", func(t *testing.T) {
_, err := db.Get(raw.Object().Address())
require.Error(t, err)
})
t.Run("put regular object", func(t *testing.T) {
err := db.Put(raw.Object(), nil)
require.NoError(t, err)
newObj, err := db.Get(raw.Object().Address())
require.NoError(t, err)
require.Equal(t, raw.Object(), newObj)
})
t.Run("put tombstone object", func(t *testing.T) {
raw.SetType(objectSDK.TypeTombstone)
raw.SetID(testOID())
err := db.Put(raw.Object(), nil)
require.NoError(t, err)
newObj, err := db.Get(raw.Object().Address())
require.NoError(t, err)
require.Equal(t, raw.Object(), newObj)
})
t.Run("put storage group object", func(t *testing.T) {
raw.SetType(objectSDK.TypeStorageGroup)
raw.SetID(testOID())
err := db.Put(raw.Object(), nil)
require.NoError(t, err)
newObj, err := db.Get(raw.Object().Address())
require.NoError(t, err)
require.Equal(t, raw.Object(), newObj)
})
t.Run("put virtual object", func(t *testing.T) {
cid := testCID()
parent := generateRawObjectWithCID(t, cid)
addAttribute(parent, "foo", "bar")
child := generateRawObjectWithCID(t, cid)
child.SetParent(parent.Object().SDK())
child.SetParentID(parent.ID())
err := db.Put(child.Object(), nil)
require.NoError(t, err)
newParent, err := db.Get(parent.Object().Address())
require.NoError(t, err)
require.True(t, binaryEqual(parent.Object(), newParent))
newChild, err := db.Get(child.Object().Address())
require.NoError(t, err)
require.True(t, binaryEqual(child.Object(), newChild))
})
}
// binary equal is used when object contains empty lists in the structure and
// requre.Equal fails on comparing <nil> and []{} lists.
func binaryEqual(a, b *object.Object) bool {
binaryA, err := a.Marshal()
if err != nil {
return false
}
binaryB, err := b.Marshal()
if err != nil {
return false
}
return bytes.Equal(binaryA, binaryB)
}

View file

@ -1,19 +0,0 @@
package meta
import (
"os"
)
// Info groups the information about DB.
type Info struct {
// Full path to the metabase.
Path string
// Permission of database file.
Permission os.FileMode
}
// DumpInfo returns information about the DB.
func (db *DB) DumpInfo() Info {
return db.info
}

View file

@ -1,424 +0,0 @@
package meta
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobovnicza"
"go.etcd.io/bbolt"
)
type (
namedBucketItem struct {
name, key, val []byte
}
)
var (
ErrUnknownObjectType = errors.New("unknown object type")
ErrIncorrectBlobovniczaUpdate = errors.New("updating blobovnicza id on object without it")
ErrIncorrectSplitInfoUpdate = errors.New("updating split info on object without it")
ErrIncorrectRootObject = errors.New("invalid root object")
)
// Put saves object header in metabase. Object payload expected to be cut.
// Big objects have nil blobovniczaID.
func (db *DB) Put(obj *object.Object, id *blobovnicza.ID) error {
return db.boltDB.Update(func(tx *bbolt.Tx) error {
return db.put(tx, obj, id, nil)
})
}
func (db *DB) put(tx *bbolt.Tx, obj *object.Object, id *blobovnicza.ID, si *objectSDK.SplitInfo) error {
isParent := si != nil
exists, err := db.exists(tx, obj.Address())
if errors.As(err, &splitInfoError) {
exists = true // object exists, however it is virtual
} else if err != nil {
return err // return any error besides SplitInfoError
}
// most right child and split header overlap parent so we have to
// check if object exists to not overwrite it twice
if exists {
// when storage engine moves small objects from one blobovniczaID
// to another, then it calls metabase.Put method with new blobovniczaID
// and this code should be triggered
if !isParent && id != nil {
return updateBlobovniczaID(tx, obj.Address(), id)
}
// when storage already has last object in split hierarchy and there is
// a linking object to put (or vice versa), we should update split info
// with object ids of these objects
if isParent {
return updateSplitInfo(tx, obj.Address(), si)
}
return nil
}
if obj.GetParent() != nil && !isParent { // limit depth by two
parentSI, err := splitInfoFromObject(obj)
if err != nil {
return err
}
err = db.put(tx, obj.GetParent(), id, parentSI)
if err != nil {
return err
}
}
// build unique indexes
uniqueIndexes, err := uniqueIndexes(obj, si, id)
if err != nil {
return fmt.Errorf("can' build unique indexes: %w", err)
}
// put unique indexes
for i := range uniqueIndexes {
err := putUniqueIndexItem(tx, uniqueIndexes[i])
if err != nil {
return err
}
}
// build list indexes
listIndexes, err := listIndexes(obj)
if err != nil {
return fmt.Errorf("can' build list indexes: %w", err)
}
// put list indexes
for i := range listIndexes {
err := putListIndexItem(tx, listIndexes[i])
if err != nil {
return err
}
}
// build fake bucket tree indexes
fkbtIndexes, err := fkbtIndexes(obj)
if err != nil {
return fmt.Errorf("can' build fake bucket tree indexes: %w", err)
}
// put fake bucket tree indexes
for i := range fkbtIndexes {
err := putFKBTIndexItem(tx, fkbtIndexes[i])
if err != nil {
return err
}
}
return nil
}
// builds list of <unique> indexes from the object.
func uniqueIndexes(obj *object.Object, si *objectSDK.SplitInfo, id *blobovnicza.ID) ([]namedBucketItem, error) {
isParent := si != nil
addr := obj.Address()
objKey := objectKey(addr.ObjectID())
result := make([]namedBucketItem, 0, 3)
// add value to primary unique bucket
if !isParent {
var bucketName []byte
switch obj.Type() {
case objectSDK.TypeRegular:
bucketName = primaryBucketName(addr.ContainerID())
case objectSDK.TypeTombstone:
bucketName = tombstoneBucketName(addr.ContainerID())
case objectSDK.TypeStorageGroup:
bucketName = storageGroupBucketName(addr.ContainerID())
default:
return nil, ErrUnknownObjectType
}
rawObject, err := obj.Marshal()
if err != nil {
return nil, fmt.Errorf("can't marshal object header: %w", err)
}
result = append(result, namedBucketItem{
name: bucketName,
key: objKey,
val: rawObject,
})
// index blobovniczaID if it is present
if id != nil {
result = append(result, namedBucketItem{
name: smallBucketName(addr.ContainerID()),
key: objKey,
val: *id,
})
}
}
// index root object
if obj.Type() == objectSDK.TypeRegular && !obj.HasParent() {
var (
err error
splitInfo []byte
)
if isParent {
splitInfo, err = si.Marshal()
if err != nil {
return nil, fmt.Errorf("can't marshal split info: %w", err)
}
}
result = append(result, namedBucketItem{
name: rootBucketName(addr.ContainerID()),
key: objKey,
val: splitInfo,
})
}
return result, nil
}
// builds list of <list> indexes from the object.
func listIndexes(obj *object.Object) ([]namedBucketItem, error) {
result := make([]namedBucketItem, 0, 3)
addr := obj.Address()
objKey := objectKey(addr.ObjectID())
// index payload hashes
result = append(result, namedBucketItem{
name: payloadHashBucketName(addr.ContainerID()),
key: obj.PayloadChecksum().Sum(),
val: objKey,
})
// index parent ids
if obj.ParentID() != nil {
result = append(result, namedBucketItem{
name: parentBucketName(addr.ContainerID()),
key: objectKey(obj.ParentID()),
val: objKey,
})
}
// index split ids
if obj.SplitID() != nil {
result = append(result, namedBucketItem{
name: splitBucketName(addr.ContainerID()),
key: obj.SplitID().ToV2(),
val: objKey,
})
}
return result, nil
}
// builds list of <fake bucket tree> indexes from the object.
func fkbtIndexes(obj *object.Object) ([]namedBucketItem, error) {
addr := obj.Address()
objKey := []byte(addr.ObjectID().String())
attrs := obj.Attributes()
result := make([]namedBucketItem, 0, 1+len(attrs))
// owner
result = append(result, namedBucketItem{
name: ownerBucketName(addr.ContainerID()),
key: []byte(obj.OwnerID().String()),
val: objKey,
})
// user specified attributes
for i := range attrs {
result = append(result, namedBucketItem{
name: attributeBucketName(addr.ContainerID(), attrs[i].Key()),
key: []byte(attrs[i].Value()),
val: objKey,
})
}
return result, nil
}
func putUniqueIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
bkt, err := tx.CreateBucketIfNotExists(item.name)
if err != nil {
return fmt.Errorf("can't create index %v: %w", item.name, err)
}
return bkt.Put(item.key, item.val)
}
func putFKBTIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
bkt, err := tx.CreateBucketIfNotExists(item.name)
if err != nil {
return fmt.Errorf("can't create index %v: %w", item.name, err)
}
fkbtRoot, err := bkt.CreateBucketIfNotExists(item.key)
if err != nil {
return fmt.Errorf("can't create fake bucket tree index %v: %w", item.key, err)
}
return fkbtRoot.Put(item.val, zeroValue)
}
func putListIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
bkt, err := tx.CreateBucketIfNotExists(item.name)
if err != nil {
return fmt.Errorf("can't create index %v: %w", item.name, err)
}
lst, err := decodeList(bkt.Get(item.key))
if err != nil {
return fmt.Errorf("can't decode leaf list %v: %w", item.key, err)
}
lst = append(lst, item.val)
encodedLst, err := encodeList(lst)
if err != nil {
return fmt.Errorf("can't encode leaf list %v: %w", item.key, err)
}
return bkt.Put(item.key, encodedLst)
}
// encodeList decodes list of bytes into a single blog for list bucket indexes.
func encodeList(lst [][]byte) ([]byte, error) {
buf := bytes.NewBuffer(nil)
encoder := gob.NewEncoder(buf)
// consider using protobuf encoding instead of glob
if err := encoder.Encode(lst); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// decodeList decodes blob into the list of bytes from list bucket index.
func decodeList(data []byte) (lst [][]byte, err error) {
if len(data) == 0 {
return nil, nil
}
decoder := gob.NewDecoder(bytes.NewReader(data))
if err := decoder.Decode(&lst); err != nil {
return nil, err
}
return lst, nil
}
// updateBlobovniczaID for existing objects if they were moved from from
// one blobovnicza to another.
func updateBlobovniczaID(tx *bbolt.Tx, addr *objectSDK.Address, id *blobovnicza.ID) error {
bkt := tx.Bucket(smallBucketName(addr.ContainerID()))
if bkt == nil {
// if object exists, don't have blobovniczaID and we want to update it
// then ignore, this should never happen
return ErrIncorrectBlobovniczaUpdate
}
objectKey := objectKey(addr.ObjectID())
if len(bkt.Get(objectKey)) == 0 {
return ErrIncorrectBlobovniczaUpdate
}
return bkt.Put(objectKey, *id)
}
// updateSpliInfo for existing objects if storage filled with extra information
// about last object in split hierarchy or linking object.
func updateSplitInfo(tx *bbolt.Tx, addr *objectSDK.Address, from *objectSDK.SplitInfo) error {
bkt := tx.Bucket(rootBucketName(addr.ContainerID()))
if bkt == nil {
// if object doesn't exists and we want to update split info on it
// then ignore, this should never happen
return ErrIncorrectSplitInfoUpdate
}
objectKey := objectKey(addr.ObjectID())
rawSplitInfo := bkt.Get(objectKey)
if len(rawSplitInfo) == 0 {
return ErrIncorrectSplitInfoUpdate
}
to := objectSDK.NewSplitInfo()
err := to.Unmarshal(rawSplitInfo)
if err != nil {
return fmt.Errorf("can't unmarshal split info from root index: %w", err)
}
result := mergeSplitInfo(from, to)
rawSplitInfo, err = result.Marshal()
if err != nil {
return fmt.Errorf("can't marhsal merged split info: %w", err)
}
return bkt.Put(objectKey, rawSplitInfo)
}
// splitInfoFromObject returns split info based on last or linkin object.
// Otherwise returns nil, nil.
func splitInfoFromObject(obj *object.Object) (*objectSDK.SplitInfo, error) {
if obj.Parent() == nil {
return nil, nil
}
info := objectSDK.NewSplitInfo()
info.SetSplitID(obj.SplitID())
switch {
case isLinkObject(obj):
info.SetLink(obj.ID())
case isLastObject(obj):
info.SetLastPart(obj.ID())
default:
return nil, ErrIncorrectRootObject // should never happen
}
return info, nil
}
// mergeSplitInfo ignores conflicts and rewrites `to` with non empty values
// from `from`.
func mergeSplitInfo(from, to *objectSDK.SplitInfo) *objectSDK.SplitInfo {
to.SetSplitID(from.SplitID()) // overwrite SplitID and ignore conflicts
if lp := from.LastPart(); lp != nil {
to.SetLastPart(lp)
}
if link := from.Link(); link != nil {
to.SetLink(link)
}
return to
}
// isLinkObject returns true if object contains parent header and list
// of children.
func isLinkObject(obj *object.Object) bool {
return len(obj.Children()) > 0 && obj.Parent() != nil
}
// isLastObject returns true if object contains only parent header without list
// of children.
func isLastObject(obj *object.Object) bool {
return len(obj.Children()) == 0 && obj.Parent() != nil
}

View file

@ -1,48 +0,0 @@
package meta_test
import (
"testing"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobovnicza"
"github.com/stretchr/testify/require"
)
func TestDB_PutBlobovnicaUpdate(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
raw1 := generateRawObject(t)
blobovniczaID := blobovnicza.ID{1, 2, 3, 4}
// put one object with blobovniczaID
err := db.Put(raw1.Object(), &blobovniczaID)
require.NoError(t, err)
fetchedBlobovniczaID, err := db.IsSmall(raw1.Object().Address())
require.NoError(t, err)
require.Equal(t, &blobovniczaID, fetchedBlobovniczaID)
t.Run("update blobovniczaID", func(t *testing.T) {
newID := blobovnicza.ID{5, 6, 7, 8}
err := db.Put(raw1.Object(), &newID)
require.NoError(t, err)
fetchedBlobovniczaID, err := db.IsSmall(raw1.Object().Address())
require.NoError(t, err)
require.Equal(t, &newID, fetchedBlobovniczaID)
})
t.Run("update blobovniczaID on bad object", func(t *testing.T) {
raw2 := generateRawObject(t)
err := db.Put(raw2.Object(), nil)
require.NoError(t, err)
fetchedBlobovniczaID, err := db.IsSmall(raw2.Object().Address())
require.NoError(t, err)
require.Nil(t, fetchedBlobovniczaID)
err = db.Put(raw2.Object(), &blobovniczaID)
require.Error(t, err)
})
}

View file

@ -1,375 +0,0 @@
package meta
import (
"encoding/binary"
"fmt"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
"github.com/nspcc-dev/neofs-api-go/pkg/object"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/pkg/errors"
"go.etcd.io/bbolt"
"go.uber.org/zap"
)
type (
// filterGroup is a structure that have search filters grouped by access
// method. We have fast filters that looks for indexes and do not unmarshal
// objects, and slow filters, that applied after fast filters created
// smaller set of objects to check.
filterGroup struct {
cid *container.ID
fastFilters object.SearchFilters
slowFilters object.SearchFilters
}
)
var ErrContainerNotInQuery = errors.New("search query does not contain container id filter")
// Select returns list of addresses of objects that match search filters.
func (db *DB) Select(fs object.SearchFilters) (res []*object.Address, err error) {
err = db.boltDB.View(func(tx *bbolt.Tx) error {
res, err = db.selectObjects(tx, fs)
return err
})
return res, err
}
func (db *DB) selectObjects(tx *bbolt.Tx, fs object.SearchFilters) ([]*object.Address, error) {
group, err := groupFilters(fs)
if err != nil {
return nil, err
}
if group.cid == nil {
return nil, ErrContainerNotInQuery
}
// keep matched addresses in this cache
// value equal to number (index+1) of latest matched filter
mAddr := make(map[string]int)
expLen := len(group.fastFilters) // expected value of matched filters in mAddr
if len(group.fastFilters) == 0 {
expLen = 1
db.selectAll(tx, group.cid, mAddr)
} else {
for i := range group.fastFilters {
db.selectFastFilter(tx, group.cid, group.fastFilters[i], mAddr, i)
}
}
res := make([]*object.Address, 0, len(mAddr))
for a, ind := range mAddr {
if ind != expLen {
continue // ignore objects with unmatched fast filters
}
addr := object.NewAddress()
if err := addr.Parse(a); err != nil {
// TODO: storage was broken, so we need to handle it
return nil, err
}
if inGraveyard(tx, addr) {
continue // ignore removed objects
}
if !db.matchSlowFilters(tx, addr, group.slowFilters) {
continue // ignore objects with unmatched slow filters
}
res = append(res, addr)
}
return res, nil
}
// selectAll adds to resulting cache all available objects in metabase.
func (db *DB) selectAll(tx *bbolt.Tx, cid *container.ID, to map[string]int) {
prefix := cid.String() + "/"
selectAllFromBucket(tx, primaryBucketName(cid), prefix, to, 0)
selectAllFromBucket(tx, tombstoneBucketName(cid), prefix, to, 0)
selectAllFromBucket(tx, storageGroupBucketName(cid), prefix, to, 0)
selectAllFromBucket(tx, parentBucketName(cid), prefix, to, 0)
}
// selectAllFromBucket goes through all keys in bucket and adds them in a
// resulting cache. Keys should be stringed object ids.
func selectAllFromBucket(tx *bbolt.Tx, name []byte, prefix string, to map[string]int, fNum int) {
bkt := tx.Bucket(name)
if bkt == nil {
return
}
_ = bkt.ForEach(func(k, v []byte) error {
key := prefix + string(k) // consider using string builders from sync.Pool
markAddressInCache(to, fNum, key)
return nil
})
}
// selectFastFilter makes fast optimized checks for well known buckets or
// looking through user attribute buckets otherwise.
func (db *DB) selectFastFilter(
tx *bbolt.Tx,
cid *container.ID, // container we search on
f object.SearchFilter, // fast filter
to map[string]int, // resulting cache
fNum int, // index of filter
) {
prefix := cid.String() + "/"
switch f.Header() {
case v2object.FilterHeaderObjectID:
db.selectObjectID(tx, f, prefix, to, fNum)
case v2object.FilterHeaderOwnerID:
bucketName := ownerBucketName(cid)
db.selectFromFKBT(tx, bucketName, f, prefix, to, fNum)
case v2object.FilterHeaderPayloadHash:
bucketName := payloadHashBucketName(cid)
db.selectFromList(tx, bucketName, f, prefix, to, fNum)
case v2object.FilterHeaderObjectType:
var bucketName []byte
switch f.Value() { // do it better after https://github.com/nspcc-dev/neofs-api/issues/84
case "Regular":
bucketName = primaryBucketName(cid)
selectAllFromBucket(tx, bucketName, prefix, to, fNum)
bucketName = parentBucketName(cid)
case "Tombstone":
bucketName = tombstoneBucketName(cid)
case "StorageGroup":
bucketName = storageGroupBucketName(cid)
default:
db.log.Debug("unknown object type", zap.String("type", f.Value()))
return
}
selectAllFromBucket(tx, bucketName, prefix, to, fNum)
case v2object.FilterHeaderParent:
bucketName := parentBucketName(cid)
db.selectFromList(tx, bucketName, f, prefix, to, fNum)
case v2object.FilterHeaderSplitID:
bucketName := splitBucketName(cid)
db.selectFromList(tx, bucketName, f, prefix, to, fNum)
case v2object.FilterPropertyRoot:
selectAllFromBucket(tx, rootBucketName(cid), prefix, to, fNum)
case v2object.FilterPropertyPhy:
selectAllFromBucket(tx, primaryBucketName(cid), prefix, to, fNum)
selectAllFromBucket(tx, tombstoneBucketName(cid), prefix, to, fNum)
selectAllFromBucket(tx, storageGroupBucketName(cid), prefix, to, fNum)
default: // user attribute
bucketName := attributeBucketName(cid, f.Header())
db.selectFromFKBT(tx, bucketName, f, prefix, to, fNum)
}
}
// selectFromList looks into <fkbt> index to find list of addresses to add in
// resulting cache.
func (db *DB) selectFromFKBT(
tx *bbolt.Tx,
name []byte, // fkbt root bucket name
f object.SearchFilter, // filter for operation and value
prefix string, // prefix to create addr from oid in index
to map[string]int, // resulting cache
fNum int, // index of filter
) { //
matchFunc, ok := db.matchers[f.Operation()]
if !ok {
db.log.Debug("missing matcher", zap.Uint32("operation", uint32(f.Operation())))
return
}
fkbtRoot := tx.Bucket(name)
if fkbtRoot == nil {
return
}
err := fkbtRoot.ForEach(func(k, _ []byte) error {
if matchFunc(f.Header(), k, f.Value()) {
fkbtLeaf := fkbtRoot.Bucket(k)
if fkbtLeaf == nil {
return nil
}
return fkbtLeaf.ForEach(func(k, _ []byte) error {
addr := prefix + string(k)
markAddressInCache(to, fNum, addr)
return nil
})
}
return nil
})
if err != nil {
db.log.Debug("error in FKBT selection", zap.String("error", err.Error()))
}
}
// selectFromList looks into <list> index to find list of addresses to add in
// resulting cache.
func (db *DB) selectFromList(
tx *bbolt.Tx,
name []byte, // list root bucket name
f object.SearchFilter, // filter for operation and value
prefix string, // prefix to create addr from oid in index
to map[string]int, // resulting cache
fNum int, // index of filter
) { //
bkt := tx.Bucket(name)
if bkt == nil {
return
}
switch f.Operation() {
case object.MatchStringEqual:
default:
db.log.Debug("unknown operation", zap.Uint32("operation", uint32(f.Operation())))
return
}
// warning: it works only for MatchStringEQ, for NotEQ you should iterate over
// bkt and apply matchFunc, don't forget to implement this when it will be
// needed. Right now it is not efficient to iterate over bucket
// when there is only MatchStringEQ.
lst, err := decodeList(bkt.Get(bucketKeyHelper(f.Header(), f.Value())))
if err != nil {
db.log.Debug("can't decode list bucket leaf", zap.String("error", err.Error()))
}
for i := range lst {
addr := prefix + string(lst[i])
markAddressInCache(to, fNum, addr)
}
}
// selectObjectID processes objectID filter with in-place optimizations.
func (db *DB) selectObjectID(
tx *bbolt.Tx,
f object.SearchFilter,
prefix string,
to map[string]int, // resulting cache
fNum int, // index of filter
) {
switch f.Operation() {
case object.MatchStringEqual:
default:
db.log.Debug("unknown operation", zap.Uint32("operation", uint32(f.Operation())))
return
}
// warning: it is in-place optimization and works only for MatchStringEQ,
// for NotEQ you should iterate over bkt and apply matchFunc
addrStr := prefix + f.Value()
addr := object.NewAddress()
err := addr.Parse(addrStr)
if err != nil {
db.log.Debug("can't decode object id address",
zap.String("addr", addrStr),
zap.String("error", err.Error()))
return
}
ok, err := db.exists(tx, addr)
if (err == nil && ok) || errors.As(err, &splitInfoError) {
markAddressInCache(to, fNum, addrStr)
}
}
// matchSlowFilters return true if object header is matched by all slow filters.
func (db *DB) matchSlowFilters(tx *bbolt.Tx, addr *object.Address, f object.SearchFilters) bool {
if len(f) == 0 {
return true
}
obj, err := db.get(tx, addr, true)
if err != nil {
return false
}
for i := range f {
matchFunc, ok := db.matchers[f[i].Operation()]
if !ok {
return false
}
var data []byte
switch f[i].Header() {
case v2object.FilterHeaderVersion:
data = []byte(obj.Version().String())
case v2object.FilterHeaderHomomorphicHash:
data = obj.PayloadHomomorphicHash().Sum()
case v2object.FilterHeaderCreationEpoch:
data = make([]byte, 8)
binary.LittleEndian.PutUint64(data, obj.CreationEpoch())
case v2object.FilterHeaderPayloadLength:
data = make([]byte, 8)
binary.LittleEndian.PutUint64(data, obj.PayloadSize())
default:
continue // ignore unknown search attributes
}
if !matchFunc(f[i].Header(), data, f[i].Value()) {
return false
}
}
return true
}
// groupFilters divides filters in two groups: fast and slow. Fast filters
// processed by indexes and slow filters processed after by unmarshaling
// object headers.
func groupFilters(filters object.SearchFilters) (*filterGroup, error) {
res := &filterGroup{
fastFilters: make(object.SearchFilters, 0, len(filters)),
slowFilters: make(object.SearchFilters, 0, len(filters)),
}
for i := range filters {
switch filters[i].Header() {
case v2object.FilterHeaderContainerID:
res.cid = container.NewID()
err := res.cid.Parse(filters[i].Value())
if err != nil {
return nil, fmt.Errorf("can't parse container id: %w", err)
}
case // slow filters
v2object.FilterHeaderVersion,
v2object.FilterHeaderCreationEpoch,
v2object.FilterHeaderPayloadLength,
v2object.FilterHeaderHomomorphicHash:
res.slowFilters = append(res.slowFilters, filters[i])
default: // fast filters or user attributes if unknown
res.fastFilters = append(res.fastFilters, filters[i])
}
}
return res, nil
}
func markAddressInCache(cache map[string]int, fNum int, addr string) {
if num := cache[addr]; num == fNum {
cache[addr] = num + 1
}
}

View file

@ -1,409 +0,0 @@
package meta_test
import (
"encoding/hex"
"testing"
"github.com/nspcc-dev/neofs-api-go/pkg"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/stretchr/testify/require"
)
func TestDB_SelectUserAttributes(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
cid := testCID()
raw1 := generateRawObjectWithCID(t, cid)
addAttribute(raw1, "foo", "bar")
addAttribute(raw1, "x", "y")
err := db.Put(raw1.Object(), nil)
require.NoError(t, err)
raw2 := generateRawObjectWithCID(t, cid)
addAttribute(raw2, "foo", "bar")
addAttribute(raw2, "x", "z")
err = db.Put(raw2.Object(), nil)
require.NoError(t, err)
raw3 := generateRawObjectWithCID(t, cid)
addAttribute(raw3, "a", "b")
err = db.Put(raw3.Object(), nil)
require.NoError(t, err)
fs := generateSearchFilter(cid)
fs.AddFilter("foo", "bar", objectSDK.MatchStringEqual)
testSelect(t, db, fs,
raw1.Object().Address(),
raw2.Object().Address(),
)
fs = generateSearchFilter(cid)
fs.AddFilter("x", "y", objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw1.Object().Address())
fs = generateSearchFilter(cid)
fs.AddFilter("a", "b", objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw3.Object().Address())
fs = generateSearchFilter(cid)
fs.AddFilter("c", "d", objectSDK.MatchStringEqual)
testSelect(t, db, fs)
fs = generateSearchFilter(cid)
testSelect(t, db, fs,
raw1.Object().Address(),
raw2.Object().Address(),
raw3.Object().Address(),
)
}
func TestDB_SelectRootPhyParent(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
cid := testCID()
// prepare
small := generateRawObjectWithCID(t, cid)
err := db.Put(small.Object(), nil)
require.NoError(t, err)
ts := generateRawObjectWithCID(t, cid)
ts.SetType(objectSDK.TypeTombstone)
err = db.Put(ts.Object(), nil)
require.NoError(t, err)
sg := generateRawObjectWithCID(t, cid)
sg.SetType(objectSDK.TypeStorageGroup)
err = db.Put(sg.Object(), nil)
require.NoError(t, err)
leftChild := generateRawObjectWithCID(t, cid)
leftChild.InitRelations()
err = db.Put(leftChild.Object(), nil)
require.NoError(t, err)
parent := generateRawObjectWithCID(t, cid)
rightChild := generateRawObjectWithCID(t, cid)
rightChild.SetParent(parent.Object().SDK())
rightChild.SetParentID(parent.ID())
err = db.Put(rightChild.Object(), nil)
require.NoError(t, err)
link := generateRawObjectWithCID(t, cid)
link.SetParent(parent.Object().SDK())
link.SetParentID(parent.ID())
link.SetChildren(leftChild.ID(), rightChild.ID())
err = db.Put(link.Object(), nil)
require.NoError(t, err)
t.Run("root objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddRootFilter()
testSelect(t, db, fs,
small.Object().Address(),
parent.Object().Address(),
)
})
t.Run("phy objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddPhyFilter()
testSelect(t, db, fs,
small.Object().Address(),
ts.Object().Address(),
sg.Object().Address(),
leftChild.Object().Address(),
rightChild.Object().Address(),
link.Object().Address(),
)
})
t.Run("regular objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderObjectType, "Regular", objectSDK.MatchStringEqual)
testSelect(t, db, fs,
small.Object().Address(),
leftChild.Object().Address(),
rightChild.Object().Address(),
link.Object().Address(),
parent.Object().Address(),
)
})
t.Run("tombstone objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderObjectType, "Tombstone", objectSDK.MatchStringEqual)
testSelect(t, db, fs, ts.Object().Address())
})
t.Run("storage group objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderObjectType, "StorageGroup", objectSDK.MatchStringEqual)
testSelect(t, db, fs, sg.Object().Address())
})
t.Run("objects with parent", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderParent,
parent.ID().String(),
objectSDK.MatchStringEqual)
testSelect(t, db, fs,
rightChild.Object().Address(),
link.Object().Address(),
)
})
t.Run("all objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
testSelect(t, db, fs,
small.Object().Address(),
ts.Object().Address(),
sg.Object().Address(),
leftChild.Object().Address(),
rightChild.Object().Address(),
link.Object().Address(),
parent.Object().Address(),
)
})
}
func TestDB_SelectInhume(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
cid := testCID()
raw1 := generateRawObjectWithCID(t, cid)
err := db.Put(raw1.Object(), nil)
require.NoError(t, err)
raw2 := generateRawObjectWithCID(t, cid)
err = db.Put(raw2.Object(), nil)
require.NoError(t, err)
fs := generateSearchFilter(cid)
testSelect(t, db, fs,
raw1.Object().Address(),
raw2.Object().Address(),
)
tombstone := objectSDK.NewAddress()
tombstone.SetContainerID(cid)
tombstone.SetObjectID(testOID())
err = db.Inhume(raw2.Object().Address(), tombstone)
require.NoError(t, err)
fs = generateSearchFilter(cid)
testSelect(t, db, fs,
raw1.Object().Address(),
)
}
func TestDB_SelectPayloadHash(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
cid := testCID()
raw1 := generateRawObjectWithCID(t, cid)
err := db.Put(raw1.Object(), nil)
require.NoError(t, err)
raw2 := generateRawObjectWithCID(t, cid)
err = db.Put(raw2.Object(), nil)
require.NoError(t, err)
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderPayloadHash,
hex.EncodeToString(raw1.PayloadChecksum().Sum()),
objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw1.Object().Address())
}
func TestDB_SelectWithSlowFilters(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
cid := testCID()
v20 := new(pkg.Version)
v20.SetMajor(2)
v21 := new(pkg.Version)
v21.SetMajor(2)
v21.SetMinor(1)
raw1 := generateRawObjectWithCID(t, cid)
raw1.SetPayloadSize(10)
raw1.SetCreationEpoch(11)
raw1.SetVersion(v20)
err := db.Put(raw1.Object(), nil)
require.NoError(t, err)
raw2 := generateRawObjectWithCID(t, cid)
raw2.SetPayloadSize(20)
raw2.SetCreationEpoch(21)
raw2.SetVersion(v21)
err = db.Put(raw2.Object(), nil)
require.NoError(t, err)
t.Run("object with TZHash", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderHomomorphicHash,
hex.EncodeToString(raw1.PayloadHomomorphicHash().Sum()),
objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw1.Object().Address())
})
t.Run("object with payload length", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderPayloadLength, "20", objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw2.Object().Address())
})
t.Run("object with creation epoch", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderCreationEpoch, "11", objectSDK.MatchStringEqual)
testSelect(t, db, fs, raw1.Object().Address())
})
t.Run("object with version", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddObjectVersionFilter(objectSDK.MatchStringEqual, v21)
testSelect(t, db, fs, raw2.Object().Address())
})
}
func generateSearchFilter(cid *container.ID) objectSDK.SearchFilters {
fs := objectSDK.SearchFilters{}
fs.AddObjectContainerIDFilter(objectSDK.MatchStringEqual, cid)
return fs
}
func TestDB_SelectObjectID(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
cid := testCID()
// prepare
parent := generateRawObjectWithCID(t, cid)
regular := generateRawObjectWithCID(t, cid)
regular.SetParentID(parent.ID())
regular.SetParent(parent.Object().SDK())
err := db.Put(regular.Object(), nil)
require.NoError(t, err)
ts := generateRawObjectWithCID(t, cid)
ts.SetType(objectSDK.TypeTombstone)
err = db.Put(ts.Object(), nil)
require.NoError(t, err)
sg := generateRawObjectWithCID(t, cid)
sg.SetType(objectSDK.TypeStorageGroup)
err = db.Put(sg.Object(), nil)
require.NoError(t, err)
t.Run("not found objects", func(t *testing.T) {
raw := generateRawObjectWithCID(t, cid)
fs := generateSearchFilter(cid)
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, raw.ID())
testSelect(t, db, fs)
})
t.Run("regular objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, regular.ID())
testSelect(t, db, fs, regular.Object().Address())
})
t.Run("tombstone objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, ts.ID())
testSelect(t, db, fs, ts.Object().Address())
})
t.Run("storage group objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, sg.ID())
testSelect(t, db, fs, sg.Object().Address())
})
t.Run("storage group objects", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, parent.ID())
testSelect(t, db, fs, parent.Object().Address())
})
}
func TestDB_SelectSplitID(t *testing.T) {
db := newDB(t)
defer releaseDB(db)
cid := testCID()
child1 := generateRawObjectWithCID(t, cid)
child2 := generateRawObjectWithCID(t, cid)
child3 := generateRawObjectWithCID(t, cid)
split1 := objectSDK.NewSplitID()
split2 := objectSDK.NewSplitID()
child1.SetSplitID(split1)
child2.SetSplitID(split1)
child3.SetSplitID(split2)
require.NoError(t, db.Put(child1.Object(), nil))
require.NoError(t, db.Put(child2.Object(), nil))
require.NoError(t, db.Put(child3.Object(), nil))
t.Run("split id", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderSplitID, split1.String(), objectSDK.MatchStringEqual)
testSelect(t, db, fs,
child1.Object().Address(),
child2.Object().Address(),
)
fs = generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderSplitID, split2.String(), objectSDK.MatchStringEqual)
testSelect(t, db, fs, child3.Object().Address())
})
t.Run("empty split", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderSplitID, "", objectSDK.MatchStringEqual)
testSelect(t, db, fs)
})
t.Run("unknown split id", func(t *testing.T) {
fs := generateSearchFilter(cid)
fs.AddFilter(v2object.FilterHeaderSplitID,
objectSDK.NewSplitID().String(),
objectSDK.MatchStringEqual)
testSelect(t, db, fs)
})
}

View file

@ -2,7 +2,7 @@ package shard
import ( import (
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor"
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase/v2" meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase"
"github.com/nspcc-dev/neofs-node/pkg/util/logger" "github.com/nspcc-dev/neofs-node/pkg/util/logger"
"go.uber.org/atomic" "go.uber.org/atomic"
) )

View file

@ -13,7 +13,7 @@ import (
"github.com/nspcc-dev/neofs-api-go/pkg/owner" "github.com/nspcc-dev/neofs-api-go/pkg/owner"
"github.com/nspcc-dev/neofs-node/pkg/core/object" "github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor"
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase/v2" meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/shard" "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/shard"
"github.com/nspcc-dev/neofs-node/pkg/util/test" "github.com/nspcc-dev/neofs-node/pkg/util/test"
"github.com/nspcc-dev/tzhash/tz" "github.com/nspcc-dev/tzhash/tz"