[#356] netmap: Implement the functionality of working with subnets

NeoFS storage node can participate in a subnet group (at least one).
According to NeoFS API V2 protocol, subnets are entered and exited through
the attributes of the node. We should provide functionality for conveniently
setting and reading attributes based on the needs of the network.

Define `NodeSubnetInfo` type which groups information about the subnet
reflected in `NodeInfo`. Implement `WriteSubnetInfo` function which writes
`SubnetInfo` data to `NodeInfo`. It will be used to prepare a request for
registration on the NeoFS network. Implement `IterateSubnets` function which
allows to iterate over all subnets of the node. Moreover, it allows you to
remove a subnet from the `NodeInfo` right during iterative traversal.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2021-11-18 19:33:32 +03:00 committed by LeL
parent 0e6e0e4678
commit 348f9498bd
2 changed files with 434 additions and 0 deletions

208
netmap/attributes.go Normal file
View file

@ -0,0 +1,208 @@
package netmap
import (
"errors"
"fmt"
"strings"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
)
// prefix of keys to subnet attributes.
const attrSubnetPrefix = "__NEOFS__SUBNET_"
const (
// subnet attribute's value denoting subnet entry
attrSubnetValEntry = "True"
// subnet attribute's value denoting subnet exit
attrSubnetValExit = "False"
)
// NodeSubnetInfo groups information about subnet which can be written to NodeInfo.
//
// Zero value represents entry to zero subnet.
type NodeSubnetInfo struct {
exit bool
id *refs.SubnetID
}
// Enters returns true iff node enters the subnet.
func (x NodeSubnetInfo) Enters() bool {
return !x.exit
}
// SetEntryFlag sets the subnet entry flag.
func (x *NodeSubnetInfo) SetEntryFlag(enters bool) {
x.exit = !enters
}
// ID returns identifier of the subnet.
func (x NodeSubnetInfo) ID() *refs.SubnetID {
return x.id
}
// SetID sets identifier of the subnet.
func (x *NodeSubnetInfo) SetID(id *refs.SubnetID) {
x.id = id
}
func subnetAttributeKey(id *refs.SubnetID) string {
txt, _ := id.MarshalText() // never returns an error
return attrSubnetPrefix + string(txt)
}
// WriteSubnetInfo writes NodeSubnetInfo to NodeInfo via attributes. NodeInfo must not be nil.
//
// Does not add (removes existing) attribute if node:
// * exists non-zero subnet;
// * enters zero subnet.
//
// Attribute key is calculated from ID using format `__NEOFS__SUBNET_%s`.
// Attribute Value is:
// * `True` if node enters the subnet;
// * `False`, otherwise.
func WriteSubnetInfo(node *NodeInfo, info NodeSubnetInfo) {
attrs := node.GetAttributes()
id := info.ID()
enters := info.Enters()
// calculate attribute key
key := subnetAttributeKey(id)
if refs.IsZeroSubnet(id) == enters {
for i := range attrs {
if attrs[i].GetKey() == key {
attrs = append(attrs[:i], attrs[i+1:]...)
}
}
} else {
var val string
if enters {
val = attrSubnetValEntry
} else {
val = attrSubnetValExit
}
presented := false
for i := range attrs {
if attrs[i].GetKey() == key {
attrs[i].SetValue(val)
presented = true
}
}
if !presented {
var attr Attribute
attr.SetKey(key)
attr.SetValue(val)
attrs = append(attrs, &attr)
}
}
node.SetAttributes(attrs)
}
// ErrRemoveSubnet is returned when a node needs to leave the subnet.
var ErrRemoveSubnet = errors.New("remove subnet")
// IterateSubnets iterates over all subnets the node belongs to and passes the IDs to f.
// Handler must not be nil.
//
// If f returns ErrRemoveSubnet, then removes subnet entry. Breaks on any other non-nil error and returns it.
//
// Returns an error if any subnet attribute has wrong format.
func IterateSubnets(node *NodeInfo, f func(refs.SubnetID) error) error {
attrs := node.GetAttributes()
var (
err error
id refs.SubnetID
metZero bool // if zero subnet's attribute was met in for-loop
)
for i := 0; i < len(attrs); i++ { // range must not be used because of attrs mutation in body
key := attrs[i].GetKey()
// cut subnet ID string
idTxt := strings.TrimPrefix(key, attrSubnetPrefix)
if idTxt == key {
// not a subnet attribute
continue
}
// check value
switch val := attrs[i].GetValue(); val {
default:
return fmt.Errorf("invalid attribute value: %s", val)
case attrSubnetValExit:
// node is outside the subnet
continue
case attrSubnetValEntry:
// required to avoid default case
}
// decode subnet ID
if err = id.UnmarshalText([]byte(idTxt)); err != nil {
return fmt.Errorf("invalid ID text: %w", err)
}
// pass ID to the handler
err = f(id)
isRemoveErr := errors.Is(err, ErrRemoveSubnet)
if err != nil && !isRemoveErr {
return err
}
if !metZero { // in order to not reset if has been already set
metZero = refs.IsZeroSubnet(&id)
if !isRemoveErr {
// no handler's error and non-zero subnet
continue
} else if metZero {
// removal error and zero subnet.
// we don't remove attribute of zero subnet because it means entry
attrs[i].SetValue(attrSubnetValExit)
continue
}
}
if isRemoveErr {
// removal error and non-zero subnet.
// we can set False or remove attribute, latter is more memory/network efficient.
attrs = append(attrs[:i], attrs[i+1:]...)
i--
}
}
if !metZero {
// missing attribute of zero subnet equivalent to entry
refs.MakeZeroSubnet(&id)
err = f(id)
if errors.Is(err, ErrRemoveSubnet) {
// zero subnet should be clearly removed with False value
var attr Attribute
attr.SetKey(subnetAttributeKey(&id))
attr.SetValue(attrSubnetValExit)
attrs = append(attrs, &attr)
}
}
node.SetAttributes(attrs)
return nil
}

226
netmap/attributes_test.go Normal file
View file

@ -0,0 +1,226 @@
package netmap_test
import (
"strconv"
"testing"
"github.com/nspcc-dev/neofs-api-go/v2/netmap"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/stretchr/testify/require"
)
func subnetAttrKey(val string) string {
return "__NEOFS__SUBNET_" + val
}
func assertSubnetAttrKey(t *testing.T, attr *netmap.Attribute, num uint32) {
require.Equal(t, subnetAttrKey(strconv.FormatUint(uint64(num), 10)), attr.GetKey())
}
func TestWriteSubnetInfo(t *testing.T) {
t.Run("entry", func(t *testing.T) {
t.Run("zero subnet", func(t *testing.T) {
var (
node netmap.NodeInfo
info netmap.NodeSubnetInfo
)
netmap.WriteSubnetInfo(&node, info)
// entry to zero subnet does not require an attribute
attrs := node.GetAttributes()
require.Empty(t, attrs)
// exit the subnet
info.SetEntryFlag(false)
netmap.WriteSubnetInfo(&node, info)
// exit from zero subnet should be clearly reflected in attributes
attrs = node.GetAttributes()
require.Len(t, attrs, 1)
attr := attrs[0]
assertSubnetAttrKey(t, attr, 0)
require.Equal(t, "False", attr.GetValue())
// again enter to zero subnet
info.SetEntryFlag(true)
netmap.WriteSubnetInfo(&node, info)
// attribute should be removed
attrs = node.GetAttributes()
require.Empty(t, attrs)
})
t.Run("non-zero subnet", func(t *testing.T) {
var (
node netmap.NodeInfo
info netmap.NodeSubnetInfo
id refs.SubnetID
)
// create non-zero subnet ID
const num = 15
id.SetValue(num)
// enter to the subnet
info.SetID(&id)
info.SetEntryFlag(true)
netmap.WriteSubnetInfo(&node, info)
// check attribute format
attrs := node.GetAttributes()
require.Len(t, attrs, 1)
attr := attrs[0]
assertSubnetAttrKey(t, attr, num)
require.Equal(t, "True", attr.GetValue())
// again exit the subnet
info.SetEntryFlag(false)
netmap.WriteSubnetInfo(&node, info)
// attribute should be removed
attrs = node.GetAttributes()
require.Empty(t, attrs)
})
})
}
func TestSubnets(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var node netmap.NodeInfo
called := 0
err := netmap.IterateSubnets(&node, func(id refs.SubnetID) error {
called++
require.True(t, refs.IsZeroSubnet(&id))
return nil
})
require.NoError(t, err)
require.EqualValues(t, 1, called)
})
t.Run("with correct attribute", func(t *testing.T) {
var (
node netmap.NodeInfo
attr netmap.Attribute
)
attr.SetKey(subnetAttrKey("13"))
attr.SetValue("True")
attrs := []*netmap.Attribute{&attr}
node.SetAttributes(attrs)
called := 0
err := netmap.IterateSubnets(&node, func(id refs.SubnetID) error {
if !refs.IsZeroSubnet(&id) {
called++
require.EqualValues(t, 13, id.GetValue())
}
return nil
})
require.NoError(t, err)
require.EqualValues(t, 1, called)
})
t.Run("with incorrect attribute", func(t *testing.T) {
assertErr := func(attr netmap.Attribute) {
var node netmap.NodeInfo
node.SetAttributes([]*netmap.Attribute{&attr})
require.Error(t, netmap.IterateSubnets(&node, func(refs.SubnetID) error {
return nil
}))
}
t.Run("incorrect key", func(t *testing.T) {
var attr netmap.Attribute
attr.SetKey(subnetAttrKey("one-two-three"))
assertErr(attr)
})
t.Run("incorrect value", func(t *testing.T) {
var attr netmap.Attribute
attr.SetKey(subnetAttrKey("1"))
for _, invalidVal := range []string{
"",
"Troo",
"Fols",
} {
attr.SetValue(invalidVal)
assertErr(attr)
}
assertErr(attr)
})
})
t.Run("remove entry", func(t *testing.T) {
t.Run("zero", func(t *testing.T) {
var node netmap.NodeInfo
err := netmap.IterateSubnets(&node, func(id refs.SubnetID) error {
if refs.IsZeroSubnet(&id) {
return netmap.ErrRemoveSubnet
}
return nil
})
require.NoError(t, err)
attrs := node.GetAttributes()
require.Len(t, attrs, 1)
attr := attrs[0]
assertSubnetAttrKey(t, attr, 0)
require.Equal(t, "False", attr.GetValue())
})
t.Run("non-zero", func(t *testing.T) {
var (
node netmap.NodeInfo
attr netmap.Attribute
)
attr.SetKey(subnetAttrKey("99"))
attr.SetValue("True")
attrs := []*netmap.Attribute{&attr}
node.SetAttributes(attrs)
err := netmap.IterateSubnets(&node, func(id refs.SubnetID) error {
if !refs.IsZeroSubnet(&id) {
return netmap.ErrRemoveSubnet
}
return nil
})
require.NoError(t, err)
attrs = node.GetAttributes()
require.Empty(t, attrs)
})
})
}