Initial commit

Initial public review release v0.10.0
This commit is contained in:
alexvanin 2020-07-10 17:17:51 +03:00 committed by Stanislav Bogatyrev
commit dadfd90dcd
276 changed files with 46331 additions and 0 deletions

392
lib/netmap/netmap.go Normal file
View file

@ -0,0 +1,392 @@
package netmap
import (
"crypto/sha256"
"encoding/json"
"reflect"
"sort"
"sync"
"github.com/nspcc-dev/neofs-api-go/bootstrap"
"github.com/nspcc-dev/netmap"
"github.com/pkg/errors"
"github.com/spaolacci/murmur3"
)
type (
// Bucket is an alias for github.com/nspcc-dev/netmap.Bucket
Bucket = netmap.Bucket
// SFGroup is an alias for github.com/nspcc-dev/netmap.SFGroup
SFGroup = netmap.SFGroup
// Select is an alias for github.com/nspcc-dev/netmap.Select
Select = netmap.Select
// Filter is an alias for github.com/nspcc-dev/netmap.Filter
Filter = netmap.Filter
// SimpleFilter is an alias for github.com/nspcc-dev/netmap.Filter
SimpleFilter = netmap.SimpleFilter
// PlacementRule is an alias for github.com/nspcc-dev/netmap.Filter
PlacementRule = netmap.PlacementRule
// NetMap is a general network map structure for NeoFS
NetMap struct {
mu *sync.RWMutex
root Bucket
items Nodes
}
// Nodes is an alias for slice of NodeInfo which is structure that describes every host
Nodes []bootstrap.NodeInfo
)
const (
// Separator separates key:value pairs in string representation of options.
Separator = netmap.Separator
// NodesBucket is the name for optionless bucket containing only nodes.
NodesBucket = netmap.NodesBucket
)
var (
// FilterIn returns filter, which checks if value is in specified list.
FilterIn = netmap.FilterIn
// FilterNotIn returns filter, which checks if value is not in specified list.
FilterNotIn = netmap.FilterNotIn
// FilterOR returns OR combination of filters.
FilterOR = netmap.FilterOR
// FilterAND returns AND combination of filters.
FilterAND = netmap.FilterAND
// FilterEQ returns filter, which checks if value is equal to v.
FilterEQ = netmap.FilterEQ
// FilterNE returns filter, which checks if value is not equal to v.
FilterNE = netmap.FilterNE
// FilterGT returns filter, which checks if value is greater than v.
FilterGT = netmap.FilterGT
// FilterGE returns filter, which checks if value is greater or equal than v.
FilterGE = netmap.FilterGE
// FilterLT returns filter, which checks if value is less than v.
FilterLT = netmap.FilterLT
// FilterLE returns filter, which checks if value is less or equal than v.
FilterLE = netmap.FilterLE
)
var errNetMapsConflict = errors.New("netmaps are in conflict")
// Copy creates new slice of copied nodes.
func (n Nodes) Copy() Nodes {
res := make(Nodes, len(n))
for i := range n {
res[i].Address = n[i].Address
res[i].Status = n[i].Status
if n[i].PubKey != nil {
res[i].PubKey = make([]byte, len(n[i].PubKey))
copy(res[i].PubKey, n[i].PubKey)
}
if n[i].Options != nil {
res[i].Options = make([]string, len(n[i].Options))
copy(res[i].Options, n[i].Options)
}
}
return res
}
// NewNetmap is an constructor.
func NewNetmap() *NetMap {
return &NetMap{
items: make([]bootstrap.NodeInfo, 0),
mu: new(sync.RWMutex),
}
}
// Equals return whether two netmap are identical.
func (n *NetMap) Equals(nm *NetMap) bool {
n.mu.RLock()
defer n.mu.RUnlock()
return len(n.items) == len(nm.items) &&
n.root.Equals(nm.root) &&
reflect.DeepEqual(n.items, nm.items)
}
// Root returns netmap root-bucket.
func (n *NetMap) Root() *Bucket {
n.mu.RLock()
cp := n.root.Copy()
n.mu.RUnlock()
return &cp
}
// Copy creates and returns full copy of target netmap.
func (n *NetMap) Copy() *NetMap {
n.mu.RLock()
defer n.mu.RUnlock()
nm := NewNetmap()
nm.items = n.items.Copy()
nm.root = n.root.Copy()
return nm
}
type hashedItem struct {
h uint32
info *bootstrap.NodeInfo
}
// Normalise reorders netmap items into some canonical order.
func (n *NetMap) Normalise() *NetMap {
nm := NewNetmap()
items := n.items.Copy()
if len(items) == 0 {
return nm
}
itemsH := make([]hashedItem, len(n.items))
for i := range itemsH {
itemsH[i].h = murmur3.Sum32(n.items[i].PubKey)
itemsH[i].info = &items[i]
}
sort.Slice(itemsH, func(i, j int) bool {
if itemsH[i].h == itemsH[j].h {
return itemsH[i].info.Address < itemsH[j].info.Address
}
return itemsH[i].h < itemsH[j].h
})
lastHash := ^itemsH[0].h
lastAddr := ""
for i := range itemsH {
if itemsH[i].h != lastHash || itemsH[i].info.Address != lastAddr {
_ = nm.AddNode(itemsH[i].info)
lastHash = itemsH[i].h
}
}
return nm
}
// Hash returns hash of n.
func (n *NetMap) Hash() (sum [32]byte) {
items := n.Normalise().Items()
w := sha256.New()
for i := range items {
data, _ := items[i].Marshal()
_, _ = w.Write(data)
}
s := w.Sum(nil)
copy(sum[:], s)
return
}
// InheritWeights calculates average capacity and minimal price, then provides buckets with IQR weight.
func (n *NetMap) InheritWeights() *NetMap {
nm := n.Copy()
// find average capacity in the network map
meanCap := nm.root.Traverse(netmap.NewMeanAgg(), netmap.CapWeightFunc).Compute()
capNorm := netmap.NewSigmoidNorm(meanCap)
// find minimal price in the network map
minPrice := nm.root.Traverse(netmap.NewMinAgg(), netmap.PriceWeightFunc).Compute()
priceNorm := netmap.NewReverseMinNorm(minPrice)
// provide all buckets with
wf := netmap.NewWeightFunc(capNorm, priceNorm)
meanAF := netmap.AggregatorFactory{New: netmap.NewMeanIQRAgg}
nm.root.TraverseTree(meanAF, wf)
return nm
}
// Merge checks if merge is possible and then add new elements from given netmap.
func (n *NetMap) Merge(n1 *NetMap) error {
n.mu.Lock()
defer n.mu.Unlock()
var (
tr = make(map[uint32]netmap.Node, len(n1.items))
items = n.items
)
loop:
for j := range n1.items {
for i := range n.items {
if n.items[i].Equals(n1.items[j]) {
tr[uint32(j)] = netmap.Node{
N: uint32(i),
C: n.items[i].Capacity(),
P: n.items[i].Price(),
}
continue loop
}
}
tr[uint32(j)] = netmap.Node{
N: uint32(len(items)),
C: n1.items[j].Capacity(),
P: n1.items[j].Price(),
}
items = append(items, n1.items[j])
}
root := n1.root.UpdateIndices(tr)
if n.root.CheckConflicts(root) {
return errNetMapsConflict
}
n.items = items
n.root.Merge(root)
return nil
}
// FindGraph finds sub-graph filtered by given SFGroup.
func (n *NetMap) FindGraph(pivot []byte, ss ...SFGroup) (c *Bucket) {
n.mu.RLock()
defer n.mu.RUnlock()
return n.root.FindGraph(pivot, ss...)
}
// FindNodes finds sub-graph filtered by given SFGroup and returns all sub-graph items.
func (n *NetMap) FindNodes(pivot []byte, ss ...SFGroup) (nodes []uint32) {
n.mu.RLock()
defer n.mu.RUnlock()
return n.root.FindNodes(pivot, ss...).Nodes()
}
// Items return slice of all NodeInfo in netmap.
func (n *NetMap) Items() []bootstrap.NodeInfo {
n.mu.RLock()
defer n.mu.RUnlock()
return n.items
}
// ItemsCopy return copied slice of all NodeInfo in netmap (is it useful?).
func (n *NetMap) ItemsCopy() Nodes {
n.mu.RLock()
defer n.mu.RUnlock()
return n.items.Copy()
}
// Add adds node with given address and given options.
func (n *NetMap) Add(addr string, pk []byte, st bootstrap.NodeStatus, opts ...string) error {
return n.AddNode(&bootstrap.NodeInfo{Address: addr, PubKey: pk, Status: st, Options: opts})
}
// Update replaces netmap with given netmap.
func (n *NetMap) Update(nxt *NetMap) {
n.mu.Lock()
defer n.mu.Unlock()
n.root = nxt.root
n.items = nxt.items
}
// GetMaxSelection returns 'maximal container' -- subgraph which contains
// any other subgraph satisfying specified selects and filters.
func (n *NetMap) GetMaxSelection(ss []Select, fs []Filter) (r *Bucket) {
return n.root.GetMaxSelection(netmap.SFGroup{Selectors: ss, Filters: fs})
}
// AddNode adds to exited or new node slice of given options.
func (n *NetMap) AddNode(nodeInfo *bootstrap.NodeInfo, opts ...string) error {
n.mu.Lock()
defer n.mu.Unlock()
info := *nodeInfo
info.Options = append(info.Options, opts...)
num := -1
// looking for existed node info item
for i := range n.items {
if n.items[i].Equals(info) {
num = i
break
}
}
// if item is not existed - add it
if num < 0 {
num = len(n.items)
n.items = append(n.items, info)
}
return n.root.AddStrawNode(netmap.Node{
N: uint32(num),
C: n.items[num].Capacity(),
P: n.items[num].Price(),
}, info.Options...)
}
// GetNodesByOption returns slice of NodeInfo that has given option.
func (n *NetMap) GetNodesByOption(opts ...string) []bootstrap.NodeInfo {
n.mu.RLock()
defer n.mu.RUnlock()
ns := n.root.GetNodesByOption(opts...)
nodes := make([]bootstrap.NodeInfo, 0, len(ns))
for _, info := range ns {
nodes = append(nodes, n.items[info.N])
}
return nodes
}
// MarshalJSON custom marshaller.
func (n *NetMap) MarshalJSON() ([]byte, error) {
n.mu.RLock()
defer n.mu.RUnlock()
return json.Marshal(n.items)
}
// UnmarshalJSON custom unmarshaller.
func (n *NetMap) UnmarshalJSON(data []byte) error {
var (
nm = NewNetmap()
items []bootstrap.NodeInfo
)
if err := json.Unmarshal(data, &items); err != nil {
return err
}
for i := range items {
if err := nm.Add(items[i].Address, items[i].PubKey, items[i].Status, items[i].Options...); err != nil {
return err
}
}
if n.mu == nil {
n.mu = new(sync.RWMutex)
}
n.mu.Lock()
n.root = nm.root
n.items = nm.items
n.mu.Unlock()
return nil
}
// Size returns number of nodes in network map.
func (n *NetMap) Size() int {
n.mu.RLock()
defer n.mu.RUnlock()
return len(n.items)
}

261
lib/netmap/netmap_test.go Normal file
View file

@ -0,0 +1,261 @@
package netmap
import (
"bytes"
"encoding/json"
"math/rand"
"sync"
"testing"
"time"
"github.com/nspcc-dev/neofs-api-go/bootstrap"
"github.com/nspcc-dev/neofs-api-go/object"
"github.com/nspcc-dev/netmap"
"github.com/stretchr/testify/require"
)
func TestNetMap_DataRace(t *testing.T) {
var (
nm = NewNetmap()
wg = new(sync.WaitGroup)
nodes = []bootstrap.NodeInfo{
{Address: "SPB1", Options: []string{"/Location:Europe/Country:USA"}},
{Address: "SPB2", Options: []string{"/Location:Europe/Country:Italy"}},
{Address: "MSK1", Options: []string{"/Location:Europe/Country:Germany"}},
{Address: "MSK2", Options: []string{"/Location:Europe/Country:Russia"}},
}
)
wg.Add(10)
for i := 0; i < 10; i++ {
go func(n int) {
for _, node := range nodes {
require.NoError(t, nm.Add(node.Address, node.PubKey, 0, node.Options...))
// t.Logf("%02d: add node %q", n, node.Address)
}
wg.Done()
}(i)
}
wg.Add(3 * 10)
for i := 0; i < 10; i++ {
go func(n int) {
nm.Copy()
// t.Logf("%02d: Copy", n)
wg.Done()
}(i)
go func(n int) {
nm.Items()
// t.Logf("%02d: Items", n)
wg.Done()
}(i)
go func(n int) {
nm.Root()
// t.Logf("%02d: Root", n)
wg.Done()
}(i)
}
wg.Wait()
}
func TestNetMapSuite(t *testing.T) {
var (
err error
nm1 = NewNetmap()
nodes = []bootstrap.NodeInfo{
{Address: "SPB1", Options: []string{"/Location:Europe/Country:USA"}, Status: 1},
{Address: "SPB2", Options: []string{"/Location:Europe/Country:Italy"}, Status: 2},
{Address: "MSK1", Options: []string{"/Location:Europe/Country:Germany"}, Status: 3},
{Address: "MSK2", Options: []string{"/Location:Europe/Country:Russia"}, Status: 4},
}
)
for _, node := range nodes {
err = nm1.Add(node.Address, nil, node.Status, node.Options...)
require.NoError(t, err)
}
t.Run("copy should work like expected", func(t *testing.T) {
nm2 := nm1.Copy()
require.Equal(t, nm1.root, nm2.root)
require.Equal(t, nm1.items, nm2.items)
})
t.Run("add node should not ignore options", func(t *testing.T) {
items := nm1.ItemsCopy()
nm2 := NewNetmap()
err = nm2.AddNode(&items[0], "/New/Option")
require.NoError(t, err)
require.Len(t, nm2.items, 1)
require.Equal(t, append(items[0].Options, "/New/Option"), nm2.items[0].Options)
})
t.Run("copyItems should work like expected", func(t *testing.T) {
require.Equal(t, nm1.items, nm1.ItemsCopy())
})
t.Run("marshal / unmarshal should be identical on same data", func(t *testing.T) {
var nm2 *NetMap
want, err := json.Marshal(nodes)
require.NoError(t, err)
actual, err := json.Marshal(nm1)
require.NoError(t, err)
require.Equal(t, want, actual)
err = json.Unmarshal(actual, &nm2)
require.NoError(t, err)
require.Equal(t, nm1.root, nm2.root)
require.Equal(t, nm1.items, nm2.items)
})
t.Run("unmarshal should override existing data", func(t *testing.T) {
var nm2 *NetMap
want, err := json.Marshal(nodes)
require.NoError(t, err)
actual, err := json.Marshal(nm1)
require.NoError(t, err)
require.Equal(t, want, actual)
nm2 = nm1.Copy()
err = nm2.Add("SOMEADDR", nil, 0, "/Location:Europe/Country:USA")
require.NoError(t, err)
err = json.Unmarshal(actual, &nm2)
require.NoError(t, err)
require.Equal(t, nm1.root, nm2.root)
require.Equal(t, nm1.items, nm2.items)
})
t.Run("unmarshal should fail on bad data", func(t *testing.T) {
var nm2 *NetMap
require.Error(t, json.Unmarshal([]byte(`"some bad data"`), &nm2))
})
t.Run("unmarshal should fail on add nodes", func(t *testing.T) {
var nm2 *NetMap
require.Error(t, json.Unmarshal([]byte(`[{"address": "SPB1","options":["1-2-3-4"]}]`), &nm2))
})
t.Run("merge two netmaps", func(t *testing.T) {
newNodes := []bootstrap.NodeInfo{
{Address: "SPB3", Options: []string{"/Location:Europe/Country:France"}},
}
nm2 := NewNetmap()
for _, node := range newNodes {
err = nm2.Add(node.Address, nil, 0, node.Options...)
require.NoError(t, err)
}
err = nm2.Merge(nm1)
require.NoError(t, err)
require.Len(t, nm2.items, len(nodes)+len(newNodes))
ns := nm2.FindNodes([]byte("pivot"), netmap.SFGroup{
Filters: []Filter{{Key: "Country", F: FilterEQ("Germany")}},
Selectors: []Select{{Count: 1, Key: NodesBucket}},
})
require.Len(t, ns, 1)
})
t.Run("weighted netmaps", func(t *testing.T) {
strawNodes := []bootstrap.NodeInfo{
{Address: "SPB2", Options: []string{"/Location:Europe/Country:Italy", "/Capacity:10", "/Price:100"}},
{Address: "MSK1", Options: []string{"/Location:Europe/Country:Germany", "/Capacity:10", "/Price:1"}},
{Address: "MSK2", Options: []string{"/Location:Europe/Country:Russia", "/Capacity:5", "/Price:10"}},
{Address: "SPB1", Options: []string{"/Location:Europe/Country:France", "/Capacity:20", "/Price:2"}},
}
nm2 := NewNetmap()
for _, node := range strawNodes {
err = nm2.Add(node.Address, nil, 0, node.Options...)
require.NoError(t, err)
}
ns1 := nm1.FindNodes([]byte("pivot"), netmap.SFGroup{
Selectors: []Select{{Count: 2, Key: NodesBucket}},
})
require.Len(t, ns1, 2)
ns2 := nm2.FindNodes([]byte("pivot"), netmap.SFGroup{
Selectors: []Select{{Count: 2, Key: NodesBucket}},
})
require.Len(t, ns2, 2)
require.NotEqual(t, ns1, ns2)
require.Equal(t, []uint32{1, 3}, ns2)
})
}
func TestNetMap_Normalise(t *testing.T) {
const testCount = 5
nodes := []bootstrap.NodeInfo{
{Address: "SPB2", PubKey: []byte{4}, Options: []string{"/Location:Europe/Country:Italy", "/Capacity:10", "/Price:100"}},
{Address: "MSK1", PubKey: []byte{2}, Options: []string{"/Location:Europe/Country:Germany", "/Capacity:10", "/Price:1"}},
{Address: "MSK2", PubKey: []byte{3}, Options: []string{"/Location:Europe/Country:Russia", "/Capacity:5", "/Price:10"}},
{Address: "SPB1", PubKey: []byte{1}, Options: []string{"/Location:Europe/Country:France", "/Capacity:20", "/Price:2"}},
}
add := func(nm *NetMap, indices ...int) {
for _, i := range indices {
err := nm.Add(nodes[i].Address, nodes[i].PubKey, 0, nodes[i].Options...)
require.NoError(t, err)
}
}
indices := []int{0, 1, 2, 3}
nm1 := NewNetmap()
add(nm1, indices...)
norm := nm1.Normalise()
for i := 0; i < testCount; i++ {
rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(indices), func(i, j int) { indices[i], indices[j] = indices[j], indices[i] })
nm := NewNetmap()
add(nm, indices...)
require.Equal(t, norm, nm.Normalise())
}
t.Run("normalise removes duplicates", func(t *testing.T) {
before := NewNetmap()
add(before, indices...)
before.items = append(before.items, before.items...)
nm := before.Normalise()
require.Len(t, nm.items, len(indices))
loop:
for i := range nodes {
for j := range nm.items {
if bytes.Equal(nm.items[j].PubKey, nodes[i].PubKey) {
continue loop
}
}
require.Fail(t, "normalized netmap does not contain '%s' node", nodes[i].Address)
}
})
}
func TestNodeInfo_Price(t *testing.T) {
var info bootstrap.NodeInfo
// too small value
info = bootstrap.NodeInfo{Options: []string{"/Price:0.01048575"}}
require.Equal(t, uint64(0), info.Price())
// min value
info = bootstrap.NodeInfo{Options: []string{"/Price:0.01048576"}}
require.Equal(t, uint64(1), info.Price())
// big value
info = bootstrap.NodeInfo{Options: []string{"/Price:1000000000.666"}}
require.Equal(t, uint64(1000000000.666*1e8/object.UnitsMB), info.Price())
}

27
lib/netmap/storage.go Normal file
View file

@ -0,0 +1,27 @@
package netmap
// GetParams is a group of parameters
// for network map receiving operation.
type GetParams struct {
}
// GetResult is a group of values
// returned by container receiving operation.
type GetResult struct {
nm *NetMap
}
// Storage is an interface of the storage of NeoFS network map.
type Storage interface {
GetNetMap(GetParams) (*GetResult, error)
}
// NetMap is a network map getter.
func (s GetResult) NetMap() *NetMap {
return s.nm
}
// SetNetMap is a network map setter.
func (s *GetResult) SetNetMap(v *NetMap) {
s.nm = v
}

View file

@ -0,0 +1,23 @@
package netmap
import (
"testing"
"github.com/nspcc-dev/neofs-api-go/bootstrap"
"github.com/stretchr/testify/require"
)
func TestGetResult(t *testing.T) {
s := GetResult{}
nm := NewNetmap()
require.NoError(t,
nm.AddNode(&bootstrap.NodeInfo{
Address: "address",
PubKey: []byte{1, 2, 3},
}),
)
s.SetNetMap(nm)
require.Equal(t, nm, s.NetMap())
}