github.com/MetalBlockchain/subnet-evm@v0.4.9/peer/peer_tracker.go (about)

     1  // (c) 2019-2022, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package peer
     5  
     6  import (
     7  	"math"
     8  	"math/rand"
     9  	"time"
    10  
    11  	"github.com/MetalBlockchain/metalgo/ids"
    12  	utils_math "github.com/MetalBlockchain/metalgo/utils/math"
    13  	"github.com/MetalBlockchain/metalgo/utils/set"
    14  	"github.com/MetalBlockchain/metalgo/version"
    15  
    16  	"github.com/ethereum/go-ethereum/log"
    17  
    18  	"github.com/MetalBlockchain/subnet-evm/metrics"
    19  )
    20  
    21  const (
    22  	bandwidthHalflife = 5 * time.Minute
    23  
    24  	// controls how eagerly we connect to new peers vs. using
    25  	// peers with known good response bandwidth.
    26  	desiredMinResponsivePeers = 20
    27  	newPeerConnectFactor      = 0.1
    28  
    29  	// controls how often we prefer a random responsive peer over the most
    30  	// performant peer.
    31  	randomPeerProbability = 0.2
    32  )
    33  
    34  // information we track on a given peer
    35  type peerInfo struct {
    36  	version   *version.Application
    37  	bandwidth utils_math.Averager
    38  }
    39  
    40  // peerTracker tracks the bandwidth of responses coming from peers,
    41  // preferring to contact peers with known good bandwidth, connecting
    42  // to new peers with an exponentially decaying probability.
    43  // Note: is not thread safe, caller must handle synchronization.
    44  type peerTracker struct {
    45  	peers                  map[ids.NodeID]*peerInfo // all peers we are connected to
    46  	numTrackedPeers        metrics.Gauge
    47  	trackedPeers           set.Set[ids.NodeID] // peers that we have sent a request to
    48  	numResponsivePeers     metrics.Gauge
    49  	responsivePeers        set.Set[ids.NodeID]     // peers that responded to the last request they were sent
    50  	bandwidthHeap          utils_math.AveragerHeap // tracks bandwidth peers are responding with
    51  	averageBandwidthMetric metrics.GaugeFloat64
    52  	averageBandwidth       utils_math.Averager
    53  }
    54  
    55  func NewPeerTracker() *peerTracker {
    56  	return &peerTracker{
    57  		peers:                  make(map[ids.NodeID]*peerInfo),
    58  		numTrackedPeers:        metrics.GetOrRegisterGauge("net_tracked_peers", nil),
    59  		trackedPeers:           make(set.Set[ids.NodeID]),
    60  		numResponsivePeers:     metrics.GetOrRegisterGauge("net_responsive_peers", nil),
    61  		responsivePeers:        make(set.Set[ids.NodeID]),
    62  		bandwidthHeap:          utils_math.NewMaxAveragerHeap(),
    63  		averageBandwidthMetric: metrics.GetOrRegisterGaugeFloat64("net_average_bandwidth", nil),
    64  		averageBandwidth:       utils_math.NewAverager(0, bandwidthHalflife, time.Now()),
    65  	}
    66  }
    67  
    68  // shouldTrackNewPeer returns true if we are not connected to enough peers.
    69  // otherwise returns true probabilistically based on the number of tracked peers.
    70  func (p *peerTracker) shouldTrackNewPeer() bool {
    71  	numResponsivePeers := p.responsivePeers.Len()
    72  	if numResponsivePeers < desiredMinResponsivePeers {
    73  		return true
    74  	}
    75  	if len(p.trackedPeers) >= len(p.peers) {
    76  		// already tracking all the peers
    77  		return false
    78  	}
    79  	newPeerProbability := math.Exp(-float64(numResponsivePeers) * newPeerConnectFactor)
    80  	return rand.Float64() < newPeerProbability
    81  }
    82  
    83  // getResponsivePeer returns a random [ids.NodeID] of a peer that has responded
    84  // to a request.
    85  func (p *peerTracker) getResponsivePeer() (ids.NodeID, utils_math.Averager, bool) {
    86  	nodeID, ok := p.responsivePeers.Peek()
    87  	if !ok {
    88  		return ids.NodeID{}, nil, false
    89  	}
    90  	averager, ok := p.bandwidthHeap.Remove(nodeID)
    91  	if ok {
    92  		return nodeID, averager, true
    93  	}
    94  	peer := p.peers[nodeID]
    95  	return nodeID, peer.bandwidth, true
    96  }
    97  
    98  func (p *peerTracker) GetAnyPeer(minVersion *version.Application) (ids.NodeID, bool) {
    99  	if p.shouldTrackNewPeer() {
   100  		for nodeID := range p.peers {
   101  			// if minVersion is specified and peer's version is less, skip
   102  			if minVersion != nil && p.peers[nodeID].version.Compare(minVersion) < 0 {
   103  				continue
   104  			}
   105  			// skip peers already tracked
   106  			if p.trackedPeers.Contains(nodeID) {
   107  				continue
   108  			}
   109  			log.Debug("peer tracking: connecting to new peer", "trackedPeers", len(p.trackedPeers), "nodeID", nodeID)
   110  			return nodeID, true
   111  		}
   112  	}
   113  	var (
   114  		nodeID   ids.NodeID
   115  		ok       bool
   116  		random   bool
   117  		averager utils_math.Averager
   118  	)
   119  	if rand.Float64() < randomPeerProbability {
   120  		random = true
   121  		nodeID, averager, ok = p.getResponsivePeer()
   122  	} else {
   123  		nodeID, averager, ok = p.bandwidthHeap.Pop()
   124  	}
   125  	if ok {
   126  		log.Debug("peer tracking: popping peer", "nodeID", nodeID, "bandwidth", averager.Read(), "random", random)
   127  		return nodeID, true
   128  	}
   129  	// if no nodes found in the bandwidth heap, return a tracked node at random
   130  	return p.trackedPeers.Peek()
   131  }
   132  
   133  func (p *peerTracker) TrackPeer(nodeID ids.NodeID) {
   134  	p.trackedPeers.Add(nodeID)
   135  	p.numTrackedPeers.Update(int64(p.trackedPeers.Len()))
   136  }
   137  
   138  func (p *peerTracker) TrackBandwidth(nodeID ids.NodeID, bandwidth float64) {
   139  	peer := p.peers[nodeID]
   140  	if peer == nil {
   141  		// we're not connected to this peer, nothing to do here
   142  		log.Debug("tracking bandwidth for untracked peer", "nodeID", nodeID)
   143  		return
   144  	}
   145  
   146  	now := time.Now()
   147  	if peer.bandwidth == nil {
   148  		peer.bandwidth = utils_math.NewAverager(bandwidth, bandwidthHalflife, now)
   149  	} else {
   150  		peer.bandwidth.Observe(bandwidth, now)
   151  	}
   152  	p.bandwidthHeap.Add(nodeID, peer.bandwidth)
   153  
   154  	if bandwidth == 0 {
   155  		p.responsivePeers.Remove(nodeID)
   156  	} else {
   157  		p.responsivePeers.Add(nodeID)
   158  		p.averageBandwidth.Observe(bandwidth, now)
   159  		p.averageBandwidthMetric.Update(p.averageBandwidth.Read())
   160  	}
   161  	p.numResponsivePeers.Update(int64(p.responsivePeers.Len()))
   162  }
   163  
   164  // Connected should be called when [nodeID] connects to this node
   165  func (p *peerTracker) Connected(nodeID ids.NodeID, nodeVersion *version.Application) {
   166  	if peer := p.peers[nodeID]; peer != nil {
   167  		// Peer is already connected, update the version if it has changed.
   168  		// Log a warning message since the consensus engine should never call Connected on a peer
   169  		// that we have already marked as Connected.
   170  		if nodeVersion.Compare(peer.version) != 0 {
   171  			p.peers[nodeID] = &peerInfo{
   172  				version:   nodeVersion,
   173  				bandwidth: peer.bandwidth,
   174  			}
   175  			log.Warn("updating node version of already connected peer", "nodeID", nodeID, "storedVersion", peer.version, "nodeVersion", nodeVersion)
   176  		} else {
   177  			log.Warn("ignoring peer connected event for already connected peer with identical version", "nodeID", nodeID)
   178  		}
   179  		return
   180  	}
   181  
   182  	p.peers[nodeID] = &peerInfo{
   183  		version: nodeVersion,
   184  	}
   185  }
   186  
   187  // Disconnected should be called when [nodeID] disconnects from this node
   188  func (p *peerTracker) Disconnected(nodeID ids.NodeID) {
   189  	p.bandwidthHeap.Remove(nodeID)
   190  	p.trackedPeers.Remove(nodeID)
   191  	p.numTrackedPeers.Update(int64(p.trackedPeers.Len()))
   192  	p.responsivePeers.Remove(nodeID)
   193  	p.numResponsivePeers.Update(int64(p.responsivePeers.Len()))
   194  	delete(p.peers, nodeID)
   195  }
   196  
   197  // Size returns the number of peers the node is connected to
   198  func (p *peerTracker) Size() int {
   199  	return len(p.peers)
   200  }