From 348f9498bd4819005453909ea4fb38bae0fde8fc Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 18 Nov 2021 19:33:32 +0300 Subject: [PATCH] [#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 --- netmap/attributes.go | 208 +++++++++++++++++++++++++++++++++++ netmap/attributes_test.go | 226 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 434 insertions(+) create mode 100644 netmap/attributes.go create mode 100644 netmap/attributes_test.go diff --git a/netmap/attributes.go b/netmap/attributes.go new file mode 100644 index 0000000..07ce268 --- /dev/null +++ b/netmap/attributes.go @@ -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 +} diff --git a/netmap/attributes_test.go b/netmap/attributes_test.go new file mode 100644 index 0000000..5cee47e --- /dev/null +++ b/netmap/attributes_test.go @@ -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) + }) + }) +}