forked from TrueCloudLab/frostfs-node
[#34] service/object: Implement object Search distributed service
Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
parent
a5ebdd1891
commit
09084a7bff
12 changed files with 825 additions and 0 deletions
64
pkg/services/object/search/local.go
Normal file
64
pkg/services/object/search/local.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package searchsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/bucket"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/localstore"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/services/object/search/query"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type localStream struct {
|
||||
query query.Query
|
||||
|
||||
storage *localstore.Storage
|
||||
}
|
||||
|
||||
func (s *localStream) stream(ctx context.Context, ch chan<- []*object.ID) error {
|
||||
idList := make([]*object.ID, 0)
|
||||
|
||||
if err := s.storage.Iterate(newFilterPipeline(s.query), func(meta *localstore.ObjectMeta) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
default:
|
||||
idList = append(idList, meta.Head().GetID())
|
||||
|
||||
return false
|
||||
}
|
||||
}); err != nil && !errors.Is(errors.Cause(err), bucket.ErrIteratingAborted) {
|
||||
return errors.Wrapf(err, "(%T) could not iterate over local storage", s)
|
||||
}
|
||||
|
||||
ch <- idList
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newFilterPipeline(q query.Query) localstore.FilterPipeline {
|
||||
res := localstore.NewFilter(&localstore.FilterParams{
|
||||
Name: "SEARCH_OBJECTS_FILTER",
|
||||
FilterFunc: func(context.Context, *localstore.ObjectMeta) *localstore.FilterResult {
|
||||
return localstore.ResultPass()
|
||||
},
|
||||
})
|
||||
|
||||
if err := res.PutSubFilter(localstore.SubFilterParams{
|
||||
FilterPipeline: localstore.NewFilter(&localstore.FilterParams{
|
||||
FilterFunc: func(_ context.Context, o *localstore.ObjectMeta) *localstore.FilterResult {
|
||||
if !q.Match(o.Head()) {
|
||||
return localstore.ResultFail()
|
||||
}
|
||||
return localstore.ResultPass()
|
||||
},
|
||||
}),
|
||||
OnIgnore: localstore.CodeFail,
|
||||
OnFail: localstore.CodeFail,
|
||||
}); err != nil {
|
||||
panic(errors.Wrap(err, "could not create all pass including filter"))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
38
pkg/services/object/search/prm.go
Normal file
38
pkg/services/object/search/prm.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package searchsvc
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/pkg/container"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/services/object/search/query"
|
||||
)
|
||||
|
||||
type Prm struct {
|
||||
local bool
|
||||
|
||||
cid *container.ID
|
||||
|
||||
query query.Query
|
||||
}
|
||||
|
||||
func (p *Prm) WithContainerID(v *container.ID) *Prm {
|
||||
if p != nil {
|
||||
p.cid = v
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Prm) WithSearchQuery(v query.Query) *Prm {
|
||||
if p != nil {
|
||||
p.query = v
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Prm) OnlyLocal(v bool) *Prm {
|
||||
if p != nil {
|
||||
p.local = v
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
11
pkg/services/object/search/query/query.go
Normal file
11
pkg/services/object/search/query/query.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||
)
|
||||
|
||||
type Query interface {
|
||||
ToSearchFilters() objectSDK.SearchFilters
|
||||
Match(*object.Object) bool
|
||||
}
|
112
pkg/services/object/search/query/v1/v1.go
Normal file
112
pkg/services/object/search/query/v1/v1.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"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-api-go/pkg/owner"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/core/object"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/services/object/search/query"
|
||||
)
|
||||
|
||||
type Query struct {
|
||||
filters []*Filter
|
||||
}
|
||||
|
||||
type matchType uint8
|
||||
|
||||
type Filter struct {
|
||||
matchType matchType
|
||||
|
||||
key, val string
|
||||
}
|
||||
|
||||
const (
|
||||
_ matchType = iota
|
||||
matchStringEqual
|
||||
)
|
||||
|
||||
func New(filters ...*Filter) query.Query {
|
||||
return &Query{
|
||||
filters: filters,
|
||||
}
|
||||
}
|
||||
|
||||
func idValue(id *objectSDK.ID) string {
|
||||
return hex.EncodeToString(id.ToV2().GetValue())
|
||||
}
|
||||
|
||||
func NewIDEqualFilter(id *objectSDK.ID) *Filter {
|
||||
return NewFilterEqual(objectSDK.HdrSysNameID, idValue(id))
|
||||
}
|
||||
|
||||
func cidValue(id *container.ID) string {
|
||||
return hex.EncodeToString(id.ToV2().GetValue())
|
||||
}
|
||||
|
||||
func NewContainerIDEqualFilter(id *container.ID) *Filter {
|
||||
return NewFilterEqual(objectSDK.HdrSysNameCID, cidValue(id))
|
||||
}
|
||||
|
||||
func ownerIDValue(id *owner.ID) string {
|
||||
return hex.EncodeToString(id.ToV2().GetValue())
|
||||
}
|
||||
|
||||
func NewOwnerIDEqualFilter(id *owner.ID) *Filter {
|
||||
return NewFilterEqual(objectSDK.HdrSysNameOwnerID, ownerIDValue(id))
|
||||
}
|
||||
|
||||
func NewFilterEqual(key, val string) *Filter {
|
||||
return &Filter{
|
||||
matchType: matchStringEqual,
|
||||
key: key,
|
||||
val: val,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) Match(obj *object.Object) bool {
|
||||
for _, f := range q.filters {
|
||||
switch f.matchType {
|
||||
case matchStringEqual:
|
||||
if !headerEqual(obj, f.key, f.val) {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported match type %d", f.matchType))
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func headerEqual(obj *object.Object, key, value string) bool {
|
||||
switch key {
|
||||
default:
|
||||
for _, attr := range obj.GetAttributes() {
|
||||
if attr.GetKey() == key && attr.GetValue() == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
case objectSDK.HdrSysNameID:
|
||||
return value == idValue(obj.GetID())
|
||||
case objectSDK.HdrSysNameCID:
|
||||
return value == cidValue(obj.GetContainerID())
|
||||
case objectSDK.HdrSysNameOwnerID:
|
||||
return value == ownerIDValue(obj.GetOwnerID())
|
||||
// TODO: add other headers
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) ToSearchFilters() objectSDK.SearchFilters {
|
||||
fs := make(objectSDK.SearchFilters, 0, len(q.filters))
|
||||
|
||||
for i := range q.filters {
|
||||
fs.AddFilter(q.filters[i].key, q.filters[i].val, objectSDK.MatchStringEqual)
|
||||
}
|
||||
|
||||
return fs
|
||||
}
|
123
pkg/services/object/search/query/v1/v1_test.go
Normal file
123
pkg/services/object/search/query/v1/v1_test.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testID(t *testing.T) *objectSDK.ID {
|
||||
cs := [sha256.Size]byte{}
|
||||
|
||||
_, err := rand.Read(cs[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
id := objectSDK.NewID()
|
||||
id.SetSHA256(cs)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func testCID(t *testing.T) *container.ID {
|
||||
cs := [sha256.Size]byte{}
|
||||
|
||||
_, err := rand.Read(cs[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
id := container.NewID()
|
||||
id.SetSHA256(cs)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func testOwnerID(t *testing.T) *owner.ID {
|
||||
w := new(owner.NEO3Wallet)
|
||||
|
||||
_, err := rand.Read(w.Bytes())
|
||||
require.NoError(t, err)
|
||||
|
||||
id := owner.NewID()
|
||||
id.SetNeo3Wallet(w)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func TestQ_Match(t *testing.T) {
|
||||
t.Run("object identifier equal", func(t *testing.T) {
|
||||
obj := object.NewRaw()
|
||||
|
||||
id := testID(t)
|
||||
obj.SetID(id)
|
||||
|
||||
q := New(
|
||||
NewIDEqualFilter(id),
|
||||
)
|
||||
|
||||
require.True(t, q.Match(obj.Object()))
|
||||
|
||||
obj.SetID(testID(t))
|
||||
|
||||
require.False(t, q.Match(obj.Object()))
|
||||
})
|
||||
|
||||
t.Run("container identifier equal", func(t *testing.T) {
|
||||
obj := object.NewRaw()
|
||||
|
||||
id := testCID(t)
|
||||
obj.SetContainerID(id)
|
||||
|
||||
q := New(
|
||||
NewContainerIDEqualFilter(id),
|
||||
)
|
||||
|
||||
require.True(t, q.Match(obj.Object()))
|
||||
|
||||
obj.SetContainerID(testCID(t))
|
||||
|
||||
require.False(t, q.Match(obj.Object()))
|
||||
})
|
||||
|
||||
t.Run("owner identifier equal", func(t *testing.T) {
|
||||
obj := object.NewRaw()
|
||||
|
||||
id := testOwnerID(t)
|
||||
obj.SetOwnerID(id)
|
||||
|
||||
q := New(
|
||||
NewOwnerIDEqualFilter(id),
|
||||
)
|
||||
|
||||
require.True(t, q.Match(obj.Object()))
|
||||
|
||||
obj.SetOwnerID(testOwnerID(t))
|
||||
|
||||
require.False(t, q.Match(obj.Object()))
|
||||
})
|
||||
|
||||
t.Run("attribute equal", func(t *testing.T) {
|
||||
obj := object.NewRaw()
|
||||
|
||||
k, v := "key", "val"
|
||||
a := new(objectSDK.Attribute)
|
||||
a.SetKey(k)
|
||||
a.SetValue(v)
|
||||
|
||||
obj.SetAttributes(a)
|
||||
|
||||
q := New(
|
||||
NewFilterEqual(k, v),
|
||||
)
|
||||
|
||||
require.True(t, q.Match(obj.Object()))
|
||||
|
||||
a.SetKey(k + "1")
|
||||
|
||||
require.False(t, q.Match(obj.Object()))
|
||||
})
|
||||
}
|
44
pkg/services/object/search/remote.go
Normal file
44
pkg/services/object/search/remote.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package searchsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/pkg/client"
|
||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/network"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type remoteStream struct {
|
||||
prm *Prm
|
||||
|
||||
key *ecdsa.PrivateKey
|
||||
|
||||
addr *network.Address
|
||||
}
|
||||
|
||||
func (s *remoteStream) stream(ctx context.Context, ch chan<- []*object.ID) error {
|
||||
addr := s.addr.NetAddr()
|
||||
|
||||
c, err := client.New(s.key,
|
||||
client.WithAddress(addr),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "(%T) could not create SDK client %s", s, addr)
|
||||
}
|
||||
|
||||
// TODO: add writer parameter to SDK client
|
||||
id, err := c.SearchObject(ctx, new(client.SearchObjectParams).
|
||||
WithContainerID(s.prm.cid).
|
||||
WithSearchFilters(s.prm.query.ToSearchFilters()),
|
||||
client.WithTTL(1), // FIXME: use constant
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "(%T) could not search objects in %s", s, addr)
|
||||
}
|
||||
|
||||
ch <- id
|
||||
|
||||
return nil
|
||||
}
|
13
pkg/services/object/search/res.go
Normal file
13
pkg/services/object/search/res.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package searchsvc
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
idList []*object.ID
|
||||
}
|
||||
|
||||
func (r *Response) IDList() []*object.ID {
|
||||
return r.idList
|
||||
}
|
98
pkg/services/object/search/service.go
Normal file
98
pkg/services/object/search/service.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package searchsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"sync"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/core/container"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/core/netmap"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/localstore"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/network"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/util"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
*cfg
|
||||
}
|
||||
|
||||
type Option func(*cfg)
|
||||
|
||||
type cfg struct {
|
||||
key *ecdsa.PrivateKey
|
||||
|
||||
localStore *localstore.Storage
|
||||
|
||||
cnrSrc container.Source
|
||||
|
||||
netMapSrc netmap.Source
|
||||
|
||||
workerPool util.WorkerPool
|
||||
|
||||
localAddrSrc network.LocalAddressSource
|
||||
}
|
||||
|
||||
func defaultCfg() *cfg {
|
||||
return &cfg{
|
||||
workerPool: new(util.SyncWorkerPool),
|
||||
}
|
||||
}
|
||||
|
||||
func NewService(opts ...Option) *Service {
|
||||
c := defaultCfg()
|
||||
|
||||
for i := range opts {
|
||||
opts[i](c)
|
||||
}
|
||||
|
||||
return &Service{
|
||||
cfg: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Service) Search(ctx context.Context, prm *Prm) (*Streamer, error) {
|
||||
return &Streamer{
|
||||
cfg: p.cfg,
|
||||
once: new(sync.Once),
|
||||
prm: prm,
|
||||
ctx: ctx,
|
||||
cache: make([][]*object.ID, 0, 10),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func WithKey(v *ecdsa.PrivateKey) Option {
|
||||
return func(c *cfg) {
|
||||
c.key = v
|
||||
}
|
||||
}
|
||||
|
||||
func WithLocalStorage(v *localstore.Storage) Option {
|
||||
return func(c *cfg) {
|
||||
c.localStore = v
|
||||
}
|
||||
}
|
||||
|
||||
func WithContainerSource(v container.Source) Option {
|
||||
return func(c *cfg) {
|
||||
c.cnrSrc = v
|
||||
}
|
||||
}
|
||||
|
||||
func WithNetworkMapSource(v netmap.Source) Option {
|
||||
return func(c *cfg) {
|
||||
c.netMapSrc = v
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerPool(v util.WorkerPool) Option {
|
||||
return func(c *cfg) {
|
||||
c.workerPool = v
|
||||
}
|
||||
}
|
||||
|
||||
func WithLocalAddressSource(v network.LocalAddressSource) Option {
|
||||
return func(c *cfg) {
|
||||
c.localAddrSrc = v
|
||||
}
|
||||
}
|
181
pkg/services/object/search/streamer.go
Normal file
181
pkg/services/object/search/streamer.go
Normal file
|
@ -0,0 +1,181 @@
|
|||
package searchsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/core/netmap"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/network"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/services/object/util"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/services/object_manager/placement"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Streamer struct {
|
||||
*cfg
|
||||
|
||||
once *sync.Once
|
||||
|
||||
prm *Prm
|
||||
|
||||
traverser *placement.Traverser
|
||||
|
||||
ctx context.Context
|
||||
|
||||
ch chan []*object.ID
|
||||
|
||||
cache [][]*object.ID
|
||||
}
|
||||
|
||||
func (p *Streamer) Recv() (*Response, error) {
|
||||
var err error
|
||||
|
||||
p.once.Do(func() {
|
||||
if err = p.preparePrm(p.prm); err == nil {
|
||||
go p.start(p.prm)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "(%T) could not start streaming", p)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return nil, errors.Wrapf(p.ctx.Err(), "(%T) context is done", p)
|
||||
case v, ok := <-p.ch:
|
||||
if !ok {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
v = p.cutCached(v)
|
||||
|
||||
return &Response{
|
||||
idList: v,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Streamer) cutCached(ids []*object.ID) []*object.ID {
|
||||
loop:
|
||||
for i := 0; i < len(ids); i++ {
|
||||
for j := range p.cache {
|
||||
for k := range p.cache[j] {
|
||||
if ids[i].Equal(p.cache[j][k]) {
|
||||
ids = append(ids[:i], ids[i+1:]...)
|
||||
|
||||
i--
|
||||
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
p.cache = append(p.cache, ids)
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
func (p *Streamer) preparePrm(prm *Prm) error {
|
||||
var err error
|
||||
|
||||
// get latest network map
|
||||
nm, err := netmap.GetLatestNetworkMap(p.netMapSrc)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "(%T) could not get latest network map", p)
|
||||
}
|
||||
|
||||
// get container to store the object
|
||||
cnr, err := p.cnrSrc.Get(prm.cid)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "(%T) could not get container by ID", p)
|
||||
}
|
||||
|
||||
// allocate placement traverser options
|
||||
traverseOpts := make([]placement.Option, 0, 4)
|
||||
|
||||
// add common options
|
||||
traverseOpts = append(traverseOpts,
|
||||
// set processing container
|
||||
placement.ForContainer(cnr),
|
||||
)
|
||||
|
||||
// create placement builder from network map
|
||||
builder := placement.NewNetworkMapBuilder(nm)
|
||||
|
||||
if prm.local {
|
||||
// restrict success count to 1 stored copy (to local storage)
|
||||
traverseOpts = append(traverseOpts, placement.SuccessAfter(1))
|
||||
|
||||
// use local-only placement builder
|
||||
builder = util.NewLocalPlacement(placement.NewNetworkMapBuilder(nm), p.localAddrSrc)
|
||||
}
|
||||
|
||||
// set placement builder
|
||||
traverseOpts = append(traverseOpts, placement.UseBuilder(builder))
|
||||
|
||||
// build placement traverser
|
||||
if p.traverser, err = placement.NewTraverser(traverseOpts...); err != nil {
|
||||
return errors.Wrapf(err, "(%T) could not build placement traverser", p)
|
||||
}
|
||||
|
||||
p.ch = make(chan []*object.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Streamer) start(prm *Prm) {
|
||||
defer close(p.ch)
|
||||
|
||||
loop:
|
||||
for {
|
||||
addrs := p.traverser.Next()
|
||||
if len(addrs) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
for i := range addrs {
|
||||
wg.Add(1)
|
||||
|
||||
addr := addrs[i]
|
||||
|
||||
if err := p.workerPool.Submit(func() {
|
||||
defer wg.Done()
|
||||
|
||||
var streamer interface {
|
||||
stream(context.Context, chan<- []*object.ID) error
|
||||
}
|
||||
|
||||
if network.IsLocalAddress(p.localAddrSrc, addr) {
|
||||
streamer = &localStream{
|
||||
query: prm.query,
|
||||
storage: p.localStore,
|
||||
}
|
||||
} else {
|
||||
streamer = &remoteStream{
|
||||
prm: prm,
|
||||
key: p.key,
|
||||
addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
if err := streamer.stream(p.ctx, p.ch); err != nil {
|
||||
// TODO: log error
|
||||
}
|
||||
}); err != nil {
|
||||
wg.Done()
|
||||
// TODO: log error
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
57
pkg/services/object/search/v2/service.go
Normal file
57
pkg/services/object/search/v2/service.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package searchsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||
searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Service implements Search operation of Object service v2.
|
||||
type Service struct {
|
||||
*cfg
|
||||
}
|
||||
|
||||
// Option represents Service constructor option.
|
||||
type Option func(*cfg)
|
||||
|
||||
type cfg struct {
|
||||
svc *searchsvc.Service
|
||||
}
|
||||
|
||||
// NewService constructs Service instance from provided options.
|
||||
func NewService(opts ...Option) *Service {
|
||||
c := new(cfg)
|
||||
|
||||
for i := range opts {
|
||||
opts[i](c)
|
||||
}
|
||||
|
||||
return &Service{
|
||||
cfg: c,
|
||||
}
|
||||
}
|
||||
|
||||
// Search calls internal service and returns v2 search object streamer.
|
||||
func (s *Service) Search(ctx context.Context, req *object.SearchRequest) (object.SearchObjectStreamer, error) {
|
||||
prm, err := toPrm(req.GetBody(), req.GetMetaHeader().GetTTL())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "(%T) could not convert search parameters", s)
|
||||
}
|
||||
|
||||
stream, err := s.svc.Search(ctx, prm)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "(%T) could not open object search stream", s)
|
||||
}
|
||||
|
||||
return &streamer{
|
||||
stream: stream,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func WithInternalService(v *searchsvc.Service) Option {
|
||||
return func(c *cfg) {
|
||||
c.svc = v
|
||||
}
|
||||
}
|
26
pkg/services/object/search/v2/streamer.go
Normal file
26
pkg/services/object/search/v2/streamer.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package searchsvc
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||
searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type streamer struct {
|
||||
stream *searchsvc.Streamer
|
||||
}
|
||||
|
||||
func (s *streamer) Recv() (*object.SearchResponse, error) {
|
||||
r, err := s.stream.Recv()
|
||||
if err != nil {
|
||||
if errors.Is(errors.Cause(err), io.EOF) {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "(%T) could not receive search response", s)
|
||||
}
|
||||
|
||||
return fromResponse(r), nil
|
||||
}
|
58
pkg/services/object/search/v2/util.go
Normal file
58
pkg/services/object/search/v2/util.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package searchsvc
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neofs-api-go/pkg/container"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/object"
|
||||
"github.com/nspcc-dev/neofs-api-go/v2/refs"
|
||||
searchsvc "github.com/nspcc-dev/neofs-node/pkg/services/object/search"
|
||||
"github.com/nspcc-dev/neofs-node/pkg/services/object/search/query"
|
||||
queryV1 "github.com/nspcc-dev/neofs-node/pkg/services/object/search/query/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func toPrm(req *object.SearchRequestBody, ttl uint32) (*searchsvc.Prm, error) {
|
||||
var q query.Query
|
||||
|
||||
switch v := req.GetVersion(); v {
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported query version #%d", v)
|
||||
case 1:
|
||||
fs := req.GetFilters()
|
||||
fsV1 := make([]*queryV1.Filter, 0, len(fs))
|
||||
|
||||
for _, f := range fs {
|
||||
switch mt := f.GetMatchType(); mt {
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported match type %d in query version #%d", mt, v)
|
||||
case object.MatchStringEqual:
|
||||
fsV1 = append(fsV1, queryV1.NewFilterEqual(f.GetName(), f.GetValue()))
|
||||
}
|
||||
}
|
||||
|
||||
q = queryV1.New(fsV1...)
|
||||
}
|
||||
|
||||
return new(searchsvc.Prm).
|
||||
WithContainerID(
|
||||
container.NewIDFromV2(req.GetContainerID()),
|
||||
).
|
||||
WithSearchQuery(q).
|
||||
OnlyLocal(ttl == 1), nil // FIXME: use constant
|
||||
}
|
||||
|
||||
func fromResponse(r *searchsvc.Response) *object.SearchResponse {
|
||||
ids := r.IDList()
|
||||
idsV2 := make([]*refs.ObjectID, 0, len(ids))
|
||||
|
||||
for i := range ids {
|
||||
idsV2 = append(idsV2, ids[i].ToV2())
|
||||
}
|
||||
|
||||
body := new(object.SearchResponseBody)
|
||||
body.SetIDList(idsV2)
|
||||
|
||||
resp := new(object.SearchResponse)
|
||||
resp.SetBody(body)
|
||||
|
||||
return resp
|
||||
}
|
Loading…
Reference in a new issue