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