forked from TrueCloudLab/frostfs-node
Remove outdated code of metabase and localstore
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
parent
869d9e571c
commit
a875d80491
41 changed files with 1725 additions and 3123 deletions
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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...)
|
||||
}
|
||||
|
|
|
@ -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 <list> bucket
|
||||
if len(newLst) == 0 {
|
||||
_ = bkt.Delete(item.key) // ignore error, best effort there
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// if list is not empty, then update it
|
||||
encodedLst, err := encodeList(lst)
|
||||
if err != nil {
|
||||
return // ignore error, best effort there
|
||||
}
|
||||
|
||||
_ = bkt.Put(item.key, encodedLst) // ignore error, best effort there
|
||||
}
|
||||
|
||||
func delUniqueIndexes(obj *object.Object, isParent bool) ([]namedBucketItem, error) {
|
||||
addr := obj.Address()
|
||||
objKey := objectKey(addr.ObjectID())
|
||||
addrKey := addressKey(addr)
|
||||
|
||||
result := make([]namedBucketItem, 0, 5)
|
||||
|
||||
// add value to primary unique bucket
|
||||
if !isParent {
|
||||
var bucketName []byte
|
||||
|
||||
switch obj.Type() {
|
||||
case objectSDK.TypeRegular:
|
||||
bucketName = primaryBucketName(addr.ContainerID())
|
||||
case objectSDK.TypeTombstone:
|
||||
bucketName = tombstoneBucketName(addr.ContainerID())
|
||||
case objectSDK.TypeStorageGroup:
|
||||
bucketName = storageGroupBucketName(addr.ContainerID())
|
||||
default:
|
||||
return nil, ErrUnknownObjectType
|
||||
}
|
||||
|
||||
result = append(result, namedBucketItem{
|
||||
name: bucketName,
|
||||
key: objKey,
|
||||
})
|
||||
}
|
||||
|
||||
result = append(result,
|
||||
namedBucketItem{ // remove from small blobovnicza id index
|
||||
name: smallBucketName(addr.ContainerID()),
|
||||
key: objKey,
|
||||
},
|
||||
namedBucketItem{ // remove from root index
|
||||
name: rootBucketName(addr.ContainerID()),
|
||||
key: objKey,
|
||||
},
|
||||
namedBucketItem{ // remove from graveyard index
|
||||
name: graveyardBucketName,
|
||||
key: addrKey,
|
||||
},
|
||||
namedBucketItem{ // remove from ToMoveIt index
|
||||
name: toMoveItBucketName,
|
||||
key: addrKey,
|
||||
},
|
||||
)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 <key> is present in bucket <name>.
|
||||
func inBucket(tx *bbolt.Tx, name, key []byte) bool {
|
||||
bkt := tx.Bucket(name)
|
||||
if bkt == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// using `get` as `exists`: https://github.com/boltdb/bolt/issues/321
|
||||
val := bkt.Get(key)
|
||||
|
||||
return len(val) != 0
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 <nil> 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 <nil> and []{} lists.
|
||||
func binaryEqual(a, b *object.Object) bool {
|
||||
binaryA, err := a.Marshal()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
binaryB, err := b.Marshal()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return bytes.Equal(binaryA, binaryB)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 <unique> indexes from the object.
|
||||
func uniqueIndexes(obj *object.Object, si *objectSDK.SplitInfo, id *blobovnicza.ID) ([]namedBucketItem, error) {
|
||||
isParent := si != nil
|
||||
addr := obj.Address()
|
||||
objKey := objectKey(addr.ObjectID())
|
||||
result := make([]namedBucketItem, 0, 3)
|
||||
|
||||
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 <list> indexes from the object.
|
||||
func listIndexes(obj *object.Object) ([]namedBucketItem, error) {
|
||||
result := make([]namedBucketItem, 0, 3)
|
||||
addr := obj.Address()
|
||||
objKey := objectKey(addr.ObjectID())
|
||||
|
||||
// index payload hashes
|
||||
result = append(result, namedBucketItem{
|
||||
name: payloadHashBucketName(addr.ContainerID()),
|
||||
key: obj.PayloadChecksum().Sum(),
|
||||
val: objKey,
|
||||
})
|
||||
|
||||
// index parent ids
|
||||
if obj.ParentID() != nil {
|
||||
result = append(result, namedBucketItem{
|
||||
name: parentBucketName(addr.ContainerID()),
|
||||
key: objectKey(obj.ParentID()),
|
||||
val: objKey,
|
||||
})
|
||||
}
|
||||
|
||||
// index split ids
|
||||
if obj.SplitID() != nil {
|
||||
result = append(result, namedBucketItem{
|
||||
name: splitBucketName(addr.ContainerID()),
|
||||
key: obj.SplitID().ToV2(),
|
||||
val: objKey,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// builds list of <fake bucket tree> indexes from the object.
|
||||
func fkbtIndexes(obj *object.Object) ([]namedBucketItem, error) {
|
||||
addr := obj.Address()
|
||||
objKey := []byte(addr.ObjectID().String())
|
||||
|
||||
attrs := obj.Attributes()
|
||||
result := make([]namedBucketItem, 0, 1+len(attrs))
|
||||
|
||||
// owner
|
||||
result = append(result, namedBucketItem{
|
||||
name: ownerBucketName(addr.ContainerID()),
|
||||
key: []byte(obj.OwnerID().String()),
|
||||
val: objKey,
|
||||
})
|
||||
|
||||
// user specified attributes
|
||||
for i := range attrs {
|
||||
result = append(result, namedBucketItem{
|
||||
name: attributeBucketName(addr.ContainerID(), attrs[i].Key()),
|
||||
key: []byte(attrs[i].Value()),
|
||||
val: objKey,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func putUniqueIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
|
||||
bkt, err := tx.CreateBucketIfNotExists(item.name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create index %v: %w", item.name, err)
|
||||
}
|
||||
|
||||
return bkt.Put(item.key, item.val)
|
||||
}
|
||||
|
||||
func putFKBTIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
|
||||
bkt, err := tx.CreateBucketIfNotExists(item.name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create index %v: %w", item.name, err)
|
||||
}
|
||||
|
||||
fkbtRoot, err := bkt.CreateBucketIfNotExists(item.key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create fake bucket tree index %v: %w", item.key, err)
|
||||
}
|
||||
|
||||
return fkbtRoot.Put(item.val, zeroValue)
|
||||
}
|
||||
|
||||
func putListIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
|
||||
bkt, err := tx.CreateBucketIfNotExists(item.name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create index %v: %w", item.name, err)
|
||||
}
|
||||
|
||||
lst, err := decodeList(bkt.Get(item.key))
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't decode leaf list %v: %w", item.key, err)
|
||||
}
|
||||
|
||||
lst = append(lst, item.val)
|
||||
|
||||
encodedLst, err := encodeList(lst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't encode leaf list %v: %w", item.key, err)
|
||||
}
|
||||
|
||||
return bkt.Put(item.key, encodedLst)
|
||||
}
|
||||
|
||||
// encodeList decodes list of bytes into a single blog for list bucket indexes.
|
||||
func encodeList(lst [][]byte) ([]byte, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
encoder := gob.NewEncoder(buf)
|
||||
|
||||
// consider using protobuf encoding instead of glob
|
||||
if err := encoder.Encode(lst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// decodeList decodes blob into the list of bytes from list bucket index.
|
||||
func decodeList(data []byte) (lst [][]byte, err error) {
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
decoder := gob.NewDecoder(bytes.NewReader(data))
|
||||
if err := decoder.Decode(&lst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
// updateBlobovniczaID for existing objects if they were moved from from
|
||||
// one blobovnicza to another.
|
||||
func updateBlobovniczaID(tx *bbolt.Tx, addr *objectSDK.Address, id *blobovnicza.ID) error {
|
||||
bkt := tx.Bucket(smallBucketName(addr.ContainerID()))
|
||||
if bkt == nil {
|
||||
// if object exists, don't have blobovniczaID and we want to update it
|
||||
// then ignore, this should never happen
|
||||
return ErrIncorrectBlobovniczaUpdate
|
||||
}
|
||||
|
||||
objectKey := objectKey(addr.ObjectID())
|
||||
|
||||
if len(bkt.Get(objectKey)) == 0 {
|
||||
return ErrIncorrectBlobovniczaUpdate
|
||||
}
|
||||
|
||||
return bkt.Put(objectKey, *id)
|
||||
}
|
||||
|
||||
// updateSpliInfo for existing objects if storage filled with extra information
|
||||
// about last object in split hierarchy or linking object.
|
||||
func updateSplitInfo(tx *bbolt.Tx, addr *objectSDK.Address, from *objectSDK.SplitInfo) error {
|
||||
bkt := tx.Bucket(rootBucketName(addr.ContainerID()))
|
||||
if bkt == nil {
|
||||
// if object doesn't exists and we want to update split info on it
|
||||
// then ignore, this should never happen
|
||||
return ErrIncorrectSplitInfoUpdate
|
||||
}
|
||||
|
||||
objectKey := objectKey(addr.ObjectID())
|
||||
|
||||
rawSplitInfo := bkt.Get(objectKey)
|
||||
if len(rawSplitInfo) == 0 {
|
||||
return ErrIncorrectSplitInfoUpdate
|
||||
}
|
||||
|
||||
to := objectSDK.NewSplitInfo()
|
||||
|
||||
err := to.Unmarshal(rawSplitInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't unmarshal split info from root index: %w", err)
|
||||
}
|
||||
|
||||
result := mergeSplitInfo(from, to)
|
||||
|
||||
rawSplitInfo, err = result.Marshal()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marhsal merged split info: %w", err)
|
||||
}
|
||||
|
||||
return bkt.Put(objectKey, rawSplitInfo)
|
||||
}
|
||||
|
||||
// splitInfoFromObject returns split info based on last or linkin object.
|
||||
// Otherwise returns nil, nil.
|
||||
func splitInfoFromObject(obj *object.Object) (*objectSDK.SplitInfo, error) {
|
||||
if obj.Parent() == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
info := objectSDK.NewSplitInfo()
|
||||
info.SetSplitID(obj.SplitID())
|
||||
|
||||
switch {
|
||||
case isLinkObject(obj):
|
||||
info.SetLink(obj.ID())
|
||||
case isLastObject(obj):
|
||||
info.SetLastPart(obj.ID())
|
||||
default:
|
||||
return nil, ErrIncorrectRootObject // should never happen
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// mergeSplitInfo ignores conflicts and rewrites `to` with non empty values
|
||||
// from `from`.
|
||||
func mergeSplitInfo(from, to *objectSDK.SplitInfo) *objectSDK.SplitInfo {
|
||||
to.SetSplitID(from.SplitID()) // overwrite SplitID and ignore conflicts
|
||||
|
||||
if lp := from.LastPart(); lp != nil {
|
||||
to.SetLastPart(lp)
|
||||
}
|
||||
|
||||
if link := from.Link(); link != nil {
|
||||
to.SetLink(link)
|
||||
}
|
||||
|
||||
return to
|
||||
}
|
||||
|
||||
// isLinkObject returns true if object contains parent header and list
|
||||
// of children.
|
||||
func isLinkObject(obj *object.Object) bool {
|
||||
return len(obj.Children()) > 0 && obj.Parent() != nil
|
||||
}
|
||||
|
||||
// isLastObject returns true if object contains only parent header without list
|
||||
// of children.
|
||||
func isLastObject(obj *object.Object) bool {
|
||||
return len(obj.Children()) == 0 && obj.Parent() != nil
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 <fkbt> index to find list of addresses to add in
|
||||
// resulting cache.
|
||||
func (db *DB) selectFromFKBT(
|
||||
tx *bbolt.Tx,
|
||||
name []byte, // fkbt root bucket name
|
||||
f object.SearchFilter, // filter for operation and value
|
||||
prefix string, // prefix to create addr from oid in index
|
||||
to map[string]int, // resulting cache
|
||||
fNum int, // index of filter
|
||||
) { //
|
||||
matchFunc, ok := db.matchers[f.Operation()]
|
||||
if !ok {
|
||||
db.log.Debug("missing matcher", zap.Uint32("operation", uint32(f.Operation())))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fkbtRoot := tx.Bucket(name)
|
||||
if fkbtRoot == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := fkbtRoot.ForEach(func(k, _ []byte) error {
|
||||
if matchFunc(f.Header(), k, f.Value()) {
|
||||
fkbtLeaf := fkbtRoot.Bucket(k)
|
||||
if fkbtLeaf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fkbtLeaf.ForEach(func(k, _ []byte) error {
|
||||
addr := prefix + string(k)
|
||||
markAddressInCache(to, fNum, addr)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
db.log.Debug("error in FKBT selection", zap.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// selectFromList looks into <list> index to find list of addresses to add in
|
||||
// resulting cache.
|
||||
func (db *DB) selectFromList(
|
||||
tx *bbolt.Tx,
|
||||
name []byte, // list root bucket name
|
||||
f object.SearchFilter, // filter for operation and value
|
||||
prefix string, // prefix to create addr from oid in index
|
||||
to map[string]int, // resulting cache
|
||||
fNum int, // index of filter
|
||||
) { //
|
||||
bkt := tx.Bucket(name)
|
||||
if bkt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch f.Operation() {
|
||||
case object.MatchStringEqual:
|
||||
default:
|
||||
db.log.Debug("unknown operation", zap.Uint32("operation", uint32(f.Operation())))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// warning: it works only for MatchStringEQ, for NotEQ you should iterate over
|
||||
// bkt and apply matchFunc, don't forget to implement this when it will be
|
||||
// needed. Right now it is not efficient to iterate over bucket
|
||||
// when there is only MatchStringEQ.
|
||||
lst, err := decodeList(bkt.Get(bucketKeyHelper(f.Header(), f.Value())))
|
||||
if err != nil {
|
||||
db.log.Debug("can't decode list bucket leaf", zap.String("error", err.Error()))
|
||||
}
|
||||
|
||||
for i := range lst {
|
||||
addr := prefix + string(lst[i])
|
||||
markAddressInCache(to, fNum, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// selectObjectID processes objectID filter with in-place optimizations.
|
||||
func (db *DB) selectObjectID(
|
||||
tx *bbolt.Tx,
|
||||
f object.SearchFilter,
|
||||
prefix string,
|
||||
to map[string]int, // resulting cache
|
||||
fNum int, // index of filter
|
||||
) {
|
||||
switch f.Operation() {
|
||||
case object.MatchStringEqual:
|
||||
default:
|
||||
db.log.Debug("unknown operation", zap.Uint32("operation", uint32(f.Operation())))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// warning: it is in-place optimization and works only for MatchStringEQ,
|
||||
// for NotEQ you should iterate over bkt and apply matchFunc
|
||||
|
||||
addrStr := prefix + f.Value()
|
||||
addr := object.NewAddress()
|
||||
|
||||
err := addr.Parse(addrStr)
|
||||
if err != nil {
|
||||
db.log.Debug("can't decode object id address",
|
||||
zap.String("addr", addrStr),
|
||||
zap.String("error", err.Error()))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ok, err := db.exists(tx, addr)
|
||||
if (err == nil && ok) || errors.As(err, &splitInfoError) {
|
||||
markAddressInCache(to, fNum, addrStr)
|
||||
}
|
||||
}
|
||||
|
||||
// matchSlowFilters return true if object header is matched by all slow filters.
|
||||
func (db *DB) matchSlowFilters(tx *bbolt.Tx, addr *object.Address, f object.SearchFilters) bool {
|
||||
if len(f) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
obj, err := db.get(tx, addr, true)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range f {
|
||||
matchFunc, ok := db.matchers[f[i].Operation()]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
var data []byte
|
||||
|
||||
switch f[i].Header() {
|
||||
case v2object.FilterHeaderVersion:
|
||||
data = []byte(obj.Version().String())
|
||||
case v2object.FilterHeaderHomomorphicHash:
|
||||
data = obj.PayloadHomomorphicHash().Sum()
|
||||
case v2object.FilterHeaderCreationEpoch:
|
||||
data = make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(data, obj.CreationEpoch())
|
||||
case v2object.FilterHeaderPayloadLength:
|
||||
data = make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(data, obj.PayloadSize())
|
||||
default:
|
||||
continue // ignore unknown search attributes
|
||||
}
|
||||
|
||||
if !matchFunc(f[i].Header(), data, f[i].Value()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// groupFilters divides filters in two groups: fast and slow. Fast filters
|
||||
// processed by indexes and slow filters processed after by unmarshaling
|
||||
// object headers.
|
||||
func groupFilters(filters object.SearchFilters) (*filterGroup, error) {
|
||||
res := &filterGroup{
|
||||
fastFilters: make(object.SearchFilters, 0, len(filters)),
|
||||
slowFilters: make(object.SearchFilters, 0, len(filters)),
|
||||
}
|
||||
|
||||
for i := range filters {
|
||||
switch filters[i].Header() {
|
||||
case v2object.FilterHeaderContainerID:
|
||||
res.cid = container.NewID()
|
||||
|
||||
err := res.cid.Parse(filters[i].Value())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't parse container id: %w", err)
|
||||
}
|
||||
case // slow filters
|
||||
v2object.FilterHeaderVersion,
|
||||
v2object.FilterHeaderCreationEpoch,
|
||||
v2object.FilterHeaderPayloadLength,
|
||||
v2object.FilterHeaderHomomorphicHash:
|
||||
res.slowFilters = append(res.slowFilters, filters[i])
|
||||
default: // fast filters or user attributes if unknown
|
||||
res.fastFilters = append(res.fastFilters, filters[i])
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func markAddressInCache(cache map[string]int, fNum int, addr string) {
|
||||
if num := cache[addr]; num == fNum {
|
||||
cache[addr] = num + 1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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...)
|
||||
}
|
|
@ -1,217 +0,0 @@
|
|||
package meta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var ErrVirtualObject = errors.New("do not remove virtual object directly")
|
||||
|
||||
// DeleteObjects marks list of objects as deleted.
|
||||
func (db *DB) Delete(lst ...*objectSDK.Address) error {
|
||||
return db.boltDB.Update(func(tx *bbolt.Tx) error {
|
||||
for i := range lst {
|
||||
err := db.delete(tx, lst[i], false)
|
||||
if err != nil {
|
||||
return err // maybe log and continue?
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (db *DB) delete(tx *bbolt.Tx, addr *objectSDK.Address, isParent bool) error {
|
||||
pl := parentLength(tx, addr) // parentLength of address, for virtual objects it is > 0
|
||||
|
||||
// do not remove virtual objects directly
|
||||
if !isParent && pl > 0 {
|
||||
return ErrVirtualObject
|
||||
}
|
||||
|
||||
// unmarshal object
|
||||
obj, err := db.get(tx, addr, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if object is an only link to a parent, then remove parent
|
||||
if parent := obj.GetParent(); parent != nil {
|
||||
if parentLength(tx, parent.Address()) == 1 {
|
||||
err = db.deleteObject(tx, obj.GetParent(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove object
|
||||
return db.deleteObject(tx, obj, isParent)
|
||||
}
|
||||
|
||||
func (db *DB) deleteObject(
|
||||
tx *bbolt.Tx,
|
||||
obj *object.Object,
|
||||
isParent bool,
|
||||
) error {
|
||||
uniqueIndexes, err := delUniqueIndexes(obj, isParent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can' build unique indexes: %w", err)
|
||||
}
|
||||
|
||||
// delete unique indexes
|
||||
for i := range uniqueIndexes {
|
||||
delUniqueIndexItem(tx, uniqueIndexes[i])
|
||||
}
|
||||
|
||||
// build list indexes
|
||||
listIndexes, err := listIndexes(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can' build list indexes: %w", err)
|
||||
}
|
||||
|
||||
// delete list indexes
|
||||
for i := range listIndexes {
|
||||
delListIndexItem(tx, listIndexes[i])
|
||||
}
|
||||
|
||||
// build fake bucket tree indexes
|
||||
fkbtIndexes, err := fkbtIndexes(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can' build fake bucket tree indexes: %w", err)
|
||||
}
|
||||
|
||||
// delete fkbt indexes
|
||||
for i := range fkbtIndexes {
|
||||
delFKBTIndexItem(tx, fkbtIndexes[i])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parentLength returns amount of available children from parentid index.
|
||||
func parentLength(tx *bbolt.Tx, addr *objectSDK.Address) int {
|
||||
bkt := tx.Bucket(parentBucketName(addr.ContainerID()))
|
||||
if bkt == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
lst, err := decodeList(bkt.Get(objectKey(addr.ObjectID())))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return len(lst)
|
||||
}
|
||||
|
||||
func delUniqueIndexItem(tx *bbolt.Tx, item namedBucketItem) {
|
||||
bkt := tx.Bucket(item.name)
|
||||
if bkt != nil {
|
||||
_ = bkt.Delete(item.key) // ignore error, best effort there
|
||||
}
|
||||
}
|
||||
|
||||
func delFKBTIndexItem(tx *bbolt.Tx, item namedBucketItem) {
|
||||
bkt := tx.Bucket(item.name)
|
||||
if bkt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fkbtRoot := bkt.Bucket(item.key)
|
||||
if fkbtRoot == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = fkbtRoot.Delete(item.val) // ignore error, best effort there
|
||||
}
|
||||
|
||||
func delListIndexItem(tx *bbolt.Tx, item namedBucketItem) {
|
||||
bkt := tx.Bucket(item.name)
|
||||
if bkt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
lst, err := decodeList(bkt.Get(item.key))
|
||||
if err != nil || len(lst) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// remove element from the list
|
||||
newLst := make([][]byte, 0, len(lst))
|
||||
|
||||
for i := range lst {
|
||||
if !bytes.Equal(item.val, lst[i]) {
|
||||
newLst = append(newLst, lst[i])
|
||||
}
|
||||
}
|
||||
|
||||
// if list empty, remove the key from <list> bucket
|
||||
if len(newLst) == 0 {
|
||||
_ = bkt.Delete(item.key) // ignore error, best effort there
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// if list is not empty, then update it
|
||||
encodedLst, err := encodeList(lst)
|
||||
if err != nil {
|
||||
return // ignore error, best effort there
|
||||
}
|
||||
|
||||
_ = bkt.Put(item.key, encodedLst) // ignore error, best effort there
|
||||
}
|
||||
|
||||
func delUniqueIndexes(obj *object.Object, isParent bool) ([]namedBucketItem, error) {
|
||||
addr := obj.Address()
|
||||
objKey := objectKey(addr.ObjectID())
|
||||
addrKey := addressKey(addr)
|
||||
|
||||
result := make([]namedBucketItem, 0, 5)
|
||||
|
||||
// add value to primary unique bucket
|
||||
if !isParent {
|
||||
var bucketName []byte
|
||||
|
||||
switch obj.Type() {
|
||||
case objectSDK.TypeRegular:
|
||||
bucketName = primaryBucketName(addr.ContainerID())
|
||||
case objectSDK.TypeTombstone:
|
||||
bucketName = tombstoneBucketName(addr.ContainerID())
|
||||
case objectSDK.TypeStorageGroup:
|
||||
bucketName = storageGroupBucketName(addr.ContainerID())
|
||||
default:
|
||||
return nil, ErrUnknownObjectType
|
||||
}
|
||||
|
||||
result = append(result, namedBucketItem{
|
||||
name: bucketName,
|
||||
key: objKey,
|
||||
})
|
||||
}
|
||||
|
||||
result = append(result,
|
||||
namedBucketItem{ // remove from small blobovnicza id index
|
||||
name: smallBucketName(addr.ContainerID()),
|
||||
key: objKey,
|
||||
},
|
||||
namedBucketItem{ // remove from root index
|
||||
name: rootBucketName(addr.ContainerID()),
|
||||
key: objKey,
|
||||
},
|
||||
namedBucketItem{ // remove from graveyard index
|
||||
name: graveyardBucketName,
|
||||
key: addrKey,
|
||||
},
|
||||
namedBucketItem{ // remove from ToMoveIt index
|
||||
name: toMoveItBucketName,
|
||||
key: addrKey,
|
||||
},
|
||||
)
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
package meta
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var ErrLackSplitInfo = errors.New("no split info on parent object")
|
||||
|
||||
// Exists returns ErrAlreadyRemoved if addr was marked as removed. Otherwise it
|
||||
// returns true if addr is in primary index or false if it is not.
|
||||
func (db *DB) Exists(addr *objectSDK.Address) (exists bool, err error) {
|
||||
err = db.boltDB.View(func(tx *bbolt.Tx) error {
|
||||
exists, err = db.exists(tx, addr)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (db *DB) exists(tx *bbolt.Tx, addr *objectSDK.Address) (exists bool, err error) {
|
||||
// check graveyard first
|
||||
if inGraveyard(tx, addr) {
|
||||
return false, object.ErrAlreadyRemoved
|
||||
}
|
||||
|
||||
objKey := objectKey(addr.ObjectID())
|
||||
|
||||
// if graveyard is empty, then check if object exists in primary bucket
|
||||
if inBucket(tx, primaryBucketName(addr.ContainerID()), objKey) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// if primary bucket is empty, then check if object exists in parent bucket
|
||||
if inBucket(tx, parentBucketName(addr.ContainerID()), objKey) {
|
||||
rawSplitInfo := getFromBucket(tx, rootBucketName(addr.ContainerID()), objKey)
|
||||
if len(rawSplitInfo) == 0 {
|
||||
return false, ErrLackSplitInfo
|
||||
}
|
||||
|
||||
splitInfo := objectSDK.NewSplitInfo()
|
||||
|
||||
err := splitInfo.Unmarshal(rawSplitInfo)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("can't unmarshal split info from root index: %w", err)
|
||||
}
|
||||
|
||||
return false, objectSDK.NewSplitInfoError(splitInfo)
|
||||
}
|
||||
|
||||
// if parent bucket is empty, then check if object exists in tombstone bucket
|
||||
if inBucket(tx, tombstoneBucketName(addr.ContainerID()), objKey) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// if parent bucket is empty, then check if object exists in storage group bucket
|
||||
return inBucket(tx, storageGroupBucketName(addr.ContainerID()), objKey), nil
|
||||
}
|
||||
|
||||
// inGraveyard returns true if object was marked as removed.
|
||||
func inGraveyard(tx *bbolt.Tx, addr *objectSDK.Address) bool {
|
||||
graveyard := tx.Bucket(graveyardBucketName)
|
||||
if graveyard == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
tombstone := graveyard.Get(addressKey(addr))
|
||||
|
||||
return len(tombstone) != 0
|
||||
}
|
||||
|
||||
// inBucket checks if key <key> is present in bucket <name>.
|
||||
func inBucket(tx *bbolt.Tx, name, key []byte) bool {
|
||||
bkt := tx.Bucket(name)
|
||||
if bkt == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// using `get` as `exists`: https://github.com/boltdb/bolt/issues/321
|
||||
val := bkt.Get(key)
|
||||
|
||||
return len(val) != 0
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
package meta_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDB_Get(t *testing.T) {
|
||||
db := newDB(t)
|
||||
defer releaseDB(db)
|
||||
|
||||
raw := generateRawObject(t)
|
||||
|
||||
// equal fails on diff of <nil> attributes and <{}> attributes,
|
||||
/* so we make non empty attribute slice in parent*/
|
||||
addAttribute(raw, "foo", "bar")
|
||||
|
||||
t.Run("object not found", func(t *testing.T) {
|
||||
_, err := db.Get(raw.Object().Address())
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("put regular object", func(t *testing.T) {
|
||||
err := db.Put(raw.Object(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
newObj, err := db.Get(raw.Object().Address())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, raw.Object(), newObj)
|
||||
})
|
||||
|
||||
t.Run("put tombstone object", func(t *testing.T) {
|
||||
raw.SetType(objectSDK.TypeTombstone)
|
||||
raw.SetID(testOID())
|
||||
|
||||
err := db.Put(raw.Object(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
newObj, err := db.Get(raw.Object().Address())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, raw.Object(), newObj)
|
||||
})
|
||||
|
||||
t.Run("put storage group object", func(t *testing.T) {
|
||||
raw.SetType(objectSDK.TypeStorageGroup)
|
||||
raw.SetID(testOID())
|
||||
|
||||
err := db.Put(raw.Object(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
newObj, err := db.Get(raw.Object().Address())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, raw.Object(), newObj)
|
||||
})
|
||||
|
||||
t.Run("put virtual object", func(t *testing.T) {
|
||||
cid := testCID()
|
||||
parent := generateRawObjectWithCID(t, cid)
|
||||
addAttribute(parent, "foo", "bar")
|
||||
|
||||
child := generateRawObjectWithCID(t, cid)
|
||||
child.SetParent(parent.Object().SDK())
|
||||
child.SetParentID(parent.ID())
|
||||
|
||||
err := db.Put(child.Object(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
newParent, err := db.Get(parent.Object().Address())
|
||||
require.NoError(t, err)
|
||||
require.True(t, binaryEqual(parent.Object(), newParent))
|
||||
|
||||
newChild, err := db.Get(child.Object().Address())
|
||||
require.NoError(t, err)
|
||||
require.True(t, binaryEqual(child.Object(), newChild))
|
||||
})
|
||||
}
|
||||
|
||||
// binary equal is used when object contains empty lists in the structure and
|
||||
// requre.Equal fails on comparing <nil> and []{} lists.
|
||||
func binaryEqual(a, b *object.Object) bool {
|
||||
binaryA, err := a.Marshal()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
binaryB, err := b.Marshal()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return bytes.Equal(binaryA, binaryB)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,424 +0,0 @@
|
|||
package meta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobovnicza"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type (
|
||||
namedBucketItem struct {
|
||||
name, key, val []byte
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownObjectType = errors.New("unknown object type")
|
||||
ErrIncorrectBlobovniczaUpdate = errors.New("updating blobovnicza id on object without it")
|
||||
ErrIncorrectSplitInfoUpdate = errors.New("updating split info on object without it")
|
||||
ErrIncorrectRootObject = errors.New("invalid root object")
|
||||
)
|
||||
|
||||
// Put saves object header in metabase. Object payload expected to be cut.
|
||||
// Big objects have nil blobovniczaID.
|
||||
func (db *DB) Put(obj *object.Object, id *blobovnicza.ID) error {
|
||||
return db.boltDB.Update(func(tx *bbolt.Tx) error {
|
||||
return db.put(tx, obj, id, nil)
|
||||
})
|
||||
}
|
||||
|
||||
func (db *DB) put(tx *bbolt.Tx, obj *object.Object, id *blobovnicza.ID, si *objectSDK.SplitInfo) error {
|
||||
isParent := si != nil
|
||||
|
||||
exists, err := db.exists(tx, obj.Address())
|
||||
|
||||
if errors.As(err, &splitInfoError) {
|
||||
exists = true // object exists, however it is virtual
|
||||
} else if err != nil {
|
||||
return err // return any error besides SplitInfoError
|
||||
}
|
||||
|
||||
// most right child and split header overlap parent so we have to
|
||||
// check if object exists to not overwrite it twice
|
||||
if exists {
|
||||
// when storage engine moves small objects from one blobovniczaID
|
||||
// to another, then it calls metabase.Put method with new blobovniczaID
|
||||
// and this code should be triggered
|
||||
if !isParent && id != nil {
|
||||
return updateBlobovniczaID(tx, obj.Address(), id)
|
||||
}
|
||||
|
||||
// when storage already has last object in split hierarchy and there is
|
||||
// a linking object to put (or vice versa), we should update split info
|
||||
// with object ids of these objects
|
||||
if isParent {
|
||||
return updateSplitInfo(tx, obj.Address(), si)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if obj.GetParent() != nil && !isParent { // limit depth by two
|
||||
parentSI, err := splitInfoFromObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.put(tx, obj.GetParent(), id, parentSI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// build unique indexes
|
||||
uniqueIndexes, err := uniqueIndexes(obj, si, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can' build unique indexes: %w", err)
|
||||
}
|
||||
|
||||
// put unique indexes
|
||||
for i := range uniqueIndexes {
|
||||
err := putUniqueIndexItem(tx, uniqueIndexes[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// build list indexes
|
||||
listIndexes, err := listIndexes(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can' build list indexes: %w", err)
|
||||
}
|
||||
|
||||
// put list indexes
|
||||
for i := range listIndexes {
|
||||
err := putListIndexItem(tx, listIndexes[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// build fake bucket tree indexes
|
||||
fkbtIndexes, err := fkbtIndexes(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can' build fake bucket tree indexes: %w", err)
|
||||
}
|
||||
|
||||
// put fake bucket tree indexes
|
||||
for i := range fkbtIndexes {
|
||||
err := putFKBTIndexItem(tx, fkbtIndexes[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// builds list of <unique> indexes from the object.
|
||||
func uniqueIndexes(obj *object.Object, si *objectSDK.SplitInfo, id *blobovnicza.ID) ([]namedBucketItem, error) {
|
||||
isParent := si != nil
|
||||
addr := obj.Address()
|
||||
objKey := objectKey(addr.ObjectID())
|
||||
result := make([]namedBucketItem, 0, 3)
|
||||
|
||||
// add value to primary unique bucket
|
||||
if !isParent {
|
||||
var bucketName []byte
|
||||
|
||||
switch obj.Type() {
|
||||
case objectSDK.TypeRegular:
|
||||
bucketName = primaryBucketName(addr.ContainerID())
|
||||
case objectSDK.TypeTombstone:
|
||||
bucketName = tombstoneBucketName(addr.ContainerID())
|
||||
case objectSDK.TypeStorageGroup:
|
||||
bucketName = storageGroupBucketName(addr.ContainerID())
|
||||
default:
|
||||
return nil, ErrUnknownObjectType
|
||||
}
|
||||
|
||||
rawObject, err := obj.Marshal()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't marshal object header: %w", err)
|
||||
}
|
||||
|
||||
result = append(result, namedBucketItem{
|
||||
name: bucketName,
|
||||
key: objKey,
|
||||
val: rawObject,
|
||||
})
|
||||
|
||||
// index blobovniczaID if it is present
|
||||
if id != nil {
|
||||
result = append(result, namedBucketItem{
|
||||
name: smallBucketName(addr.ContainerID()),
|
||||
key: objKey,
|
||||
val: *id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// index root object
|
||||
if obj.Type() == objectSDK.TypeRegular && !obj.HasParent() {
|
||||
var (
|
||||
err error
|
||||
splitInfo []byte
|
||||
)
|
||||
|
||||
if isParent {
|
||||
splitInfo, err = si.Marshal()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't marshal split info: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, namedBucketItem{
|
||||
name: rootBucketName(addr.ContainerID()),
|
||||
key: objKey,
|
||||
val: splitInfo,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// builds list of <list> indexes from the object.
|
||||
func listIndexes(obj *object.Object) ([]namedBucketItem, error) {
|
||||
result := make([]namedBucketItem, 0, 3)
|
||||
addr := obj.Address()
|
||||
objKey := objectKey(addr.ObjectID())
|
||||
|
||||
// index payload hashes
|
||||
result = append(result, namedBucketItem{
|
||||
name: payloadHashBucketName(addr.ContainerID()),
|
||||
key: obj.PayloadChecksum().Sum(),
|
||||
val: objKey,
|
||||
})
|
||||
|
||||
// index parent ids
|
||||
if obj.ParentID() != nil {
|
||||
result = append(result, namedBucketItem{
|
||||
name: parentBucketName(addr.ContainerID()),
|
||||
key: objectKey(obj.ParentID()),
|
||||
val: objKey,
|
||||
})
|
||||
}
|
||||
|
||||
// index split ids
|
||||
if obj.SplitID() != nil {
|
||||
result = append(result, namedBucketItem{
|
||||
name: splitBucketName(addr.ContainerID()),
|
||||
key: obj.SplitID().ToV2(),
|
||||
val: objKey,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// builds list of <fake bucket tree> indexes from the object.
|
||||
func fkbtIndexes(obj *object.Object) ([]namedBucketItem, error) {
|
||||
addr := obj.Address()
|
||||
objKey := []byte(addr.ObjectID().String())
|
||||
|
||||
attrs := obj.Attributes()
|
||||
result := make([]namedBucketItem, 0, 1+len(attrs))
|
||||
|
||||
// owner
|
||||
result = append(result, namedBucketItem{
|
||||
name: ownerBucketName(addr.ContainerID()),
|
||||
key: []byte(obj.OwnerID().String()),
|
||||
val: objKey,
|
||||
})
|
||||
|
||||
// user specified attributes
|
||||
for i := range attrs {
|
||||
result = append(result, namedBucketItem{
|
||||
name: attributeBucketName(addr.ContainerID(), attrs[i].Key()),
|
||||
key: []byte(attrs[i].Value()),
|
||||
val: objKey,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func putUniqueIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
|
||||
bkt, err := tx.CreateBucketIfNotExists(item.name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create index %v: %w", item.name, err)
|
||||
}
|
||||
|
||||
return bkt.Put(item.key, item.val)
|
||||
}
|
||||
|
||||
func putFKBTIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
|
||||
bkt, err := tx.CreateBucketIfNotExists(item.name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create index %v: %w", item.name, err)
|
||||
}
|
||||
|
||||
fkbtRoot, err := bkt.CreateBucketIfNotExists(item.key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create fake bucket tree index %v: %w", item.key, err)
|
||||
}
|
||||
|
||||
return fkbtRoot.Put(item.val, zeroValue)
|
||||
}
|
||||
|
||||
func putListIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
|
||||
bkt, err := tx.CreateBucketIfNotExists(item.name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create index %v: %w", item.name, err)
|
||||
}
|
||||
|
||||
lst, err := decodeList(bkt.Get(item.key))
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't decode leaf list %v: %w", item.key, err)
|
||||
}
|
||||
|
||||
lst = append(lst, item.val)
|
||||
|
||||
encodedLst, err := encodeList(lst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't encode leaf list %v: %w", item.key, err)
|
||||
}
|
||||
|
||||
return bkt.Put(item.key, encodedLst)
|
||||
}
|
||||
|
||||
// encodeList decodes list of bytes into a single blog for list bucket indexes.
|
||||
func encodeList(lst [][]byte) ([]byte, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
encoder := gob.NewEncoder(buf)
|
||||
|
||||
// consider using protobuf encoding instead of glob
|
||||
if err := encoder.Encode(lst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// decodeList decodes blob into the list of bytes from list bucket index.
|
||||
func decodeList(data []byte) (lst [][]byte, err error) {
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
decoder := gob.NewDecoder(bytes.NewReader(data))
|
||||
if err := decoder.Decode(&lst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
// updateBlobovniczaID for existing objects if they were moved from from
|
||||
// one blobovnicza to another.
|
||||
func updateBlobovniczaID(tx *bbolt.Tx, addr *objectSDK.Address, id *blobovnicza.ID) error {
|
||||
bkt := tx.Bucket(smallBucketName(addr.ContainerID()))
|
||||
if bkt == nil {
|
||||
// if object exists, don't have blobovniczaID and we want to update it
|
||||
// then ignore, this should never happen
|
||||
return ErrIncorrectBlobovniczaUpdate
|
||||
}
|
||||
|
||||
objectKey := objectKey(addr.ObjectID())
|
||||
|
||||
if len(bkt.Get(objectKey)) == 0 {
|
||||
return ErrIncorrectBlobovniczaUpdate
|
||||
}
|
||||
|
||||
return bkt.Put(objectKey, *id)
|
||||
}
|
||||
|
||||
// updateSpliInfo for existing objects if storage filled with extra information
|
||||
// about last object in split hierarchy or linking object.
|
||||
func updateSplitInfo(tx *bbolt.Tx, addr *objectSDK.Address, from *objectSDK.SplitInfo) error {
|
||||
bkt := tx.Bucket(rootBucketName(addr.ContainerID()))
|
||||
if bkt == nil {
|
||||
// if object doesn't exists and we want to update split info on it
|
||||
// then ignore, this should never happen
|
||||
return ErrIncorrectSplitInfoUpdate
|
||||
}
|
||||
|
||||
objectKey := objectKey(addr.ObjectID())
|
||||
|
||||
rawSplitInfo := bkt.Get(objectKey)
|
||||
if len(rawSplitInfo) == 0 {
|
||||
return ErrIncorrectSplitInfoUpdate
|
||||
}
|
||||
|
||||
to := objectSDK.NewSplitInfo()
|
||||
|
||||
err := to.Unmarshal(rawSplitInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't unmarshal split info from root index: %w", err)
|
||||
}
|
||||
|
||||
result := mergeSplitInfo(from, to)
|
||||
|
||||
rawSplitInfo, err = result.Marshal()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marhsal merged split info: %w", err)
|
||||
}
|
||||
|
||||
return bkt.Put(objectKey, rawSplitInfo)
|
||||
}
|
||||
|
||||
// splitInfoFromObject returns split info based on last or linkin object.
|
||||
// Otherwise returns nil, nil.
|
||||
func splitInfoFromObject(obj *object.Object) (*objectSDK.SplitInfo, error) {
|
||||
if obj.Parent() == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
info := objectSDK.NewSplitInfo()
|
||||
info.SetSplitID(obj.SplitID())
|
||||
|
||||
switch {
|
||||
case isLinkObject(obj):
|
||||
info.SetLink(obj.ID())
|
||||
case isLastObject(obj):
|
||||
info.SetLastPart(obj.ID())
|
||||
default:
|
||||
return nil, ErrIncorrectRootObject // should never happen
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// mergeSplitInfo ignores conflicts and rewrites `to` with non empty values
|
||||
// from `from`.
|
||||
func mergeSplitInfo(from, to *objectSDK.SplitInfo) *objectSDK.SplitInfo {
|
||||
to.SetSplitID(from.SplitID()) // overwrite SplitID and ignore conflicts
|
||||
|
||||
if lp := from.LastPart(); lp != nil {
|
||||
to.SetLastPart(lp)
|
||||
}
|
||||
|
||||
if link := from.Link(); link != nil {
|
||||
to.SetLink(link)
|
||||
}
|
||||
|
||||
return to
|
||||
}
|
||||
|
||||
// isLinkObject returns true if object contains parent header and list
|
||||
// of children.
|
||||
func isLinkObject(obj *object.Object) bool {
|
||||
return len(obj.Children()) > 0 && obj.Parent() != nil
|
||||
}
|
||||
|
||||
// isLastObject returns true if object contains only parent header without list
|
||||
// of children.
|
||||
func isLastObject(obj *object.Object) bool {
|
||||
return len(obj.Children()) == 0 && obj.Parent() != nil
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -1,375 +0,0 @@
|
|||
package meta
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/pkg/container"
|
||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||
"github.com/pkg/errors"
|
||||
"go.etcd.io/bbolt"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
// filterGroup is a structure that have search filters grouped by access
|
||||
// method. We have fast filters that looks for indexes and do not unmarshal
|
||||
// objects, and slow filters, that applied after fast filters created
|
||||
// smaller set of objects to check.
|
||||
filterGroup struct {
|
||||
cid *container.ID
|
||||
fastFilters object.SearchFilters
|
||||
slowFilters object.SearchFilters
|
||||
}
|
||||
)
|
||||
|
||||
var ErrContainerNotInQuery = errors.New("search query does not contain container id filter")
|
||||
|
||||
// Select returns list of addresses of objects that match search filters.
|
||||
func (db *DB) Select(fs object.SearchFilters) (res []*object.Address, err error) {
|
||||
err = db.boltDB.View(func(tx *bbolt.Tx) error {
|
||||
res, err = db.selectObjects(tx, fs)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (db *DB) selectObjects(tx *bbolt.Tx, fs object.SearchFilters) ([]*object.Address, error) {
|
||||
group, err := groupFilters(fs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if group.cid == nil {
|
||||
return nil, ErrContainerNotInQuery
|
||||
}
|
||||
|
||||
// keep matched addresses in this cache
|
||||
// value equal to number (index+1) of latest matched filter
|
||||
mAddr := make(map[string]int)
|
||||
|
||||
expLen := len(group.fastFilters) // expected value of matched filters in mAddr
|
||||
|
||||
if len(group.fastFilters) == 0 {
|
||||
expLen = 1
|
||||
|
||||
db.selectAll(tx, group.cid, mAddr)
|
||||
} else {
|
||||
for i := range group.fastFilters {
|
||||
db.selectFastFilter(tx, group.cid, group.fastFilters[i], mAddr, i)
|
||||
}
|
||||
}
|
||||
|
||||
res := make([]*object.Address, 0, len(mAddr))
|
||||
|
||||
for a, ind := range mAddr {
|
||||
if ind != expLen {
|
||||
continue // ignore objects with unmatched fast filters
|
||||
}
|
||||
|
||||
addr := object.NewAddress()
|
||||
if err := addr.Parse(a); err != nil {
|
||||
// TODO: storage was broken, so we need to handle it
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if inGraveyard(tx, addr) {
|
||||
continue // ignore removed objects
|
||||
}
|
||||
|
||||
if !db.matchSlowFilters(tx, addr, group.slowFilters) {
|
||||
continue // ignore objects with unmatched slow filters
|
||||
}
|
||||
|
||||
res = append(res, addr)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// selectAll adds to resulting cache all available objects in metabase.
|
||||
func (db *DB) selectAll(tx *bbolt.Tx, cid *container.ID, to map[string]int) {
|
||||
prefix := cid.String() + "/"
|
||||
|
||||
selectAllFromBucket(tx, primaryBucketName(cid), prefix, to, 0)
|
||||
selectAllFromBucket(tx, tombstoneBucketName(cid), prefix, to, 0)
|
||||
selectAllFromBucket(tx, storageGroupBucketName(cid), prefix, to, 0)
|
||||
selectAllFromBucket(tx, parentBucketName(cid), prefix, to, 0)
|
||||
}
|
||||
|
||||
// selectAllFromBucket goes through all keys in bucket and adds them in a
|
||||
// resulting cache. Keys should be stringed object ids.
|
||||
func selectAllFromBucket(tx *bbolt.Tx, name []byte, prefix string, to map[string]int, fNum int) {
|
||||
bkt := tx.Bucket(name)
|
||||
if bkt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = bkt.ForEach(func(k, v []byte) error {
|
||||
key := prefix + string(k) // consider using string builders from sync.Pool
|
||||
markAddressInCache(to, fNum, key)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// selectFastFilter makes fast optimized checks for well known buckets or
|
||||
// looking through user attribute buckets otherwise.
|
||||
func (db *DB) selectFastFilter(
|
||||
tx *bbolt.Tx,
|
||||
cid *container.ID, // container we search on
|
||||
f object.SearchFilter, // fast filter
|
||||
to map[string]int, // resulting cache
|
||||
fNum int, // index of filter
|
||||
) {
|
||||
prefix := cid.String() + "/"
|
||||
|
||||
switch f.Header() {
|
||||
case v2object.FilterHeaderObjectID:
|
||||
db.selectObjectID(tx, f, prefix, to, fNum)
|
||||
case v2object.FilterHeaderOwnerID:
|
||||
bucketName := ownerBucketName(cid)
|
||||
db.selectFromFKBT(tx, bucketName, f, prefix, to, fNum)
|
||||
case v2object.FilterHeaderPayloadHash:
|
||||
bucketName := payloadHashBucketName(cid)
|
||||
db.selectFromList(tx, bucketName, f, prefix, to, fNum)
|
||||
case v2object.FilterHeaderObjectType:
|
||||
var bucketName []byte
|
||||
|
||||
switch f.Value() { // do it better after https://github.com/nspcc-dev/neofs-api/issues/84
|
||||
case "Regular":
|
||||
bucketName = primaryBucketName(cid)
|
||||
|
||||
selectAllFromBucket(tx, bucketName, prefix, to, fNum)
|
||||
|
||||
bucketName = parentBucketName(cid)
|
||||
case "Tombstone":
|
||||
bucketName = tombstoneBucketName(cid)
|
||||
case "StorageGroup":
|
||||
bucketName = storageGroupBucketName(cid)
|
||||
default:
|
||||
db.log.Debug("unknown object type", zap.String("type", f.Value()))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
selectAllFromBucket(tx, bucketName, prefix, to, fNum)
|
||||
case v2object.FilterHeaderParent:
|
||||
bucketName := parentBucketName(cid)
|
||||
db.selectFromList(tx, bucketName, f, prefix, to, fNum)
|
||||
case v2object.FilterHeaderSplitID:
|
||||
bucketName := splitBucketName(cid)
|
||||
db.selectFromList(tx, bucketName, f, prefix, to, fNum)
|
||||
case v2object.FilterPropertyRoot:
|
||||
selectAllFromBucket(tx, rootBucketName(cid), prefix, to, fNum)
|
||||
case v2object.FilterPropertyPhy:
|
||||
selectAllFromBucket(tx, primaryBucketName(cid), prefix, to, fNum)
|
||||
selectAllFromBucket(tx, tombstoneBucketName(cid), prefix, to, fNum)
|
||||
selectAllFromBucket(tx, storageGroupBucketName(cid), prefix, to, fNum)
|
||||
default: // user attribute
|
||||
bucketName := attributeBucketName(cid, f.Header())
|
||||
db.selectFromFKBT(tx, bucketName, f, prefix, to, fNum)
|
||||
}
|
||||
}
|
||||
|
||||
// selectFromList looks into <fkbt> index to find list of addresses to add in
|
||||
// resulting cache.
|
||||
func (db *DB) selectFromFKBT(
|
||||
tx *bbolt.Tx,
|
||||
name []byte, // fkbt root bucket name
|
||||
f object.SearchFilter, // filter for operation and value
|
||||
prefix string, // prefix to create addr from oid in index
|
||||
to map[string]int, // resulting cache
|
||||
fNum int, // index of filter
|
||||
) { //
|
||||
matchFunc, ok := db.matchers[f.Operation()]
|
||||
if !ok {
|
||||
db.log.Debug("missing matcher", zap.Uint32("operation", uint32(f.Operation())))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fkbtRoot := tx.Bucket(name)
|
||||
if fkbtRoot == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := fkbtRoot.ForEach(func(k, _ []byte) error {
|
||||
if matchFunc(f.Header(), k, f.Value()) {
|
||||
fkbtLeaf := fkbtRoot.Bucket(k)
|
||||
if fkbtLeaf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fkbtLeaf.ForEach(func(k, _ []byte) error {
|
||||
addr := prefix + string(k)
|
||||
markAddressInCache(to, fNum, addr)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
db.log.Debug("error in FKBT selection", zap.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// selectFromList looks into <list> index to find list of addresses to add in
|
||||
// resulting cache.
|
||||
func (db *DB) selectFromList(
|
||||
tx *bbolt.Tx,
|
||||
name []byte, // list root bucket name
|
||||
f object.SearchFilter, // filter for operation and value
|
||||
prefix string, // prefix to create addr from oid in index
|
||||
to map[string]int, // resulting cache
|
||||
fNum int, // index of filter
|
||||
) { //
|
||||
bkt := tx.Bucket(name)
|
||||
if bkt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch f.Operation() {
|
||||
case object.MatchStringEqual:
|
||||
default:
|
||||
db.log.Debug("unknown operation", zap.Uint32("operation", uint32(f.Operation())))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// warning: it works only for MatchStringEQ, for NotEQ you should iterate over
|
||||
// bkt and apply matchFunc, don't forget to implement this when it will be
|
||||
// needed. Right now it is not efficient to iterate over bucket
|
||||
// when there is only MatchStringEQ.
|
||||
lst, err := decodeList(bkt.Get(bucketKeyHelper(f.Header(), f.Value())))
|
||||
if err != nil {
|
||||
db.log.Debug("can't decode list bucket leaf", zap.String("error", err.Error()))
|
||||
}
|
||||
|
||||
for i := range lst {
|
||||
addr := prefix + string(lst[i])
|
||||
markAddressInCache(to, fNum, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// selectObjectID processes objectID filter with in-place optimizations.
|
||||
func (db *DB) selectObjectID(
|
||||
tx *bbolt.Tx,
|
||||
f object.SearchFilter,
|
||||
prefix string,
|
||||
to map[string]int, // resulting cache
|
||||
fNum int, // index of filter
|
||||
) {
|
||||
switch f.Operation() {
|
||||
case object.MatchStringEqual:
|
||||
default:
|
||||
db.log.Debug("unknown operation", zap.Uint32("operation", uint32(f.Operation())))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// warning: it is in-place optimization and works only for MatchStringEQ,
|
||||
// for NotEQ you should iterate over bkt and apply matchFunc
|
||||
|
||||
addrStr := prefix + f.Value()
|
||||
addr := object.NewAddress()
|
||||
|
||||
err := addr.Parse(addrStr)
|
||||
if err != nil {
|
||||
db.log.Debug("can't decode object id address",
|
||||
zap.String("addr", addrStr),
|
||||
zap.String("error", err.Error()))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ok, err := db.exists(tx, addr)
|
||||
if (err == nil && ok) || errors.As(err, &splitInfoError) {
|
||||
markAddressInCache(to, fNum, addrStr)
|
||||
}
|
||||
}
|
||||
|
||||
// matchSlowFilters return true if object header is matched by all slow filters.
|
||||
func (db *DB) matchSlowFilters(tx *bbolt.Tx, addr *object.Address, f object.SearchFilters) bool {
|
||||
if len(f) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
obj, err := db.get(tx, addr, true)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range f {
|
||||
matchFunc, ok := db.matchers[f[i].Operation()]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
var data []byte
|
||||
|
||||
switch f[i].Header() {
|
||||
case v2object.FilterHeaderVersion:
|
||||
data = []byte(obj.Version().String())
|
||||
case v2object.FilterHeaderHomomorphicHash:
|
||||
data = obj.PayloadHomomorphicHash().Sum()
|
||||
case v2object.FilterHeaderCreationEpoch:
|
||||
data = make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(data, obj.CreationEpoch())
|
||||
case v2object.FilterHeaderPayloadLength:
|
||||
data = make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(data, obj.PayloadSize())
|
||||
default:
|
||||
continue // ignore unknown search attributes
|
||||
}
|
||||
|
||||
if !matchFunc(f[i].Header(), data, f[i].Value()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// groupFilters divides filters in two groups: fast and slow. Fast filters
|
||||
// processed by indexes and slow filters processed after by unmarshaling
|
||||
// object headers.
|
||||
func groupFilters(filters object.SearchFilters) (*filterGroup, error) {
|
||||
res := &filterGroup{
|
||||
fastFilters: make(object.SearchFilters, 0, len(filters)),
|
||||
slowFilters: make(object.SearchFilters, 0, len(filters)),
|
||||
}
|
||||
|
||||
for i := range filters {
|
||||
switch filters[i].Header() {
|
||||
case v2object.FilterHeaderContainerID:
|
||||
res.cid = container.NewID()
|
||||
|
||||
err := res.cid.Parse(filters[i].Value())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't parse container id: %w", err)
|
||||
}
|
||||
case // slow filters
|
||||
v2object.FilterHeaderVersion,
|
||||
v2object.FilterHeaderCreationEpoch,
|
||||
v2object.FilterHeaderPayloadLength,
|
||||
v2object.FilterHeaderHomomorphicHash:
|
||||
res.slowFilters = append(res.slowFilters, filters[i])
|
||||
default: // fast filters or user attributes if unknown
|
||||
res.fastFilters = append(res.fastFilters, filters[i])
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func markAddressInCache(cache map[string]int, fNum int, addr string) {
|
||||
if num := cache[addr]; num == fNum {
|
||||
cache[addr] = num + 1
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue