frostfs-sdk-go/netmap/node_info.go
Evgenii Stratonikov d282cd094f
All checks were successful
DCO / DCO (pull_request) Successful in 23s
Code generation / Generate proto (pull_request) Successful in 1m27s
Tests and linters / Tests (pull_request) Successful in 1m27s
Tests and linters / Lint (pull_request) Successful in 2m35s
[#355] netmap: Cache price and capacity attributes
They are used by HRW sorting and it makes sense to store them separately
instead of iterating over all attributes each time we need them.
It also simplifies code: we already parse them in NodeInfo.readFromV2(),
so just save the result.

```
goos: linux
goarch: amd64
pkg: git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
                                                              │     old     │                 new                 │
                                                              │   sec/op    │   sec/op     vs base                │
Netmap_ContainerNodes/REP_2-8                                   5.923µ ± 0%   5.209µ ± 1%  -12.05% (p=0.000 n=10)
Netmap_ContainerNodes/REP_2_IN_X_CBF_2_SELECT_2_FROM_*_AS_X-8   5.931µ ± 7%   5.088µ ± 1%  -14.22% (p=0.000 n=10)
geomean                                                         5.927µ        5.148µ       -13.14%

                                                              │     old      │                 new                 │
                                                              │     B/op     │     B/op      vs base               │
Netmap_ContainerNodes/REP_2-8                                   7.609Ki ± 0%   8.172Ki ± 0%  +7.39% (p=0.000 n=10)
Netmap_ContainerNodes/REP_2_IN_X_CBF_2_SELECT_2_FROM_*_AS_X-8   7.031Ki ± 0%   7.469Ki ± 0%  +6.22% (p=0.000 n=10)
geomean                                                         7.315Ki        7.812Ki       +6.81%

                                                              │    old     │                 new                 │
                                                              │ allocs/op  │ allocs/op   vs base                 │
Netmap_ContainerNodes/REP_2-8                                   77.00 ± 0%   77.00 ± 0%       ~ (p=1.000 n=10) ¹
Netmap_ContainerNodes/REP_2_IN_X_CBF_2_SELECT_2_FROM_*_AS_X-8   77.00 ± 0%   77.00 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                                         77.00        77.00       +0.00%
¹ all samples are equal
```

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2025-04-07 17:47:08 +03:00

615 lines
18 KiB
Go

package netmap
import (
"errors"
"fmt"
"iter"
"slices"
"strconv"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/netmap"
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
"git.frostfs.info/TrueCloudLab/hrw"
)
// NodeInfo groups information about FrostFS storage node which is reflected
// in the FrostFS network map. Storage nodes advertise this information when
// registering with the FrostFS network. After successful registration, information
// about the nodes is available to all network participants to work with the network
// map (mainly to comply with container storage policies).
//
// NodeInfo is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/netmap.NodeInfo
// message. See ReadFromV2 / WriteToV2 methods.
//
// Instances can be created using built-in var declaration.
type NodeInfo struct {
m netmap.NodeInfo
hash uint64
capacity uint64
price uint64
}
// reads NodeInfo from netmap.NodeInfo message. If checkFieldPresence is set,
// returns an error on absence of any protocol-required field. Verifies format of any
// presented field according to FrostFS API V2 protocol.
func (x *NodeInfo) readFromV2(m netmap.NodeInfo, checkFieldPresence bool) error {
var err error
var capacity, price uint64
binPublicKey := m.GetPublicKey()
if checkFieldPresence && len(binPublicKey) == 0 {
return errors.New("missing public key")
}
if checkFieldPresence && m.NumberOfAddresses() <= 0 {
return errors.New("missing network endpoints")
}
attributes := m.GetAttributes()
mAttr := make(map[string]struct{}, len(attributes))
for i := range attributes {
key := attributes[i].GetKey()
if key == "" {
return fmt.Errorf("empty key of the attribute #%d", i)
} else if _, ok := mAttr[key]; ok {
return fmt.Errorf("duplicate attributes %s", key)
}
mAttr[key] = struct{}{}
switch {
case key == attrCapacity:
capacity, err = strconv.ParseUint(attributes[i].GetValue(), 10, 64)
if err != nil {
return fmt.Errorf("invalid %s attribute: %w", attrCapacity, err)
}
case key == attrPrice:
price, err = strconv.ParseUint(attributes[i].GetValue(), 10, 64)
if err != nil {
return fmt.Errorf("invalid %s attribute: %w", attrPrice, err)
}
default:
if attributes[i].GetValue() == "" {
return fmt.Errorf("empty value of the attribute %s", key)
}
}
}
x.m = m
x.hash = hrw.Hash(binPublicKey)
x.capacity = capacity
x.price = price
return nil
}
// ReadFromV2 reads NodeInfo from the netmap.NodeInfo message. Checks if the
// message conforms to FrostFS API V2 protocol.
//
// See also WriteToV2.
func (x *NodeInfo) ReadFromV2(m netmap.NodeInfo) error {
return x.readFromV2(m, true)
}
// WriteToV2 writes NodeInfo to the netmap.NodeInfo message. The message MUST NOT
// be nil.
//
// See also ReadFromV2.
func (x NodeInfo) WriteToV2(m *netmap.NodeInfo) {
*m = x.m
}
// Marshal encodes NodeInfo into a binary format of the FrostFS API protocol
// (Protocol Buffers with direct field order).
//
// See also Unmarshal.
func (x NodeInfo) Marshal() []byte {
var m netmap.NodeInfo
x.WriteToV2(&m)
return m.StableMarshal(nil)
}
// Unmarshal decodes FrostFS API protocol binary format into the NodeInfo
// (Protocol Buffers with direct field order). Returns an error describing
// a format violation.
//
// See also Marshal.
func (x *NodeInfo) Unmarshal(data []byte) error {
var m netmap.NodeInfo
err := m.Unmarshal(data)
if err != nil {
return err
}
return x.readFromV2(m, false)
}
// MarshalJSON encodes NodeInfo into a JSON format of the FrostFS API protocol
// (Protocol Buffers JSON).
//
// See also UnmarshalJSON.
func (x NodeInfo) MarshalJSON() ([]byte, error) {
var m netmap.NodeInfo
x.WriteToV2(&m)
return m.MarshalJSON()
}
// UnmarshalJSON decodes FrostFS API protocol JSON format into the NodeInfo
// (Protocol Buffers JSON). Returns an error describing a format violation.
//
// See also MarshalJSON.
func (x *NodeInfo) UnmarshalJSON(data []byte) error {
var m netmap.NodeInfo
err := m.UnmarshalJSON(data)
if err != nil {
return err
}
return x.readFromV2(m, false)
}
// SetPublicKey sets binary-encoded public key bound to the node. The key
// authenticates the storage node, so it MUST be unique within the network.
//
// Argument MUST NOT be mutated, make a copy first.
//
// See also PublicKey.
func (x *NodeInfo) SetPublicKey(key []byte) {
x.m.SetPublicKey(key)
x.hash = hrw.Hash(x.m.GetPublicKey())
}
// PublicKey returns value set using SetPublicKey.
//
// Zero NodeInfo has no public key, which is incorrect according to
// FrostFS system requirements.
//
// Return value MUST not be mutated, make a copy first.
func (x NodeInfo) PublicKey() []byte {
return x.m.GetPublicKey()
}
// StringifyPublicKey returns HEX representation of PublicKey.
func StringifyPublicKey(node NodeInfo) string {
return frostfscrypto.StringifyKeyBinary(node.PublicKey())
}
// SetNetworkEndpoints sets list to the announced node's network endpoints.
// Node MUSt have at least one announced endpoint. List MUST be unique.
// Endpoints are used for communication with the storage node within FrostFS
// network. It is expected that node serves storage node services on these
// endpoints (it also adds a wait on their network availability).
//
// Argument MUST NOT be mutated, make a copy first.
//
// See also IterateNetworkEndpoints.
func (x *NodeInfo) SetNetworkEndpoints(v ...string) {
x.m.SetAddresses(v...)
}
// NumberOfNetworkEndpoints returns number of network endpoints announced by the node.
//
// See also SetNetworkEndpoints.
func (x NodeInfo) NumberOfNetworkEndpoints() int {
return x.m.NumberOfAddresses()
}
// IterateNetworkEndpoints iterates over network endpoints announced by the
// node and pass them into f. Breaks iteration on f's true return. Handler
// MUST NOT be nil.
//
// Zero NodeInfo contains no endpoints which is incorrect according to
// FrostFS system requirements.
//
// See also SetNetworkEndpoints.
//
// Deprecated: use [NodeInfo.NetworkEndpoints] instead.
func (x NodeInfo) IterateNetworkEndpoints(f func(string) bool) {
for s := range x.NetworkEndpoints() {
if f(s) {
return
}
}
}
// NetworkEndpoints returns an iterator over network endpoints announced by the
// node.
//
// See also SetNetworkEndpoints.
func (x NodeInfo) NetworkEndpoints() iter.Seq[string] {
return x.m.Addresses()
}
// IterateNetworkEndpoints is an extra-sugared function over IterateNetworkEndpoints
// method which allows to unconditionally iterate over all node's network endpoints.
//
// Deprecated: use [NodeInfo.NetworkEndpoints] instead.
func IterateNetworkEndpoints(node NodeInfo, f func(string)) {
node.IterateNetworkEndpoints(func(addr string) bool {
f(addr)
return false
})
}
// assert NodeInfo type provides hrw.Hasher required for HRW sorting.
var _ hrw.Hasher = NodeInfo{}
// Hash implements hrw.Hasher interface.
//
// Hash is needed to support weighted HRW therefore sort function sorts nodes
// based on their public key. Hash isn't expected to be used directly.
func (x NodeInfo) Hash() uint64 {
if x.hash != 0 {
return x.hash
}
return hrw.Hash(x.m.GetPublicKey())
}
func (x *NodeInfo) setNumericAttribute(key string, num uint64) {
x.SetAttribute(key, strconv.FormatUint(num, 10))
}
// SetPrice sets the storage cost declared by the node. By default, zero
// price is announced.
func (x *NodeInfo) SetPrice(price uint64) {
x.setNumericAttribute(attrPrice, price)
x.price = price
}
// Price returns price set using SetPrice.
//
// Zero NodeInfo has zero price.
func (x NodeInfo) Price() uint64 {
return x.price
}
// SetCapacity sets the storage capacity declared by the node. By default, zero
// capacity is announced.
func (x *NodeInfo) SetCapacity(capacity uint64) {
x.setNumericAttribute(attrCapacity, capacity)
x.capacity = capacity
}
const attrUNLOCODE = "UN-LOCODE"
// SetLOCODE specifies node's geographic location in UN/LOCODE format. Each
// storage node MUST declare it for entrance to the FrostFS network. Node MAY
// declare the code of the nearest location as needed, for example, when it is
// impossible to unambiguously attribute the node to any location from UN/LOCODE
// database.
//
// See also LOCODE.
func (x *NodeInfo) SetLOCODE(locode string) {
x.SetAttribute(attrUNLOCODE, locode)
}
// LOCODE returns node's location code set using SetLOCODE.
//
// Zero NodeInfo has empty location code which is invalid according to
// FrostFS API system requirement.
func (x NodeInfo) LOCODE() string {
return x.Attribute(attrUNLOCODE)
}
// SetCountryCode sets code of the country in ISO 3166-1_alpha-2 to which
// storage node belongs (or the closest one).
//
// SetCountryCode is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetCountryCode(countryCode string) {
x.SetAttribute("CountryCode", countryCode)
}
// SetCountryName sets short name of the country in ISO-3166 format to which
// storage node belongs (or the closest one).
//
// SetCountryName is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetCountryName(country string) {
x.SetAttribute("Country", country)
}
// SetLocationName sets storage node's location name from "NameWoDiacritics"
// column in the UN/LOCODE record corresponding to the specified LOCODE.
//
// SetLocationName is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetLocationName(location string) {
x.SetAttribute("Location", location)
}
// SetSubdivisionCode sets storage node's subdivision code from "SubDiv" column in
// the UN/LOCODE record corresponding to the specified LOCODE.
//
// SetSubdivisionCode is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetSubdivisionCode(subDiv string) {
x.SetAttribute("SubDivCode", subDiv)
}
// SetSubdivisionName sets storage node's subdivision name in ISO 3166-2 format.
//
// SetSubdivisionName is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetSubdivisionName(subDiv string) {
x.SetAttribute("SubDiv", subDiv)
}
// SetContinentName sets name of the storage node's continent from
// Seven-Continent model.
//
// SetContinentName is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetContinentName(continent string) {
x.SetAttribute("Continent", continent)
}
// Enumeration of well-known attributes.
const (
// attrPrice is a key to the node attribute that indicates the
// price in GAS tokens for storing one GB of data during one Epoch.
attrPrice = "Price"
// attrCapacity is a key to the node attribute that indicates the
// total available disk space in Gigabytes.
attrCapacity = "Capacity"
// attrExternalAddr is a key for the attribute storing node external addresses.
attrExternalAddr = "ExternalAddr"
// sepExternalAddr is a separator for multi-value ExternalAddr attribute.
sepExternalAddr = ","
)
// SetExternalAddresses sets multi-addresses to use
// to connect to this node from outside.
//
// Panics if addr is an empty list.
func (x *NodeInfo) SetExternalAddresses(addr ...string) {
x.SetAttribute(attrExternalAddr, strings.Join(addr, sepExternalAddr))
}
// ExternalAddresses returns list of multi-addresses to use
// to connect to this node from outside.
func (x NodeInfo) ExternalAddresses() []string {
a := x.Attribute(attrExternalAddr)
if len(a) == 0 {
return nil
}
return strings.Split(a, sepExternalAddr)
}
// NumberOfAttributes returns number of attributes announced by the node.
//
// See also SetAttribute.
func (x NodeInfo) NumberOfAttributes() int {
return len(x.m.GetAttributes())
}
// Attributes returns an iterator over node attributes.
func (x NodeInfo) Attributes() iter.Seq2[string, string] {
return func(yield func(string, string) bool) {
a := x.m.GetAttributes()
for i := range a {
if !yield(a[i].GetKey(), a[i].GetValue()) {
break
}
}
}
}
// IterateAttributes iterates over all node attributes and passes the into f.
// Handler MUST NOT be nil.
//
// Deprecated: use [NodeInfo.Attributes] instead.
func (x NodeInfo) IterateAttributes(f func(key, value string)) {
a := x.m.GetAttributes()
for i := range a {
f(a[i].GetKey(), a[i].GetValue())
}
}
// SetAttribute sets value of the node attribute value by the given key.
// Both key and value MUST NOT be empty.
func (x *NodeInfo) SetAttribute(key, value string) {
if key == "" {
panic("empty key in SetAttribute")
} else if value == "" {
panic("empty value in SetAttribute")
}
// NodeInfo with non-numeric `Price`` or `Capacity` attributes
// is considered invalid by NodeInfo.readFromV2().
// Here we have no way to signal an error, and panic seems an overkill.
// So, set cached fields only if we can parse the value and 0 parsing fails.
switch key {
case attrPrice:
x.price, _ = strconv.ParseUint(value, 10, 64)
case attrCapacity:
x.capacity, _ = strconv.ParseUint(value, 10, 64)
}
a := x.m.GetAttributes()
for i := range a {
if a[i].GetKey() == key {
a[i].SetValue(value)
return
}
}
a = append(a, netmap.Attribute{})
a[len(a)-1].SetKey(key)
a[len(a)-1].SetValue(value)
x.m.SetAttributes(a)
}
// Attribute returns value of the node attribute set using SetAttribute by the
// given key. Returns empty string if attribute is missing.
func (x NodeInfo) Attribute(key string) string {
a := x.m.GetAttributes()
for i := range a {
if a[i].GetKey() == key {
return a[i].GetValue()
}
}
return ""
}
// SortAttributes sorts node attributes set using SetAttribute lexicographically.
// The method is only needed to make NodeInfo consistent, e.g. for signing.
func (x *NodeInfo) SortAttributes() {
as := x.m.GetAttributes()
if len(as) == 0 {
return
}
slices.SortFunc(as, func(ai, aj netmap.Attribute) int {
if r := strings.Compare(ai.GetKey(), aj.GetKey()); r != 0 {
return r
}
return strings.Compare(ai.GetValue(), aj.GetValue())
})
x.m.SetAttributes(as)
}
// SetOffline sets the state of the node to "offline". When a node updates
// information about itself in the network map, this action is interpreted as
// an intention to leave the network.
//
// See also IsOffline.
//
// Deprecated: use SetStatus instead.
func (x *NodeInfo) SetOffline() {
x.m.SetState(netmap.Offline)
}
// IsOffline checks if the node is in the "offline" state.
//
// Zero NodeInfo has undefined state which is not offline (note that it does not
// mean online).
//
// See also SetOffline.
//
// Deprecated: use Status instead.
func (x NodeInfo) IsOffline() bool {
return x.m.GetState() == netmap.Offline
}
// SetOnline sets the state of the node to "online". When a node updates
// information about itself in the network map, this
// action is interpreted as an intention to enter the network.
//
// See also IsOnline.
//
// Deprecated: use SetStatus instead.
func (x *NodeInfo) SetOnline() {
x.m.SetState(netmap.Online)
}
// IsOnline checks if the node is in the "online" state.
//
// Zero NodeInfo has undefined state which is not online (note that it does not
// mean offline).
//
// See also SetOnline.
//
// Deprecated: use Status instead.
func (x NodeInfo) IsOnline() bool {
return x.m.GetState() == netmap.Online
}
// SetMaintenance sets the state of the node to "maintenance". When a node updates
// information about itself in the network map, this
// state declares temporal unavailability for a node.
//
// See also IsMaintenance.
//
// Deprecated: use SetStatus instead.
func (x *NodeInfo) SetMaintenance() {
x.m.SetState(netmap.Maintenance)
}
// IsMaintenance checks if the node is in the "maintenance" state.
//
// Zero NodeInfo has undefined state.
//
// See also SetMaintenance.
//
// Deprecated: use Status instead.
func (x NodeInfo) IsMaintenance() bool {
return x.m.GetState() == netmap.Maintenance
}
type NodeState netmap.NodeState
const (
UnspecifiedState = NodeState(netmap.UnspecifiedState)
Online = NodeState(netmap.Online)
Offline = NodeState(netmap.Offline)
Maintenance = NodeState(netmap.Maintenance)
)
// ToV2 converts NodeState to v2.
func (ns NodeState) ToV2() netmap.NodeState {
return netmap.NodeState(ns)
}
// FromV2 reads NodeState to v2.
func (ns *NodeState) FromV2(state netmap.NodeState) {
*ns = NodeState(state)
}
// Status returns the current state of the node in the network map.
//
// Zero NodeInfo has an undefined state, neither online nor offline.
func (x NodeInfo) Status() NodeState {
return NodeState(x.m.GetState())
}
// SetState updates the state of the node in the network map.
//
// The state determines the node's current status within the network:
// - "online": Indicates the node intends to enter the network.
// - "offline": Indicates the node intends to leave the network.
// - "maintenance": Indicates the node is temporarily unavailable.
//
// See also Status.
func (x *NodeInfo) SetStatus(state NodeState) {
x.m.SetState(netmap.NodeState(state))
}
// Clone returns a copy of NodeInfo.
func (x *NodeInfo) Clone() *NodeInfo {
if x == nil {
return nil
}
return &NodeInfo{
hash: x.hash,
m: *x.m.Clone(),
}
}
// String implements fmt.Stringer.
//
// String is designed to be human-readable, and its format MAY differ between
// SDK versions.
func (ns NodeState) String() string {
return netmap.NodeState(ns).String()
}
// IsOnline checks if the current state is "online".
func (ns NodeState) IsOnline() bool { return ns == Online }
// IsOffline checks if the current state is "offline".
func (ns NodeState) IsOffline() bool { return ns == Offline }
// IsMaintenance checks if the current state is "maintenance".
func (ns NodeState) IsMaintenance() bool { return ns == Maintenance }