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()) }) }