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"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/bucket/fsbucket"
|
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/bucket/fsbucket"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine"
|
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/engine"
|
||||||
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase/v2"
|
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/shard"
|
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/shard"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/morph/client"
|
"github.com/nspcc-dev/neofs-node/pkg/morph/client"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/morph/client/container/wrapper"
|
"github.com/nspcc-dev/neofs-node/pkg/morph/client/container/wrapper"
|
||||||
|
|
|
@ -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
|
package meta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/util/logger"
|
"github.com/nspcc-dev/neofs-node/pkg/util/logger"
|
||||||
|
@ -10,30 +15,36 @@ import (
|
||||||
|
|
||||||
// DB represents local metabase of storage node.
|
// DB represents local metabase of storage node.
|
||||||
type DB struct {
|
type DB struct {
|
||||||
info Info
|
|
||||||
|
|
||||||
*cfg
|
*cfg
|
||||||
|
|
||||||
matchers map[object.SearchMatchType]func(string, string, string) bool
|
matchers map[object.SearchMatchType]func(string, []byte, string) bool
|
||||||
|
|
||||||
|
boltDB *bbolt.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option is an option of DB constructor.
|
// Option is an option of DB constructor.
|
||||||
type Option func(*cfg)
|
type Option func(*cfg)
|
||||||
|
|
||||||
type cfg struct {
|
type cfg struct {
|
||||||
boltDB *bbolt.DB
|
boltOptions *bbolt.Options // optional
|
||||||
|
|
||||||
|
info Info
|
||||||
|
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultCfg() *cfg {
|
func defaultCfg() *cfg {
|
||||||
return &cfg{
|
return &cfg{
|
||||||
|
info: Info{
|
||||||
|
Permission: os.ModePerm, // 0777
|
||||||
|
},
|
||||||
|
|
||||||
log: zap.L(),
|
log: zap.L(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDB creates, initializes and returns DB instance.
|
// New creates and returns new Metabase instance.
|
||||||
func NewDB(opts ...Option) *DB {
|
func New(opts ...Option) *DB {
|
||||||
c := defaultCfg()
|
c := defaultCfg()
|
||||||
|
|
||||||
for i := range opts {
|
for i := range opts {
|
||||||
|
@ -41,43 +52,51 @@ func NewDB(opts ...Option) *DB {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DB{
|
return &DB{
|
||||||
info: Info{
|
|
||||||
Path: c.boltDB.Path(),
|
|
||||||
},
|
|
||||||
cfg: c,
|
cfg: c,
|
||||||
matchers: map[object.SearchMatchType]func(string, string, string) bool{
|
matchers: map[object.SearchMatchType]func(string, []byte, string) bool{
|
||||||
object.MatchUnknown: unknownMatcher,
|
object.MatchUnknown: unknownMatcher,
|
||||||
object.MatchStringEqual: stringEqualMatcher,
|
object.MatchStringEqual: stringEqualMatcher,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) Close() error {
|
func stringEqualMatcher(key string, objVal []byte, filterVal string) bool {
|
||||||
return db.boltDB.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringEqualMatcher(key, objVal, filterVal string) bool {
|
|
||||||
switch key {
|
switch key {
|
||||||
default:
|
default:
|
||||||
return objVal == filterVal
|
return string(objVal) == filterVal
|
||||||
case v2object.FilterPropertyPhy, v2object.FilterPropertyRoot:
|
case v2object.FilterHeaderPayloadHash, v2object.FilterHeaderHomomorphicHash:
|
||||||
return true
|
return hex.EncodeToString(objVal) == filterVal
|
||||||
|
case v2object.FilterHeaderCreationEpoch, v2object.FilterHeaderPayloadLength:
|
||||||
|
return strconv.FormatUint(binary.LittleEndian.Uint64(objVal), 10) == filterVal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func unknownMatcher(key, _, _ string) bool {
|
func unknownMatcher(_ string, _ []byte, _ string) bool {
|
||||||
switch key {
|
return false
|
||||||
default:
|
|
||||||
return false
|
|
||||||
case v2object.FilterPropertyPhy, v2object.FilterPropertyRoot:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromBoltDB returns option to construct DB from BoltDB instance.
|
// bucketKeyHelper returns byte representation of val that is used as a key
|
||||||
func FromBoltDB(db *bbolt.DB) Option {
|
// in boltDB. Useful for getting filter values from unique and list indexes.
|
||||||
return func(c *cfg) {
|
func bucketKeyHelper(hdr string, val string) []byte {
|
||||||
c.boltDB = db
|
switch hdr {
|
||||||
|
case v2object.FilterHeaderPayloadHash:
|
||||||
|
v, err := hex.DecodeString(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
case v2object.FilterHeaderSplitID:
|
||||||
|
s := object.NewSplitID()
|
||||||
|
|
||||||
|
err := s.Parse(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.ToV2()
|
||||||
|
default:
|
||||||
|
return []byte(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,3 +106,25 @@ func WithLogger(l *logger.Logger) Option {
|
||||||
c.log = l
|
c.log = l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithBoltDBOptions returns option to specify BoltDB options.
|
||||||
|
func WithBoltDBOptions(opts *bbolt.Options) Option {
|
||||||
|
return func(c *cfg) {
|
||||||
|
c.boltOptions = opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPath returns option to set system path to Metabase.
|
||||||
|
func WithPath(path string) Option {
|
||||||
|
return func(c *cfg) {
|
||||||
|
c.info.Path = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPermissions returns option to specify permission bits
|
||||||
|
// of Metabase system path.
|
||||||
|
func WithPermissions(perm os.FileMode) Option {
|
||||||
|
return func(c *cfg) {
|
||||||
|
c.info.Permission = perm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package meta
|
package meta_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
@ -11,13 +11,13 @@ import (
|
||||||
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/owner"
|
"github.com/nspcc-dev/neofs-api-go/pkg/owner"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||||
|
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/util/test"
|
"github.com/nspcc-dev/neofs-node/pkg/util/test"
|
||||||
"github.com/pkg/errors"
|
"github.com/nspcc-dev/tzhash/tz"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.etcd.io/bbolt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testSelect(t *testing.T, db *DB, fs objectSDK.SearchFilters, exp ...*objectSDK.Address) {
|
func testSelect(t *testing.T, db *meta.DB, fs objectSDK.SearchFilters, exp ...*objectSDK.Address) {
|
||||||
res, err := db.Select(fs)
|
res, err := db.Select(fs)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, res, len(exp))
|
require.Len(t, res, len(exp))
|
||||||
|
@ -29,7 +29,7 @@ func testSelect(t *testing.T, db *DB, fs objectSDK.SearchFilters, exp ...*object
|
||||||
|
|
||||||
func testCID() *container.ID {
|
func testCID() *container.ID {
|
||||||
cs := [sha256.Size]byte{}
|
cs := [sha256.Size]byte{}
|
||||||
rand.Read(cs[:])
|
_, _ = rand.Read(cs[:])
|
||||||
|
|
||||||
id := container.NewID()
|
id := container.NewID()
|
||||||
id.SetSHA256(cs)
|
id.SetSHA256(cs)
|
||||||
|
@ -39,7 +39,7 @@ func testCID() *container.ID {
|
||||||
|
|
||||||
func testOID() *objectSDK.ID {
|
func testOID() *objectSDK.ID {
|
||||||
cs := [sha256.Size]byte{}
|
cs := [sha256.Size]byte{}
|
||||||
rand.Read(cs[:])
|
_, _ = rand.Read(cs[:])
|
||||||
|
|
||||||
id := objectSDK.NewID()
|
id := objectSDK.NewID()
|
||||||
id.SetSHA256(cs)
|
id.SetSHA256(cs)
|
||||||
|
@ -47,221 +47,67 @@ func testOID() *objectSDK.ID {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDB(t *testing.T) {
|
func newDB(t testing.TB) *meta.DB {
|
||||||
|
path := t.Name()
|
||||||
|
|
||||||
|
bdb := meta.New(meta.WithPath(path), meta.WithPermissions(0600))
|
||||||
|
|
||||||
|
require.NoError(t, bdb.Open())
|
||||||
|
|
||||||
|
return bdb
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseDB(db *meta.DB) {
|
||||||
|
db.Close()
|
||||||
|
os.Remove(db.DumpInfo().Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRawObject(t *testing.T) *object.RawObject {
|
||||||
|
return generateRawObjectWithCID(t, testCID())
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRawObjectWithCID(t *testing.T, cid *container.ID) *object.RawObject {
|
||||||
version := pkg.NewVersion()
|
version := pkg.NewVersion()
|
||||||
version.SetMajor(2)
|
version.SetMajor(2)
|
||||||
version.SetMinor(1)
|
version.SetMinor(1)
|
||||||
|
|
||||||
cid := testCID()
|
|
||||||
|
|
||||||
w, err := owner.NEO3WalletFromPublicKey(&test.DecodeKey(-1).PublicKey)
|
w, err := owner.NEO3WalletFromPublicKey(&test.DecodeKey(-1).PublicKey)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ownerID := owner.NewID()
|
ownerID := owner.NewID()
|
||||||
ownerID.SetNeo3Wallet(w)
|
ownerID.SetNeo3Wallet(w)
|
||||||
|
|
||||||
oid := testOID()
|
csum := new(pkg.Checksum)
|
||||||
|
csum.SetSHA256(sha256.Sum256(w.Bytes()))
|
||||||
|
|
||||||
|
csumTZ := new(pkg.Checksum)
|
||||||
|
csumTZ.SetTillichZemor(tz.Sum(csum.Sum()))
|
||||||
|
|
||||||
obj := object.NewRaw()
|
obj := object.NewRaw()
|
||||||
obj.SetID(oid)
|
obj.SetID(testOID())
|
||||||
obj.SetOwnerID(ownerID)
|
obj.SetOwnerID(ownerID)
|
||||||
obj.SetContainerID(cid)
|
obj.SetContainerID(cid)
|
||||||
obj.SetVersion(version)
|
obj.SetVersion(version)
|
||||||
|
obj.SetPayloadChecksum(csum)
|
||||||
|
obj.SetPayloadHomomorphicHash(csumTZ)
|
||||||
|
|
||||||
k, v := "key", "value"
|
return obj
|
||||||
|
|
||||||
a := objectSDK.NewAttribute()
|
|
||||||
a.SetKey(k)
|
|
||||||
a.SetValue(v)
|
|
||||||
|
|
||||||
obj.SetAttributes(a)
|
|
||||||
|
|
||||||
db := newDB(t)
|
|
||||||
|
|
||||||
defer releaseDB(db)
|
|
||||||
|
|
||||||
o := obj.Object()
|
|
||||||
|
|
||||||
require.NoError(t, db.Put(o))
|
|
||||||
|
|
||||||
o2, err := db.Get(o.Address())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, o, o2)
|
|
||||||
|
|
||||||
fs := objectSDK.SearchFilters{}
|
|
||||||
|
|
||||||
// filter container ID
|
|
||||||
fs.AddObjectContainerIDFilter(objectSDK.MatchStringEqual, cid)
|
|
||||||
testSelect(t, db, fs, o.Address())
|
|
||||||
|
|
||||||
// filter owner ID
|
|
||||||
fs.AddObjectOwnerIDFilter(objectSDK.MatchStringEqual, ownerID)
|
|
||||||
testSelect(t, db, fs, o.Address())
|
|
||||||
|
|
||||||
// filter attribute
|
|
||||||
fs.AddFilter(k, v, objectSDK.MatchStringEqual)
|
|
||||||
testSelect(t, db, fs, o.Address())
|
|
||||||
|
|
||||||
// filter mismatch
|
|
||||||
fs.AddFilter(k, v+"1", objectSDK.MatchStringEqual)
|
|
||||||
testSelect(t, db, fs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDB_Delete(t *testing.T) {
|
func generateAddress() *objectSDK.Address {
|
||||||
db := newDB(t)
|
addr := objectSDK.NewAddress()
|
||||||
|
addr.SetContainerID(testCID())
|
||||||
|
addr.SetObjectID(testOID())
|
||||||
|
|
||||||
defer releaseDB(db)
|
return addr
|
||||||
|
|
||||||
obj := object.NewRaw()
|
|
||||||
obj.SetContainerID(testCID())
|
|
||||||
obj.SetID(testOID())
|
|
||||||
|
|
||||||
o := obj.Object()
|
|
||||||
|
|
||||||
require.NoError(t, db.Put(o))
|
|
||||||
|
|
||||||
addr := o.Address()
|
|
||||||
|
|
||||||
_, err := db.Get(addr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
fs := objectSDK.SearchFilters{}
|
|
||||||
fs.AddObjectContainerIDFilter(objectSDK.MatchStringEqual, o.ContainerID())
|
|
||||||
|
|
||||||
testSelect(t, db, fs, o.Address())
|
|
||||||
|
|
||||||
require.NoError(t, db.Delete(addr))
|
|
||||||
|
|
||||||
_, err = db.Get(addr)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
testSelect(t, db, fs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDB_SelectProperties(t *testing.T) {
|
func addAttribute(obj *object.RawObject, key, val string) {
|
||||||
db := newDB(t)
|
attr := objectSDK.NewAttribute()
|
||||||
|
attr.SetKey(key)
|
||||||
|
attr.SetValue(val)
|
||||||
|
|
||||||
defer releaseDB(db)
|
attrs := obj.Attributes()
|
||||||
|
attrs = append(attrs, attr)
|
||||||
parent := object.NewRaw()
|
obj.SetAttributes(attrs...)
|
||||||
parent.SetContainerID(testCID())
|
|
||||||
parent.SetID(testOID())
|
|
||||||
|
|
||||||
child := object.NewRaw()
|
|
||||||
child.SetContainerID(testCID())
|
|
||||||
child.SetID(testOID())
|
|
||||||
child.SetParent(parent.Object().SDK())
|
|
||||||
|
|
||||||
parAddr := parent.Object().Address()
|
|
||||||
childAddr := child.Object().Address()
|
|
||||||
|
|
||||||
require.NoError(t, db.Put(child.Object()))
|
|
||||||
|
|
||||||
// root filter
|
|
||||||
fs := objectSDK.SearchFilters{}
|
|
||||||
fs.AddRootFilter()
|
|
||||||
testSelect(t, db, fs, parAddr)
|
|
||||||
|
|
||||||
// phy filter
|
|
||||||
fs = fs[:0]
|
|
||||||
fs.AddPhyFilter()
|
|
||||||
testSelect(t, db, fs, childAddr)
|
|
||||||
|
|
||||||
lnk := object.NewRaw()
|
|
||||||
lnk.SetContainerID(testCID())
|
|
||||||
lnk.SetID(testOID())
|
|
||||||
lnk.SetChildren(testOID())
|
|
||||||
|
|
||||||
require.NoError(t, db.Put(lnk.Object()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDB_Path(t *testing.T) {
|
|
||||||
path := t.Name()
|
|
||||||
|
|
||||||
bdb, err := bbolt.Open(path, 0600, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
db := NewDB(FromBoltDB(bdb))
|
|
||||||
|
|
||||||
defer releaseDB(db)
|
|
||||||
|
|
||||||
require.Equal(t, path, db.DumpInfo().Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDB(t testing.TB) *DB {
|
|
||||||
path := t.Name()
|
|
||||||
|
|
||||||
bdb, err := bbolt.Open(path, 0600, nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return NewDB(FromBoltDB(bdb))
|
|
||||||
}
|
|
||||||
|
|
||||||
func releaseDB(db *DB) {
|
|
||||||
db.Close()
|
|
||||||
os.Remove(db.DumpInfo().Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSelectNonExistentAttributes(t *testing.T) {
|
|
||||||
db := newDB(t)
|
|
||||||
|
|
||||||
defer releaseDB(db)
|
|
||||||
|
|
||||||
obj := object.NewRaw()
|
|
||||||
obj.SetID(testOID())
|
|
||||||
obj.SetContainerID(testCID())
|
|
||||||
|
|
||||||
require.NoError(t, db.Put(obj.Object()))
|
|
||||||
|
|
||||||
fs := objectSDK.SearchFilters{}
|
|
||||||
|
|
||||||
// add filter by non-existent attribute
|
|
||||||
fs.AddFilter("key", "value", objectSDK.MatchStringEqual)
|
|
||||||
|
|
||||||
res, err := db.Select(fs)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVirtualObject(t *testing.T) {
|
|
||||||
db := newDB(t)
|
|
||||||
defer releaseDB(db)
|
|
||||||
|
|
||||||
// create object with parent
|
|
||||||
obj := generateObject(t, testPrm{
|
|
||||||
withParent: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, db.Put(obj))
|
|
||||||
|
|
||||||
childAddr := obj.Address()
|
|
||||||
parAddr := obj.GetParent().Address()
|
|
||||||
|
|
||||||
// child object must be readable
|
|
||||||
_, err := db.Get(childAddr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// parent object must not be readable
|
|
||||||
_, err = db.Get(parAddr)
|
|
||||||
require.True(t, errors.Is(err, object.ErrNotFound))
|
|
||||||
|
|
||||||
fs := objectSDK.SearchFilters{}
|
|
||||||
|
|
||||||
// both objects should appear in selection
|
|
||||||
testSelect(t, db, fs, childAddr, parAddr)
|
|
||||||
|
|
||||||
// filter leaves
|
|
||||||
fs.AddPhyFilter()
|
|
||||||
|
|
||||||
// only child object should appear
|
|
||||||
testSelect(t, db, fs, childAddr)
|
|
||||||
|
|
||||||
fs = fs[:0]
|
|
||||||
|
|
||||||
// filter non-leaf objects
|
|
||||||
fs.AddRootFilter()
|
|
||||||
|
|
||||||
// only parent object should appear
|
|
||||||
testSelect(t, db, fs, parAddr)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,217 @@
|
||||||
package meta
|
package meta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
"bytes"
|
||||||
"github.com/pkg/errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
|
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var tombstoneBucket = []byte("tombstones")
|
var ErrVirtualObject = errors.New("do not remove virtual object directly")
|
||||||
|
|
||||||
// Delete marks object as deleted.
|
|
||||||
func (db *DB) Delete(addr *object.Address) error {
|
|
||||||
return db.delete(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func objectRemoved(tx *bbolt.Tx, addr []byte) bool {
|
|
||||||
tombstoneBucket := tx.Bucket(tombstoneBucket)
|
|
||||||
|
|
||||||
return tombstoneBucket != nil && tombstoneBucket.Get(addr) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteObjects marks list of objects as deleted.
|
// DeleteObjects marks list of objects as deleted.
|
||||||
func (db *DB) DeleteObjects(list ...*object.Address) {
|
func (db *DB) Delete(lst ...*objectSDK.Address) error {
|
||||||
if err := db.delete(list...); err != nil {
|
|
||||||
db.log.Error("could not delete object list",
|
|
||||||
zap.String("error", err.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) delete(list ...*object.Address) error {
|
|
||||||
return db.boltDB.Update(func(tx *bbolt.Tx) error {
|
return db.boltDB.Update(func(tx *bbolt.Tx) error {
|
||||||
bucket, err := tx.CreateBucketIfNotExists(tombstoneBucket)
|
for i := range lst {
|
||||||
if err != nil {
|
err := db.delete(tx, lst[i], false)
|
||||||
return errors.Wrapf(err, "(%T) could not create tombstone bucket", db)
|
if err != nil {
|
||||||
}
|
return err // maybe log and continue?
|
||||||
|
|
||||||
for i := range list {
|
|
||||||
if err := bucket.Put(addressKey(list[i]), nil); err != nil {
|
|
||||||
return errors.Wrapf(err, "(%T) could not put to tombstone bucket", db)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) delete(tx *bbolt.Tx, addr *objectSDK.Address, isParent bool) error {
|
||||||
|
pl := parentLength(tx, addr) // parentLength of address, for virtual objects it is > 0
|
||||||
|
|
||||||
|
// do not remove virtual objects directly
|
||||||
|
if !isParent && pl > 0 {
|
||||||
|
return ErrVirtualObject
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshal object
|
||||||
|
obj, err := db.get(tx, addr, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if object is an only link to a parent, then remove parent
|
||||||
|
if parent := obj.GetParent(); parent != nil {
|
||||||
|
if parentLength(tx, parent.Address()) == 1 {
|
||||||
|
err = db.deleteObject(tx, obj.GetParent(), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove object
|
||||||
|
return db.deleteObject(tx, obj, isParent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) deleteObject(
|
||||||
|
tx *bbolt.Tx,
|
||||||
|
obj *object.Object,
|
||||||
|
isParent bool,
|
||||||
|
) error {
|
||||||
|
uniqueIndexes, err := delUniqueIndexes(obj, isParent)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can' build unique indexes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete unique indexes
|
||||||
|
for i := range uniqueIndexes {
|
||||||
|
delUniqueIndexItem(tx, uniqueIndexes[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// build list indexes
|
||||||
|
listIndexes, err := listIndexes(obj)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can' build list indexes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete list indexes
|
||||||
|
for i := range listIndexes {
|
||||||
|
delListIndexItem(tx, listIndexes[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// build fake bucket tree indexes
|
||||||
|
fkbtIndexes, err := fkbtIndexes(obj)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can' build fake bucket tree indexes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete fkbt indexes
|
||||||
|
for i := range fkbtIndexes {
|
||||||
|
delFKBTIndexItem(tx, fkbtIndexes[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parentLength returns amount of available children from parentid index.
|
||||||
|
func parentLength(tx *bbolt.Tx, addr *objectSDK.Address) int {
|
||||||
|
bkt := tx.Bucket(parentBucketName(addr.ContainerID()))
|
||||||
|
if bkt == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
lst, err := decodeList(bkt.Get(objectKey(addr.ObjectID())))
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(lst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delUniqueIndexItem(tx *bbolt.Tx, item namedBucketItem) {
|
||||||
|
bkt := tx.Bucket(item.name)
|
||||||
|
if bkt != nil {
|
||||||
|
_ = bkt.Delete(item.key) // ignore error, best effort there
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func delFKBTIndexItem(tx *bbolt.Tx, item namedBucketItem) {
|
||||||
|
bkt := tx.Bucket(item.name)
|
||||||
|
if bkt == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fkbtRoot := bkt.Bucket(item.key)
|
||||||
|
if fkbtRoot == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = fkbtRoot.Delete(item.val) // ignore error, best effort there
|
||||||
|
}
|
||||||
|
|
||||||
|
func delListIndexItem(tx *bbolt.Tx, item namedBucketItem) {
|
||||||
|
bkt := tx.Bucket(item.name)
|
||||||
|
if bkt == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lst, err := decodeList(bkt.Get(item.key))
|
||||||
|
if err != nil || len(lst) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove element from the list
|
||||||
|
newLst := make([][]byte, 0, len(lst))
|
||||||
|
|
||||||
|
for i := range lst {
|
||||||
|
if !bytes.Equal(item.val, lst[i]) {
|
||||||
|
newLst = append(newLst, lst[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if list empty, remove the key from <list> bucket
|
||||||
|
if len(newLst) == 0 {
|
||||||
|
_ = bkt.Delete(item.key) // ignore error, best effort there
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if list is not empty, then update it
|
||||||
|
encodedLst, err := encodeList(lst)
|
||||||
|
if err != nil {
|
||||||
|
return // ignore error, best effort there
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = bkt.Put(item.key, encodedLst) // ignore error, best effort there
|
||||||
|
}
|
||||||
|
|
||||||
|
func delUniqueIndexes(obj *object.Object, isParent bool) ([]namedBucketItem, error) {
|
||||||
|
addr := obj.Address()
|
||||||
|
objKey := objectKey(addr.ObjectID())
|
||||||
|
addrKey := addressKey(addr)
|
||||||
|
|
||||||
|
result := make([]namedBucketItem, 0, 5)
|
||||||
|
|
||||||
|
// add value to primary unique bucket
|
||||||
|
if !isParent {
|
||||||
|
var bucketName []byte
|
||||||
|
|
||||||
|
switch obj.Type() {
|
||||||
|
case objectSDK.TypeRegular:
|
||||||
|
bucketName = primaryBucketName(addr.ContainerID())
|
||||||
|
case objectSDK.TypeTombstone:
|
||||||
|
bucketName = tombstoneBucketName(addr.ContainerID())
|
||||||
|
case objectSDK.TypeStorageGroup:
|
||||||
|
bucketName = storageGroupBucketName(addr.ContainerID())
|
||||||
|
default:
|
||||||
|
return nil, ErrUnknownObjectType
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, namedBucketItem{
|
||||||
|
name: bucketName,
|
||||||
|
key: objKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result,
|
||||||
|
namedBucketItem{ // remove from small blobovnicza id index
|
||||||
|
name: smallBucketName(addr.ContainerID()),
|
||||||
|
key: objKey,
|
||||||
|
},
|
||||||
|
namedBucketItem{ // remove from root index
|
||||||
|
name: rootBucketName(addr.ContainerID()),
|
||||||
|
key: objKey,
|
||||||
|
},
|
||||||
|
namedBucketItem{ // remove from graveyard index
|
||||||
|
name: graveyardBucketName,
|
||||||
|
key: addrKey,
|
||||||
|
},
|
||||||
|
namedBucketItem{ // remove from ToMoveIt index
|
||||||
|
name: toMoveItBucketName,
|
||||||
|
key: addrKey,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,71 +1,60 @@
|
||||||
package meta
|
package meta_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkDB_Delete(b *testing.B) {
|
func TestDB_Delete(t *testing.T) {
|
||||||
db := newDB(b)
|
|
||||||
|
|
||||||
defer releaseDB(db)
|
|
||||||
|
|
||||||
var existingAddr *object.Address
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
obj := generateObject(b, testPrm{})
|
|
||||||
|
|
||||||
existingAddr = obj.Address()
|
|
||||||
|
|
||||||
require.NoError(b, db.Put(obj))
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Run("existing address", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
err := db.Delete(existingAddr)
|
|
||||||
|
|
||||||
b.StopTimer()
|
|
||||||
require.NoError(b, err)
|
|
||||||
b.StartTimer()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("non-existing address", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
b.StopTimer()
|
|
||||||
addr := object.NewAddress()
|
|
||||||
addr.SetContainerID(testCID())
|
|
||||||
addr.SetObjectID(testOID())
|
|
||||||
b.StartTimer()
|
|
||||||
|
|
||||||
err := db.Delete(addr)
|
|
||||||
|
|
||||||
b.StopTimer()
|
|
||||||
require.NoError(b, err)
|
|
||||||
b.StartTimer()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDB_DeleteObjects(t *testing.T) {
|
|
||||||
db := newDB(t)
|
db := newDB(t)
|
||||||
defer releaseDB(db)
|
defer releaseDB(db)
|
||||||
|
|
||||||
o1 := generateObject(t, testPrm{})
|
cid := testCID()
|
||||||
o2 := generateObject(t, testPrm{})
|
parent := generateRawObjectWithCID(t, cid)
|
||||||
|
addAttribute(parent, "foo", "bar")
|
||||||
|
|
||||||
require.NoError(t, db.Put(o1))
|
child := generateRawObjectWithCID(t, cid)
|
||||||
require.NoError(t, db.Put(o2))
|
child.SetParent(parent.Object().SDK())
|
||||||
|
child.SetParentID(parent.ID())
|
||||||
|
|
||||||
db.DeleteObjects(o1.Address(), o2.Address())
|
// put object with parent
|
||||||
|
err := db.Put(child.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
testSelect(t, db, object.SearchFilters{})
|
// fill ToMoveIt index
|
||||||
|
err = db.ToMoveIt(child.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// check if Movable list is not empty
|
||||||
|
l, err := db.Movable()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, l, 1)
|
||||||
|
|
||||||
|
// inhume parent and child so they will be on graveyard
|
||||||
|
ts := generateRawObjectWithCID(t, cid)
|
||||||
|
|
||||||
|
err = db.Inhume(child.Object().Address(), ts.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = db.Inhume(child.Object().Address(), ts.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// delete object
|
||||||
|
err = db.Delete(child.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// check if there is no data in Movable index
|
||||||
|
l, err = db.Movable()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, l, 0)
|
||||||
|
|
||||||
|
// check if they removed from graveyard
|
||||||
|
ok, err := db.Exists(child.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
ok, err = db.Exists(parent.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, ok)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,88 @@
|
||||||
package meta
|
package meta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||||
"github.com/pkg/errors"
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Exists checks if object is presented in metabase.
|
var ErrLackSplitInfo = errors.New("no split info on parent object")
|
||||||
func (db *DB) Exists(addr *objectSDK.Address) (bool, error) {
|
|
||||||
// FIXME: temp solution, avoid direct Get usage
|
|
||||||
_, err := db.Get(addr)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, object.ErrNotFound) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, err
|
// Exists returns ErrAlreadyRemoved if addr was marked as removed. Otherwise it
|
||||||
|
// returns true if addr is in primary index or false if it is not.
|
||||||
|
func (db *DB) Exists(addr *objectSDK.Address) (exists bool, err error) {
|
||||||
|
err = db.boltDB.View(func(tx *bbolt.Tx) error {
|
||||||
|
exists, err = db.exists(tx, addr)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return exists, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) exists(tx *bbolt.Tx, addr *objectSDK.Address) (exists bool, err error) {
|
||||||
|
// check graveyard first
|
||||||
|
if inGraveyard(tx, addr) {
|
||||||
|
return false, object.ErrAlreadyRemoved
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
objKey := objectKey(addr.ObjectID())
|
||||||
|
|
||||||
|
// if graveyard is empty, then check if object exists in primary bucket
|
||||||
|
if inBucket(tx, primaryBucketName(addr.ContainerID()), objKey) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if primary bucket is empty, then check if object exists in parent bucket
|
||||||
|
if inBucket(tx, parentBucketName(addr.ContainerID()), objKey) {
|
||||||
|
rawSplitInfo := getFromBucket(tx, rootBucketName(addr.ContainerID()), objKey)
|
||||||
|
if len(rawSplitInfo) == 0 {
|
||||||
|
return false, ErrLackSplitInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
splitInfo := objectSDK.NewSplitInfo()
|
||||||
|
|
||||||
|
err := splitInfo.Unmarshal(rawSplitInfo)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("can't unmarshal split info from root index: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, objectSDK.NewSplitInfoError(splitInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if parent bucket is empty, then check if object exists in tombstone bucket
|
||||||
|
if inBucket(tx, tombstoneBucketName(addr.ContainerID()), objKey) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if parent bucket is empty, then check if object exists in storage group bucket
|
||||||
|
return inBucket(tx, storageGroupBucketName(addr.ContainerID()), objKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// inGraveyard returns true if object was marked as removed.
|
||||||
|
func inGraveyard(tx *bbolt.Tx, addr *objectSDK.Address) bool {
|
||||||
|
graveyard := tx.Bucket(graveyardBucketName)
|
||||||
|
if graveyard == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tombstone := graveyard.Get(addressKey(addr))
|
||||||
|
|
||||||
|
return len(tombstone) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// inBucket checks if key <key> is present in bucket <name>.
|
||||||
|
func inBucket(tx *bbolt.Tx, name, key []byte) bool {
|
||||||
|
bkt := tx.Bucket(name)
|
||||||
|
if bkt == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// using `get` as `exists`: https://github.com/boltdb/bolt/issues/321
|
||||||
|
val := bkt.Get(key)
|
||||||
|
|
||||||
|
return len(val) != 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,96 @@
|
||||||
package meta
|
package meta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/pkg/container"
|
||||||
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get returns object header for specified address.
|
// Get returns object header for specified address.
|
||||||
func (db *DB) Get(addr *objectSDK.Address) (*object.Object, error) {
|
func (db *DB) Get(addr *objectSDK.Address) (obj *object.Object, err error) {
|
||||||
var obj *object.Object
|
err = db.boltDB.View(func(tx *bbolt.Tx) error {
|
||||||
|
obj, err = db.get(tx, addr, true)
|
||||||
if err := db.boltDB.View(func(tx *bbolt.Tx) error {
|
|
||||||
addrKey := addressKey(addr)
|
|
||||||
|
|
||||||
// check if object marked as deleted
|
|
||||||
if objectRemoved(tx, addrKey) {
|
|
||||||
return object.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
primaryBucket := tx.Bucket(primaryBucket)
|
|
||||||
if primaryBucket == nil {
|
|
||||||
return object.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
data := primaryBucket.Get(addrKey)
|
|
||||||
if data == nil {
|
|
||||||
return object.ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
obj = object.New()
|
|
||||||
|
|
||||||
err = obj.Unmarshal(data)
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
})
|
||||||
|
|
||||||
|
return obj, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) get(tx *bbolt.Tx, addr *objectSDK.Address, checkGraveyard bool) (*object.Object, error) {
|
||||||
|
obj := object.New()
|
||||||
|
key := objectKey(addr.ObjectID())
|
||||||
|
cid := addr.ContainerID()
|
||||||
|
|
||||||
|
if checkGraveyard && inGraveyard(tx, addr) {
|
||||||
|
return nil, object.ErrAlreadyRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
// check in primary index
|
||||||
|
data := getFromBucket(tx, primaryBucketName(cid), key)
|
||||||
|
if len(data) != 0 {
|
||||||
|
return obj, obj.Unmarshal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not found then check in tombstone index
|
||||||
|
data = getFromBucket(tx, tombstoneBucketName(cid), key)
|
||||||
|
if len(data) != 0 {
|
||||||
|
return obj, obj.Unmarshal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not found then check in storage group index
|
||||||
|
data = getFromBucket(tx, storageGroupBucketName(cid), key)
|
||||||
|
if len(data) != 0 {
|
||||||
|
return obj, obj.Unmarshal(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not found then check if object is a virtual
|
||||||
|
return getVirtualObject(tx, cid, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFromBucket(tx *bbolt.Tx, name, key []byte) []byte {
|
||||||
|
bkt := tx.Bucket(name)
|
||||||
|
if bkt == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return bkt.Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVirtualObject(tx *bbolt.Tx, cid *container.ID, key []byte) (*object.Object, error) {
|
||||||
|
parentBucket := tx.Bucket(parentBucketName(cid))
|
||||||
|
if parentBucket == nil {
|
||||||
|
return nil, object.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
relativeLst, err := decodeList(parentBucket.Get(key))
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj, nil
|
if len(relativeLst) == 0 { // this should never happen though
|
||||||
|
return nil, object.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick last item, for now there is not difference which address to pick
|
||||||
|
// but later list might be sorted so first or last value can be more
|
||||||
|
// prioritized to choose
|
||||||
|
virtualOID := relativeLst[len(relativeLst)-1]
|
||||||
|
data := getFromBucket(tx, primaryBucketName(cid), virtualOID)
|
||||||
|
|
||||||
|
child := object.New()
|
||||||
|
|
||||||
|
err = child.Unmarshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't unmarshal child with parent: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if child.GetParent() == nil { // this should never happen though
|
||||||
|
return nil, object.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return child.GetParent(), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +1,96 @@
|
||||||
package meta
|
package meta_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
|
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BenchmarkDB_Get(b *testing.B) {
|
func TestDB_Get(t *testing.T) {
|
||||||
db := newDB(b)
|
db := newDB(t)
|
||||||
|
|
||||||
defer releaseDB(db)
|
defer releaseDB(db)
|
||||||
|
|
||||||
var existingAddr *object.Address
|
raw := generateRawObject(t)
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
// equal fails on diff of <nil> attributes and <{}> attributes,
|
||||||
obj := generateObject(b, testPrm{})
|
/* so we make non empty attribute slice in parent*/
|
||||||
|
addAttribute(raw, "foo", "bar")
|
||||||
|
|
||||||
existingAddr = obj.Address()
|
t.Run("object not found", func(t *testing.T) {
|
||||||
|
_, err := db.Get(raw.Object().Address())
|
||||||
require.NoError(b, db.Put(obj))
|
require.Error(t, err)
|
||||||
}
|
|
||||||
|
|
||||||
b.Run("existing address", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, err := db.Get(existingAddr)
|
|
||||||
|
|
||||||
b.StopTimer()
|
|
||||||
require.NoError(b, err)
|
|
||||||
b.StartTimer()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Run("non-existing address", func(b *testing.B) {
|
t.Run("put regular object", func(t *testing.T) {
|
||||||
b.ReportAllocs()
|
err := db.Put(raw.Object(), nil)
|
||||||
b.ResetTimer()
|
require.NoError(t, err)
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
newObj, err := db.Get(raw.Object().Address())
|
||||||
b.StopTimer()
|
require.NoError(t, err)
|
||||||
addr := object.NewAddress()
|
require.Equal(t, raw.Object(), newObj)
|
||||||
addr.SetContainerID(testCID())
|
})
|
||||||
addr.SetObjectID(testOID())
|
|
||||||
b.StartTimer()
|
|
||||||
|
|
||||||
_, err := db.Get(addr)
|
t.Run("put tombstone object", func(t *testing.T) {
|
||||||
|
raw.SetType(objectSDK.TypeTombstone)
|
||||||
|
raw.SetID(testOID())
|
||||||
|
|
||||||
b.StopTimer()
|
err := db.Put(raw.Object(), nil)
|
||||||
require.Error(b, err)
|
require.NoError(t, err)
|
||||||
b.StartTimer()
|
|
||||||
}
|
newObj, err := db.Get(raw.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, raw.Object(), newObj)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("put storage group object", func(t *testing.T) {
|
||||||
|
raw.SetType(objectSDK.TypeStorageGroup)
|
||||||
|
raw.SetID(testOID())
|
||||||
|
|
||||||
|
err := db.Put(raw.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
newObj, err := db.Get(raw.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, raw.Object(), newObj)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("put virtual object", func(t *testing.T) {
|
||||||
|
cid := testCID()
|
||||||
|
parent := generateRawObjectWithCID(t, cid)
|
||||||
|
addAttribute(parent, "foo", "bar")
|
||||||
|
|
||||||
|
child := generateRawObjectWithCID(t, cid)
|
||||||
|
child.SetParent(parent.Object().SDK())
|
||||||
|
child.SetParentID(parent.ID())
|
||||||
|
|
||||||
|
err := db.Put(child.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
newParent, err := db.Get(parent.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, binaryEqual(parent.Object(), newParent))
|
||||||
|
|
||||||
|
newChild, err := db.Get(child.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, binaryEqual(child.Object(), newChild))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// binary equal is used when object contains empty lists in the structure and
|
||||||
|
// requre.Equal fails on comparing <nil> and []{} lists.
|
||||||
|
func binaryEqual(a, b *object.Object) bool {
|
||||||
|
binaryA, err := a.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
binaryB, err := b.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.Equal(binaryA, binaryB)
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
package meta
|
package meta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
// Info groups the information about DB.
|
// Info groups the information about DB.
|
||||||
type Info struct {
|
type Info struct {
|
||||||
// Full path to the metabase.
|
// Full path to the metabase.
|
||||||
Path string
|
Path string
|
||||||
|
|
||||||
|
// Permission of database file.
|
||||||
|
Permission os.FileMode
|
||||||
}
|
}
|
||||||
|
|
||||||
// DumpInfo returns information about the DB.
|
// DumpInfo returns information about the DB.
|
||||||
|
|
|
@ -1,145 +1,424 @@
|
||||||
package meta
|
package meta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||||
"github.com/pkg/errors"
|
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobovnicza"
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type bucketItem struct {
|
type (
|
||||||
key, val string
|
namedBucketItem struct {
|
||||||
}
|
name, key, val []byte
|
||||||
|
}
|
||||||
var (
|
|
||||||
primaryBucket = []byte("objects")
|
|
||||||
indexBucket = []byte("index")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Put saves object in DB.
|
var (
|
||||||
//
|
ErrUnknownObjectType = errors.New("unknown object type")
|
||||||
// Object payload expected to be cut.
|
ErrIncorrectBlobovniczaUpdate = errors.New("updating blobovnicza id on object without it")
|
||||||
func (db *DB) Put(obj *object.Object) error {
|
ErrIncorrectSplitInfoUpdate = errors.New("updating split info on object without it")
|
||||||
|
ErrIncorrectRootObject = errors.New("invalid root object")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Put saves object header in metabase. Object payload expected to be cut.
|
||||||
|
// Big objects have nil blobovniczaID.
|
||||||
|
func (db *DB) Put(obj *object.Object, id *blobovnicza.ID) error {
|
||||||
return db.boltDB.Update(func(tx *bbolt.Tx) error {
|
return db.boltDB.Update(func(tx *bbolt.Tx) error {
|
||||||
par := false
|
return db.put(tx, obj, id, nil)
|
||||||
|
|
||||||
for ; obj != nil; obj, par = obj.GetParent(), true {
|
|
||||||
// create primary bucket (addr: header)
|
|
||||||
primaryBucket, err := tx.CreateBucketIfNotExists(primaryBucket)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "(%T) could not create primary bucket", db)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := obj.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "(%T) could not marshal the object", db)
|
|
||||||
}
|
|
||||||
|
|
||||||
addrKey := addressKey(obj.Address())
|
|
||||||
|
|
||||||
if !par {
|
|
||||||
// put header to primary bucket
|
|
||||||
if err := primaryBucket.Put(addrKey, data); err != nil {
|
|
||||||
return errors.Wrapf(err, "(%T) could not put item to primary bucket", db)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create bucket for indices
|
|
||||||
indexBucket, err := tx.CreateBucketIfNotExists(indexBucket)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "(%T) could not create index bucket", db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate indexed values for object
|
|
||||||
indices := objectIndices(obj, par)
|
|
||||||
|
|
||||||
for i := range indices {
|
|
||||||
// create index bucket
|
|
||||||
keyBucket, err := indexBucket.CreateBucketIfNotExists([]byte(indices[i].key))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "(%T) could not create bucket for header key", db)
|
|
||||||
}
|
|
||||||
|
|
||||||
v := []byte(indices[i].val)
|
|
||||||
|
|
||||||
// create address bucket for the value
|
|
||||||
valBucket, err := keyBucket.CreateBucketIfNotExists(nonEmptyKeyBytes(v))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "(%T) could not create bucket for header value", db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// put object address to value bucket
|
|
||||||
if err := valBucket.Put(addrKey, nil); err != nil {
|
|
||||||
return errors.Wrapf(err, "(%T) could not put item to header bucket", db)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func nonEmptyKeyBytes(key []byte) []byte {
|
func (db *DB) put(tx *bbolt.Tx, obj *object.Object, id *blobovnicza.ID, si *objectSDK.SplitInfo) error {
|
||||||
return append([]byte{0}, key...)
|
isParent := si != nil
|
||||||
|
|
||||||
|
exists, err := db.exists(tx, obj.Address())
|
||||||
|
|
||||||
|
if errors.As(err, &splitInfoError) {
|
||||||
|
exists = true // object exists, however it is virtual
|
||||||
|
} else if err != nil {
|
||||||
|
return err // return any error besides SplitInfoError
|
||||||
|
}
|
||||||
|
|
||||||
|
// most right child and split header overlap parent so we have to
|
||||||
|
// check if object exists to not overwrite it twice
|
||||||
|
if exists {
|
||||||
|
// when storage engine moves small objects from one blobovniczaID
|
||||||
|
// to another, then it calls metabase.Put method with new blobovniczaID
|
||||||
|
// and this code should be triggered
|
||||||
|
if !isParent && id != nil {
|
||||||
|
return updateBlobovniczaID(tx, obj.Address(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// when storage already has last object in split hierarchy and there is
|
||||||
|
// a linking object to put (or vice versa), we should update split info
|
||||||
|
// with object ids of these objects
|
||||||
|
if isParent {
|
||||||
|
return updateSplitInfo(tx, obj.Address(), si)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.GetParent() != nil && !isParent { // limit depth by two
|
||||||
|
parentSI, err := splitInfoFromObject(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.put(tx, obj.GetParent(), id, parentSI)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build unique indexes
|
||||||
|
uniqueIndexes, err := uniqueIndexes(obj, si, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can' build unique indexes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// put unique indexes
|
||||||
|
for i := range uniqueIndexes {
|
||||||
|
err := putUniqueIndexItem(tx, uniqueIndexes[i])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build list indexes
|
||||||
|
listIndexes, err := listIndexes(obj)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can' build list indexes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// put list indexes
|
||||||
|
for i := range listIndexes {
|
||||||
|
err := putListIndexItem(tx, listIndexes[i])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build fake bucket tree indexes
|
||||||
|
fkbtIndexes, err := fkbtIndexes(obj)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can' build fake bucket tree indexes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// put fake bucket tree indexes
|
||||||
|
for i := range fkbtIndexes {
|
||||||
|
err := putFKBTIndexItem(tx, fkbtIndexes[i])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cutKeyBytes(key []byte) []byte {
|
// builds list of <unique> indexes from the object.
|
||||||
return key[1:]
|
func uniqueIndexes(obj *object.Object, si *objectSDK.SplitInfo, id *blobovnicza.ID) ([]namedBucketItem, error) {
|
||||||
}
|
isParent := si != nil
|
||||||
|
addr := obj.Address()
|
||||||
|
objKey := objectKey(addr.ObjectID())
|
||||||
|
result := make([]namedBucketItem, 0, 3)
|
||||||
|
|
||||||
func addressKey(addr *objectSDK.Address) []byte {
|
// add value to primary unique bucket
|
||||||
return []byte(addr.String())
|
if !isParent {
|
||||||
}
|
var bucketName []byte
|
||||||
|
|
||||||
func objectIndices(obj *object.Object, parent bool) []bucketItem {
|
switch obj.Type() {
|
||||||
as := obj.Attributes()
|
case objectSDK.TypeRegular:
|
||||||
|
bucketName = primaryBucketName(addr.ContainerID())
|
||||||
|
case objectSDK.TypeTombstone:
|
||||||
|
bucketName = tombstoneBucketName(addr.ContainerID())
|
||||||
|
case objectSDK.TypeStorageGroup:
|
||||||
|
bucketName = storageGroupBucketName(addr.ContainerID())
|
||||||
|
default:
|
||||||
|
return nil, ErrUnknownObjectType
|
||||||
|
}
|
||||||
|
|
||||||
res := make([]bucketItem, 0, 6+len(as)) // 6 predefined buckets and object attributes
|
rawObject, err := obj.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't marshal object header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
res = append(res,
|
result = append(result, namedBucketItem{
|
||||||
bucketItem{
|
name: bucketName,
|
||||||
key: v2object.FilterHeaderVersion,
|
key: objKey,
|
||||||
val: obj.Version().String(),
|
val: rawObject,
|
||||||
},
|
})
|
||||||
bucketItem{
|
|
||||||
key: v2object.FilterHeaderContainerID,
|
|
||||||
val: obj.ContainerID().String(),
|
|
||||||
},
|
|
||||||
bucketItem{
|
|
||||||
key: v2object.FilterHeaderOwnerID,
|
|
||||||
val: obj.OwnerID().String(),
|
|
||||||
},
|
|
||||||
bucketItem{
|
|
||||||
key: v2object.FilterHeaderParent,
|
|
||||||
val: obj.ParentID().String(),
|
|
||||||
},
|
|
||||||
bucketItem{
|
|
||||||
key: v2object.FilterHeaderObjectID,
|
|
||||||
val: obj.ID().String(),
|
|
||||||
},
|
|
||||||
// TODO: add remaining fields after neofs-api#72
|
|
||||||
)
|
|
||||||
|
|
||||||
|
// index blobovniczaID if it is present
|
||||||
|
if id != nil {
|
||||||
|
result = append(result, namedBucketItem{
|
||||||
|
name: smallBucketName(addr.ContainerID()),
|
||||||
|
key: objKey,
|
||||||
|
val: *id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// index root object
|
||||||
if obj.Type() == objectSDK.TypeRegular && !obj.HasParent() {
|
if obj.Type() == objectSDK.TypeRegular && !obj.HasParent() {
|
||||||
res = append(res, bucketItem{
|
var (
|
||||||
key: v2object.FilterPropertyRoot,
|
err error
|
||||||
|
splitInfo []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
if isParent {
|
||||||
|
splitInfo, err = si.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't marshal split info: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, namedBucketItem{
|
||||||
|
name: rootBucketName(addr.ContainerID()),
|
||||||
|
key: objKey,
|
||||||
|
val: splitInfo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if !parent {
|
return result, nil
|
||||||
res = append(res, bucketItem{
|
}
|
||||||
key: v2object.FilterPropertyPhy,
|
|
||||||
})
|
// builds list of <list> indexes from the object.
|
||||||
}
|
func listIndexes(obj *object.Object) ([]namedBucketItem, error) {
|
||||||
|
result := make([]namedBucketItem, 0, 3)
|
||||||
for _, a := range as {
|
addr := obj.Address()
|
||||||
res = append(res, bucketItem{
|
objKey := objectKey(addr.ObjectID())
|
||||||
key: a.Key(),
|
|
||||||
val: a.Value(),
|
// index payload hashes
|
||||||
})
|
result = append(result, namedBucketItem{
|
||||||
}
|
name: payloadHashBucketName(addr.ContainerID()),
|
||||||
|
key: obj.PayloadChecksum().Sum(),
|
||||||
return res
|
val: objKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
// index parent ids
|
||||||
|
if obj.ParentID() != nil {
|
||||||
|
result = append(result, namedBucketItem{
|
||||||
|
name: parentBucketName(addr.ContainerID()),
|
||||||
|
key: objectKey(obj.ParentID()),
|
||||||
|
val: objKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// index split ids
|
||||||
|
if obj.SplitID() != nil {
|
||||||
|
result = append(result, namedBucketItem{
|
||||||
|
name: splitBucketName(addr.ContainerID()),
|
||||||
|
key: obj.SplitID().ToV2(),
|
||||||
|
val: objKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// builds list of <fake bucket tree> indexes from the object.
|
||||||
|
func fkbtIndexes(obj *object.Object) ([]namedBucketItem, error) {
|
||||||
|
addr := obj.Address()
|
||||||
|
objKey := []byte(addr.ObjectID().String())
|
||||||
|
|
||||||
|
attrs := obj.Attributes()
|
||||||
|
result := make([]namedBucketItem, 0, 1+len(attrs))
|
||||||
|
|
||||||
|
// owner
|
||||||
|
result = append(result, namedBucketItem{
|
||||||
|
name: ownerBucketName(addr.ContainerID()),
|
||||||
|
key: []byte(obj.OwnerID().String()),
|
||||||
|
val: objKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
// user specified attributes
|
||||||
|
for i := range attrs {
|
||||||
|
result = append(result, namedBucketItem{
|
||||||
|
name: attributeBucketName(addr.ContainerID(), attrs[i].Key()),
|
||||||
|
key: []byte(attrs[i].Value()),
|
||||||
|
val: objKey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func putUniqueIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
|
||||||
|
bkt, err := tx.CreateBucketIfNotExists(item.name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't create index %v: %w", item.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bkt.Put(item.key, item.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func putFKBTIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
|
||||||
|
bkt, err := tx.CreateBucketIfNotExists(item.name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't create index %v: %w", item.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fkbtRoot, err := bkt.CreateBucketIfNotExists(item.key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't create fake bucket tree index %v: %w", item.key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fkbtRoot.Put(item.val, zeroValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func putListIndexItem(tx *bbolt.Tx, item namedBucketItem) error {
|
||||||
|
bkt, err := tx.CreateBucketIfNotExists(item.name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't create index %v: %w", item.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lst, err := decodeList(bkt.Get(item.key))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't decode leaf list %v: %w", item.key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lst = append(lst, item.val)
|
||||||
|
|
||||||
|
encodedLst, err := encodeList(lst)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't encode leaf list %v: %w", item.key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bkt.Put(item.key, encodedLst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeList decodes list of bytes into a single blog for list bucket indexes.
|
||||||
|
func encodeList(lst [][]byte) ([]byte, error) {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
encoder := gob.NewEncoder(buf)
|
||||||
|
|
||||||
|
// consider using protobuf encoding instead of glob
|
||||||
|
if err := encoder.Encode(lst); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeList decodes blob into the list of bytes from list bucket index.
|
||||||
|
func decodeList(data []byte) (lst [][]byte, err error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := gob.NewDecoder(bytes.NewReader(data))
|
||||||
|
if err := decoder.Decode(&lst); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return lst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateBlobovniczaID for existing objects if they were moved from from
|
||||||
|
// one blobovnicza to another.
|
||||||
|
func updateBlobovniczaID(tx *bbolt.Tx, addr *objectSDK.Address, id *blobovnicza.ID) error {
|
||||||
|
bkt := tx.Bucket(smallBucketName(addr.ContainerID()))
|
||||||
|
if bkt == nil {
|
||||||
|
// if object exists, don't have blobovniczaID and we want to update it
|
||||||
|
// then ignore, this should never happen
|
||||||
|
return ErrIncorrectBlobovniczaUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
objectKey := objectKey(addr.ObjectID())
|
||||||
|
|
||||||
|
if len(bkt.Get(objectKey)) == 0 {
|
||||||
|
return ErrIncorrectBlobovniczaUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
return bkt.Put(objectKey, *id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateSpliInfo for existing objects if storage filled with extra information
|
||||||
|
// about last object in split hierarchy or linking object.
|
||||||
|
func updateSplitInfo(tx *bbolt.Tx, addr *objectSDK.Address, from *objectSDK.SplitInfo) error {
|
||||||
|
bkt := tx.Bucket(rootBucketName(addr.ContainerID()))
|
||||||
|
if bkt == nil {
|
||||||
|
// if object doesn't exists and we want to update split info on it
|
||||||
|
// then ignore, this should never happen
|
||||||
|
return ErrIncorrectSplitInfoUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
objectKey := objectKey(addr.ObjectID())
|
||||||
|
|
||||||
|
rawSplitInfo := bkt.Get(objectKey)
|
||||||
|
if len(rawSplitInfo) == 0 {
|
||||||
|
return ErrIncorrectSplitInfoUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
to := objectSDK.NewSplitInfo()
|
||||||
|
|
||||||
|
err := to.Unmarshal(rawSplitInfo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't unmarshal split info from root index: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := mergeSplitInfo(from, to)
|
||||||
|
|
||||||
|
rawSplitInfo, err = result.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't marhsal merged split info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bkt.Put(objectKey, rawSplitInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitInfoFromObject returns split info based on last or linkin object.
|
||||||
|
// Otherwise returns nil, nil.
|
||||||
|
func splitInfoFromObject(obj *object.Object) (*objectSDK.SplitInfo, error) {
|
||||||
|
if obj.Parent() == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
info := objectSDK.NewSplitInfo()
|
||||||
|
info.SetSplitID(obj.SplitID())
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case isLinkObject(obj):
|
||||||
|
info.SetLink(obj.ID())
|
||||||
|
case isLastObject(obj):
|
||||||
|
info.SetLastPart(obj.ID())
|
||||||
|
default:
|
||||||
|
return nil, ErrIncorrectRootObject // should never happen
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeSplitInfo ignores conflicts and rewrites `to` with non empty values
|
||||||
|
// from `from`.
|
||||||
|
func mergeSplitInfo(from, to *objectSDK.SplitInfo) *objectSDK.SplitInfo {
|
||||||
|
to.SetSplitID(from.SplitID()) // overwrite SplitID and ignore conflicts
|
||||||
|
|
||||||
|
if lp := from.LastPart(); lp != nil {
|
||||||
|
to.SetLastPart(lp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if link := from.Link(); link != nil {
|
||||||
|
to.SetLink(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
return to
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLinkObject returns true if object contains parent header and list
|
||||||
|
// of children.
|
||||||
|
func isLinkObject(obj *object.Object) bool {
|
||||||
|
return len(obj.Children()) > 0 && obj.Parent() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLastObject returns true if object contains only parent header without list
|
||||||
|
// of children.
|
||||||
|
func isLastObject(obj *object.Object) bool {
|
||||||
|
return len(obj.Children()) == 0 && obj.Parent() != nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,136 +1,48 @@
|
||||||
package meta
|
package meta_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg"
|
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobovnicza"
|
||||||
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/owner"
|
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/util/test"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testPrm struct {
|
func TestDB_PutBlobovnicaUpdate(t *testing.T) {
|
||||||
withParent bool
|
db := newDB(t)
|
||||||
|
|
||||||
attrNum int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p testPrm) String() string {
|
|
||||||
return fmt.Sprintf("[with_parent:%t, attributes:%d]",
|
|
||||||
p.withParent,
|
|
||||||
p.attrNum,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateChecksum() *pkg.Checksum {
|
|
||||||
cs := pkg.NewChecksum()
|
|
||||||
|
|
||||||
sh := [sha256.Size]byte{}
|
|
||||||
rand.Read(sh[:])
|
|
||||||
|
|
||||||
cs.SetSHA256(sh)
|
|
||||||
|
|
||||||
return cs
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateObject(t require.TestingT, prm testPrm) *object.Object {
|
|
||||||
obj := object.NewRaw()
|
|
||||||
obj.SetID(testOID())
|
|
||||||
obj.SetVersion(pkg.SDKVersion())
|
|
||||||
obj.SetContainerID(testCID())
|
|
||||||
obj.SetPayloadChecksum(generateChecksum())
|
|
||||||
obj.SetPayloadHomomorphicHash(generateChecksum())
|
|
||||||
obj.SetType(objectSDK.TypeRegular)
|
|
||||||
|
|
||||||
as := make([]*objectSDK.Attribute, 0, prm.attrNum)
|
|
||||||
|
|
||||||
for i := 0; i < prm.attrNum; i++ {
|
|
||||||
a := objectSDK.NewAttribute()
|
|
||||||
|
|
||||||
k := make([]byte, 32)
|
|
||||||
rand.Read(k)
|
|
||||||
a.SetKey(string(k))
|
|
||||||
|
|
||||||
v := make([]byte, 32)
|
|
||||||
rand.Read(v)
|
|
||||||
a.SetValue(string(v))
|
|
||||||
|
|
||||||
as = append(as, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
obj.SetAttributes(as...)
|
|
||||||
|
|
||||||
wallet, err := owner.NEO3WalletFromPublicKey(&test.DecodeKey(-1).PublicKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ownerID := owner.NewID()
|
|
||||||
ownerID.SetNeo3Wallet(wallet)
|
|
||||||
|
|
||||||
obj.SetOwnerID(ownerID)
|
|
||||||
|
|
||||||
if prm.withParent {
|
|
||||||
prm.withParent = false
|
|
||||||
|
|
||||||
obj.SetChildren(testOID())
|
|
||||||
obj.SetPreviousID(testOID())
|
|
||||||
obj.SetParentID(testOID())
|
|
||||||
obj.SetParent(generateObject(t, prm).SDK())
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj.Object()
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkDB_Put(b *testing.B) {
|
|
||||||
db := newDB(b)
|
|
||||||
|
|
||||||
defer releaseDB(db)
|
defer releaseDB(db)
|
||||||
|
|
||||||
for _, prm := range []testPrm{
|
raw1 := generateRawObject(t)
|
||||||
{
|
blobovniczaID := blobovnicza.ID{1, 2, 3, 4}
|
||||||
withParent: false,
|
|
||||||
attrNum: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
withParent: true,
|
|
||||||
attrNum: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
withParent: false,
|
|
||||||
attrNum: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
withParent: true,
|
|
||||||
attrNum: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
withParent: false,
|
|
||||||
attrNum: 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
withParent: true,
|
|
||||||
attrNum: 1000,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
b.Run(prm.String(), func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
// put one object with blobovniczaID
|
||||||
b.StopTimer()
|
err := db.Put(raw1.Object(), &blobovniczaID)
|
||||||
obj := generateObject(b, prm)
|
require.NoError(t, err)
|
||||||
b.StartTimer()
|
|
||||||
|
|
||||||
err := db.Put(obj)
|
fetchedBlobovniczaID, err := db.IsSmall(raw1.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, &blobovniczaID, fetchedBlobovniczaID)
|
||||||
|
|
||||||
b.StopTimer()
|
t.Run("update blobovniczaID", func(t *testing.T) {
|
||||||
require.NoError(b, err)
|
newID := blobovnicza.ID{5, 6, 7, 8}
|
||||||
b.StartTimer()
|
|
||||||
}
|
err := db.Put(raw1.Object(), &newID)
|
||||||
})
|
require.NoError(t, err)
|
||||||
}
|
|
||||||
|
fetchedBlobovniczaID, err := db.IsSmall(raw1.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, &newID, fetchedBlobovniczaID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("update blobovniczaID on bad object", func(t *testing.T) {
|
||||||
|
raw2 := generateRawObject(t)
|
||||||
|
err := db.Put(raw2.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
fetchedBlobovniczaID, err := db.IsSmall(raw2.Object().Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, fetchedBlobovniczaID)
|
||||||
|
|
||||||
|
err = db.Put(raw2.Object(), &blobovniczaID)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,87 +1,73 @@
|
||||||
package meta
|
package meta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/pkg/container"
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.etcd.io/bbolt"
|
"go.etcd.io/bbolt"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// filterGroup is a structure that have search filters grouped by access
|
||||||
|
// method. We have fast filters that looks for indexes and do not unmarshal
|
||||||
|
// objects, and slow filters, that applied after fast filters created
|
||||||
|
// smaller set of objects to check.
|
||||||
|
filterGroup struct {
|
||||||
|
cid *container.ID
|
||||||
|
fastFilters object.SearchFilters
|
||||||
|
slowFilters object.SearchFilters
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrContainerNotInQuery = errors.New("search query does not contain container id filter")
|
||||||
|
|
||||||
// Select returns list of addresses of objects that match search filters.
|
// Select returns list of addresses of objects that match search filters.
|
||||||
func (db *DB) Select(fs object.SearchFilters) (res []*object.Address, err error) {
|
func (db *DB) Select(fs object.SearchFilters) (res []*object.Address, err error) {
|
||||||
err = db.boltDB.View(func(tx *bbolt.Tx) error {
|
err = db.boltDB.View(func(tx *bbolt.Tx) error {
|
||||||
res, err = db.selectObjects(tx, fs)
|
res, err = db.selectObjects(tx, fs)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) selectObjects(tx *bbolt.Tx, fs object.SearchFilters) ([]*object.Address, error) {
|
func (db *DB) selectObjects(tx *bbolt.Tx, fs object.SearchFilters) ([]*object.Address, error) {
|
||||||
if len(fs) == 0 {
|
group, err := groupFilters(fs)
|
||||||
return db.selectAll(tx)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// get indexed bucket
|
if group.cid == nil {
|
||||||
indexBucket := tx.Bucket(indexBucket)
|
return nil, ErrContainerNotInQuery
|
||||||
if indexBucket == nil {
|
|
||||||
// empty storage
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep processed addresses
|
// keep matched addresses in this cache
|
||||||
// value equal to number (index+1) of latest matched filter
|
// value equal to number (index+1) of latest matched filter
|
||||||
mAddr := make(map[string]int)
|
mAddr := make(map[string]int)
|
||||||
|
|
||||||
for fNum := range fs {
|
expLen := len(group.fastFilters) // expected value of matched filters in mAddr
|
||||||
matchFunc, ok := db.matchers[fs[fNum].Operation()]
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.Errorf("no function for matcher %v", fs[fNum].Operation())
|
|
||||||
}
|
|
||||||
|
|
||||||
key := fs[fNum].Header()
|
if len(group.fastFilters) == 0 {
|
||||||
|
expLen = 1
|
||||||
|
|
||||||
// get bucket with values
|
db.selectAll(tx, group.cid, mAddr)
|
||||||
keyBucket := indexBucket.Bucket([]byte(key))
|
} else {
|
||||||
if keyBucket == nil {
|
for i := range group.fastFilters {
|
||||||
// no object has this attribute => empty result
|
db.selectFastFilter(tx, group.cid, group.fastFilters[i], mAddr, i)
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fVal := fs[fNum].Value()
|
|
||||||
|
|
||||||
// iterate over all existing values for the key
|
|
||||||
if err := keyBucket.ForEach(func(k, v []byte) error {
|
|
||||||
include := matchFunc(key, string(cutKeyBytes(k)), fVal)
|
|
||||||
|
|
||||||
if include {
|
|
||||||
return keyBucket.Bucket(k).ForEach(func(k, _ []byte) error {
|
|
||||||
if num := mAddr[string(k)]; num == fNum {
|
|
||||||
// otherwise object does not match current or some previous filter
|
|
||||||
mAddr[string(k)] = fNum + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "(%T) could not iterate bucket %s", db, key)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fLen := len(fs)
|
|
||||||
res := make([]*object.Address, 0, len(mAddr))
|
res := make([]*object.Address, 0, len(mAddr))
|
||||||
|
|
||||||
for a, ind := range mAddr {
|
for a, ind := range mAddr {
|
||||||
if ind != fLen {
|
if ind != expLen {
|
||||||
continue
|
continue // ignore objects with unmatched fast filters
|
||||||
}
|
|
||||||
|
|
||||||
// check if object marked as deleted
|
|
||||||
if objectRemoved(tx, []byte(a)) {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := object.NewAddress()
|
addr := object.NewAddress()
|
||||||
|
@ -90,60 +76,300 @@ func (db *DB) selectObjects(tx *bbolt.Tx, fs object.SearchFilters) ([]*object.Ad
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if inGraveyard(tx, addr) {
|
||||||
|
continue // ignore removed objects
|
||||||
|
}
|
||||||
|
|
||||||
|
if !db.matchSlowFilters(tx, addr, group.slowFilters) {
|
||||||
|
continue // ignore objects with unmatched slow filters
|
||||||
|
}
|
||||||
|
|
||||||
res = append(res, addr)
|
res = append(res, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) selectAll(tx *bbolt.Tx) ([]*object.Address, error) {
|
// selectAll adds to resulting cache all available objects in metabase.
|
||||||
result := map[string]struct{}{}
|
func (db *DB) selectAll(tx *bbolt.Tx, cid *container.ID, to map[string]int) {
|
||||||
|
prefix := cid.String() + "/"
|
||||||
|
|
||||||
primaryBucket := tx.Bucket(primaryBucket)
|
selectAllFromBucket(tx, primaryBucketName(cid), prefix, to, 0)
|
||||||
indexBucket := tx.Bucket(indexBucket)
|
selectAllFromBucket(tx, tombstoneBucketName(cid), prefix, to, 0)
|
||||||
|
selectAllFromBucket(tx, storageGroupBucketName(cid), prefix, to, 0)
|
||||||
|
selectAllFromBucket(tx, parentBucketName(cid), prefix, to, 0)
|
||||||
|
}
|
||||||
|
|
||||||
if primaryBucket == nil || indexBucket == nil {
|
// selectAllFromBucket goes through all keys in bucket and adds them in a
|
||||||
return nil, nil
|
// resulting cache. Keys should be stringed object ids.
|
||||||
|
func selectAllFromBucket(tx *bbolt.Tx, name []byte, prefix string, to map[string]int, fNum int) {
|
||||||
|
bkt := tx.Bucket(name)
|
||||||
|
if bkt == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := primaryBucket.ForEach(func(k, _ []byte) error {
|
_ = bkt.ForEach(func(k, v []byte) error {
|
||||||
result[string(k)] = struct{}{}
|
key := prefix + string(k) // consider using string builders from sync.Pool
|
||||||
|
markAddressInCache(to, fNum, key)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
})
|
||||||
return nil, errors.Wrapf(err, "(%T) could not iterate primary bucket", db)
|
}
|
||||||
}
|
|
||||||
|
// selectFastFilter makes fast optimized checks for well known buckets or
|
||||||
rootBucket := indexBucket.Bucket([]byte(v2object.FilterPropertyRoot))
|
// looking through user attribute buckets otherwise.
|
||||||
if rootBucket != nil {
|
func (db *DB) selectFastFilter(
|
||||||
rootBucket = rootBucket.Bucket(nonEmptyKeyBytes(nil))
|
tx *bbolt.Tx,
|
||||||
}
|
cid *container.ID, // container we search on
|
||||||
|
f object.SearchFilter, // fast filter
|
||||||
if rootBucket != nil {
|
to map[string]int, // resulting cache
|
||||||
if err := rootBucket.ForEach(func(k, v []byte) error {
|
fNum int, // index of filter
|
||||||
result[string(k)] = struct{}{}
|
) {
|
||||||
|
prefix := cid.String() + "/"
|
||||||
return nil
|
|
||||||
}); err != nil {
|
switch f.Header() {
|
||||||
return nil, errors.Wrapf(err, "(%T) could not iterate root bucket", db)
|
case v2object.FilterHeaderObjectID:
|
||||||
}
|
db.selectObjectID(tx, f, prefix, to, fNum)
|
||||||
}
|
case v2object.FilterHeaderOwnerID:
|
||||||
|
bucketName := ownerBucketName(cid)
|
||||||
list := make([]*object.Address, 0, len(result))
|
db.selectFromFKBT(tx, bucketName, f, prefix, to, fNum)
|
||||||
|
case v2object.FilterHeaderPayloadHash:
|
||||||
for k := range result {
|
bucketName := payloadHashBucketName(cid)
|
||||||
// check if object marked as deleted
|
db.selectFromList(tx, bucketName, f, prefix, to, fNum)
|
||||||
if objectRemoved(tx, []byte(k)) {
|
case v2object.FilterHeaderObjectType:
|
||||||
continue
|
var bucketName []byte
|
||||||
}
|
|
||||||
|
switch f.Value() { // do it better after https://github.com/nspcc-dev/neofs-api/issues/84
|
||||||
addr := object.NewAddress()
|
case "Regular":
|
||||||
if err := addr.Parse(k); err != nil {
|
bucketName = primaryBucketName(cid)
|
||||||
return nil, err // TODO: storage was broken, so we need to handle it
|
|
||||||
}
|
selectAllFromBucket(tx, bucketName, prefix, to, fNum)
|
||||||
|
|
||||||
list = append(list, addr)
|
bucketName = parentBucketName(cid)
|
||||||
}
|
case "Tombstone":
|
||||||
|
bucketName = tombstoneBucketName(cid)
|
||||||
return list, nil
|
case "StorageGroup":
|
||||||
|
bucketName = storageGroupBucketName(cid)
|
||||||
|
default:
|
||||||
|
db.log.Debug("unknown object type", zap.String("type", f.Value()))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAllFromBucket(tx, bucketName, prefix, to, fNum)
|
||||||
|
case v2object.FilterHeaderParent:
|
||||||
|
bucketName := parentBucketName(cid)
|
||||||
|
db.selectFromList(tx, bucketName, f, prefix, to, fNum)
|
||||||
|
case v2object.FilterHeaderSplitID:
|
||||||
|
bucketName := splitBucketName(cid)
|
||||||
|
db.selectFromList(tx, bucketName, f, prefix, to, fNum)
|
||||||
|
case v2object.FilterPropertyRoot:
|
||||||
|
selectAllFromBucket(tx, rootBucketName(cid), prefix, to, fNum)
|
||||||
|
case v2object.FilterPropertyPhy:
|
||||||
|
selectAllFromBucket(tx, primaryBucketName(cid), prefix, to, fNum)
|
||||||
|
selectAllFromBucket(tx, tombstoneBucketName(cid), prefix, to, fNum)
|
||||||
|
selectAllFromBucket(tx, storageGroupBucketName(cid), prefix, to, fNum)
|
||||||
|
default: // user attribute
|
||||||
|
bucketName := attributeBucketName(cid, f.Header())
|
||||||
|
db.selectFromFKBT(tx, bucketName, f, prefix, to, fNum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectFromList looks into <fkbt> index to find list of addresses to add in
|
||||||
|
// resulting cache.
|
||||||
|
func (db *DB) selectFromFKBT(
|
||||||
|
tx *bbolt.Tx,
|
||||||
|
name []byte, // fkbt root bucket name
|
||||||
|
f object.SearchFilter, // filter for operation and value
|
||||||
|
prefix string, // prefix to create addr from oid in index
|
||||||
|
to map[string]int, // resulting cache
|
||||||
|
fNum int, // index of filter
|
||||||
|
) { //
|
||||||
|
matchFunc, ok := db.matchers[f.Operation()]
|
||||||
|
if !ok {
|
||||||
|
db.log.Debug("missing matcher", zap.Uint32("operation", uint32(f.Operation())))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fkbtRoot := tx.Bucket(name)
|
||||||
|
if fkbtRoot == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := fkbtRoot.ForEach(func(k, _ []byte) error {
|
||||||
|
if matchFunc(f.Header(), k, f.Value()) {
|
||||||
|
fkbtLeaf := fkbtRoot.Bucket(k)
|
||||||
|
if fkbtLeaf == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fkbtLeaf.ForEach(func(k, _ []byte) error {
|
||||||
|
addr := prefix + string(k)
|
||||||
|
markAddressInCache(to, fNum, addr)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
db.log.Debug("error in FKBT selection", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectFromList looks into <list> index to find list of addresses to add in
|
||||||
|
// resulting cache.
|
||||||
|
func (db *DB) selectFromList(
|
||||||
|
tx *bbolt.Tx,
|
||||||
|
name []byte, // list root bucket name
|
||||||
|
f object.SearchFilter, // filter for operation and value
|
||||||
|
prefix string, // prefix to create addr from oid in index
|
||||||
|
to map[string]int, // resulting cache
|
||||||
|
fNum int, // index of filter
|
||||||
|
) { //
|
||||||
|
bkt := tx.Bucket(name)
|
||||||
|
if bkt == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch f.Operation() {
|
||||||
|
case object.MatchStringEqual:
|
||||||
|
default:
|
||||||
|
db.log.Debug("unknown operation", zap.Uint32("operation", uint32(f.Operation())))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// warning: it works only for MatchStringEQ, for NotEQ you should iterate over
|
||||||
|
// bkt and apply matchFunc, don't forget to implement this when it will be
|
||||||
|
// needed. Right now it is not efficient to iterate over bucket
|
||||||
|
// when there is only MatchStringEQ.
|
||||||
|
lst, err := decodeList(bkt.Get(bucketKeyHelper(f.Header(), f.Value())))
|
||||||
|
if err != nil {
|
||||||
|
db.log.Debug("can't decode list bucket leaf", zap.String("error", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range lst {
|
||||||
|
addr := prefix + string(lst[i])
|
||||||
|
markAddressInCache(to, fNum, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectObjectID processes objectID filter with in-place optimizations.
|
||||||
|
func (db *DB) selectObjectID(
|
||||||
|
tx *bbolt.Tx,
|
||||||
|
f object.SearchFilter,
|
||||||
|
prefix string,
|
||||||
|
to map[string]int, // resulting cache
|
||||||
|
fNum int, // index of filter
|
||||||
|
) {
|
||||||
|
switch f.Operation() {
|
||||||
|
case object.MatchStringEqual:
|
||||||
|
default:
|
||||||
|
db.log.Debug("unknown operation", zap.Uint32("operation", uint32(f.Operation())))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// warning: it is in-place optimization and works only for MatchStringEQ,
|
||||||
|
// for NotEQ you should iterate over bkt and apply matchFunc
|
||||||
|
|
||||||
|
addrStr := prefix + f.Value()
|
||||||
|
addr := object.NewAddress()
|
||||||
|
|
||||||
|
err := addr.Parse(addrStr)
|
||||||
|
if err != nil {
|
||||||
|
db.log.Debug("can't decode object id address",
|
||||||
|
zap.String("addr", addrStr),
|
||||||
|
zap.String("error", err.Error()))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := db.exists(tx, addr)
|
||||||
|
if (err == nil && ok) || errors.As(err, &splitInfoError) {
|
||||||
|
markAddressInCache(to, fNum, addrStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchSlowFilters return true if object header is matched by all slow filters.
|
||||||
|
func (db *DB) matchSlowFilters(tx *bbolt.Tx, addr *object.Address, f object.SearchFilters) bool {
|
||||||
|
if len(f) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := db.get(tx, addr, true)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range f {
|
||||||
|
matchFunc, ok := db.matchers[f[i].Operation()]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
|
||||||
|
switch f[i].Header() {
|
||||||
|
case v2object.FilterHeaderVersion:
|
||||||
|
data = []byte(obj.Version().String())
|
||||||
|
case v2object.FilterHeaderHomomorphicHash:
|
||||||
|
data = obj.PayloadHomomorphicHash().Sum()
|
||||||
|
case v2object.FilterHeaderCreationEpoch:
|
||||||
|
data = make([]byte, 8)
|
||||||
|
binary.LittleEndian.PutUint64(data, obj.CreationEpoch())
|
||||||
|
case v2object.FilterHeaderPayloadLength:
|
||||||
|
data = make([]byte, 8)
|
||||||
|
binary.LittleEndian.PutUint64(data, obj.PayloadSize())
|
||||||
|
default:
|
||||||
|
continue // ignore unknown search attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matchFunc(f[i].Header(), data, f[i].Value()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupFilters divides filters in two groups: fast and slow. Fast filters
|
||||||
|
// processed by indexes and slow filters processed after by unmarshaling
|
||||||
|
// object headers.
|
||||||
|
func groupFilters(filters object.SearchFilters) (*filterGroup, error) {
|
||||||
|
res := &filterGroup{
|
||||||
|
fastFilters: make(object.SearchFilters, 0, len(filters)),
|
||||||
|
slowFilters: make(object.SearchFilters, 0, len(filters)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range filters {
|
||||||
|
switch filters[i].Header() {
|
||||||
|
case v2object.FilterHeaderContainerID:
|
||||||
|
res.cid = container.NewID()
|
||||||
|
|
||||||
|
err := res.cid.Parse(filters[i].Value())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't parse container id: %w", err)
|
||||||
|
}
|
||||||
|
case // slow filters
|
||||||
|
v2object.FilterHeaderVersion,
|
||||||
|
v2object.FilterHeaderCreationEpoch,
|
||||||
|
v2object.FilterHeaderPayloadLength,
|
||||||
|
v2object.FilterHeaderHomomorphicHash:
|
||||||
|
res.slowFilters = append(res.slowFilters, filters[i])
|
||||||
|
default: // fast filters or user attributes if unknown
|
||||||
|
res.fastFilters = append(res.fastFilters, filters[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAddressInCache(cache map[string]int, fNum int, addr string) {
|
||||||
|
if num := cache[addr]; num == fNum {
|
||||||
|
cache[addr] = num + 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,226 +1,409 @@
|
||||||
package meta
|
package meta_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/pkg"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/pkg/container"
|
||||||
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func addNFilters(fs *objectSDK.SearchFilters, n int) {
|
func TestDB_SelectUserAttributes(t *testing.T) {
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
key := make([]byte, 32)
|
|
||||||
rand.Read(key)
|
|
||||||
|
|
||||||
val := make([]byte, 32)
|
|
||||||
rand.Read(val)
|
|
||||||
|
|
||||||
fs.AddFilter(string(key), string(val), objectSDK.MatchStringEqual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkDB_Select(b *testing.B) {
|
|
||||||
db := newDB(b)
|
|
||||||
|
|
||||||
defer releaseDB(db)
|
|
||||||
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
obj := generateObject(b, testPrm{
|
|
||||||
withParent: true,
|
|
||||||
attrNum: 100,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(b, db.Put(obj))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range []struct {
|
|
||||||
name string
|
|
||||||
filters func(*objectSDK.SearchFilters)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
filters: func(*objectSDK.SearchFilters) {
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1 filter",
|
|
||||||
filters: func(fs *objectSDK.SearchFilters) {
|
|
||||||
addNFilters(fs, 1)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "10 filters",
|
|
||||||
filters: func(fs *objectSDK.SearchFilters) {
|
|
||||||
addNFilters(fs, 10)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "100 filters",
|
|
||||||
filters: func(fs *objectSDK.SearchFilters) {
|
|
||||||
addNFilters(fs, 100)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1000 filters",
|
|
||||||
filters: func(fs *objectSDK.SearchFilters) {
|
|
||||||
addNFilters(fs, 1000)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
b.Run(item.name, func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
b.StopTimer()
|
|
||||||
fs := new(objectSDK.SearchFilters)
|
|
||||||
item.filters(fs)
|
|
||||||
b.StartTimer()
|
|
||||||
|
|
||||||
_, err := db.Select(*fs)
|
|
||||||
|
|
||||||
b.StopTimer()
|
|
||||||
require.NoError(b, err)
|
|
||||||
b.StartTimer()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMismatchAfterMatch(t *testing.T) {
|
|
||||||
db := newDB(t)
|
db := newDB(t)
|
||||||
defer releaseDB(db)
|
defer releaseDB(db)
|
||||||
|
|
||||||
obj := generateObject(t, testPrm{
|
cid := testCID()
|
||||||
attrNum: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, db.Put(obj))
|
raw1 := generateRawObjectWithCID(t, cid)
|
||||||
|
addAttribute(raw1, "foo", "bar")
|
||||||
|
addAttribute(raw1, "x", "y")
|
||||||
|
|
||||||
a := obj.Attributes()[0]
|
err := db.Put(raw1.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
fs := objectSDK.SearchFilters{}
|
raw2 := generateRawObjectWithCID(t, cid)
|
||||||
|
addAttribute(raw2, "foo", "bar")
|
||||||
|
addAttribute(raw2, "x", "z")
|
||||||
|
|
||||||
// 1st - mismatching filter
|
err = db.Put(raw2.Object(), nil)
|
||||||
fs.AddFilter(a.Key(), a.Value()+"1", objectSDK.MatchStringEqual)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// 2nd - matching filter
|
raw3 := generateRawObjectWithCID(t, cid)
|
||||||
fs.AddFilter(a.Key(), a.Value(), objectSDK.MatchStringEqual)
|
addAttribute(raw3, "a", "b")
|
||||||
|
|
||||||
|
err = db.Put(raw3.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddFilter("foo", "bar", objectSDK.MatchStringEqual)
|
||||||
|
testSelect(t, db, fs,
|
||||||
|
raw1.Object().Address(),
|
||||||
|
raw2.Object().Address(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fs = generateSearchFilter(cid)
|
||||||
|
fs.AddFilter("x", "y", objectSDK.MatchStringEqual)
|
||||||
|
testSelect(t, db, fs, raw1.Object().Address())
|
||||||
|
|
||||||
|
fs = generateSearchFilter(cid)
|
||||||
|
fs.AddFilter("a", "b", objectSDK.MatchStringEqual)
|
||||||
|
testSelect(t, db, fs, raw3.Object().Address())
|
||||||
|
|
||||||
|
fs = generateSearchFilter(cid)
|
||||||
|
fs.AddFilter("c", "d", objectSDK.MatchStringEqual)
|
||||||
testSelect(t, db, fs)
|
testSelect(t, db, fs)
|
||||||
|
|
||||||
|
fs = generateSearchFilter(cid)
|
||||||
|
testSelect(t, db, fs,
|
||||||
|
raw1.Object().Address(),
|
||||||
|
raw2.Object().Address(),
|
||||||
|
raw3.Object().Address(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addCommonAttribute(objs ...*object.Object) *objectSDK.Attribute {
|
func TestDB_SelectRootPhyParent(t *testing.T) {
|
||||||
aCommon := objectSDK.NewAttribute()
|
db := newDB(t)
|
||||||
aCommon.SetKey("common key")
|
defer releaseDB(db)
|
||||||
aCommon.SetValue("common value")
|
|
||||||
|
|
||||||
for _, o := range objs {
|
cid := testCID()
|
||||||
object.NewRawFromObject(o).SetAttributes(
|
|
||||||
append(o.Attributes(), aCommon)...,
|
// prepare
|
||||||
|
|
||||||
|
small := generateRawObjectWithCID(t, cid)
|
||||||
|
err := db.Put(small.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ts := generateRawObjectWithCID(t, cid)
|
||||||
|
ts.SetType(objectSDK.TypeTombstone)
|
||||||
|
err = db.Put(ts.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sg := generateRawObjectWithCID(t, cid)
|
||||||
|
sg.SetType(objectSDK.TypeStorageGroup)
|
||||||
|
err = db.Put(sg.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leftChild := generateRawObjectWithCID(t, cid)
|
||||||
|
leftChild.InitRelations()
|
||||||
|
err = db.Put(leftChild.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
parent := generateRawObjectWithCID(t, cid)
|
||||||
|
|
||||||
|
rightChild := generateRawObjectWithCID(t, cid)
|
||||||
|
rightChild.SetParent(parent.Object().SDK())
|
||||||
|
rightChild.SetParentID(parent.ID())
|
||||||
|
err = db.Put(rightChild.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
link := generateRawObjectWithCID(t, cid)
|
||||||
|
link.SetParent(parent.Object().SDK())
|
||||||
|
link.SetParentID(parent.ID())
|
||||||
|
link.SetChildren(leftChild.ID(), rightChild.ID())
|
||||||
|
|
||||||
|
err = db.Put(link.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("root objects", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddRootFilter()
|
||||||
|
testSelect(t, db, fs,
|
||||||
|
small.Object().Address(),
|
||||||
|
parent.Object().Address(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
return aCommon
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSelectRemoved(t *testing.T) {
|
|
||||||
db := newDB(t)
|
|
||||||
defer releaseDB(db)
|
|
||||||
|
|
||||||
// create 2 objects
|
|
||||||
obj1 := generateObject(t, testPrm{})
|
|
||||||
obj2 := generateObject(t, testPrm{})
|
|
||||||
|
|
||||||
// add common attribute
|
|
||||||
a := addCommonAttribute(obj1, obj2)
|
|
||||||
|
|
||||||
// add to DB
|
|
||||||
require.NoError(t, db.Put(obj1))
|
|
||||||
require.NoError(t, db.Put(obj2))
|
|
||||||
|
|
||||||
fs := objectSDK.SearchFilters{}
|
|
||||||
fs.AddFilter(a.Key(), a.Value(), objectSDK.MatchStringEqual)
|
|
||||||
|
|
||||||
testSelect(t, db, fs, obj1.Address(), obj2.Address())
|
|
||||||
|
|
||||||
// remote 1st object
|
|
||||||
require.NoError(t, db.Delete(obj1.Address()))
|
|
||||||
|
|
||||||
testSelect(t, db, fs, obj2.Address())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMissingObjectAttribute(t *testing.T) {
|
|
||||||
db := newDB(t)
|
|
||||||
defer releaseDB(db)
|
|
||||||
|
|
||||||
// add object w/o attribute
|
|
||||||
obj1 := generateObject(t, testPrm{
|
|
||||||
attrNum: 1,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// add object w/o attribute
|
t.Run("phy objects", func(t *testing.T) {
|
||||||
obj2 := generateObject(t, testPrm{})
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddPhyFilter()
|
||||||
|
testSelect(t, db, fs,
|
||||||
|
small.Object().Address(),
|
||||||
|
ts.Object().Address(),
|
||||||
|
sg.Object().Address(),
|
||||||
|
leftChild.Object().Address(),
|
||||||
|
rightChild.Object().Address(),
|
||||||
|
link.Object().Address(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
a1 := obj1.Attributes()[0]
|
t.Run("regular objects", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddFilter(v2object.FilterHeaderObjectType, "Regular", objectSDK.MatchStringEqual)
|
||||||
|
testSelect(t, db, fs,
|
||||||
|
small.Object().Address(),
|
||||||
|
leftChild.Object().Address(),
|
||||||
|
rightChild.Object().Address(),
|
||||||
|
link.Object().Address(),
|
||||||
|
parent.Object().Address(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// add common attribute
|
t.Run("tombstone objects", func(t *testing.T) {
|
||||||
aCommon := addCommonAttribute(obj1, obj2)
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddFilter(v2object.FilterHeaderObjectType, "Tombstone", objectSDK.MatchStringEqual)
|
||||||
|
testSelect(t, db, fs, ts.Object().Address())
|
||||||
|
})
|
||||||
|
|
||||||
// write to DB
|
t.Run("storage group objects", func(t *testing.T) {
|
||||||
require.NoError(t, db.Put(obj1))
|
fs := generateSearchFilter(cid)
|
||||||
require.NoError(t, db.Put(obj2))
|
fs.AddFilter(v2object.FilterHeaderObjectType, "StorageGroup", objectSDK.MatchStringEqual)
|
||||||
|
testSelect(t, db, fs, sg.Object().Address())
|
||||||
|
})
|
||||||
|
|
||||||
fs := objectSDK.SearchFilters{}
|
t.Run("objects with parent", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddFilter(v2object.FilterHeaderParent,
|
||||||
|
parent.ID().String(),
|
||||||
|
objectSDK.MatchStringEqual)
|
||||||
|
|
||||||
// 1st filter by common attribute
|
testSelect(t, db, fs,
|
||||||
fs.AddFilter(aCommon.Key(), aCommon.Value(), objectSDK.MatchStringEqual)
|
rightChild.Object().Address(),
|
||||||
|
link.Object().Address(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// next filter by attribute from 1st object only
|
t.Run("all objects", func(t *testing.T) {
|
||||||
fs.AddFilter(a1.Key(), a1.Value(), objectSDK.MatchStringEqual)
|
fs := generateSearchFilter(cid)
|
||||||
|
testSelect(t, db, fs,
|
||||||
testSelect(t, db, fs, obj1.Address())
|
small.Object().Address(),
|
||||||
|
ts.Object().Address(),
|
||||||
|
sg.Object().Address(),
|
||||||
|
leftChild.Object().Address(),
|
||||||
|
rightChild.Object().Address(),
|
||||||
|
link.Object().Address(),
|
||||||
|
parent.Object().Address(),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectParentID(t *testing.T) {
|
func TestDB_SelectInhume(t *testing.T) {
|
||||||
db := newDB(t)
|
db := newDB(t)
|
||||||
defer releaseDB(db)
|
defer releaseDB(db)
|
||||||
|
|
||||||
// generate 2 objects
|
cid := testCID()
|
||||||
obj1 := generateObject(t, testPrm{})
|
|
||||||
obj2 := generateObject(t, testPrm{})
|
|
||||||
|
|
||||||
// set parent ID of 1st object
|
raw1 := generateRawObjectWithCID(t, cid)
|
||||||
par := testOID()
|
err := db.Put(raw1.Object(), nil)
|
||||||
object.NewRawFromObject(obj1).SetParentID(par)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// store objects
|
raw2 := generateRawObjectWithCID(t, cid)
|
||||||
require.NoError(t, db.Put(obj1))
|
err = db.Put(raw2.Object(), nil)
|
||||||
require.NoError(t, db.Put(obj2))
|
require.NoError(t, err)
|
||||||
|
|
||||||
// filter by parent ID
|
fs := generateSearchFilter(cid)
|
||||||
fs := objectSDK.SearchFilters{}
|
testSelect(t, db, fs,
|
||||||
fs.AddParentIDFilter(objectSDK.MatchStringEqual, par)
|
raw1.Object().Address(),
|
||||||
|
raw2.Object().Address(),
|
||||||
|
)
|
||||||
|
|
||||||
testSelect(t, db, fs, obj1.Address())
|
tombstone := objectSDK.NewAddress()
|
||||||
|
tombstone.SetContainerID(cid)
|
||||||
|
tombstone.SetObjectID(testOID())
|
||||||
|
|
||||||
|
err = db.Inhume(raw2.Object().Address(), tombstone)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
fs = generateSearchFilter(cid)
|
||||||
|
testSelect(t, db, fs,
|
||||||
|
raw1.Object().Address(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectObjectID(t *testing.T) {
|
func TestDB_SelectPayloadHash(t *testing.T) {
|
||||||
db := newDB(t)
|
db := newDB(t)
|
||||||
defer releaseDB(db)
|
defer releaseDB(db)
|
||||||
|
|
||||||
// generate object
|
cid := testCID()
|
||||||
obj := generateObject(t, testPrm{})
|
|
||||||
|
|
||||||
// store objects
|
raw1 := generateRawObjectWithCID(t, cid)
|
||||||
require.NoError(t, db.Put(obj))
|
err := db.Put(raw1.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
// filter by object ID
|
raw2 := generateRawObjectWithCID(t, cid)
|
||||||
fs := objectSDK.SearchFilters{}
|
err = db.Put(raw2.Object(), nil)
|
||||||
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, obj.ID())
|
require.NoError(t, err)
|
||||||
|
|
||||||
testSelect(t, db, fs, obj.Address())
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddFilter(v2object.FilterHeaderPayloadHash,
|
||||||
|
hex.EncodeToString(raw1.PayloadChecksum().Sum()),
|
||||||
|
objectSDK.MatchStringEqual)
|
||||||
|
|
||||||
|
testSelect(t, db, fs, raw1.Object().Address())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDB_SelectWithSlowFilters(t *testing.T) {
|
||||||
|
db := newDB(t)
|
||||||
|
defer releaseDB(db)
|
||||||
|
|
||||||
|
cid := testCID()
|
||||||
|
|
||||||
|
v20 := new(pkg.Version)
|
||||||
|
v20.SetMajor(2)
|
||||||
|
|
||||||
|
v21 := new(pkg.Version)
|
||||||
|
v21.SetMajor(2)
|
||||||
|
v21.SetMinor(1)
|
||||||
|
|
||||||
|
raw1 := generateRawObjectWithCID(t, cid)
|
||||||
|
raw1.SetPayloadSize(10)
|
||||||
|
raw1.SetCreationEpoch(11)
|
||||||
|
raw1.SetVersion(v20)
|
||||||
|
err := db.Put(raw1.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
raw2 := generateRawObjectWithCID(t, cid)
|
||||||
|
raw2.SetPayloadSize(20)
|
||||||
|
raw2.SetCreationEpoch(21)
|
||||||
|
raw2.SetVersion(v21)
|
||||||
|
err = db.Put(raw2.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("object with TZHash", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddFilter(v2object.FilterHeaderHomomorphicHash,
|
||||||
|
hex.EncodeToString(raw1.PayloadHomomorphicHash().Sum()),
|
||||||
|
objectSDK.MatchStringEqual)
|
||||||
|
|
||||||
|
testSelect(t, db, fs, raw1.Object().Address())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("object with payload length", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddFilter(v2object.FilterHeaderPayloadLength, "20", objectSDK.MatchStringEqual)
|
||||||
|
|
||||||
|
testSelect(t, db, fs, raw2.Object().Address())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("object with creation epoch", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddFilter(v2object.FilterHeaderCreationEpoch, "11", objectSDK.MatchStringEqual)
|
||||||
|
|
||||||
|
testSelect(t, db, fs, raw1.Object().Address())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("object with version", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddObjectVersionFilter(objectSDK.MatchStringEqual, v21)
|
||||||
|
testSelect(t, db, fs, raw2.Object().Address())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSearchFilter(cid *container.ID) objectSDK.SearchFilters {
|
||||||
|
fs := objectSDK.SearchFilters{}
|
||||||
|
fs.AddObjectContainerIDFilter(objectSDK.MatchStringEqual, cid)
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDB_SelectObjectID(t *testing.T) {
|
||||||
|
db := newDB(t)
|
||||||
|
defer releaseDB(db)
|
||||||
|
|
||||||
|
cid := testCID()
|
||||||
|
|
||||||
|
// prepare
|
||||||
|
|
||||||
|
parent := generateRawObjectWithCID(t, cid)
|
||||||
|
|
||||||
|
regular := generateRawObjectWithCID(t, cid)
|
||||||
|
regular.SetParentID(parent.ID())
|
||||||
|
regular.SetParent(parent.Object().SDK())
|
||||||
|
|
||||||
|
err := db.Put(regular.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ts := generateRawObjectWithCID(t, cid)
|
||||||
|
ts.SetType(objectSDK.TypeTombstone)
|
||||||
|
err = db.Put(ts.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sg := generateRawObjectWithCID(t, cid)
|
||||||
|
sg.SetType(objectSDK.TypeStorageGroup)
|
||||||
|
err = db.Put(sg.Object(), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("not found objects", func(t *testing.T) {
|
||||||
|
raw := generateRawObjectWithCID(t, cid)
|
||||||
|
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, raw.ID())
|
||||||
|
|
||||||
|
testSelect(t, db, fs)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("regular objects", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, regular.ID())
|
||||||
|
testSelect(t, db, fs, regular.Object().Address())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("tombstone objects", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, ts.ID())
|
||||||
|
testSelect(t, db, fs, ts.Object().Address())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("storage group objects", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, sg.ID())
|
||||||
|
testSelect(t, db, fs, sg.Object().Address())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("storage group objects", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddObjectIDFilter(objectSDK.MatchStringEqual, parent.ID())
|
||||||
|
testSelect(t, db, fs, parent.Object().Address())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDB_SelectSplitID(t *testing.T) {
|
||||||
|
db := newDB(t)
|
||||||
|
defer releaseDB(db)
|
||||||
|
|
||||||
|
cid := testCID()
|
||||||
|
|
||||||
|
child1 := generateRawObjectWithCID(t, cid)
|
||||||
|
child2 := generateRawObjectWithCID(t, cid)
|
||||||
|
child3 := generateRawObjectWithCID(t, cid)
|
||||||
|
|
||||||
|
split1 := objectSDK.NewSplitID()
|
||||||
|
split2 := objectSDK.NewSplitID()
|
||||||
|
|
||||||
|
child1.SetSplitID(split1)
|
||||||
|
child2.SetSplitID(split1)
|
||||||
|
child3.SetSplitID(split2)
|
||||||
|
|
||||||
|
require.NoError(t, db.Put(child1.Object(), nil))
|
||||||
|
require.NoError(t, db.Put(child2.Object(), nil))
|
||||||
|
require.NoError(t, db.Put(child3.Object(), nil))
|
||||||
|
|
||||||
|
t.Run("split id", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddFilter(v2object.FilterHeaderSplitID, split1.String(), objectSDK.MatchStringEqual)
|
||||||
|
testSelect(t, db, fs,
|
||||||
|
child1.Object().Address(),
|
||||||
|
child2.Object().Address(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fs = generateSearchFilter(cid)
|
||||||
|
fs.AddFilter(v2object.FilterHeaderSplitID, split2.String(), objectSDK.MatchStringEqual)
|
||||||
|
testSelect(t, db, fs, child3.Object().Address())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty split", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddFilter(v2object.FilterHeaderSplitID, "", objectSDK.MatchStringEqual)
|
||||||
|
testSelect(t, db, fs)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unknown split id", func(t *testing.T) {
|
||||||
|
fs := generateSearchFilter(cid)
|
||||||
|
fs.AddFilter(v2object.FilterHeaderSplitID,
|
||||||
|
objectSDK.NewSplitID().String(),
|
||||||
|
objectSDK.MatchStringEqual)
|
||||||
|
testSelect(t, db, fs)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
import (
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor"
|
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor"
|
||||||
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase/v2"
|
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/util/logger"
|
"github.com/nspcc-dev/neofs-node/pkg/util/logger"
|
||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/owner"
|
"github.com/nspcc-dev/neofs-api-go/pkg/owner"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor"
|
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor"
|
||||||
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase/v2"
|
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/shard"
|
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/shard"
|
||||||
"github.com/nspcc-dev/neofs-node/pkg/util/test"
|
"github.com/nspcc-dev/neofs-node/pkg/util/test"
|
||||||
"github.com/nspcc-dev/tzhash/tz"
|
"github.com/nspcc-dev/tzhash/tz"
|
||||||
|
|
Loading…
Reference in a new issue