diff --git a/cmd/neofs-node/config.go b/cmd/neofs-node/config.go index bf4951c2..7256e6c8 100644 --- a/cmd/neofs-node/config.go +++ b/cmd/neofs-node/config.go @@ -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/fsbucket" "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/morph/client" "github.com/nspcc-dev/neofs-node/pkg/morph/client/container/wrapper" diff --git a/pkg/local_object_storage/localstore/methods.go b/pkg/local_object_storage/localstore/methods.go deleted file mode 100644 index 1c4fdeef..00000000 --- a/pkg/local_object_storage/localstore/methods.go +++ /dev/null @@ -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) -} diff --git a/pkg/local_object_storage/localstore/storage.go b/pkg/local_object_storage/localstore/storage.go deleted file mode 100644 index add90ac4..00000000 --- a/pkg/local_object_storage/localstore/storage.go +++ /dev/null @@ -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 - } - } -} diff --git a/pkg/local_object_storage/metabase/v2/cleanup.go b/pkg/local_object_storage/metabase/cleanup.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/cleanup.go rename to pkg/local_object_storage/metabase/cleanup.go diff --git a/pkg/local_object_storage/metabase/v2/containers.go b/pkg/local_object_storage/metabase/containers.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/containers.go rename to pkg/local_object_storage/metabase/containers.go diff --git a/pkg/local_object_storage/metabase/v2/containers_test.go b/pkg/local_object_storage/metabase/containers_test.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/containers_test.go rename to pkg/local_object_storage/metabase/containers_test.go diff --git a/pkg/local_object_storage/metabase/v2/control.go b/pkg/local_object_storage/metabase/control.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/control.go rename to pkg/local_object_storage/metabase/control.go diff --git a/pkg/local_object_storage/metabase/db.go b/pkg/local_object_storage/metabase/db.go index 57989093..c55c70f9 100644 --- a/pkg/local_object_storage/metabase/db.go +++ b/pkg/local_object_storage/metabase/db.go @@ -1,6 +1,11 @@ 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" @@ -10,30 +15,36 @@ import ( // DB represents local metabase of storage node. type DB struct { - info Info - *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. type Option func(*cfg) type cfg struct { - boltDB *bbolt.DB + boltOptions *bbolt.Options // optional + + info Info log *logger.Logger } func defaultCfg() *cfg { return &cfg{ + info: Info{ + Permission: os.ModePerm, // 0777 + }, + log: zap.L(), } } -// NewDB creates, initializes and returns DB instance. -func NewDB(opts ...Option) *DB { +// New creates and returns new Metabase instance. +func New(opts ...Option) *DB { c := defaultCfg() for i := range opts { @@ -41,43 +52,51 @@ func NewDB(opts ...Option) *DB { } return &DB{ - info: Info{ - Path: c.boltDB.Path(), - }, cfg: c, - matchers: map[object.SearchMatchType]func(string, string, string) bool{ + matchers: map[object.SearchMatchType]func(string, []byte, string) bool{ object.MatchUnknown: unknownMatcher, object.MatchStringEqual: stringEqualMatcher, }, } } -func (db *DB) Close() error { - return db.boltDB.Close() -} - -func stringEqualMatcher(key, objVal, filterVal string) bool { +func stringEqualMatcher(key string, objVal []byte, filterVal string) bool { switch key { default: - return objVal == filterVal - case v2object.FilterPropertyPhy, v2object.FilterPropertyRoot: - return true + 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(key, _, _ string) bool { - switch key { - default: - return false - case v2object.FilterPropertyPhy, v2object.FilterPropertyRoot: - return true - } +func unknownMatcher(_ string, _ []byte, _ string) bool { + return false } -// FromBoltDB returns option to construct DB from BoltDB instance. -func FromBoltDB(db *bbolt.DB) Option { - return func(c *cfg) { - c.boltDB = db +// 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) } } @@ -87,3 +106,25 @@ func WithLogger(l *logger.Logger) Option { 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 + } +} diff --git a/pkg/local_object_storage/metabase/db_test.go b/pkg/local_object_storage/metabase/db_test.go index b5fbeabe..126bbec9 100644 --- a/pkg/local_object_storage/metabase/db_test.go +++ b/pkg/local_object_storage/metabase/db_test.go @@ -1,4 +1,4 @@ -package meta +package meta_test import ( "crypto/rand" @@ -11,13 +11,13 @@ import ( 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" "github.com/nspcc-dev/neofs-node/pkg/util/test" - "github.com/pkg/errors" + "github.com/nspcc-dev/tzhash/tz" "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) require.NoError(t, err) 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 { cs := [sha256.Size]byte{} - rand.Read(cs[:]) + _, _ = rand.Read(cs[:]) id := container.NewID() id.SetSHA256(cs) @@ -39,7 +39,7 @@ func testCID() *container.ID { func testOID() *objectSDK.ID { cs := [sha256.Size]byte{} - rand.Read(cs[:]) + _, _ = rand.Read(cs[:]) id := objectSDK.NewID() id.SetSHA256(cs) @@ -47,221 +47,67 @@ func testOID() *objectSDK.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.SetMajor(2) version.SetMinor(1) - cid := testCID() - w, err := owner.NEO3WalletFromPublicKey(&test.DecodeKey(-1).PublicKey) require.NoError(t, err) ownerID := owner.NewID() 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.SetID(oid) + obj.SetID(testOID()) obj.SetOwnerID(ownerID) obj.SetContainerID(cid) obj.SetVersion(version) + obj.SetPayloadChecksum(csum) + obj.SetPayloadHomomorphicHash(csumTZ) - k, v := "key", "value" - - 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) + return obj } -func TestDB_Delete(t *testing.T) { - db := newDB(t) +func generateAddress() *objectSDK.Address { + addr := objectSDK.NewAddress() + addr.SetContainerID(testCID()) + addr.SetObjectID(testOID()) - defer releaseDB(db) - - 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) + return addr } -func TestDB_SelectProperties(t *testing.T) { - db := newDB(t) +func addAttribute(obj *object.RawObject, key, val string) { + attr := objectSDK.NewAttribute() + attr.SetKey(key) + attr.SetValue(val) - defer releaseDB(db) - - parent := object.NewRaw() - 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) + attrs := obj.Attributes() + attrs = append(attrs, attr) + obj.SetAttributes(attrs...) } diff --git a/pkg/local_object_storage/metabase/delete.go b/pkg/local_object_storage/metabase/delete.go index 4d26f60c..913f0489 100644 --- a/pkg/local_object_storage/metabase/delete.go +++ b/pkg/local_object_storage/metabase/delete.go @@ -1,47 +1,217 @@ package meta import ( - "github.com/nspcc-dev/neofs-api-go/pkg/object" - "github.com/pkg/errors" + "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" - "go.uber.org/zap" ) -var tombstoneBucket = []byte("tombstones") - -// 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 -} +var ErrVirtualObject = errors.New("do not remove virtual object directly") // DeleteObjects marks list of objects as deleted. -func (db *DB) DeleteObjects(list ...*object.Address) { - 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 { +func (db *DB) Delete(lst ...*objectSDK.Address) error { return db.boltDB.Update(func(tx *bbolt.Tx) error { - bucket, err := tx.CreateBucketIfNotExists(tombstoneBucket) - if err != nil { - return errors.Wrapf(err, "(%T) could not create tombstone bucket", db) - } - - 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) + 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 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 +} diff --git a/pkg/local_object_storage/metabase/delete_test.go b/pkg/local_object_storage/metabase/delete_test.go index 49a72c8b..c056a9b3 100644 --- a/pkg/local_object_storage/metabase/delete_test.go +++ b/pkg/local_object_storage/metabase/delete_test.go @@ -1,71 +1,60 @@ -package meta +package meta_test import ( "testing" - "github.com/nspcc-dev/neofs-api-go/pkg/object" "github.com/stretchr/testify/require" ) -func BenchmarkDB_Delete(b *testing.B) { - 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) { +func TestDB_Delete(t *testing.T) { db := newDB(t) defer releaseDB(db) - o1 := generateObject(t, testPrm{}) - o2 := generateObject(t, testPrm{}) + cid := testCID() + parent := generateRawObjectWithCID(t, cid) + addAttribute(parent, "foo", "bar") - require.NoError(t, db.Put(o1)) - require.NoError(t, db.Put(o2)) + child := generateRawObjectWithCID(t, cid) + 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) } diff --git a/pkg/local_object_storage/metabase/exists.go b/pkg/local_object_storage/metabase/exists.go index 63b0a7f6..94ae99ec 100644 --- a/pkg/local_object_storage/metabase/exists.go +++ b/pkg/local_object_storage/metabase/exists.go @@ -1,22 +1,88 @@ package meta import ( + "errors" + "fmt" + objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/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. -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 - } +var ErrLackSplitInfo = errors.New("no split info on parent object") - 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 is present in bucket . +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 } diff --git a/pkg/local_object_storage/metabase/v2/exists_test.go b/pkg/local_object_storage/metabase/exists_test.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/exists_test.go rename to pkg/local_object_storage/metabase/exists_test.go diff --git a/pkg/local_object_storage/metabase/get.go b/pkg/local_object_storage/metabase/get.go index 79f89da3..695ad8fb 100644 --- a/pkg/local_object_storage/metabase/get.go +++ b/pkg/local_object_storage/metabase/get.go @@ -1,43 +1,96 @@ 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) (*object.Object, error) { - var obj *object.Object - - 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) +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 - }); 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 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 } diff --git a/pkg/local_object_storage/metabase/get_test.go b/pkg/local_object_storage/metabase/get_test.go index 919b7305..d0de02ef 100644 --- a/pkg/local_object_storage/metabase/get_test.go +++ b/pkg/local_object_storage/metabase/get_test.go @@ -1,56 +1,96 @@ -package meta +package meta_test import ( + "bytes" "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" ) -func BenchmarkDB_Get(b *testing.B) { - db := newDB(b) - +func TestDB_Get(t *testing.T) { + db := newDB(t) defer releaseDB(db) - var existingAddr *object.Address + raw := generateRawObject(t) - for i := 0; i < 10; i++ { - obj := generateObject(b, testPrm{}) + // equal fails on diff of attributes and <{}> attributes, + /* so we make non empty attribute slice in parent*/ + addAttribute(raw, "foo", "bar") - 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.Get(existingAddr) - - b.StopTimer() - require.NoError(b, err) - b.StartTimer() - } + t.Run("object not found", func(t *testing.T) { + _, err := db.Get(raw.Object().Address()) + require.Error(t, err) }) - b.Run("non-existing address", func(b *testing.B) { - b.ReportAllocs() - b.ResetTimer() + t.Run("put regular object", func(t *testing.T) { + err := db.Put(raw.Object(), nil) + require.NoError(t, err) - for i := 0; i < b.N; i++ { - b.StopTimer() - addr := object.NewAddress() - addr.SetContainerID(testCID()) - addr.SetObjectID(testOID()) - b.StartTimer() + newObj, err := db.Get(raw.Object().Address()) + require.NoError(t, err) + require.Equal(t, raw.Object(), newObj) + }) - _, err := db.Get(addr) + t.Run("put tombstone object", func(t *testing.T) { + raw.SetType(objectSDK.TypeTombstone) + raw.SetID(testOID()) - b.StopTimer() - require.Error(b, err) - b.StartTimer() - } + 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 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) +} diff --git a/pkg/local_object_storage/metabase/info.go b/pkg/local_object_storage/metabase/info.go index 906e2120..967e9c71 100644 --- a/pkg/local_object_storage/metabase/info.go +++ b/pkg/local_object_storage/metabase/info.go @@ -1,9 +1,16 @@ 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. diff --git a/pkg/local_object_storage/metabase/v2/inhume.go b/pkg/local_object_storage/metabase/inhume.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/inhume.go rename to pkg/local_object_storage/metabase/inhume.go diff --git a/pkg/local_object_storage/metabase/v2/inhume_test.go b/pkg/local_object_storage/metabase/inhume_test.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/inhume_test.go rename to pkg/local_object_storage/metabase/inhume_test.go diff --git a/pkg/local_object_storage/metabase/v2/movable.go b/pkg/local_object_storage/metabase/movable.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/movable.go rename to pkg/local_object_storage/metabase/movable.go diff --git a/pkg/local_object_storage/metabase/v2/movable_test.go b/pkg/local_object_storage/metabase/movable_test.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/movable_test.go rename to pkg/local_object_storage/metabase/movable_test.go diff --git a/pkg/local_object_storage/metabase/put.go b/pkg/local_object_storage/metabase/put.go index cda37b89..f7a7979c 100644 --- a/pkg/local_object_storage/metabase/put.go +++ b/pkg/local_object_storage/metabase/put.go @@ -1,145 +1,424 @@ package meta import ( + "bytes" + "encoding/gob" + "errors" + "fmt" + 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/pkg/errors" + "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobovnicza" "go.etcd.io/bbolt" ) -type bucketItem struct { - key, val string -} - -var ( - primaryBucket = []byte("objects") - indexBucket = []byte("index") +type ( + namedBucketItem struct { + name, key, val []byte + } ) -// Put saves object in DB. -// -// Object payload expected to be cut. -func (db *DB) Put(obj *object.Object) error { +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 { - par := false - - 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 + return db.put(tx, obj, id, nil) }) } -func nonEmptyKeyBytes(key []byte) []byte { - return append([]byte{0}, key...) +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 } -func cutKeyBytes(key []byte) []byte { - return key[1:] -} +// builds list of 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) -func addressKey(addr *objectSDK.Address) []byte { - return []byte(addr.String()) -} + // add value to primary unique bucket + if !isParent { + var bucketName []byte -func objectIndices(obj *object.Object, parent bool) []bucketItem { - as := obj.Attributes() + 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 + } - 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, - bucketItem{ - key: v2object.FilterHeaderVersion, - val: obj.Version().String(), - }, - 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 - ) + 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() { - res = append(res, bucketItem{ - key: v2object.FilterPropertyRoot, + 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, }) } - if !parent { - res = append(res, bucketItem{ - key: v2object.FilterPropertyPhy, - }) - } - - for _, a := range as { - res = append(res, bucketItem{ - key: a.Key(), - val: a.Value(), - }) - } - - return res + return result, nil +} + +// builds list of 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 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 } diff --git a/pkg/local_object_storage/metabase/put_test.go b/pkg/local_object_storage/metabase/put_test.go index 3e3dc11d..0e140b2b 100644 --- a/pkg/local_object_storage/metabase/put_test.go +++ b/pkg/local_object_storage/metabase/put_test.go @@ -1,136 +1,48 @@ -package meta +package meta_test import ( - "crypto/rand" - "crypto/sha256" - "fmt" "testing" - "github.com/nspcc-dev/neofs-api-go/pkg" - 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/nspcc-dev/neofs-node/pkg/local_object_storage/blobovnicza" "github.com/stretchr/testify/require" ) -type testPrm struct { - withParent bool - - 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) - +func TestDB_PutBlobovnicaUpdate(t *testing.T) { + db := newDB(t) defer releaseDB(db) - for _, prm := range []testPrm{ - { - 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() + raw1 := generateRawObject(t) + blobovniczaID := blobovnicza.ID{1, 2, 3, 4} - for i := 0; i < b.N; i++ { - b.StopTimer() - obj := generateObject(b, prm) - b.StartTimer() + // put one object with blobovniczaID + err := db.Put(raw1.Object(), &blobovniczaID) + require.NoError(t, err) - err := db.Put(obj) + fetchedBlobovniczaID, err := db.IsSmall(raw1.Object().Address()) + require.NoError(t, err) + require.Equal(t, &blobovniczaID, fetchedBlobovniczaID) - b.StopTimer() - require.NoError(b, err) - b.StartTimer() - } - }) - } + 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) + }) } diff --git a/pkg/local_object_storage/metabase/select.go b/pkg/local_object_storage/metabase/select.go index 1537e654..1bb2f0a1 100644 --- a/pkg/local_object_storage/metabase/select.go +++ b/pkg/local_object_storage/metabase/select.go @@ -1,87 +1,73 @@ 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 + return res, err } func (db *DB) selectObjects(tx *bbolt.Tx, fs object.SearchFilters) ([]*object.Address, error) { - if len(fs) == 0 { - return db.selectAll(tx) + group, err := groupFilters(fs) + if err != nil { + return nil, err } - // get indexed bucket - indexBucket := tx.Bucket(indexBucket) - if indexBucket == nil { - // empty storage - return nil, nil + if group.cid == nil { + return nil, ErrContainerNotInQuery } - // keep processed addresses + // keep matched addresses in this cache // value equal to number (index+1) of latest matched filter mAddr := make(map[string]int) - for fNum := range fs { - matchFunc, ok := db.matchers[fs[fNum].Operation()] - if !ok { - return nil, errors.Errorf("no function for matcher %v", fs[fNum].Operation()) - } + expLen := len(group.fastFilters) // expected value of matched filters in mAddr - key := fs[fNum].Header() + if len(group.fastFilters) == 0 { + expLen = 1 - // get bucket with values - keyBucket := indexBucket.Bucket([]byte(key)) - if keyBucket == nil { - // no object has this attribute => empty result - 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) + db.selectAll(tx, group.cid, mAddr) + } else { + for i := range group.fastFilters { + db.selectFastFilter(tx, group.cid, group.fastFilters[i], mAddr, i) } } - fLen := len(fs) res := make([]*object.Address, 0, len(mAddr)) for a, ind := range mAddr { - if ind != fLen { - continue - } - - // check if object marked as deleted - if objectRemoved(tx, []byte(a)) { - continue + if ind != expLen { + continue // ignore objects with unmatched fast filters } addr := object.NewAddress() @@ -90,60 +76,300 @@ func (db *DB) selectObjects(tx *bbolt.Tx, fs object.SearchFilters) ([]*object.Ad 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 } -func (db *DB) selectAll(tx *bbolt.Tx) ([]*object.Address, error) { - result := map[string]struct{}{} +// 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() + "/" - primaryBucket := tx.Bucket(primaryBucket) - indexBucket := tx.Bucket(indexBucket) + 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) +} - if primaryBucket == nil || indexBucket == nil { - return nil, nil +// 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 } - if err := primaryBucket.ForEach(func(k, _ []byte) error { - result[string(k)] = struct{}{} + _ = bkt.ForEach(func(k, v []byte) error { + key := prefix + string(k) // consider using string builders from sync.Pool + markAddressInCache(to, fNum, key) return nil - }); err != nil { - return nil, errors.Wrapf(err, "(%T) could not iterate primary bucket", db) - } - - rootBucket := indexBucket.Bucket([]byte(v2object.FilterPropertyRoot)) - if rootBucket != nil { - rootBucket = rootBucket.Bucket(nonEmptyKeyBytes(nil)) - } - - if rootBucket != nil { - if err := rootBucket.ForEach(func(k, v []byte) error { - result[string(k)] = struct{}{} - - return nil - }); err != nil { - return nil, errors.Wrapf(err, "(%T) could not iterate root bucket", db) - } - } - - list := make([]*object.Address, 0, len(result)) - - for k := range result { - // check if object marked as deleted - if objectRemoved(tx, []byte(k)) { - continue - } - - addr := object.NewAddress() - if err := addr.Parse(k); err != nil { - return nil, err // TODO: storage was broken, so we need to handle it - } - - list = append(list, addr) - } - - return list, 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 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 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 + } } diff --git a/pkg/local_object_storage/metabase/select_test.go b/pkg/local_object_storage/metabase/select_test.go index 43371548..3abbafc7 100644 --- a/pkg/local_object_storage/metabase/select_test.go +++ b/pkg/local_object_storage/metabase/select_test.go @@ -1,226 +1,409 @@ -package meta +package meta_test import ( - "crypto/rand" + "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" - "github.com/nspcc-dev/neofs-node/pkg/core/object" + v2object "github.com/nspcc-dev/neofs-api-go/v2/object" "github.com/stretchr/testify/require" ) -func addNFilters(fs *objectSDK.SearchFilters, n int) { - 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) { +func TestDB_SelectUserAttributes(t *testing.T) { db := newDB(t) defer releaseDB(db) - obj := generateObject(t, testPrm{ - attrNum: 1, - }) + cid := testCID() - 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 - fs.AddFilter(a.Key(), a.Value()+"1", objectSDK.MatchStringEqual) + err = db.Put(raw2.Object(), nil) + require.NoError(t, err) - // 2nd - matching filter - fs.AddFilter(a.Key(), a.Value(), objectSDK.MatchStringEqual) + 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 addCommonAttribute(objs ...*object.Object) *objectSDK.Attribute { - aCommon := objectSDK.NewAttribute() - aCommon.SetKey("common key") - aCommon.SetValue("common value") +func TestDB_SelectRootPhyParent(t *testing.T) { + db := newDB(t) + defer releaseDB(db) - for _, o := range objs { - object.NewRawFromObject(o).SetAttributes( - append(o.Attributes(), aCommon)..., + 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(), ) - } - - 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 - obj2 := generateObject(t, testPrm{}) + 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(), + ) + }) - 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 - aCommon := addCommonAttribute(obj1, obj2) + 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()) + }) - // write to DB - require.NoError(t, db.Put(obj1)) - require.NoError(t, db.Put(obj2)) + 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()) + }) - 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 - fs.AddFilter(aCommon.Key(), aCommon.Value(), objectSDK.MatchStringEqual) + testSelect(t, db, fs, + rightChild.Object().Address(), + link.Object().Address(), + ) + }) - // next filter by attribute from 1st object only - fs.AddFilter(a1.Key(), a1.Value(), objectSDK.MatchStringEqual) - - testSelect(t, db, fs, obj1.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 TestSelectParentID(t *testing.T) { +func TestDB_SelectInhume(t *testing.T) { db := newDB(t) defer releaseDB(db) - // generate 2 objects - obj1 := generateObject(t, testPrm{}) - obj2 := generateObject(t, testPrm{}) + cid := testCID() - // set parent ID of 1st object - par := testOID() - object.NewRawFromObject(obj1).SetParentID(par) + raw1 := generateRawObjectWithCID(t, cid) + err := db.Put(raw1.Object(), nil) + require.NoError(t, err) - // store objects - require.NoError(t, db.Put(obj1)) - require.NoError(t, db.Put(obj2)) + raw2 := generateRawObjectWithCID(t, cid) + err = db.Put(raw2.Object(), nil) + require.NoError(t, err) - // filter by parent ID - fs := objectSDK.SearchFilters{} - fs.AddParentIDFilter(objectSDK.MatchStringEqual, par) + fs := generateSearchFilter(cid) + testSelect(t, db, fs, + 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) defer releaseDB(db) - // generate object - obj := generateObject(t, testPrm{}) + cid := testCID() - // store objects - require.NoError(t, db.Put(obj)) + raw1 := generateRawObjectWithCID(t, cid) + err := db.Put(raw1.Object(), nil) + require.NoError(t, err) - // filter by object ID - fs := objectSDK.SearchFilters{} - fs.AddObjectIDFilter(objectSDK.MatchStringEqual, obj.ID()) + raw2 := generateRawObjectWithCID(t, cid) + err = db.Put(raw2.Object(), nil) + 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) + }) } diff --git a/pkg/local_object_storage/metabase/v2/small.go b/pkg/local_object_storage/metabase/small.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/small.go rename to pkg/local_object_storage/metabase/small.go diff --git a/pkg/local_object_storage/metabase/v2/small_test.go b/pkg/local_object_storage/metabase/small_test.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/small_test.go rename to pkg/local_object_storage/metabase/small_test.go diff --git a/pkg/local_object_storage/metabase/v2/util.go b/pkg/local_object_storage/metabase/util.go similarity index 100% rename from pkg/local_object_storage/metabase/v2/util.go rename to pkg/local_object_storage/metabase/util.go diff --git a/pkg/local_object_storage/metabase/v2/db.go b/pkg/local_object_storage/metabase/v2/db.go deleted file mode 100644 index c55c70f9..00000000 --- a/pkg/local_object_storage/metabase/v2/db.go +++ /dev/null @@ -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 - } -} diff --git a/pkg/local_object_storage/metabase/v2/db_test.go b/pkg/local_object_storage/metabase/v2/db_test.go deleted file mode 100644 index 17452aae..00000000 --- a/pkg/local_object_storage/metabase/v2/db_test.go +++ /dev/null @@ -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...) -} diff --git a/pkg/local_object_storage/metabase/v2/delete.go b/pkg/local_object_storage/metabase/v2/delete.go deleted file mode 100644 index 913f0489..00000000 --- a/pkg/local_object_storage/metabase/v2/delete.go +++ /dev/null @@ -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 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 -} diff --git a/pkg/local_object_storage/metabase/v2/delete_test.go b/pkg/local_object_storage/metabase/v2/delete_test.go deleted file mode 100644 index c056a9b3..00000000 --- a/pkg/local_object_storage/metabase/v2/delete_test.go +++ /dev/null @@ -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) -} diff --git a/pkg/local_object_storage/metabase/v2/exists.go b/pkg/local_object_storage/metabase/v2/exists.go deleted file mode 100644 index 94ae99ec..00000000 --- a/pkg/local_object_storage/metabase/v2/exists.go +++ /dev/null @@ -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 is present in bucket . -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 -} diff --git a/pkg/local_object_storage/metabase/v2/get.go b/pkg/local_object_storage/metabase/v2/get.go deleted file mode 100644 index 695ad8fb..00000000 --- a/pkg/local_object_storage/metabase/v2/get.go +++ /dev/null @@ -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 -} diff --git a/pkg/local_object_storage/metabase/v2/get_test.go b/pkg/local_object_storage/metabase/v2/get_test.go deleted file mode 100644 index d0de02ef..00000000 --- a/pkg/local_object_storage/metabase/v2/get_test.go +++ /dev/null @@ -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 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 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) -} diff --git a/pkg/local_object_storage/metabase/v2/info.go b/pkg/local_object_storage/metabase/v2/info.go deleted file mode 100644 index 967e9c71..00000000 --- a/pkg/local_object_storage/metabase/v2/info.go +++ /dev/null @@ -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 -} diff --git a/pkg/local_object_storage/metabase/v2/put.go b/pkg/local_object_storage/metabase/v2/put.go deleted file mode 100644 index f7a7979c..00000000 --- a/pkg/local_object_storage/metabase/v2/put.go +++ /dev/null @@ -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 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 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 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 -} diff --git a/pkg/local_object_storage/metabase/v2/put_test.go b/pkg/local_object_storage/metabase/v2/put_test.go deleted file mode 100644 index 0e140b2b..00000000 --- a/pkg/local_object_storage/metabase/v2/put_test.go +++ /dev/null @@ -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) - }) -} diff --git a/pkg/local_object_storage/metabase/v2/select.go b/pkg/local_object_storage/metabase/v2/select.go deleted file mode 100644 index 1bb2f0a1..00000000 --- a/pkg/local_object_storage/metabase/v2/select.go +++ /dev/null @@ -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 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 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 - } -} diff --git a/pkg/local_object_storage/metabase/v2/select_test.go b/pkg/local_object_storage/metabase/v2/select_test.go deleted file mode 100644 index 3abbafc7..00000000 --- a/pkg/local_object_storage/metabase/v2/select_test.go +++ /dev/null @@ -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) - }) -} diff --git a/pkg/local_object_storage/shard/shard.go b/pkg/local_object_storage/shard/shard.go index 2579117f..96b0d088 100644 --- a/pkg/local_object_storage/shard/shard.go +++ b/pkg/local_object_storage/shard/shard.go @@ -2,7 +2,7 @@ package shard import ( "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" "go.uber.org/atomic" ) diff --git a/pkg/local_object_storage/shard/shard_test.go b/pkg/local_object_storage/shard/shard_test.go index 1fabd16a..0d243bc3 100644 --- a/pkg/local_object_storage/shard/shard_test.go +++ b/pkg/local_object_storage/shard/shard_test.go @@ -13,7 +13,7 @@ import ( "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/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/util/test" "github.com/nspcc-dev/tzhash/tz"