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 }