Skip to content
This repository has been archived by the owner on Mar 29, 2024. It is now read-only.

Commit

Permalink
Randomize top nearby pins and routes for load balancing
Browse files Browse the repository at this point in the history
  • Loading branch information
dhaavi committed Mar 15, 2023
1 parent fe992c8 commit e9b23b4
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 39 deletions.
2 changes: 1 addition & 1 deletion captain/navigation.go
Expand Up @@ -77,7 +77,7 @@ findCandidates:
candidates, err := navigator.Main.FindNearestHubs(
locations.BestV4().LocationOrNil(),
locations.BestV6().LocationOrNil(),
opts, navigator.HomeHub, navigator.DefaultMaxFindMatches,
opts, navigator.HomeHub,
)
if err != nil {
if errors.Is(err, navigator.ErrEmptyMap) {
Expand Down
2 changes: 0 additions & 2 deletions crew/connect.go
Expand Up @@ -152,7 +152,6 @@ func (t *Tunnel) establish(ctx context.Context) (err error) {
routes, err = navigator.Main.FindRouteToHub(
sticksTo.Pin.Hub.ID,
t.connInfo.TunnelOpts,
navigator.DefaultMaxFindMatches,
)
if err != nil {
log.Tracer(ctx).Tracef("spn/crew: failed to find route to stickied %s: %s", sticksTo.Pin.Hub, err)
Expand All @@ -168,7 +167,6 @@ func (t *Tunnel) establish(ctx context.Context) (err error) {
routes, err = navigator.Main.FindRoutes(
t.connInfo.Entity.IP,
t.connInfo.TunnelOpts,
navigator.DefaultMaxFindMatches,
)
if err != nil {
return fmt.Errorf("failed to find routes to %s: %w", t.connInfo.Entity.IP, err)
Expand Down
71 changes: 57 additions & 14 deletions navigator/findnearest.go
Expand Up @@ -3,6 +3,7 @@ package navigator
import (
"errors"
"fmt"
mrand "math/rand"
"sort"
"strings"
"time"
Expand All @@ -11,17 +12,25 @@ import (
"github.com/safing/spn/hub"
)

// DefaultMaxFindMatches defines a default value of how many matches a find
// operation in a map should return.
const DefaultMaxFindMatches = 100
const (
// defaultMaxNearbyMatches defines a default value of how many matches a
// nearby pin find operation in a map should return.
defaultMaxNearbyMatches = 100

// defaultRandomizeNearbyPinTopPercent defines the top percent of a nearby
// pins set that should be randomized for balancing purposes.
// Range: 0-1.
defaultRandomizeNearbyPinTopPercent = 0.1
)

// nearbyPins is a list of nearby Pins to a certain location.
type nearbyPins struct {
pins []*nearbyPin
minPins int
maxPins int
maxCost float32
cutOffLimit float32
pins []*nearbyPin
minPins int
maxPins int
maxCost float32
cutOffLimit float32
randomizeTopPercent float32
}

// nearbyPin represents a Pin and the proximity to a certain location.
Expand Down Expand Up @@ -97,8 +106,37 @@ func (nb *nearbyPins) clean() {
}
}

// randomizeTop randomized to the top nearest pins for balancing the network.
func (nb *nearbyPins) randomizeTop() {
switch {
case nb.randomizeTopPercent == 0:
// Check if randomization is enabled.
return
case len(nb.pins) < 2:
// Check if we have enough pins to work with.
return
}

// Find randomization set.
randomizeUpTo := len(nb.pins)
threshold := nb.pins[0].cost * (1 + nb.randomizeTopPercent)
for i, nb := range nb.pins {
// Find first value above the threshold to stop.
if nb.cost > threshold {
randomizeUpTo = i
break
}
}

// Shuffle top set.
if randomizeUpTo >= 2 {
r := mrand.New(mrand.NewSource(time.Now().UnixNano()))
r.Shuffle(randomizeUpTo, nb.Swap)
}
}

// FindNearestHubs searches for the nearest Hubs to the given IP address. The returned Hubs must not be modified in any way.
func (m *Map) FindNearestHubs(locationV4, locationV6 *geoip.Location, opts *Options, matchFor HubType, maxMatches int) ([]*hub.Hub, error) {
func (m *Map) FindNearestHubs(locationV4, locationV6 *geoip.Location, opts *Options, matchFor HubType) ([]*hub.Hub, error) {
m.RLock()
defer m.RUnlock()

Expand All @@ -113,7 +151,7 @@ func (m *Map) FindNearestHubs(locationV4, locationV6 *geoip.Location, opts *Opti
}

// Find nearest Pins.
nearby, err := m.findNearestPins(locationV4, locationV6, opts, matchFor, maxMatches)
nearby, err := m.findNearestPins(locationV4, locationV6, opts, matchFor)
if err != nil {
return nil, err
}
Expand All @@ -126,22 +164,24 @@ func (m *Map) FindNearestHubs(locationV4, locationV6 *geoip.Location, opts *Opti
return hubs, nil
}

func (m *Map) findNearestPins(locationV4, locationV6 *geoip.Location, opts *Options, matchFor HubType, maxMatches int) (*nearbyPins, error) {
func (m *Map) findNearestPins(locationV4, locationV6 *geoip.Location, opts *Options, matchFor HubType) (*nearbyPins, error) {
// Fail if no location is provided.
if locationV4 == nil && locationV6 == nil {
return nil, errors.New("no location provided")
}

// Raise maxMatches to nearestPinsMinimum.
maxMatches := defaultMaxNearbyMatches
if maxMatches < nearestPinsMinimum {
maxMatches = nearestPinsMinimum
}

// Create nearby Pins list.
nearby := &nearbyPins{
minPins: nearestPinsMinimum,
maxPins: maxMatches,
cutOffLimit: nearestPinsMaxCostDifference,
minPins: nearestPinsMinimum,
maxPins: maxMatches,
cutOffLimit: nearestPinsMaxCostDifference,
randomizeTopPercent: defaultRandomizeNearbyPinTopPercent,
}

// Create pin matcher.
Expand Down Expand Up @@ -260,6 +300,9 @@ func (m *Map) findNearestPins(locationV4, locationV6 *geoip.Location, opts *Opti
// Clean one last time and return the list.
nearby.clean()

// Randomize top nearest pins for load balancing.
nearby.randomizeTop()

// Debugging:
// if matchFor == HomeHub {
// log.Debug("spn/navigator: nearby pins:")
Expand Down
6 changes: 3 additions & 3 deletions navigator/findnearest_test.go
Expand Up @@ -16,7 +16,7 @@ func TestFindNearest(t *testing.T) {
// Create a random destination address
ip4, loc4 := createGoodIP(true)

nbPins, err := m.findNearestPins(loc4, nil, m.DefaultOptions(), DestinationHub, DefaultMaxFindMatches)
nbPins, err := m.findNearestPins(loc4, nil, m.DefaultOptions(), DestinationHub)
if err != nil {
t.Error(err)
} else {
Expand All @@ -28,7 +28,7 @@ func TestFindNearest(t *testing.T) {
// Create a random destination address
ip6, loc6 := createGoodIP(true)

nbPins, err := m.findNearestPins(nil, loc6, m.DefaultOptions(), DestinationHub, DefaultMaxFindMatches)
nbPins, err := m.findNearestPins(nil, loc6, m.DefaultOptions(), DestinationHub)
if err != nil {
t.Error(err)
} else {
Expand Down Expand Up @@ -68,7 +68,7 @@ func findFakeHomeHub(m *Map) {
_, loc4 := createGoodIP(true)
_, loc6 := createGoodIP(false)

nbPins, err := m.findNearestPins(loc4, loc6, m.defaultOptions(), HomeHub, DefaultMaxFindMatches)
nbPins, err := m.findNearestPins(loc4, loc6, m.defaultOptions(), HomeHub)
if err != nil {
panic(err)
}
Expand Down
42 changes: 34 additions & 8 deletions navigator/findroutes.go
Expand Up @@ -8,8 +8,19 @@ import (
"github.com/safing/portmaster/intel/geoip"
)

const (
// defaultMaxRouteMatches defines a default value of how many matches a
// route find operation in a map should return.
defaultMaxRouteMatches = 10

// defaultRandomizeRoutesTopPercent defines the top percent of a routes
// set that should be randomized for balancing purposes.
// Range: 0-1.
defaultRandomizeRoutesTopPercent = 0.1
)

// FindRoutes finds possible routes to the given IP, with the given options.
func (m *Map) FindRoutes(ip net.IP, opts *Options, maxRoutes int) (*Routes, error) {
func (m *Map) FindRoutes(ip net.IP, opts *Options) (*Routes, error) {
m.Lock()
defer m.Unlock()

Expand Down Expand Up @@ -55,16 +66,16 @@ func (m *Map) FindRoutes(ip net.IP, opts *Options, maxRoutes int) (*Routes, erro
}

// Find nearest Pins.
nearby, err := m.findNearestPins(locationV4, locationV6, opts.Matcher(DestinationHub, m.intel), maxRoutes)
nearby, err := m.findNearestPins(locationV4, locationV6, opts, DestinationHub)
if err != nil {
return nil, err
}

return m.findRoutes(nearby, opts, maxRoutes)
return m.findRoutes(nearby, opts)
}

// FindRouteToHub finds possible routes to the given Hub, with the given options.
func (m *Map) FindRouteToHub(hubID string, opts *Options, maxRoutes int) (*Routes, error) {
func (m *Map) FindRouteToHub(hubID string, opts *Options) (*Routes, error) {
m.Lock()
defer m.Unlock()

Expand All @@ -84,10 +95,10 @@ func (m *Map) FindRouteToHub(hubID string, opts *Options, maxRoutes int) (*Route
}

// Find a route to the given Hub.
return m.findRoutes(nearby, opts, maxRoutes)
return m.findRoutes(nearby, opts)
}

func (m *Map) findRoutes(dsts *nearbyPins, opts *Options, maxRoutes int) (*Routes, error) {
func (m *Map) findRoutes(dsts *nearbyPins, opts *Options) (*Routes, error) {
if m.home == nil {
return nil, ErrHomeHubUnset
}
Expand All @@ -100,11 +111,15 @@ func (m *Map) findRoutes(dsts *nearbyPins, opts *Options, maxRoutes int) (*Route

// Create routes collector.
routes := &Routes{
maxRoutes: maxRoutes,
maxRoutes: defaultMaxRouteMatches,
randomizeTopPercent: defaultRandomizeRoutesTopPercent,
}

// TODO: Start from the destination and use HopDistance to prioritize
// TODO:
// Start from the destination and use HopDistance to prioritize
// exploring routes that are in the right direction.
// How would we handle selecting the destination node based on route to client?
// Should we just try all destinations?

// Create initial route.
route := &Route{
Expand Down Expand Up @@ -189,6 +204,17 @@ func (m *Map) findRoutes(dsts *nearbyPins, opts *Options, maxRoutes int) (*Route
return nil, errors.New("failed to find any routes")
}

// Randomize top routes for load balancing.
routes.randomizeTop()

// Copy remaining data to routes.
routes.makeExportReady(opts.RoutingProfile)

// Debugging:
// log.Debug("spn/navigator: routes:")
// for _, route := range routes.All {
// log.Debugf("spn/navigator: %s", route)
// }

return routes, nil
}
4 changes: 2 additions & 2 deletions navigator/findroutes_test.go
Expand Up @@ -17,7 +17,7 @@ func TestFindRoutes(t *testing.T) {
// Create a random destination address
dstIP, _ := createGoodIP(i%2 == 0)

routes, err := m.FindRoutes(dstIP, m.DefaultOptions(), 10)
routes, err := m.FindRoutes(dstIP, m.DefaultOptions())
switch {
case err != nil:
t.Error(err)
Expand All @@ -44,7 +44,7 @@ func BenchmarkFindRoutes(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
routes, err := m.FindRoutes(preGenIPs[i%len(preGenIPs)], m.DefaultOptions(), 10)
routes, err := m.FindRoutes(preGenIPs[i%len(preGenIPs)], m.DefaultOptions())
if err != nil {
b.Error(err)
} else {
Expand Down
56 changes: 47 additions & 9 deletions navigator/route.go
Expand Up @@ -2,15 +2,18 @@ package navigator

import (
"fmt"
mrand "math/rand"
"sort"
"strings"
"time"
)

// Routes holds a collection of Routes.
type Routes struct {
All []*Route
maxCost float32 // automatic
maxRoutes int // manual setting
All []*Route
randomizeTopPercent float32
maxCost float32 // automatic
maxRoutes int // manual setting
}

// Len is the number of elements in the collection.
Expand Down Expand Up @@ -60,6 +63,35 @@ func (r *Routes) clean() {
}
}

// randomizeTop randomized to the top nearest pins for balancing the network.
func (r *Routes) randomizeTop() {
switch {
case r.randomizeTopPercent == 0:
// Check if randomization is enabled.
return
case len(r.All) < 2:
// Check if we have enough pins to work with.
return
}

// Find randomization set.
randomizeUpTo := len(r.All)
threshold := r.All[0].TotalCost * (1 + r.randomizeTopPercent)
for i, r := range r.All {
// Find first value above the threshold to stop.
if r.TotalCost > threshold {
randomizeUpTo = i
break
}
}

// Shuffle top set.
if randomizeUpTo >= 2 {
mr := mrand.New(mrand.NewSource(time.Now().UnixNano()))
mr.Shuffle(randomizeUpTo, r.Swap)
}
}

// Route is a path through the map.
type Route struct {
// Path is a list of Transit Hubs and the Destination Hub, including the Cost
Expand Down Expand Up @@ -119,8 +151,8 @@ func (r *Route) recalculateTotalCost() {
r.TotalCost = r.DstCost
for _, hop := range r.Path {
if hop.pin.HasActiveTerminal() {
// If we have an active connection, only take 90% of the cost.
r.TotalCost += hop.Cost * 0.9
// If we have an active connection, only take 80% of the cost.
r.TotalCost += hop.Cost * 0.8
} else {
r.TotalCost += hop.Cost
}
Expand All @@ -139,6 +171,7 @@ func (r *Route) CopyUpTo(n int) *Route {

newRoute := &Route{
Path: make([]*Hop, n),
DstCost: r.DstCost,
TotalCost: r.TotalCost,
}
copy(newRoute.Path, r.Path)
Expand Down Expand Up @@ -174,10 +207,15 @@ func (hop *Hop) Pin() *Pin {
}

func (r *Route) String() string {
s := make([]string, 0, len(r.Path)+1)
for _, hop := range r.Path {
s = append(s, fmt.Sprintf("=> %.2f$ %s", hop.Cost, hop.pin))
s := make([]string, 0, len(r.Path)+2)
s = append(s, fmt.Sprintf("route with %.2fc:", r.TotalCost))
for i, hop := range r.Path {
if i == 0 {
s = append(s, hop.pin.String())
} else {
s = append(s, fmt.Sprintf("--> %.2fc %s", hop.Cost, hop.pin))
}
}
s = append(s, fmt.Sprintf("=> %.2f$", r.DstCost))
s = append(s, fmt.Sprintf("--> %.2fc", r.DstCost))
return strings.Join(s, " ")
}

0 comments on commit e9b23b4

Please sign in to comment.