Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serve ObjectService.SearchV2 RPC #3111

Merged
merged 2 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Changelog for NeoFS Node
- `logger.timestamp` config option (#3105)
- Container system attributes verification on IR side (#3107)
- IR `fschain.consensus.rpc.max_websocket_clients` and `fschain.consensus.rpc.session_pool_size` config options (#3126)
- `ObjectService.SearchV2` SN API (#3111)

### Fixed
- `neofs-cli object delete` command output (#3056)
Expand Down
15 changes: 14 additions & 1 deletion cmd/neofs-node/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
containercore "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/engine"
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase"
morphClient "github.com/nspcc-dev/neofs-node/pkg/morph/client"
cntClient "github.com/nspcc-dev/neofs-node/pkg/morph/client/container"
objectService "github.com/nspcc-dev/neofs-node/pkg/services/object"
Expand Down Expand Up @@ -303,10 +304,11 @@
)

storage := storageForObjectService{
local: ls,

Check warning on line 307 in cmd/neofs-node/object.go

View check run for this annotation

Codecov / codecov/patch

cmd/neofs-node/object.go#L307

Added line #L307 was not covered by tests
putSvc: sPut,
keys: keyStorage,
}
server := objectService.New(objSvc, mNumber, fsChain, storage, c.shared.basics.key.PrivateKey, c.metricsCollector, aclChecker, aclSvc)
server := objectService.New(objSvc, mNumber, fsChain, storage, c.shared.basics.key.PrivateKey, c.metricsCollector, aclChecker, aclSvc, coreConstructor)

Check warning on line 311 in cmd/neofs-node/object.go

View check run for this annotation

Codecov / codecov/patch

cmd/neofs-node/object.go#L311

Added line #L311 was not covered by tests

for _, srv := range c.cfgGRPC.servers {
protoobject.RegisterObjectServiceServer(srv, server)
Expand Down Expand Up @@ -603,6 +605,11 @@
return x.containerNodes.forEachContainerNodePublicKeyInLastTwoEpochs(id, f)
}

// ForEachContainerNode implements [objectService.FSChain] interface.
func (x *fsChainForObjects) ForEachContainerNode(cnr cid.ID, f func(netmapsdk.NodeInfo) bool) error {
return x.containerNodes.forEachContainerNode(cnr, false, f)

Check warning on line 610 in cmd/neofs-node/object.go

View check run for this annotation

Codecov / codecov/patch

cmd/neofs-node/object.go#L609-L610

Added lines #L609 - L610 were not covered by tests
}

// IsOwnPublicKey checks whether given binary-encoded public key is assigned to
// local storage node in the network map.
//
Expand All @@ -616,10 +623,16 @@
func (x *fsChainForObjects) LocalNodeUnderMaintenance() bool { return x.isMaintenance.Load() }

type storageForObjectService struct {
local *engine.StorageEngine
putSvc *putsvc.Service
keys *util.KeyStorage
}

// SearchObjects implements [objectService.Storage] interface.
func (x storageForObjectService) SearchObjects(cnr cid.ID, fs objectSDK.SearchFilters, attrs []string, cursor *meta.SearchCursor, count uint16) ([]client.SearchResultItem, *meta.SearchCursor, error) {
return x.local.Search(cnr, fs, attrs, cursor, count)

Check warning on line 633 in cmd/neofs-node/object.go

View check run for this annotation

Codecov / codecov/patch

cmd/neofs-node/object.go#L632-L633

Added lines #L632 - L633 were not covered by tests
}

func (x storageForObjectService) VerifyAndStoreObjectLocally(obj objectSDK.Object) error {
return x.putSvc.ValidateAndStoreObjectLocally(obj)
}
Expand Down
128 changes: 128 additions & 0 deletions pkg/core/object/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package object

import (
"bytes"
"math/big"
"slices"
"strings"

"github.com/nspcc-dev/neofs-sdk-go/client"
)

// TODO: docs.
func MergeSearchResults(lim uint16, withAttr bool, sets [][]client.SearchResultItem, mores []bool) ([]client.SearchResultItem, bool) {
if lim == 0 || len(sets) == 0 {
return nil, false
}
if len(sets) == 1 {
n := min(uint16(len(sets[0])), lim)
return sets[0][:n], n < lim || slices.Contains(mores, true)
}
lim = calcMaxUniqueSearchResults(lim, sets)
res := make([]client.SearchResultItem, 0, lim)
var more bool
var minInt *big.Int
for minInd := -1; ; minInd, minInt = -1, nil {
for i := range sets {
if len(sets[i]) == 0 {
continue
}
if minInd < 0 {
minInd = i
if withAttr {
minInt, _ = new(big.Int).SetString(sets[i][0].Attributes[0], 10)
}
continue
}
cmpID := bytes.Compare(sets[i][0].ID[:], sets[minInd][0].ID[:])
if cmpID == 0 {
continue
}
if withAttr {
var curInt *big.Int
if minInt != nil {
curInt, _ = new(big.Int).SetString(sets[i][0].Attributes[0], 10)
}
var cmpAttr int
if curInt != nil {
cmpAttr = curInt.Cmp(minInt)
} else {
cmpAttr = strings.Compare(sets[i][0].Attributes[0], sets[minInd][0].Attributes[0])
}

Check warning on line 51 in pkg/core/object/metadata.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/object/metadata.go#L50-L51

Added lines #L50 - L51 were not covered by tests
if cmpAttr != 0 {
if cmpAttr < 0 {
minInd = i
if minInt != nil {
minInt = curInt
} else {
minInt, _ = new(big.Int).SetString(sets[i][0].Attributes[0], 10)
}

Check warning on line 59 in pkg/core/object/metadata.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/object/metadata.go#L58-L59

Added lines #L58 - L59 were not covered by tests
}
continue
}
}
if cmpID < 0 {
minInd = i
if withAttr {
minInt, _ = new(big.Int).SetString(sets[minInd][0].Attributes[0], 10)
}

Check warning on line 68 in pkg/core/object/metadata.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/object/metadata.go#L67-L68

Added lines #L67 - L68 were not covered by tests
}
}
if minInd < 0 {
break
}
res = append(res, sets[minInd][0])
if uint16(len(res)) == lim {
if more = len(sets[minInd]) > 1 || slices.Contains(mores, true); !more {
loop:
for i := range sets {
if i == minInd {
continue
}
for j := range sets[i] {
if more = sets[i][j].ID != sets[minInd][0].ID; more {
break loop
}
}
}
}
break
}
for i := range sets {
if i == minInd {
continue
}
for j := range sets[i] {
if sets[i][j].ID == sets[minInd][0].ID {
sets[i] = sets[i][j+1:]
break
}
}
}
sets[minInd] = sets[minInd][1:]
}
return res, more
}

func calcMaxUniqueSearchResults(lim uint16, sets [][]client.SearchResultItem) uint16 {
n := uint16(len(sets[0]))
if n >= lim {
return lim
}

Check warning on line 111 in pkg/core/object/metadata.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/object/metadata.go#L110-L111

Added lines #L110 - L111 were not covered by tests
for i := 1; i < len(sets); i++ {
nextItem:
for j := range sets[i] {
for k := range i {
for l := range sets[k] {
if sets[k][l].ID == sets[i][j].ID {
continue nextItem
}
}
}
if n++; n == lim {
return n
}
}
}
return n
}
185 changes: 185 additions & 0 deletions pkg/core/object/metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package object_test

import (
"bytes"
"slices"
"strconv"
"testing"

. "github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/nspcc-dev/neofs-sdk-go/client"
oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test"
"github.com/stretchr/testify/require"
)

func searchResultFromIDs(n int) []client.SearchResultItem {
ids := oidtest.IDs(n)
s := make([]client.SearchResultItem, len(ids))
for i := range ids {
s[i].ID = ids[i]
}
slices.SortFunc(s, func(a, b client.SearchResultItem) int { return bytes.Compare(a.ID[:], b.ID[:]) })
return s
}

func assertMergeResult(t testing.TB, res, expRes []client.SearchResultItem, more, expMore bool) {
require.Len(t, res, len(expRes))
require.EqualValues(t, len(expRes), cap(res))
require.Equal(t, expRes, res)
require.Equal(t, expMore, more)
}

func TestMergeSearchResults(t *testing.T) {
t.Run("zero limit", func(t *testing.T) {
res, more := MergeSearchResults(0, false, [][]client.SearchResultItem{searchResultFromIDs(2)}, nil)
require.Nil(t, res)
require.False(t, more)
})
t.Run("no sets", func(t *testing.T) {
res, more := MergeSearchResults(1000, false, nil, nil)
require.Nil(t, res)
require.False(t, more)
})
t.Run("empty sets only", func(t *testing.T) {
res, more := MergeSearchResults(1000, false, make([][]client.SearchResultItem, 1000), nil)
require.Empty(t, res)
require.False(t, more)
})
t.Run("with empty sets", func(t *testing.T) {
all := []client.SearchResultItem{
{ID: oidtest.ID(), Attributes: []string{"12", "d"}},
{ID: oidtest.ID(), Attributes: []string{"23", "c"}},
{ID: oidtest.ID(), Attributes: []string{"34", "b"}},
{ID: oidtest.ID(), Attributes: []string{"45", "a"}},
}
sets := [][]client.SearchResultItem{
nil,
{all[0], all[2]},
{},
{all[1], all[3]},
nil,
all,
}
res, more := MergeSearchResults(1000, true, sets, nil)
assertMergeResult(t, res, all, more, false)
})
t.Run("concat", func(t *testing.T) {
t.Run("no attributes", func(t *testing.T) {
all := searchResultFromIDs(10)
var sets [][]client.SearchResultItem
for i := range len(all) / 2 {
sets = append(sets, []client.SearchResultItem{all[2*i], all[2*i+1]})
}
res, more := MergeSearchResults(1000, false, sets, nil)
assertMergeResult(t, res, all, more, false)
t.Run("reverse", func(t *testing.T) {
var sets [][]client.SearchResultItem
for i := range len(all) / 2 {
sets = append(sets, []client.SearchResultItem{all[2*i], all[2*i+1]})
}
slices.Reverse(sets)
res, more := MergeSearchResults(1000, false, sets, nil)
assertMergeResult(t, res, all, more, false)
})
})
t.Run("with attributes", func(t *testing.T) {
all := searchResultFromIDs(10)
slices.Reverse(all)
for i := range all {
all[i].Attributes = []string{strconv.Itoa(i)}
}
var sets [][]client.SearchResultItem
for i := range len(all) / 2 {
sets = append(sets, []client.SearchResultItem{all[2*i], all[2*i+1]})
}
res, more := MergeSearchResults(1000, true, sets, nil)
assertMergeResult(t, res, all, more, false)
t.Run("reverse", func(t *testing.T) {
var sets [][]client.SearchResultItem
for i := range len(all) / 2 {
sets = append(sets, []client.SearchResultItem{all[2*i], all[2*i+1]})
}
slices.Reverse(sets)
res, more := MergeSearchResults(1000, true, sets, nil)
assertMergeResult(t, res, all, more, false)
})
})
})
t.Run("intersecting", func(t *testing.T) {
all := searchResultFromIDs(10)
var sets [][]client.SearchResultItem
for i := range len(all) - 1 {
sets = append(sets, []client.SearchResultItem{all[i], all[i+1]})
}
res, more := MergeSearchResults(1000, false, sets, nil)
assertMergeResult(t, res, all, more, false)
t.Run("with attributes", func(t *testing.T) {
all := searchResultFromIDs(10)
slices.Reverse(all)
for i := range all {
all[i].Attributes = []string{strconv.Itoa(i)}
}
var sets [][]client.SearchResultItem
for i := range len(all) - 1 {
sets = append(sets, []client.SearchResultItem{all[i], all[i+1]})
}
res, more := MergeSearchResults(1000, true, sets, nil)
assertMergeResult(t, res, all, more, false)
})
})
t.Run("cursors", func(t *testing.T) {
all := searchResultFromIDs(10)
t.Run("more items in last set", func(t *testing.T) {
res, more := MergeSearchResults(5, false, [][]client.SearchResultItem{
all[:3],
all[:6],
all[:2],
}, nil)
assertMergeResult(t, res, all[:5], more, true)
})
t.Run("more items in other set", func(t *testing.T) {
res, more := MergeSearchResults(5, false, [][]client.SearchResultItem{
all[:3],
all[:5],
all,
}, nil)
assertMergeResult(t, res, all[:5], more, true)
})
t.Run("flag", func(t *testing.T) {
res, more := MergeSearchResults(5, false, [][]client.SearchResultItem{
all[:1],
all[:5],
all[:2],
}, []bool{
true,
false,
false,
})
assertMergeResult(t, res, all[:5], more, true)
})
})
t.Run("integers", func(t *testing.T) {
vals := []string{
"-111111111111111111111111111111111111111111111111111111",
"-18446744073709551615",
"-1", "0", "1",
"18446744073709551615",
"111111111111111111111111111111111111111111111111111111",
}
all := searchResultFromIDs(len(vals))
slices.Reverse(all)
for i := range all {
all[i].Attributes = []string{vals[i]}
}
for _, sets := range [][][]client.SearchResultItem{
{all},
{all[:len(all)/2], all[len(all)/2:]},
{all[len(all)/2:], all[:len(all)/2]},
{all[6:7], all[0:1], all[5:6], all[1:2], all[4:5], all[2:3], all[3:4]},
{all[5:], all[1:3], all[0:4], all[3:]},
} {
res, more := MergeSearchResults(uint16(len(all)), true, sets, nil)
assertMergeResult(t, res, all, more, false)
}
})
}
Loading
Loading