[#227] netmap: Do not use intermediate types for placement

Support preprocessing within the `NodeInfo` type. Provide methods for
placement directly from the `NodeInfo` type. Returns slice of slices of
`NodeInfo` from placement methods of `Netmap`. Remove no longer needed
`Node` and `Nodes` types.

```
name            old time/op    new time/op    delta
ManySelects-12    19.7µs ±14%    15.8µs ±15%  -19.70%  (p=0.000 n=20+20)

name            old alloc/op   new alloc/op   delta
ManySelects-12    8.65kB ± 0%    6.22kB ± 0%  -28.03%  (p=0.000 n=20+20)

name            old allocs/op  new allocs/op  delta
ManySelects-12      82.0 ± 0%      81.0 ± 0%   -1.22%  (p=0.000 n=20+20)
```

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2022-06-07 05:12:39 +03:00 committed by LeL
parent 2b21146185
commit 723ba5ee45
12 changed files with 305 additions and 246 deletions

View file

@ -57,7 +57,7 @@ type (
} }
// weightFunc calculates n's weight. // weightFunc calculates n's weight.
weightFunc = func(n *Node) float64 weightFunc = func(NodeInfo) float64
) )
var ( var (
@ -76,8 +76,8 @@ var (
// newWeightFunc returns weightFunc which multiplies normalized // newWeightFunc returns weightFunc which multiplies normalized
// capacity and price. // capacity and price.
func newWeightFunc(capNorm, priceNorm normalizer) weightFunc { func newWeightFunc(capNorm, priceNorm normalizer) weightFunc {
return func(n *Node) float64 { return func(n NodeInfo) float64 {
return capNorm.Normalize(float64(n.Capacity)) * priceNorm.Normalize(float64(n.Price)) return capNorm.Normalize(float64(n.capacity())) * priceNorm.Normalize(float64(n.price()))
} }
} }

View file

@ -1,19 +0,0 @@
package netmap
// ContainerNodes represents nodes in the container.
type ContainerNodes interface {
Replicas() []Nodes
Flatten() Nodes
}
type containerNodes []Nodes
// Flatten returns list of all nodes from the container.
func (c containerNodes) Flatten() Nodes {
return flattenNodes(c)
}
// Replicas return list of container replicas.
func (c containerNodes) Replicas() []Nodes {
return c
}

View file

@ -15,7 +15,7 @@ type context struct {
// Selectors stores processed selectors. // Selectors stores processed selectors.
Selectors map[string]*Selector Selectors map[string]*Selector
// Selections stores result of selector processing. // Selections stores result of selector processing.
Selections map[string][]Nodes Selections map[string][]nodes
// numCache stores parsed numeric values. // numCache stores parsed numeric values.
numCache map[string]uint64 numCache map[string]uint64
@ -54,11 +54,11 @@ func newContext(nm *Netmap) *context {
Netmap: nm, Netmap: nm,
Filters: make(map[string]*Filter), Filters: make(map[string]*Filter),
Selectors: make(map[string]*Selector), Selectors: make(map[string]*Selector),
Selections: make(map[string][]Nodes), Selections: make(map[string][]nodes),
numCache: make(map[string]uint64), numCache: make(map[string]uint64),
aggregator: newMeanIQRAgg, aggregator: newMeanIQRAgg,
weightFunc: GetDefaultWeightFunc(nm.Nodes), weightFunc: GetDefaultWeightFunc(nm.nodes),
cbf: defaultCBF, cbf: defaultCBF,
} }
} }
@ -79,13 +79,13 @@ func (c *context) setCBF(cbf uint32) {
} }
// GetDefaultWeightFunc returns default weighting function. // GetDefaultWeightFunc returns default weighting function.
func GetDefaultWeightFunc(ns Nodes) weightFunc { func GetDefaultWeightFunc(ns nodes) weightFunc {
mean := newMeanAgg() mean := newMeanAgg()
min := newMinAgg() min := newMinAgg()
for i := range ns { for i := range ns {
mean.Add(float64(ns[i].Capacity)) mean.Add(float64(ns[i].capacity()))
min.Add(float64(ns[i].Price)) min.Add(float64(ns[i].price()))
} }
return newWeightFunc( return newWeightFunc(

View file

@ -15,7 +15,7 @@ type Filter netmap.Filter
const MainFilterName = "*" const MainFilterName = "*"
// applyFilter applies named filter to b. // applyFilter applies named filter to b.
func (c *context) applyFilter(name string, b *Node) bool { func (c *context) applyFilter(name string, b NodeInfo) bool {
return name == MainFilterName || c.match(c.Filters[name], b) return name == MainFilterName || c.match(c.Filters[name], b)
} }
@ -86,7 +86,7 @@ func (c *context) processFilter(f *Filter, top bool) error {
// match matches f against b. It returns no errors because // match matches f against b. It returns no errors because
// filter should have been parsed during context creation // filter should have been parsed during context creation
// and missing node properties are considered as a regular fail. // and missing node properties are considered as a regular fail.
func (c *context) match(f *Filter, b *Node) bool { func (c *context) match(f *Filter, b NodeInfo) bool {
switch f.Operation() { switch f.Operation() {
case OpAND, OpOR: case OpAND, OpOR:
for _, lf := range f.InnerFilters() { for _, lf := range f.InnerFilters() {
@ -106,24 +106,24 @@ func (c *context) match(f *Filter, b *Node) bool {
} }
} }
func (c *context) matchKeyValue(f *Filter, b *Node) bool { func (c *context) matchKeyValue(f *Filter, b NodeInfo) bool {
switch f.Operation() { switch f.Operation() {
case OpEQ: case OpEQ:
return b.Attribute(f.Key()) == f.Value() return b.attribute(f.Key()) == f.Value()
case OpNE: case OpNE:
return b.Attribute(f.Key()) != f.Value() return b.attribute(f.Key()) != f.Value()
default: default:
var attr uint64 var attr uint64
switch f.Key() { switch f.Key() {
case AttrPrice: case AttrPrice:
attr = b.Price attr = b.price()
case AttrCapacity: case AttrCapacity:
attr = b.Capacity attr = b.capacity()
default: default:
var err error var err error
attr, err = strconv.ParseUint(b.Attribute(f.Key()), 10, 64) attr, err = strconv.ParseUint(b.attribute(f.Key()), 10, 64)
if err != nil { if err != nil {
// Note: because filters are somewhat independent from nodes attributes, // Note: because filters are somewhat independent from nodes attributes,
// We don't report an error here, and fail filter instead. // We don't report an error here, and fail filter instead.

View file

@ -17,9 +17,8 @@ func TestContext_ProcessFilters(t *testing.T) {
newFilter("", "IntField", "123", OpLT), newFilter("", "IntField", "123", OpLT),
newFilter("GoodRating", "", "", 0)), newFilter("GoodRating", "", "", 0)),
} }
nm, err := NewNetmap(nil)
require.NoError(t, err) c := newContext(new(Netmap))
c := newContext(nm)
p := newPlacementPolicy(1, nil, nil, fs) p := newPlacementPolicy(1, nil, nil, fs)
require.NoError(t, c.processFilters(p)) require.NoError(t, c.processFilters(p))
require.Equal(t, 3, len(c.Filters)) require.Equal(t, 3, len(c.Filters))
@ -81,10 +80,16 @@ func TestContext_ProcessFiltersInvalid(t *testing.T) {
} }
func TestFilter_MatchSimple_InvalidOp(t *testing.T) { func TestFilter_MatchSimple_InvalidOp(t *testing.T) {
b := &Node{AttrMap: map[string]string{ var aRating NodeAttribute
"Rating": "4", aRating.SetKey("Rating")
"Country": "Germany", aRating.SetValue("4")
}}
var aCountry NodeAttribute
aRating.SetKey("Country")
aRating.SetValue("Germany")
var b NodeInfo
b.SetAttributes(aRating, aCountry)
f := newFilter("Main", "Rating", "5", OpEQ) f := newFilter("Main", "Rating", "5", OpEQ)
c := newContext(new(Netmap)) c := newContext(new(Netmap))

View file

@ -26,7 +26,7 @@ type TestCase struct {
} }
} }
func compareNodes(t testing.TB, expected [][]int, nodes Nodes, actual []Nodes) { func compareNodes(t testing.TB, expected [][]int, nodes nodes, actual [][]NodeInfo) {
require.Equal(t, len(expected), len(actual)) require.Equal(t, len(expected), len(actual))
for i := range expected { for i := range expected {
require.Equal(t, len(expected[i]), len(actual[i])) require.Equal(t, len(expected[i]), len(actual[i]))
@ -56,9 +56,8 @@ func TestPlacementPolicy_Interopability(t *testing.T) {
copy(srcNodes, tc.Nodes) copy(srcNodes, tc.Nodes)
t.Run(tc.Name, func(t *testing.T) { t.Run(tc.Name, func(t *testing.T) {
nodes := NodesFromInfo(tc.Nodes) var nm Netmap
nm, err := NewNetmap(nodes) nm.SetNodes(tc.Nodes)
require.NoError(t, err)
for name, tt := range tc.Tests { for name, tt := range tc.Tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
@ -70,13 +69,12 @@ func TestPlacementPolicy_Interopability(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, srcNodes, tc.Nodes) require.Equal(t, srcNodes, tc.Nodes)
res := v.Replicas() compareNodes(t, tt.Result, tc.Nodes, v)
compareNodes(t, tt.Result, nodes, res)
if tt.Placement.Result != nil { if tt.Placement.Result != nil {
res, err := nm.GetPlacementVectors(v, tt.Placement.Pivot) res, err := nm.GetPlacementVectors(v, tt.Placement.Pivot)
require.NoError(t, err) require.NoError(t, err)
compareNodes(t, tt.Placement.Result, nodes, res) compareNodes(t, tt.Placement.Result, tc.Nodes, res)
require.Equal(t, srcNodes, tc.Nodes) require.Equal(t, srcNodes, tc.Nodes)
} }
} }
@ -103,8 +101,8 @@ func BenchmarkPlacementPolicyInteropability(b *testing.B) {
require.NoError(b, json.Unmarshal(bs, &tc), "cannot unmarshal %s", ds[i].Name()) require.NoError(b, json.Unmarshal(bs, &tc), "cannot unmarshal %s", ds[i].Name())
b.Run(tc.Name, func(b *testing.B) { b.Run(tc.Name, func(b *testing.B) {
nodes := NodesFromInfo(tc.Nodes) var nm Netmap
nm, err := NewNetmap(nodes) nm.SetNodes(tc.Nodes)
require.NoError(b, err) require.NoError(b, err)
for name, tt := range tc.Tests { for name, tt := range tc.Tests {
@ -121,15 +119,14 @@ func BenchmarkPlacementPolicyInteropability(b *testing.B) {
} else { } else {
require.NoError(b, err) require.NoError(b, err)
res := v.Replicas() compareNodes(b, tt.Result, tc.Nodes, v)
compareNodes(b, tt.Result, nodes, res)
if tt.Placement.Result != nil { if tt.Placement.Result != nil {
b.StartTimer() b.StartTimer()
res, err := nm.GetPlacementVectors(v, tt.Placement.Pivot) res, err := nm.GetPlacementVectors(v, tt.Placement.Pivot)
b.StopTimer() b.StopTimer()
require.NoError(b, err) require.NoError(b, err)
compareNodes(b, tt.Placement.Result, nodes, res) compareNodes(b, tt.Placement.Result, tc.Nodes, res)
} }
} }
} }
@ -149,9 +146,8 @@ func BenchmarkManySelects(b *testing.B) {
tt, ok := tc.Tests["Select"] tt, ok := tc.Tests["Select"]
require.True(b, ok) require.True(b, ok)
nodes := NodesFromInfo(tc.Nodes) var nm Netmap
nm, err := NewNetmap(nodes) nm.SetNodes(tc.Nodes)
require.NoError(b, err)
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()

View file

@ -1,33 +1,140 @@
package netmap package netmap
import ( import (
"bytes"
"fmt" "fmt"
"strconv"
"github.com/nspcc-dev/hrw" "github.com/nspcc-dev/hrw"
"github.com/nspcc-dev/neofs-api-go/v2/netmap"
) )
const defaultCBF = 3 const defaultCBF = 3
var _, _ hrw.Hasher = NodeInfo{}, nodes{}
// Hash implements hrw.Hasher interface.
//
// Hash is needed to support weighted HRW therefore sort function sorts nodes
// based on their public key.
func (i NodeInfo) Hash() uint64 {
return hrw.Hash(i.m.GetPublicKey())
}
func (i NodeInfo) less(i2 NodeInfo) bool {
return bytes.Compare(i.PublicKey(), i2.PublicKey()) < 0
}
// attribute returns value of the node attribute by the given key. Returns empty
// string if attribute is missing.
//
// Method is needed to internal placement needs.
func (i NodeInfo) attribute(key string) string {
as := i.m.GetAttributes()
for j := range as {
if as[j].GetKey() == key {
return as[j].GetValue()
}
}
return ""
}
func (i *NodeInfo) syncAttributes() {
as := i.m.GetAttributes()
for j := range as {
switch as[j].GetKey() {
case AttrPrice:
i.priceAttr, _ = strconv.ParseUint(as[j].GetValue(), 10, 64)
case AttrCapacity:
i.capAttr, _ = strconv.ParseUint(as[j].GetValue(), 10, 64)
}
}
}
func (i *NodeInfo) setPrice(price uint64) {
i.priceAttr = price
as := i.m.GetAttributes()
for j := range as {
if as[j].GetKey() == AttrPrice {
as[j].SetValue(strconv.FormatUint(i.capAttr, 10))
return
}
}
as = append(as, netmap.Attribute{})
as[len(as)-1].SetKey(AttrPrice)
as[len(as)-1].SetValue(strconv.FormatUint(i.capAttr, 10))
i.m.SetAttributes(as)
}
func (i *NodeInfo) price() uint64 {
return i.priceAttr
}
func (i *NodeInfo) setCapacity(capacity uint64) {
i.capAttr = capacity
as := i.m.GetAttributes()
for j := range as {
if as[j].GetKey() == AttrCapacity {
as[j].SetValue(strconv.FormatUint(i.capAttr, 10))
return
}
}
as = append(as, netmap.Attribute{})
as[len(as)-1].SetKey(AttrCapacity)
as[len(as)-1].SetValue(strconv.FormatUint(i.capAttr, 10))
i.m.SetAttributes(as)
}
func (i NodeInfo) capacity() uint64 {
return i.capAttr
}
// Netmap represents netmap which contains preprocessed nodes. // Netmap represents netmap which contains preprocessed nodes.
type Netmap struct { type Netmap struct {
Nodes Nodes nodes []NodeInfo
} }
// NewNetmap constructs netmap from the list of raw nodes. func (m *Netmap) SetNodes(nodes []NodeInfo) {
func NewNetmap(nodes Nodes) (*Netmap, error) { m.nodes = nodes
return &Netmap{
Nodes: nodes,
}, nil
} }
func flattenNodes(ns []Nodes) Nodes { type nodes []NodeInfo
// 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
}
// weights returns slice of nodes weights W.
func (n nodes) weights(wf weightFunc) []float64 {
w := make([]float64, 0, len(n))
for i := range n {
w = append(w, wf(n[i]))
}
return w
}
func flattenNodes(ns []nodes) nodes {
var sz, i int var sz, i int
for i = range ns { for i = range ns {
sz += len(ns[i]) sz += len(ns[i])
} }
result := make(Nodes, 0, sz) result := make(nodes, 0, sz)
for i := range ns { for i := range ns {
result = append(result, ns[i]...) result = append(result, ns[i]...)
@ -37,15 +144,15 @@ func flattenNodes(ns []Nodes) Nodes {
} }
// GetPlacementVectors returns placement vectors for an object given containerNodes cnt. // GetPlacementVectors returns placement vectors for an object given containerNodes cnt.
func (m *Netmap) GetPlacementVectors(cnt ContainerNodes, pivot []byte) ([]Nodes, error) { func (m *Netmap) GetPlacementVectors(vectors [][]NodeInfo, pivot []byte) ([][]NodeInfo, error) {
h := hrw.Hash(pivot) h := hrw.Hash(pivot)
wf := GetDefaultWeightFunc(m.Nodes) wf := GetDefaultWeightFunc(m.nodes)
result := make([]Nodes, len(cnt.Replicas())) result := make([][]NodeInfo, len(vectors))
for i, rep := range cnt.Replicas() { for i := range vectors {
result[i] = make(Nodes, len(rep)) result[i] = make([]NodeInfo, len(vectors[i]))
copy(result[i], rep) copy(result[i], vectors[i])
hrw.SortSliceByWeightValue(result[i], result[i].Weights(wf), h) hrw.SortSliceByWeightValue(result[i], nodes(result[i]).weights(wf), h)
} }
return result, nil return result, nil
@ -54,7 +161,7 @@ func (m *Netmap) GetPlacementVectors(cnt ContainerNodes, pivot []byte) ([]Nodes,
// GetContainerNodes returns nodes corresponding to each replica. // GetContainerNodes returns nodes corresponding to each replica.
// Order of returned nodes corresponds to order of replicas in p. // Order of returned nodes corresponds to order of replicas in p.
// pivot is a seed for HRW sorting. // pivot is a seed for HRW sorting.
func (m *Netmap) GetContainerNodes(p *PlacementPolicy, pivot []byte) (ContainerNodes, error) { func (m *Netmap) GetContainerNodes(p *PlacementPolicy, pivot []byte) ([][]NodeInfo, error) {
c := newContext(m) c := newContext(m)
c.setPivot(pivot) c.setPivot(pivot)
c.setCBF(p.ContainerBackupFactor()) c.setCBF(p.ContainerBackupFactor())
@ -67,7 +174,7 @@ func (m *Netmap) GetContainerNodes(p *PlacementPolicy, pivot []byte) (ContainerN
return nil, err return nil, err
} }
result := make([]Nodes, len(p.Replicas())) result := make([][]NodeInfo, len(p.Replicas()))
for i, r := range p.Replicas() { for i, r := range p.Replicas() {
if r.Selector() == "" { if r.Selector() == "" {
@ -99,5 +206,5 @@ func (m *Netmap) GetContainerNodes(p *PlacementPolicy, pivot []byte) (ContainerN
result[i] = append(result[i], flattenNodes(nodes)...) result[i] = append(result[i], flattenNodes(nodes)...)
} }
return containerNodes(result), nil return result, nil
} }

View file

@ -1,28 +1,9 @@
package netmap package netmap
import ( import (
"strconv"
"github.com/nspcc-dev/hrw"
"github.com/nspcc-dev/neofs-api-go/v2/netmap" "github.com/nspcc-dev/neofs-api-go/v2/netmap"
) )
type (
// Node is a wrapper over NodeInfo.
Node struct {
ID uint64
Index int
Capacity uint64
Price uint64
AttrMap map[string]string
*NodeInfo
}
// Nodes represents slice of graph leafs.
Nodes []Node
)
// NodeState is an enumeration of various states of the NeoFS node. // NodeState is an enumeration of various states of the NeoFS node.
type NodeState uint32 type NodeState uint32
@ -30,7 +11,13 @@ type NodeState uint32
type NodeAttribute netmap.Attribute type NodeAttribute netmap.Attribute
// NodeInfo represents v2 compatible descriptor of the NeoFS node. // NodeInfo represents v2 compatible descriptor of the NeoFS node.
type NodeInfo netmap.NodeInfo type NodeInfo struct {
priceAttr uint64
capAttr uint64
m *netmap.NodeInfo
}
const ( const (
_ NodeState = iota _ NodeState = iota
@ -87,76 +74,10 @@ const (
AttrContinent = "Continent" AttrContinent = "Continent"
) )
var _ hrw.Hasher = (*Node)(nil)
// Hash is a function from hrw.Hasher interface. It is implemented
// to support weighted hrw therefore sort function sorts nodes
// based on their `N` value.
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))
for i := range infos {
nodes[i] = *newNodeV2(i, &infos[i])
}
return nodes
}
func newNodeV2(index int, ni *NodeInfo) *Node {
n := &Node{
ID: hrw.Hash(ni.PublicKey()),
Index: index,
AttrMap: make(map[string]string, len(ni.Attributes())),
NodeInfo: ni,
}
for _, attr := range ni.Attributes() {
switch attr.Key() {
case AttrCapacity:
n.Capacity, _ = strconv.ParseUint(attr.Value(), 10, 64)
case AttrPrice:
n.Price, _ = strconv.ParseUint(attr.Value(), 10, 64)
}
n.AttrMap[attr.Key()] = attr.Value()
}
return n
}
// Weights returns slice of nodes weights W.
func (n Nodes) Weights(wf weightFunc) []float64 {
w := make([]float64, 0, len(n))
for i := range n {
w = append(w, wf(&n[i]))
}
return w
}
// Attribute returns value of attribute k.
func (n *Node) Attribute(k string) string {
return n.AttrMap[k]
}
// GetBucketWeight computes weight for a Bucket. // GetBucketWeight computes weight for a Bucket.
func GetBucketWeight(ns Nodes, a aggregator, wf weightFunc) float64 { func GetBucketWeight(ns nodes, a aggregator, wf weightFunc) float64 {
for i := range ns { for i := range ns {
a.Add(wf(&ns[i])) a.Add(wf(ns[i]))
} }
return a.Compute() return a.Compute()
@ -307,29 +228,41 @@ func NewNodeInfo() *NodeInfo {
// //
// Nil netmap.NodeInfo converts to nil. // Nil netmap.NodeInfo converts to nil.
func NewNodeInfoFromV2(i *netmap.NodeInfo) *NodeInfo { func NewNodeInfoFromV2(i *netmap.NodeInfo) *NodeInfo {
return (*NodeInfo)(i) var res NodeInfo
res.m = i
res.syncAttributes()
return &res
} }
// ToV2 converts NodeInfo to v2 NodeInfo. // ToV2 converts NodeInfo to v2 NodeInfo.
// //
// Nil NodeInfo converts to nil. // Nil NodeInfo converts to nil.
func (i *NodeInfo) ToV2() *netmap.NodeInfo { func (i *NodeInfo) ToV2() *netmap.NodeInfo {
return (*netmap.NodeInfo)(i) if i == nil {
return nil
}
return i.m
} }
// PublicKey returns public key of the node in a binary format. // PublicKey returns public key of the node in a binary format.
func (i *NodeInfo) PublicKey() []byte { func (i *NodeInfo) PublicKey() []byte {
return (*netmap.NodeInfo)(i).GetPublicKey() return i.m.GetPublicKey()
} }
// SetPublicKey sets public key of the node in a binary format. // SetPublicKey sets public key of the node in a binary format.
func (i *NodeInfo) SetPublicKey(key []byte) { func (i *NodeInfo) SetPublicKey(key []byte) {
(*netmap.NodeInfo)(i).SetPublicKey(key) if i.m == nil {
i.m = new(netmap.NodeInfo)
}
i.m.SetPublicKey(key)
} }
// NumberOfAddresses returns number of network addresses of the node. // NumberOfAddresses returns number of network addresses of the node.
func (i *NodeInfo) NumberOfAddresses() int { func (i *NodeInfo) NumberOfAddresses() int {
return (*netmap.NodeInfo)(i).NumberOfAddresses() return i.m.NumberOfAddresses()
} }
// IterateAddresses iterates over network addresses of the node. // IterateAddresses iterates over network addresses of the node.
@ -337,7 +270,7 @@ func (i *NodeInfo) NumberOfAddresses() int {
// //
// Handler should not be nil. // Handler should not be nil.
func (i *NodeInfo) IterateAddresses(f func(string) bool) { func (i *NodeInfo) IterateAddresses(f func(string) bool) {
(*netmap.NodeInfo)(i).IterateAddresses(f) i.m.IterateAddresses(f)
} }
// IterateAllAddresses is a helper function to unconditionally // IterateAllAddresses is a helper function to unconditionally
@ -351,7 +284,11 @@ func IterateAllAddresses(i *NodeInfo, f func(string)) {
// SetAddresses sets list of network addresses of the node. // SetAddresses sets list of network addresses of the node.
func (i *NodeInfo) SetAddresses(v ...string) { func (i *NodeInfo) SetAddresses(v ...string) {
(*netmap.NodeInfo)(i).SetAddresses(v...) if i.m == nil {
i.m = new(netmap.NodeInfo)
}
i.m.SetAddresses(v...)
} }
// Attributes returns list of the node attributes. // Attributes returns list of the node attributes.
@ -360,7 +297,7 @@ func (i *NodeInfo) Attributes() []NodeAttribute {
return nil return nil
} }
as := (*netmap.NodeInfo)(i).GetAttributes() as := i.m.GetAttributes()
if as == nil { if as == nil {
return nil return nil
@ -383,38 +320,58 @@ func (i *NodeInfo) SetAttributes(as ...NodeAttribute) {
asV2[ind] = *as[ind].ToV2() asV2[ind] = *as[ind].ToV2()
} }
(*netmap.NodeInfo)(i). if i.m == nil {
SetAttributes(asV2) i.m = new(netmap.NodeInfo)
}
i.m.SetAttributes(asV2)
} }
// State returns node state. // State returns node state.
func (i *NodeInfo) State() NodeState { func (i *NodeInfo) State() NodeState {
return NodeStateFromV2( return NodeStateFromV2(i.m.GetState())
(*netmap.NodeInfo)(i).GetState(),
)
} }
// SetState sets node state. // SetState sets node state.
func (i *NodeInfo) SetState(s NodeState) { func (i *NodeInfo) SetState(s NodeState) {
(*netmap.NodeInfo)(i).SetState(s.ToV2()) if i.m == nil {
i.m = new(netmap.NodeInfo)
}
i.m.SetState(s.ToV2())
} }
// Marshal marshals NodeInfo into a protobuf binary form. // Marshal marshals NodeInfo into a protobuf binary form.
func (i *NodeInfo) Marshal() ([]byte, error) { func (i *NodeInfo) Marshal() ([]byte, error) {
return (*netmap.NodeInfo)(i).StableMarshal(nil), nil return i.m.StableMarshal(nil), nil
} }
// Unmarshal unmarshals protobuf binary representation of NodeInfo. // Unmarshal unmarshals protobuf binary representation of NodeInfo.
func (i *NodeInfo) Unmarshal(data []byte) error { func (i *NodeInfo) Unmarshal(data []byte) error {
return (*netmap.NodeInfo)(i).Unmarshal(data) if i.m == nil {
i.m = new(netmap.NodeInfo)
}
return i.m.Unmarshal(data)
} }
// MarshalJSON encodes NodeInfo to protobuf JSON format. // MarshalJSON encodes NodeInfo to protobuf JSON format.
func (i *NodeInfo) MarshalJSON() ([]byte, error) { func (i *NodeInfo) MarshalJSON() ([]byte, error) {
return (*netmap.NodeInfo)(i).MarshalJSON() return i.m.MarshalJSON()
} }
// UnmarshalJSON decodes NodeInfo from protobuf JSON format. // UnmarshalJSON decodes NodeInfo from protobuf JSON format.
func (i *NodeInfo) UnmarshalJSON(data []byte) error { func (i *NodeInfo) UnmarshalJSON(data []byte) error {
return (*netmap.NodeInfo)(i).UnmarshalJSON(data) if i.m == nil {
i.m = new(netmap.NodeInfo)
}
err := i.m.UnmarshalJSON(data)
if err != nil {
return err
}
i.syncAttributes()
return nil
} }

View file

@ -95,7 +95,7 @@ func TestNodeInfoFromV2(t *testing.T) {
t.Run("from nil", func(t *testing.T) { t.Run("from nil", func(t *testing.T) {
var x *netmap.NodeInfo var x *netmap.NodeInfo
require.Nil(t, NewNodeInfoFromV2(x)) require.Nil(t, NewNodeInfoFromV2(x).m)
}) })
t.Run("from non-nil", func(t *testing.T) { t.Run("from non-nil", func(t *testing.T) {

View file

@ -49,7 +49,7 @@ func GetNodesCount(_ *PlacementPolicy, s *Selector) (int, int) {
// getSelection returns nodes grouped by s.attribute. // getSelection returns nodes grouped by s.attribute.
// Last argument specifies if more buckets can be used to fulfill CBF. // Last argument specifies if more buckets can be used to fulfill CBF.
func (c *context) getSelection(p *PlacementPolicy, s *Selector) ([]Nodes, error) { func (c *context) getSelection(p *PlacementPolicy, s *Selector) ([]nodes, error) {
bucketCount, nodesInBucket := GetNodesCount(p, s) bucketCount, nodesInBucket := GetNodesCount(p, s)
buckets := c.getSelectionBase(p.SubnetID(), s) buckets := c.getSelectionBase(p.SubnetID(), s)
@ -64,7 +64,7 @@ func (c *context) getSelection(p *PlacementPolicy, s *Selector) ([]Nodes, error)
if len(c.pivot) == 0 { if len(c.pivot) == 0 {
if s.Attribute() == "" { if s.Attribute() == "" {
sort.Slice(buckets, func(i, j int) bool { sort.Slice(buckets, func(i, j int) bool {
return buckets[i].nodes[0].ID < buckets[j].nodes[0].ID return buckets[i].nodes[0].less(buckets[j].nodes[0])
}) })
} else { } else {
sort.Slice(buckets, func(i, j int) bool { sort.Slice(buckets, func(i, j int) bool {
@ -74,52 +74,52 @@ func (c *context) getSelection(p *PlacementPolicy, s *Selector) ([]Nodes, error)
} }
maxNodesInBucket := nodesInBucket * int(c.cbf) maxNodesInBucket := nodesInBucket * int(c.cbf)
nodes := make([]Nodes, 0, len(buckets)) res := make([]nodes, 0, len(buckets))
fallback := make([]Nodes, 0, len(buckets)) fallback := make([]nodes, 0, len(buckets))
for i := range buckets { for i := range buckets {
ns := buckets[i].nodes ns := buckets[i].nodes
if len(ns) >= maxNodesInBucket { if len(ns) >= maxNodesInBucket {
nodes = append(nodes, ns[:maxNodesInBucket]) res = append(res, ns[:maxNodesInBucket])
} else if len(ns) >= nodesInBucket { } else if len(ns) >= nodesInBucket {
fallback = append(fallback, ns) fallback = append(fallback, ns)
} }
} }
if len(nodes) < bucketCount { if len(res) < bucketCount {
// Fallback to using minimum allowed backup factor (1). // Fallback to using minimum allowed backup factor (1).
nodes = append(nodes, fallback...) res = append(res, fallback...)
if len(nodes) < bucketCount { if len(res) < bucketCount {
return nil, fmt.Errorf("%w: '%s'", ErrNotEnoughNodes, s.Name()) return nil, fmt.Errorf("%w: '%s'", ErrNotEnoughNodes, s.Name())
} }
} }
if len(c.pivot) != 0 { if len(c.pivot) != 0 {
weights := make([]float64, len(nodes)) weights := make([]float64, len(res))
for i := range nodes { for i := range res {
weights[i] = GetBucketWeight(nodes[i], c.aggregator(), c.weightFunc) weights[i] = GetBucketWeight(res[i], c.aggregator(), c.weightFunc)
} }
hrw.SortSliceByWeightValue(nodes, weights, c.pivotHash) hrw.SortSliceByWeightValue(res, weights, c.pivotHash)
} }
if s.Attribute() == "" { if s.Attribute() == "" {
nodes, fallback = nodes[:bucketCount], nodes[bucketCount:] res, fallback = res[:bucketCount], res[bucketCount:]
for i := range fallback { for i := range fallback {
index := i % bucketCount index := i % bucketCount
if len(nodes[index]) >= maxNodesInBucket { if len(res[index]) >= maxNodesInBucket {
break break
} }
nodes[index] = append(nodes[index], fallback[i]...) res[index] = append(res[index], fallback[i]...)
} }
} }
return nodes[:bucketCount], nil return res[:bucketCount], nil
} }
type nodeAttrPair struct { type nodeAttrPair struct {
attr string attr string
nodes Nodes nodes nodes
} }
// getSelectionBase returns nodes grouped by selector attribute. // getSelectionBase returns nodes grouped by selector attribute.
@ -128,25 +128,25 @@ func (c *context) getSelectionBase(subnetID *subnetid.ID, s *Selector) []nodeAtt
f := c.Filters[s.Filter()] f := c.Filters[s.Filter()]
isMain := s.Filter() == MainFilterName isMain := s.Filter() == MainFilterName
result := []nodeAttrPair{} result := []nodeAttrPair{}
nodeMap := map[string]Nodes{} nodeMap := map[string][]NodeInfo{}
attr := s.Attribute() attr := s.Attribute()
for i := range c.Netmap.Nodes { for i := range c.Netmap.nodes {
var sid subnetid.ID var sid subnetid.ID
if subnetID != nil { if subnetID != nil {
sid = *subnetID sid = *subnetID
} }
// TODO(fyrchik): make `BelongsToSubnet` to accept pointer // TODO(fyrchik): make `BelongsToSubnet` to accept pointer
if !BelongsToSubnet(c.Netmap.Nodes[i].NodeInfo, sid) { if !BelongsToSubnet(&c.Netmap.nodes[i], sid) {
continue continue
} }
if isMain || c.match(f, &c.Netmap.Nodes[i]) { if isMain || c.match(f, c.Netmap.nodes[i]) {
if attr == "" { if attr == "" {
// Default attribute is transparent identifier which is different for every node. // Default attribute is transparent identifier which is different for every node.
result = append(result, nodeAttrPair{attr: "", nodes: Nodes{c.Netmap.Nodes[i]}}) result = append(result, nodeAttrPair{attr: "", nodes: nodes{c.Netmap.nodes[i]}})
} else { } else {
v := c.Netmap.Nodes[i].Attribute(attr) v := c.Netmap.nodes[i].attribute(attr)
nodeMap[v] = append(nodeMap[v], c.Netmap.Nodes[i]) nodeMap[v] = append(nodeMap[v], c.Netmap.nodes[i])
} }
} }
} }
@ -159,7 +159,7 @@ func (c *context) getSelectionBase(subnetID *subnetid.ID, s *Selector) []nodeAtt
if len(c.pivot) != 0 { if len(c.pivot) != 0 {
for i := range result { for i := range result {
hrw.SortSliceByWeightValue(result[i].nodes, result[i].nodes.Weights(c.weightFunc), c.pivotHash) hrw.SortSliceByWeightValue(result[i].nodes, result[i].nodes.weights(c.weightFunc), c.pivotHash)
} }
} }

View file

@ -16,74 +16,76 @@ import (
func BenchmarkHRWSort(b *testing.B) { func BenchmarkHRWSort(b *testing.B) {
const netmapSize = 1000 const netmapSize = 1000
nodes := make([]Nodes, netmapSize) vectors := make([]nodes, netmapSize)
weights := make([]float64, netmapSize) weights := make([]float64, netmapSize)
for i := range nodes { for i := range vectors {
nodes[i] = Nodes{{ key := make([]byte, 33)
ID: rand.Uint64(), rand.Read(key)
Index: i,
Capacity: 100, var node NodeInfo
Price: 1, node.setPrice(1)
AttrMap: nil, node.setCapacity(100)
}} node.SetPublicKey(key)
vectors[i] = nodes{node}
weights[i] = float64(rand.Uint32()%10) / 10.0 weights[i] = float64(rand.Uint32()%10) / 10.0
} }
pivot := rand.Uint64() pivot := rand.Uint64()
b.Run("sort by index, no weight", func(b *testing.B) { b.Run("sort by index, no weight", func(b *testing.B) {
realNodes := make([]Nodes, netmapSize) realNodes := make([]nodes, netmapSize)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
b.StopTimer() b.StopTimer()
copy(realNodes, nodes) copy(realNodes, vectors)
b.StartTimer() b.StartTimer()
hrw.SortSliceByIndex(realNodes, pivot) hrw.SortSliceByIndex(realNodes, pivot)
} }
}) })
b.Run("sort by value, no weight", func(b *testing.B) { b.Run("sort by value, no weight", func(b *testing.B) {
realNodes := make([]Nodes, netmapSize) realNodes := make([]nodes, netmapSize)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
b.StopTimer() b.StopTimer()
copy(realNodes, nodes) copy(realNodes, vectors)
b.StartTimer() b.StartTimer()
hrw.SortSliceByValue(realNodes, pivot) hrw.SortSliceByValue(realNodes, pivot)
} }
}) })
b.Run("only sort by index", func(b *testing.B) { b.Run("only sort by index", func(b *testing.B) {
realNodes := make([]Nodes, netmapSize) realNodes := make([]nodes, netmapSize)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
b.StopTimer() b.StopTimer()
copy(realNodes, nodes) copy(realNodes, vectors)
b.StartTimer() b.StartTimer()
hrw.SortSliceByWeightIndex(realNodes, weights, pivot) hrw.SortSliceByWeightIndex(realNodes, weights, pivot)
} }
}) })
b.Run("sort by value", func(b *testing.B) { b.Run("sort by value", func(b *testing.B) {
realNodes := make([]Nodes, netmapSize) realNodes := make([]nodes, netmapSize)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
b.StopTimer() b.StopTimer()
copy(realNodes, nodes) copy(realNodes, vectors)
b.StartTimer() b.StartTimer()
hrw.SortSliceByWeightValue(realNodes, weights, pivot) hrw.SortSliceByWeightValue(realNodes, weights, pivot)
} }
}) })
b.Run("sort by ID, then by index (deterministic)", func(b *testing.B) { b.Run("sort by ID, then by index (deterministic)", func(b *testing.B) {
realNodes := make([]Nodes, netmapSize) realNodes := make([]nodes, netmapSize)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
b.StopTimer() b.StopTimer()
copy(realNodes, nodes) copy(realNodes, vectors)
b.StartTimer() b.StartTimer()
sort.Slice(nodes, func(i, j int) bool { sort.Slice(vectors, func(i, j int) bool {
return nodes[i][0].ID < nodes[j][0].ID return vectors[i][0].less(vectors[j][0])
}) })
hrw.SortSliceByWeightIndex(realNodes, weights, pivot) hrw.SortSliceByWeightIndex(realNodes, weights, pivot)
} }
@ -123,8 +125,8 @@ func BenchmarkPolicyHRWType(b *testing.B) {
nodes[i].SetPublicKey(pub) nodes[i].SetPublicKey(pub)
} }
nm, err := NewNetmap(NodesFromInfo(nodes)) var nm Netmap
require.NoError(b, err) nm.SetNodes(nodes)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
@ -150,8 +152,8 @@ func TestPlacementPolicy_DeterministicOrder(t *testing.T) {
newFilter("loc2", "Location", "Shanghai", OpNE), newFilter("loc2", "Location", "Shanghai", OpNE),
}) })
nodes := make([]NodeInfo, netmapSize) nodeList := make([]NodeInfo, netmapSize)
for i := range nodes { for i := range nodeList {
var loc string var loc string
switch i % 20 { switch i % 20 {
case 0: case 0:
@ -162,20 +164,27 @@ func TestPlacementPolicy_DeterministicOrder(t *testing.T) {
// Having the same price and capacity ensures equal weights for all nodes. // Having the same price and capacity ensures equal weights for all nodes.
// This way placement is more dependent on the initial order. // This way placement is more dependent on the initial order.
nodes[i] = nodeInfoFromAttributes("Location", loc, "Price", "1", "Capacity", "10") nodeList[i] = nodeInfoFromAttributes("Location", loc, "Price", "1", "Capacity", "10")
pub := make([]byte, 33) pub := make([]byte, 33)
pub[0] = byte(i) pub[0] = byte(i)
nodes[i].SetPublicKey(pub) nodeList[i].SetPublicKey(pub)
} }
nm, err := NewNetmap(NodesFromInfo(nodes)) var nm Netmap
require.NoError(t, err) nm.SetNodes(nodeList)
getIndices := func(t *testing.T) (int, int) {
getIndices := func(t *testing.T) (uint64, uint64) {
v, err := nm.GetContainerNodes(p, []byte{1}) v, err := nm.GetContainerNodes(p, []byte{1})
require.NoError(t, err) require.NoError(t, err)
ns := v.Flatten()
nss := make([]nodes, len(v))
for i := range v {
nss[i] = v[i]
}
ns := flattenNodes(nss)
require.Equal(t, 2, len(ns)) require.Equal(t, 2, len(ns))
return ns[0].Index, ns[1].Index return ns[0].Hash(), ns[1].Hash()
} }
a, b := getIndices(t) a, b := getIndices(t)
@ -212,9 +221,9 @@ func TestPlacementPolicy_ProcessSelectors(t *testing.T) {
nodeInfoFromAttributes("Country", "Russia", "Rating", "9", "City", "SPB"), nodeInfoFromAttributes("Country", "Russia", "Rating", "9", "City", "SPB"),
} }
nm, err := NewNetmap(NodesFromInfo(nodes)) var nm Netmap
require.NoError(t, err) nm.SetNodes(nodes)
c := newContext(nm) c := newContext(&nm)
c.setCBF(p.ContainerBackupFactor()) c.setCBF(p.ContainerBackupFactor())
require.NoError(t, c.processFilters(p)) require.NoError(t, c.processFilters(p))
require.NoError(t, c.processSelectors(p)) require.NoError(t, c.processSelectors(p))
@ -229,7 +238,7 @@ func TestPlacementPolicy_ProcessSelectors(t *testing.T) {
for _, res := range sel { for _, res := range sel {
require.Equal(t, nodesInBucket, len(res), targ) require.Equal(t, nodesInBucket, len(res), targ)
for j := range res { for j := range res {
require.True(t, c.applyFilter(s.Filter(), &res[j]), targ) require.True(t, c.applyFilter(s.Filter(), res[j]), targ)
} }
} }
} }

View file

@ -31,7 +31,11 @@ func (i *NodeInfo) changeSubnet(id subnetid.ID, isMember bool) {
info.SetID(&idv2) info.SetID(&idv2)
info.SetEntryFlag(isMember) info.SetEntryFlag(isMember)
netmap.WriteSubnetInfo((*netmap.NodeInfo)(i), info) if i.m == nil {
i.m = new(netmap.NodeInfo)
}
netmap.WriteSubnetInfo(i.m, info)
} }
// ErrRemoveSubnet is returned when a node needs to leave the subnet. // ErrRemoveSubnet is returned when a node needs to leave the subnet.
@ -48,7 +52,7 @@ var ErrRemoveSubnet = netmap.ErrRemoveSubnet
func (i *NodeInfo) IterateSubnets(f func(subnetid.ID) error) error { func (i *NodeInfo) IterateSubnets(f func(subnetid.ID) error) error {
var id subnetid.ID var id subnetid.ID
return netmap.IterateSubnets((*netmap.NodeInfo)(i), func(idv2 refs.SubnetID) error { return netmap.IterateSubnets(i.m, func(idv2 refs.SubnetID) error {
err := id.ReadFromV2(idv2) err := id.ReadFromV2(idv2)
if err != nil { if err != nil {
return fmt.Errorf("invalid subnet: %w", err) return fmt.Errorf("invalid subnet: %w", err)