frostfs-sdk-go/netmap/selector_test.go

708 lines
20 KiB
Go

package netmap
import (
"errors"
"fmt"
"math/rand"
"sort"
"strconv"
"testing"
"github.com/nspcc-dev/hrw"
"github.com/nspcc-dev/neofs-api-go/v2/netmap"
testv2 "github.com/nspcc-dev/neofs-api-go/v2/netmap/test"
"github.com/stretchr/testify/require"
)
func BenchmarkHRWSort(b *testing.B) {
const netmapSize = 1000
nodes := make([]Nodes, netmapSize)
weights := make([]float64, netmapSize)
for i := range nodes {
nodes[i] = Nodes{{
ID: rand.Uint64(),
Index: i,
Capacity: 100,
Price: 1,
AttrMap: nil,
}}
weights[i] = float64(rand.Uint32()%10) / 10.0
}
pivot := rand.Uint64()
b.Run("sort by index, no weight", func(b *testing.B) {
realNodes := make([]Nodes, netmapSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
copy(realNodes, nodes)
b.StartTimer()
hrw.SortSliceByIndex(realNodes, pivot)
}
})
b.Run("sort by value, no weight", func(b *testing.B) {
realNodes := make([]Nodes, netmapSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
copy(realNodes, nodes)
b.StartTimer()
hrw.SortSliceByValue(realNodes, pivot)
}
})
b.Run("only sort by index", func(b *testing.B) {
realNodes := make([]Nodes, netmapSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
copy(realNodes, nodes)
b.StartTimer()
hrw.SortSliceByWeightIndex(realNodes, weights, pivot)
}
})
b.Run("sort by value", func(b *testing.B) {
realNodes := make([]Nodes, netmapSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
copy(realNodes, nodes)
b.StartTimer()
hrw.SortSliceByWeightValue(realNodes, weights, pivot)
}
})
b.Run("sort by ID, then by index (deterministic)", func(b *testing.B) {
realNodes := make([]Nodes, netmapSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
copy(realNodes, nodes)
b.StartTimer()
sort.Slice(nodes, func(i, j int) bool {
return nodes[i][0].ID < nodes[j][0].ID
})
hrw.SortSliceByWeightIndex(realNodes, weights, pivot)
}
})
}
func BenchmarkPolicyHRWType(b *testing.B) {
const netmapSize = 100
p := newPlacementPolicy(1,
[]*Replica{
newReplica(1, "loc1"),
newReplica(1, "loc2")},
[]*Selector{
newSelector("loc1", "Location", ClauseSame, 1, "loc1"),
newSelector("loc2", "Location", ClauseSame, 1, "loc2")},
[]*Filter{
newFilter("loc1", "Location", "Shanghai", OpEQ),
newFilter("loc2", "Location", "Shanghai", OpNE),
})
nodes := make([]NodeInfo, netmapSize)
for i := range nodes {
var loc string
switch i % 20 {
case 0:
loc = "Shanghai"
default:
loc = strconv.Itoa(i % 20)
}
// Having the same price and capacity ensures equal weights for all nodes.
// This way placement is more dependent on the initial order.
nodes[i] = nodeInfoFromAttributes("Location", loc, "Price", "1", "Capacity", "10")
pub := make([]byte, 33)
pub[0] = byte(i)
nodes[i].SetPublicKey(pub)
}
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(b, err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := nm.GetContainerNodes(p, []byte{1})
if err != nil {
b.Fatal()
}
}
}
func TestPlacementPolicy_DeterministicOrder(t *testing.T) {
const netmapSize = 100
p := newPlacementPolicy(1,
[]*Replica{
newReplica(1, "loc1"),
newReplica(1, "loc2")},
[]*Selector{
newSelector("loc1", "Location", ClauseSame, 1, "loc1"),
newSelector("loc2", "Location", ClauseSame, 1, "loc2")},
[]*Filter{
newFilter("loc1", "Location", "Shanghai", OpEQ),
newFilter("loc2", "Location", "Shanghai", OpNE),
})
nodes := make([]NodeInfo, netmapSize)
for i := range nodes {
var loc string
switch i % 20 {
case 0:
loc = "Shanghai"
default:
loc = strconv.Itoa(i % 20)
}
// Having the same price and capacity ensures equal weights for all nodes.
// This way placement is more dependent on the initial order.
nodes[i] = nodeInfoFromAttributes("Location", loc, "Price", "1", "Capacity", "10")
pub := make([]byte, 33)
pub[0] = byte(i)
nodes[i].SetPublicKey(pub)
}
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(t, err)
getIndices := func(t *testing.T) (int, int) {
v, err := nm.GetContainerNodes(p, []byte{1})
require.NoError(t, err)
ns := v.Flatten()
require.Equal(t, 2, len(ns))
return ns[0].Index, ns[1].Index
}
a, b := getIndices(t)
for i := 0; i < 10; i++ {
x, y := getIndices(t)
require.Equal(t, a, x)
require.Equal(t, b, y)
}
}
func TestPlacementPolicy_UnspecifiedClause(t *testing.T) {
p := newPlacementPolicy(1,
[]*Replica{newReplica(1, "X")},
[]*Selector{
newSelector("X", "", ClauseDistinct, 4, "*"),
},
nil,
)
nodes := []NodeInfo{
nodeInfoFromAttributes("ID", "1", "Country", "RU", "City", "St.Petersburg", "SSD", "0"),
nodeInfoFromAttributes("ID", "2", "Country", "RU", "City", "St.Petersburg", "SSD", "1"),
nodeInfoFromAttributes("ID", "3", "Country", "RU", "City", "Moscow", "SSD", "1"),
nodeInfoFromAttributes("ID", "4", "Country", "RU", "City", "Moscow", "SSD", "1"),
}
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(t, err)
v, err := nm.GetContainerNodes(p, nil)
require.NoError(t, err)
require.Equal(t, 4, len(v.Flatten()))
}
func TestPlacementPolicy_Minimal(t *testing.T) {
nodes := []NodeInfo{
nodeInfoFromAttributes("City", "Saint-Petersburg"),
nodeInfoFromAttributes("City", "Moscow"),
nodeInfoFromAttributes("City", "Berlin"),
nodeInfoFromAttributes("City", "Paris"),
}
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(t, err)
runTest := func(t *testing.T, rep uint32, expectError bool) {
p := newPlacementPolicy(0,
[]*Replica{newReplica(rep, "")},
nil, nil)
v, err := nm.GetContainerNodes(p, nil)
if expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
count := int(rep * defaultCBF)
if count > len(nm.Nodes) {
count = len(nm.Nodes)
}
require.EqualValues(t, count, len(v.Flatten()))
}
t.Run("REP 1", func(t *testing.T) {
runTest(t, 1, false)
})
t.Run("REP 3", func(t *testing.T) {
runTest(t, 3, false)
})
t.Run("REP 5", func(t *testing.T) {
runTest(t, 5, true)
})
}
// Issue #215.
func TestPlacementPolicy_MultipleREP(t *testing.T) {
p := newPlacementPolicy(1,
[]*Replica{
newReplica(1, "LOC_SPB_PLACE"),
newReplica(1, "LOC_MSK_PLACE"),
},
[]*Selector{
newSelector("LOC_SPB_PLACE", "", ClauseUnspecified, 1, "LOC_SPB"),
newSelector("LOC_MSK_PLACE", "", ClauseUnspecified, 1, "LOC_MSK"),
},
[]*Filter{
newFilter("LOC_SPB", "City", "Saint-Petersburg", OpEQ),
newFilter("LOC_MSK", "City", "Moscow", OpEQ),
},
)
nodes := []NodeInfo{
nodeInfoFromAttributes("City", "Saint-Petersburg"),
nodeInfoFromAttributes("City", "Moscow"),
nodeInfoFromAttributes("City", "Berlin"),
nodeInfoFromAttributes("City", "Paris"),
}
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(t, err)
v, err := nm.GetContainerNodes(p, nil)
require.NoError(t, err)
rs := v.Replicas()
require.Equal(t, 2, len(rs))
require.Equal(t, 1, len(rs[0]))
require.Equal(t, "Saint-Petersburg", rs[0][0].Attribute("City"))
require.Equal(t, 1, len(rs[1]))
require.Equal(t, "Moscow", rs[1][0].Attribute("City"))
}
func TestPlacementPolicy_DefaultCBF(t *testing.T) {
p := newPlacementPolicy(0,
[]*Replica{
newReplica(1, "EU"),
},
[]*Selector{
newSelector("EU", "Location", ClauseSame, 1, "*"),
},
nil)
nodes := []NodeInfo{
nodeInfoFromAttributes("Location", "Europe", "Country", "RU", "City", "St.Petersburg"),
nodeInfoFromAttributes("Location", "Europe", "Country", "RU", "City", "Moscow"),
nodeInfoFromAttributes("Location", "Europe", "Country", "DE", "City", "Berlin"),
nodeInfoFromAttributes("Location", "Europe", "Country", "FR", "City", "Paris"),
}
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(t, err)
v, err := nm.GetContainerNodes(p, nil)
require.NoError(t, err)
require.Equal(t, defaultCBF, len(v.Flatten()))
}
func TestPlacementPolicy_GetPlacementVectors(t *testing.T) {
p := newPlacementPolicy(2,
[]*Replica{
newReplica(1, "SPB"),
newReplica(2, "Americas"),
},
[]*Selector{
newSelector("SPB", "City", ClauseSame, 1, "SPBSSD"),
newSelector("Americas", "City", ClauseDistinct, 2, "Americas"),
},
[]*Filter{
newFilter("SPBSSD", "", "", OpAND,
newFilter("", "Country", "RU", OpEQ),
newFilter("", "City", "St.Petersburg", OpEQ),
newFilter("", "SSD", "1", OpEQ)),
newFilter("Americas", "", "", OpOR,
newFilter("", "Continent", "NA", OpEQ),
newFilter("", "Continent", "SA", OpEQ)),
})
nodes := []NodeInfo{
nodeInfoFromAttributes("ID", "1", "Country", "RU", "City", "St.Petersburg", "SSD", "0"),
nodeInfoFromAttributes("ID", "2", "Country", "RU", "City", "St.Petersburg", "SSD", "1"),
nodeInfoFromAttributes("ID", "3", "Country", "RU", "City", "Moscow", "SSD", "1"),
nodeInfoFromAttributes("ID", "4", "Country", "RU", "City", "Moscow", "SSD", "1"),
nodeInfoFromAttributes("ID", "5", "Country", "RU", "City", "St.Petersburg", "SSD", "1"),
nodeInfoFromAttributes("ID", "6", "Continent", "NA", "City", "NewYork"),
nodeInfoFromAttributes("ID", "7", "Continent", "AF", "City", "Cairo"),
nodeInfoFromAttributes("ID", "8", "Continent", "AF", "City", "Cairo"),
nodeInfoFromAttributes("ID", "9", "Continent", "SA", "City", "Lima"),
nodeInfoFromAttributes("ID", "10", "Continent", "AF", "City", "Cairo"),
nodeInfoFromAttributes("ID", "11", "Continent", "NA", "City", "NewYork"),
nodeInfoFromAttributes("ID", "12", "Continent", "NA", "City", "LosAngeles"),
nodeInfoFromAttributes("ID", "13", "Continent", "SA", "City", "Lima"),
}
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(t, err)
v, err := nm.GetContainerNodes(p, nil)
require.NoError(t, err)
require.Equal(t, 2, len(v.Replicas()))
require.Equal(t, 6, len(v.Flatten()))
require.Equal(t, 2, len(v.Replicas()[0]))
ids := map[string]struct{}{}
for _, ni := range v.Replicas()[0] {
require.Equal(t, "RU", ni.Attribute("Country"))
require.Equal(t, "St.Petersburg", ni.Attribute("City"))
require.Equal(t, "1", ni.Attribute("SSD"))
ids[ni.Attribute("ID")] = struct{}{}
}
require.Equal(t, len(v.Replicas()[0]), len(ids), "not all nodes we distinct")
require.Equal(t, 4, len(v.Replicas()[1])) // 2 cities * 2 HRWB
ids = map[string]struct{}{}
for _, ni := range v.Replicas()[1] {
require.Contains(t, []string{"NA", "SA"}, ni.Attribute("Continent"))
ids[ni.Attribute("ID")] = struct{}{}
}
require.Equal(t, len(v.Replicas()[1]), len(ids), "not all nodes we distinct")
}
func TestPlacementPolicy_LowerBound(t *testing.T) {
p := newPlacementPolicy(
2, // backup factor
[]*Replica{
newReplica(1, "X"),
},
[]*Selector{
newSelector("X", "Country", ClauseSame, 2, "*"),
},
nil, // filters
)
nodes := []NodeInfo{
nodeInfoFromAttributes("ID", "1", "Country", "DE"),
nodeInfoFromAttributes("ID", "2", "Country", "DE"),
nodeInfoFromAttributes("ID", "3", "Country", "DE"),
}
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(t, err)
v, err := nm.GetContainerNodes(p, nil)
require.NoError(t, err)
require.Equal(t, 3, len(v.Flatten()))
}
func TestIssue213(t *testing.T) {
p := newPlacementPolicy(1,
[]*Replica{
newReplica(4, ""),
},
[]*Selector{
newSelector("", "", ClauseDistinct, 4, "LOC_EU"),
},
[]*Filter{
newFilter("LOC_EU", "Location", "Europe", OpEQ),
})
nodes := []NodeInfo{
nodeInfoFromAttributes("Location", "Europe", "Country", "Russia", "City", "Moscow"),
nodeInfoFromAttributes("Location", "Europe", "Country", "Russia", "City", "Saint-Petersburg"),
nodeInfoFromAttributes("Location", "Europe", "Country", "Sweden", "City", "Stockholm"),
nodeInfoFromAttributes("Location", "Europe", "Country", "Finalnd", "City", "Helsinki"),
}
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(t, err)
v, err := nm.GetContainerNodes(p, nil)
require.NoError(t, err)
require.Equal(t, 4, len(v.Flatten()))
}
func TestPlacementPolicy_ProcessSelectors(t *testing.T) {
p := newPlacementPolicy(2, nil,
[]*Selector{
newSelector("SameRU", "City", ClauseSame, 2, "FromRU"),
newSelector("DistinctRU", "City", ClauseDistinct, 2, "FromRU"),
newSelector("Good", "Country", ClauseDistinct, 2, "Good"),
newSelector("Main", "Country", ClauseDistinct, 3, "*"),
},
[]*Filter{
newFilter("FromRU", "Country", "Russia", OpEQ),
newFilter("Good", "Rating", "4", OpGE),
})
nodes := []NodeInfo{
nodeInfoFromAttributes("Country", "Russia", "Rating", "1", "City", "SPB"),
nodeInfoFromAttributes("Country", "Germany", "Rating", "5", "City", "Berlin"),
nodeInfoFromAttributes("Country", "Russia", "Rating", "6", "City", "Moscow"),
nodeInfoFromAttributes("Country", "France", "Rating", "4", "City", "Paris"),
nodeInfoFromAttributes("Country", "France", "Rating", "1", "City", "Lyon"),
nodeInfoFromAttributes("Country", "Russia", "Rating", "5", "City", "SPB"),
nodeInfoFromAttributes("Country", "Russia", "Rating", "7", "City", "Moscow"),
nodeInfoFromAttributes("Country", "Germany", "Rating", "3", "City", "Darmstadt"),
nodeInfoFromAttributes("Country", "Germany", "Rating", "7", "City", "Frankfurt"),
nodeInfoFromAttributes("Country", "Russia", "Rating", "9", "City", "SPB"),
nodeInfoFromAttributes("Country", "Russia", "Rating", "9", "City", "SPB"),
}
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(t, err)
c := NewContext(nm)
c.setCBF(p.ContainerBackupFactor())
require.NoError(t, c.processFilters(p))
require.NoError(t, c.processSelectors(p))
for _, s := range p.Selectors() {
sel := c.Selections[s.Name()]
s := c.Selectors[s.Name()]
bucketCount, nodesInBucket := GetNodesCount(p, s)
nodesInBucket *= int(c.cbf)
targ := fmt.Sprintf("selector '%s'", s.Name())
require.Equal(t, bucketCount, len(sel), targ)
for _, res := range sel {
require.Equal(t, nodesInBucket, len(res), targ)
for j := range res {
require.True(t, c.applyFilter(s.Filter(), res[j]), targ)
}
}
}
}
func TestPlacementPolicy_ProcessSelectorsHRW(t *testing.T) {
p := newPlacementPolicy(1, nil,
[]*Selector{
newSelector("Main", "Country", ClauseDistinct, 3, "*"),
}, nil)
// bucket weight order: RU > DE > FR
nodes := []NodeInfo{
nodeInfoFromAttributes("Country", "Germany", AttrPrice, "2", AttrCapacity, "10000"),
nodeInfoFromAttributes("Country", "Germany", AttrPrice, "4", AttrCapacity, "1"),
nodeInfoFromAttributes("Country", "France", AttrPrice, "3", AttrCapacity, "10"),
nodeInfoFromAttributes("Country", "Russia", AttrPrice, "2", AttrCapacity, "10000"),
nodeInfoFromAttributes("Country", "Russia", AttrPrice, "1", AttrCapacity, "10000"),
nodeInfoFromAttributes("Country", "Russia", AttrCapacity, "10000"),
nodeInfoFromAttributes("Country", "France", AttrPrice, "100", AttrCapacity, "1"),
nodeInfoFromAttributes("Country", "France", AttrPrice, "7", AttrCapacity, "10000"),
nodeInfoFromAttributes("Country", "Russia", AttrPrice, "2", AttrCapacity, "1"),
}
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(t, err)
c := NewContext(nm)
c.setPivot([]byte("containerID"))
c.setCBF(p.ContainerBackupFactor())
c.weightFunc = newWeightFunc(newMaxNorm(10000), newReverseMinNorm(1))
c.aggregator = func() aggregator {
return new(maxAgg)
}
require.NoError(t, c.processFilters(p))
require.NoError(t, c.processSelectors(p))
cnt := c.Selections["Main"]
expected := []Nodes{
{{Index: 4, Capacity: 10000, Price: 1}}, // best RU
{{Index: 0, Capacity: 10000, Price: 2}}, // best DE
{{Index: 7, Capacity: 10000, Price: 7}}, // best FR
}
require.Equal(t, len(expected), len(cnt))
for i := range expected {
require.Equal(t, len(expected[i]), len(cnt[i]))
require.Equal(t, expected[i][0].Index, cnt[i][0].Index)
require.Equal(t, expected[i][0].Capacity, cnt[i][0].Capacity)
require.Equal(t, expected[i][0].Price, cnt[i][0].Price)
}
res, err := nm.GetPlacementVectors(containerNodes(cnt), []byte("objectID"))
require.NoError(t, err)
require.Equal(t, res, cnt)
}
func newMaxNorm(max float64) normalizer {
return &maxNorm{max: max}
}
func TestPlacementPolicy_ProcessSelectorsInvalid(t *testing.T) {
testCases := []struct {
name string
p *PlacementPolicy
err error
}{
{
"MissingSelector",
newPlacementPolicy(2, nil,
[]*Selector{nil},
[]*Filter{}),
ErrMissingField,
},
{
"InvalidFilterReference",
newPlacementPolicy(1, nil,
[]*Selector{newSelector("MyStore", "Country", ClauseDistinct, 1, "FromNL")},
[]*Filter{newFilter("FromRU", "Country", "Russia", OpEQ)}),
ErrFilterNotFound,
},
{
"NotEnoughNodes (backup factor)",
newPlacementPolicy(2, nil,
[]*Selector{newSelector("MyStore", "Country", ClauseDistinct, 2, "FromRU")},
[]*Filter{newFilter("FromRU", "Country", "Russia", OpEQ)}),
ErrNotEnoughNodes,
},
{
"NotEnoughNodes (buckets)",
newPlacementPolicy(1, nil,
[]*Selector{newSelector("MyStore", "Country", ClauseDistinct, 2, "FromRU")},
[]*Filter{newFilter("FromRU", "Country", "Russia", OpEQ)}),
ErrNotEnoughNodes,
},
}
nodes := []NodeInfo{
nodeInfoFromAttributes("Country", "Russia"),
nodeInfoFromAttributes("Country", "Germany"),
nodeInfoFromAttributes(),
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
nm, err := NewNetmap(NodesFromInfo(nodes))
require.NoError(t, err)
c := NewContext(nm)
c.setCBF(tc.p.ContainerBackupFactor())
require.NoError(t, c.processFilters(tc.p))
err = c.processSelectors(tc.p)
require.True(t, errors.Is(err, tc.err), "got: %v", err)
})
}
}
func testSelector() *Selector {
s := new(Selector)
s.SetName("name")
s.SetCount(3)
s.SetFilter("filter")
s.SetAttribute("attribute")
s.SetClause(ClauseDistinct)
return s
}
func TestSelector_Name(t *testing.T) {
s := NewSelector()
name := "some name"
s.SetName(name)
require.Equal(t, name, s.Name())
}
func TestSelector_Count(t *testing.T) {
s := NewSelector()
c := uint32(3)
s.SetCount(c)
require.Equal(t, c, s.Count())
}
func TestSelector_Clause(t *testing.T) {
s := NewSelector()
c := ClauseSame
s.SetClause(c)
require.Equal(t, c, s.Clause())
}
func TestSelector_Attribute(t *testing.T) {
s := NewSelector()
a := "some attribute"
s.SetAttribute(a)
require.Equal(t, a, s.Attribute())
}
func TestSelector_Filter(t *testing.T) {
s := NewSelector()
f := "some filter"
s.SetFilter(f)
require.Equal(t, f, s.Filter())
}
func TestSelectorEncoding(t *testing.T) {
s := newSelector("name", "atte", ClauseSame, 1, "filter")
t.Run("binary", func(t *testing.T) {
data, err := s.Marshal()
require.NoError(t, err)
s2 := NewSelector()
require.NoError(t, s2.Unmarshal(data))
require.Equal(t, s, s2)
})
t.Run("json", func(t *testing.T) {
data, err := s.MarshalJSON()
require.NoError(t, err)
s2 := NewSelector()
require.NoError(t, s2.UnmarshalJSON(data))
require.Equal(t, s, s2)
})
}
func TestSelector_ToV2(t *testing.T) {
t.Run("nil", func(t *testing.T) {
var x *Selector
require.Nil(t, x.ToV2())
})
}
func TestNewSelectorFromV2(t *testing.T) {
t.Run("from nil", func(t *testing.T) {
var x *netmap.Selector
require.Nil(t, NewSelectorFromV2(x))
})
t.Run("from non-nil", func(t *testing.T) {
sV2 := testv2.GenerateSelector(false)
s := NewSelectorFromV2(sV2)
require.Equal(t, sV2, s.ToV2())
})
}
func TestNewSelector(t *testing.T) {
t.Run("default values", func(t *testing.T) {
s := NewSelector()
// check initial values
require.Zero(t, s.Count())
require.Equal(t, ClauseUnspecified, s.Clause())
require.Empty(t, s.Attribute())
require.Empty(t, s.Name())
require.Empty(t, s.Filter())
// convert to v2 message
sV2 := s.ToV2()
require.Zero(t, sV2.GetCount())
require.Equal(t, netmap.UnspecifiedClause, sV2.GetClause())
require.Empty(t, sV2.GetAttribute())
require.Empty(t, sV2.GetName())
require.Empty(t, sV2.GetFilter())
})
}