satellite/{nodeselection,overlay}: support annotations on node filters

Change-Id: I844d8a25042750aae189175842113e2f052d5b17
This commit is contained in:
Márton Elek 2023-08-01 13:50:22 +02:00 committed by Storj Robot
parent b70fb2f87f
commit 0e17b1018c
7 changed files with 189 additions and 30 deletions

View File

@ -15,6 +15,41 @@ type NodeFilter interface {
MatchInclude(node *SelectedNode) bool MatchInclude(node *SelectedNode) bool
} }
// AnnotatedNodeFilter is just a NodeFilter with additional annotations.
type AnnotatedNodeFilter struct {
Filter NodeFilter
Annotations map[string]string
}
// MatchInclude implements NodeFilter.
func (a AnnotatedNodeFilter) MatchInclude(node *SelectedNode) bool {
return a.Filter.MatchInclude(node)
}
// WithAnnotation adds annotations to a NodeFilter.
func WithAnnotation(filter NodeFilter, name string, value string) NodeFilter {
if anf, ok := filter.(AnnotatedNodeFilter); ok {
anf.Annotations[name] = value
return anf
}
return AnnotatedNodeFilter{
Filter: filter,
Annotations: map[string]string{
name: value,
},
}
}
// GetAnnotation retrieves annotation from AnnotatedNodeFilter.
func GetAnnotation(filter NodeFilter, name string) string {
if annotated, ok := filter.(AnnotatedNodeFilter); ok {
return annotated.Annotations[name]
}
return ""
}
var _ NodeFilter = AnnotatedNodeFilter{}
// NodeFilters is a collection of multiple node filters (all should vote with true). // NodeFilters is a collection of multiple node filters (all should vote with true).
type NodeFilters []NodeFilter type NodeFilters []NodeFilter

View File

@ -21,17 +21,17 @@ type State struct {
// netByID returns subnet based on storj.NodeID // netByID returns subnet based on storj.NodeID
netByID map[storj.NodeID]string netByID map[storj.NodeID]string
// distinct contains selectors for distinct selection.
distinct struct { // byNetwork contains selectors for distinct selection.
byNetwork struct {
Reputable SelectBySubnet Reputable SelectBySubnet
New SelectBySubnet New SelectBySubnet
} }
}
// Stats contains state information. byID struct {
type Stats struct { Reputable SelectByID
New int New SelectByID
Reputable int }
} }
// Selector defines interface for selecting nodes. // Selector defines interface for selecting nodes.
@ -53,17 +53,32 @@ func NewState(reputableNodes, newNodes []*SelectedNode) *State {
state.netByID[node.ID] = node.LastNet state.netByID[node.ID] = node.LastNet
} }
state.distinct.Reputable = SelectBySubnetFromNodes(reputableNodes) state.byNetwork.Reputable = SelectBySubnetFromNodes(reputableNodes)
state.distinct.New = SelectBySubnetFromNodes(newNodes) state.byNetwork.New = SelectBySubnetFromNodes(newNodes)
state.byID.Reputable = SelectByID(reputableNodes)
state.byID.New = SelectByID(newNodes)
return state return state
} }
// SelectionType defines how to select nodes randomly.
type SelectionType int8
const (
// SelectionTypeByNetwork chooses subnets randomly, and one node from each subnet.
SelectionTypeByNetwork = iota
// SelectionTypeByID chooses nodes randomly.
SelectionTypeByID
)
// Request contains arguments for State.Request. // Request contains arguments for State.Request.
type Request struct { type Request struct {
Count int Count int
NewFraction float64 NewFraction float64
NodeFilters NodeFilters NodeFilters NodeFilters
SelectionType SelectionType
} }
// Select selects requestedCount nodes where there will be newFraction nodes. // Select selects requestedCount nodes where there will be newFraction nodes.
@ -81,8 +96,16 @@ func (state *State) Select(ctx context.Context, request Request) (_ []*SelectedN
var reputableNodes Selector var reputableNodes Selector
var newNodes Selector var newNodes Selector
reputableNodes = state.distinct.Reputable switch request.SelectionType {
newNodes = state.distinct.New case SelectionTypeByNetwork:
reputableNodes = state.byNetwork.Reputable
newNodes = state.byNetwork.New
case SelectionTypeByID:
reputableNodes = state.byID.Reputable
newNodes = state.byID.New
default:
return nil, errs.New("Unsupported selection type: %d", request.SelectionType)
}
// Get a random selection of new nodes out of the cache first so that if there aren't // Get a random selection of new nodes out of the cache first so that if there aren't
// enough new nodes on the network, we can fall back to using reputable nodes instead. // enough new nodes on the network, we can fall back to using reputable nodes instead.

View File

@ -144,7 +144,7 @@ func (state *DownloadSelectionCacheState) IPs(nodes []storj.NodeID) map[storj.No
} }
// FilteredIPs returns node ip:port for nodes that are in state. Results are filtered out.. // FilteredIPs returns node ip:port for nodes that are in state. Results are filtered out..
func (state *DownloadSelectionCacheState) FilteredIPs(nodes []storj.NodeID, filter nodeselection.NodeFilters) map[storj.NodeID]string { func (state *DownloadSelectionCacheState) FilteredIPs(nodes []storj.NodeID, filter nodeselection.NodeFilter) map[storj.NodeID]string {
xs := make(map[storj.NodeID]string, len(nodes)) xs := make(map[storj.NodeID]string, len(nodes))
for _, nodeID := range nodes { for _, nodeID := range nodes {
if n, exists := state.byID[nodeID]; exists && filter.MatchInclude(n) { if n, exists := state.byID[nodeID]; exists && filter.MatchInclude(n) {

View File

@ -11,7 +11,6 @@ import (
"github.com/jtolio/mito" "github.com/jtolio/mito"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/zeebo/errs" "github.com/zeebo/errs"
"golang.org/x/exp/slices"
"storj.io/common/storj" "storj.io/common/storj"
"storj.io/common/storj/location" "storj.io/common/storj/location"
@ -19,11 +18,11 @@ import (
) )
// PlacementRules can crate filter based on the placement identifier. // PlacementRules can crate filter based on the placement identifier.
type PlacementRules func(constraint storj.PlacementConstraint) (filter nodeselection.NodeFilters) type PlacementRules func(constraint storj.PlacementConstraint) (filter nodeselection.NodeFilter)
// ConfigurablePlacementRule can include the placement definitions for each known identifier. // ConfigurablePlacementRule can include the placement definitions for each known identifier.
type ConfigurablePlacementRule struct { type ConfigurablePlacementRule struct {
placements map[storj.PlacementConstraint]nodeselection.NodeFilters placements map[storj.PlacementConstraint]nodeselection.NodeFilter
} }
// String implements pflag.Value. // String implements pflag.Value.
@ -42,7 +41,7 @@ func (d *ConfigurablePlacementRule) String() string {
// Set implements pflag.Value. // Set implements pflag.Value.
func (d *ConfigurablePlacementRule) Set(s string) error { func (d *ConfigurablePlacementRule) Set(s string) error {
if d.placements == nil { if d.placements == nil {
d.placements = make(map[storj.PlacementConstraint]nodeselection.NodeFilters) d.placements = make(map[storj.PlacementConstraint]nodeselection.NodeFilter)
} }
d.AddLegacyStaticRules() d.AddLegacyStaticRules()
return d.AddPlacementFromString(s) return d.AddPlacementFromString(s)
@ -58,7 +57,7 @@ var _ pflag.Value = &ConfigurablePlacementRule{}
// NewPlacementRules creates a fully initialized NewPlacementRules. // NewPlacementRules creates a fully initialized NewPlacementRules.
func NewPlacementRules() *ConfigurablePlacementRule { func NewPlacementRules() *ConfigurablePlacementRule {
return &ConfigurablePlacementRule{ return &ConfigurablePlacementRule{
placements: map[storj.PlacementConstraint]nodeselection.NodeFilters{}, placements: make(map[storj.PlacementConstraint]nodeselection.NodeFilter),
} }
} }
@ -72,8 +71,8 @@ func (d *ConfigurablePlacementRule) AddLegacyStaticRules() {
} }
// AddPlacementRule registers a new placement. // AddPlacementRule registers a new placement.
func (d *ConfigurablePlacementRule) AddPlacementRule(id storj.PlacementConstraint, filters nodeselection.NodeFilters) { func (d *ConfigurablePlacementRule) AddPlacementRule(id storj.PlacementConstraint, filter nodeselection.NodeFilter) {
d.placements[id] = filters d.placements[id] = filter
} }
// AddPlacementFromString parses placement definition form string representations from id:definition;id:definition;... // AddPlacementFromString parses placement definition form string representations from id:definition;id:definition;...
@ -116,6 +115,17 @@ func (d *ConfigurablePlacementRule) AddPlacementFromString(definitions string) e
} }
return res, nil return res, nil
}, },
"annotated": func(filter nodeselection.NodeFilter, kv map[string]string) (nodeselection.AnnotatedNodeFilter, error) {
return nodeselection.AnnotatedNodeFilter{
Filter: filter,
Annotations: kv,
}, nil
},
"annotation": func(key string, value string) (map[string]string, error) {
return map[string]string{
key: value,
}, nil
},
} }
for _, definition := range strings.Split(definitions, ";") { for _, definition := range strings.Split(definitions, ";") {
definition = strings.TrimSpace(definition) definition = strings.TrimSpace(definition)
@ -132,18 +142,18 @@ func (d *ConfigurablePlacementRule) AddPlacementFromString(definitions string) e
if err != nil { if err != nil {
return errs.Wrap(err) return errs.Wrap(err)
} }
d.placements[storj.PlacementConstraint(id)] = val.(nodeselection.NodeFilters) d.placements[storj.PlacementConstraint(id)] = val.(nodeselection.NodeFilter)
} }
return nil return nil
} }
// CreateFilters implements PlacementCondition. // CreateFilters implements PlacementCondition.
func (d *ConfigurablePlacementRule) CreateFilters(constraint storj.PlacementConstraint) (filter nodeselection.NodeFilters) { func (d *ConfigurablePlacementRule) CreateFilters(constraint storj.PlacementConstraint) (filter nodeselection.NodeFilter) {
if constraint == storj.EveryCountry { if constraint == storj.EveryCountry {
return nodeselection.NodeFilters{} return nodeselection.NodeFilters{}
} }
if filters, found := d.placements[constraint]; found { if filters, found := d.placements[constraint]; found {
return slices.Clone(filters) return filters
} }
return nodeselection.NodeFilters{ return nodeselection.NodeFilters{
nodeselection.ExcludeAllFilter{}, nodeselection.ExcludeAllFilter{},

View File

@ -111,6 +111,18 @@ func TestPlacementFromString(t *testing.T) {
CountryCode: location.Germany, CountryCode: location.Germany,
})) }))
})
t.Run("annotated", func(t *testing.T) {
p := NewPlacementRules()
err := p.AddPlacementFromString(`11:annotated(country("GB"),annotation("autoExcludeSubnet","off"))`)
require.NoError(t, err)
filters := p.placements[storj.PlacementConstraint(11)]
require.True(t, filters.MatchInclude(&nodeselection.SelectedNode{
CountryCode: location.UnitedKingdom,
}))
require.Equal(t, nodeselection.GetAnnotation(filters, "autoExcludeSubnet"), "off")
}) })
t.Run("legacy geofencing rules", func(t *testing.T) { t.Run("legacy geofencing rules", func(t *testing.T) {

View File

@ -13,6 +13,14 @@ import (
"storj.io/storj/satellite/nodeselection" "storj.io/storj/satellite/nodeselection"
) )
const (
// AutoExcludeSubnet is placement annotation key to turn off subnet restrictions.
AutoExcludeSubnet = "autoExcludeSubnet"
// AutoExcludeSubnetOFF is the value of AutoExcludeSubnet to disable subnet restrictions.
AutoExcludeSubnetOFF = "off"
)
// UploadSelectionDB implements the database for upload selection cache. // UploadSelectionDB implements the database for upload selection cache.
// //
// architecture: Database // architecture: Database
@ -96,19 +104,31 @@ func (cache *UploadSelectionCache) GetNodes(ctx context.Context, req FindStorage
return nil, Error.Wrap(err) return nil, Error.Wrap(err)
} }
filters := cache.placementRules(req.Placement) placementRules := cache.placementRules(req.Placement)
useSubnetExclusion := nodeselection.GetAnnotation(placementRules, AutoExcludeSubnet) != AutoExcludeSubnetOFF
filters := nodeselection.NodeFilters{placementRules}
if len(req.ExcludedIDs) > 0 { if len(req.ExcludedIDs) > 0 {
if useSubnetExclusion {
filters = append(filters, state.ExcludeNetworksBasedOnNodes(req.ExcludedIDs)) filters = append(filters, state.ExcludeNetworksBasedOnNodes(req.ExcludedIDs))
} else {
filters = append(filters, nodeselection.ExcludedIDs(req.ExcludedIDs))
}
} }
filters = append(filters, cache.defaultFilters) filters = append(filters, cache.defaultFilters)
filters = filters.WithAutoExcludeSubnets()
selected, err := state.Select(ctx, nodeselection.Request{ selectionReq := nodeselection.Request{
Count: req.RequestedCount, Count: req.RequestedCount,
NewFraction: cache.selectionConfig.NewNodeFraction, NewFraction: cache.selectionConfig.NewNodeFraction,
NodeFilters: filters, NodeFilters: filters,
}) }
if !useSubnetExclusion {
selectionReq.SelectionType = nodeselection.SelectionTypeByID
}
selected, err := state.Select(ctx, selectionReq)
if nodeselection.ErrNotEnoughNodes.Has(err) { if nodeselection.ErrNotEnoughNodes.Has(err) {
err = ErrNotEnoughNodes.Wrap(err) err = ErrNotEnoughNodes.Wrap(err)
} }

View File

@ -215,6 +215,7 @@ func TestGetNodes(t *testing.T) {
} }
placementRules := overlay.NewPlacementRules() placementRules := overlay.NewPlacementRules()
placementRules.AddPlacementRule(storj.PlacementConstraint(5), nodeselection.NodeFilters{}.WithCountryFilter(location.NewSet(location.Germany))) placementRules.AddPlacementRule(storj.PlacementConstraint(5), nodeselection.NodeFilters{}.WithCountryFilter(location.NewSet(location.Germany)))
placementRules.AddPlacementRule(storj.PlacementConstraint(6), nodeselection.WithAnnotation(nodeselection.NodeFilters{}.WithCountryFilter(location.NewSet(location.Germany)), overlay.AutoExcludeSubnet, overlay.AutoExcludeSubnetOFF))
cache, err := overlay.NewUploadSelectionCache(zap.NewNop(), cache, err := overlay.NewUploadSelectionCache(zap.NewNop(),
db.OverlayCache(), db.OverlayCache(),
@ -239,6 +240,7 @@ func TestGetNodes(t *testing.T) {
t.Run("normal selection", func(t *testing.T) { t.Run("normal selection", func(t *testing.T) {
t.Run("get 2", func(t *testing.T) { t.Run("get 2", func(t *testing.T) {
t.Parallel()
// confirm cache.GetNodes returns the correct nodes // confirm cache.GetNodes returns the correct nodes
selectedNodes, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{RequestedCount: 2}) selectedNodes, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{RequestedCount: 2})
require.NoError(t, err) require.NoError(t, err)
@ -253,6 +255,7 @@ func TestGetNodes(t *testing.T) {
} }
}) })
t.Run("too much", func(t *testing.T) { t.Run("too much", func(t *testing.T) {
t.Parallel()
// we have 5 subnets (1 new, 4 vetted), with two nodes in each // we have 5 subnets (1 new, 4 vetted), with two nodes in each
_, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{RequestedCount: 6}) _, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{RequestedCount: 6})
require.Error(t, err) require.Error(t, err)
@ -262,6 +265,7 @@ func TestGetNodes(t *testing.T) {
t.Run("using country filter", func(t *testing.T) { t.Run("using country filter", func(t *testing.T) {
t.Run("normal", func(t *testing.T) { t.Run("normal", func(t *testing.T) {
t.Parallel()
selectedNodes, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{ selectedNodes, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{
RequestedCount: 3, RequestedCount: 3,
Placement: 5, Placement: 5,
@ -270,6 +274,7 @@ func TestGetNodes(t *testing.T) {
require.Len(t, selectedNodes, 3) require.Len(t, selectedNodes, 3)
}) })
t.Run("too much", func(t *testing.T) { t.Run("too much", func(t *testing.T) {
t.Parallel()
_, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{ _, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{
RequestedCount: 4, RequestedCount: 4,
Placement: 5, Placement: 5,
@ -278,6 +283,60 @@ func TestGetNodes(t *testing.T) {
}) })
}) })
t.Run("using country without subnets", func(t *testing.T) {
t.Run("normal", func(t *testing.T) {
t.Parallel()
// it's possible to get 5 only because we don't use subnet exclusions.
selectedNodes, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{
RequestedCount: 5,
Placement: 6,
})
require.NoError(t, err)
require.Len(t, selectedNodes, 5)
})
t.Run("too much", func(t *testing.T) {
t.Parallel()
_, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{
RequestedCount: 6,
Placement: 6,
})
require.Error(t, err)
})
})
t.Run("using country without subnets and exclusions", func(t *testing.T) {
// DE nodes: 0 (subet:A), 2 (A), 4 (B) 6(C) 8(C, but not vetted)
// if everything works well, we can exclude 0, and got 3 (2,4,6)
// unless somebody removes the 2 (because it's in the same subnet as 0)
selectedNodes, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{
RequestedCount: 3,
Placement: 6,
ExcludedIDs: []storj.NodeID{
nodeIds[0],
},
})
require.NoError(t, err)
require.Len(t, selectedNodes, 3)
})
t.Run("check subnet selection", func(t *testing.T) {
for i := 0; i < 10; i++ {
selectedNodes, err := cache.GetNodes(ctx, overlay.FindStorageNodesRequest{
RequestedCount: 3,
Placement: 0,
})
require.NoError(t, err)
subnets := map[string]struct{}{}
for _, node := range selectedNodes {
subnets[node.LastNet] = struct{}{}
}
require.Len(t, selectedNodes, 3)
require.Len(t, subnets, 3)
}
})
}) })
} }