diff --git a/netmap/node_info.go b/netmap/node_info.go index 08c50e43..3a3b57f7 100644 --- a/netmap/node_info.go +++ b/netmap/node_info.go @@ -96,6 +96,16 @@ func (n Node) Hash() uint64 { return n.ID } +// Hash is a function from hrw.Hasher interface. It is implemented +// to support weighted hrw sorting of buckets. Each bucket is already sorted by hrw, +// thus giving us needed "randomness". +func (n Nodes) Hash() uint64 { + if len(n) > 0 { + return n[0].Hash() + } + return 0 +} + // NodesFromInfo converts slice of NodeInfo to a generic node slice. func NodesFromInfo(infos []NodeInfo) Nodes { nodes := make(Nodes, len(infos)) diff --git a/netmap/selector.go b/netmap/selector.go index 236259b7..87192a00 100644 --- a/netmap/selector.go +++ b/netmap/selector.go @@ -58,8 +58,11 @@ func (c *Context) getSelection(p *PlacementPolicy, s *Selector) ([]Nodes, error) return nil, fmt.Errorf("%w: '%s'", ErrNotEnoughNodes, s.Name()) } + // We need deterministic output in case there is no pivot. + // If pivot is set, buckets are sorted by HRW. + // However, because initial order influences HRW order for buckets with equal weights, + // we also need to have deterministic input to HRW sorting routine. if len(c.pivot) == 0 { - // Deterministic order in case of zero seed. if s.Attribute() == "" { sort.Slice(buckets, func(i, j int) bool { return buckets[i].nodes[0].ID < buckets[j].nodes[0].ID @@ -98,7 +101,7 @@ func (c *Context) getSelection(p *PlacementPolicy, s *Selector) ([]Nodes, error) weights[i] = GetBucketWeight(nodes[i], c.aggregator(), c.weightFunc) } - hrw.SortSliceByWeightIndex(nodes, weights, c.pivotHash) + hrw.SortSliceByWeightValue(nodes, weights, c.pivotHash) } if s.Attribute() == "" { diff --git a/netmap/selector_test.go b/netmap/selector_test.go index 2aa9798c..b84cb1d2 100644 --- a/netmap/selector_test.go +++ b/netmap/selector_test.go @@ -3,13 +3,190 @@ 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")},