github.com/ethereum/go-ethereum@v1.16.1/eth/dropper.go (about) 1 // Copyright 2025 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package eth 18 19 import ( 20 mrand "math/rand" 21 "slices" 22 "sync" 23 "time" 24 25 "github.com/ethereum/go-ethereum/common" 26 "github.com/ethereum/go-ethereum/common/mclock" 27 "github.com/ethereum/go-ethereum/log" 28 "github.com/ethereum/go-ethereum/metrics" 29 "github.com/ethereum/go-ethereum/p2p" 30 ) 31 32 const ( 33 // Interval between peer drop events (uniform between min and max) 34 peerDropIntervalMin = 3 * time.Minute 35 // Interval between peer drop events (uniform between min and max) 36 peerDropIntervalMax = 7 * time.Minute 37 // Avoid dropping peers for some time after connection 38 doNotDropBefore = 10 * time.Minute 39 // How close to max should we initiate the drop timer. O should be fine, 40 // dropping when no more peers can be added. Larger numbers result in more 41 // aggressive drop behavior. 42 peerDropThreshold = 0 43 ) 44 45 var ( 46 // droppedInbound is the number of inbound peers dropped 47 droppedInbound = metrics.NewRegisteredMeter("eth/dropper/inbound", nil) 48 // droppedOutbound is the number of outbound peers dropped 49 droppedOutbound = metrics.NewRegisteredMeter("eth/dropper/outbound", nil) 50 ) 51 52 // dropper monitors the state of the peer pool and makes changes as follows: 53 // - during sync the Downloader handles peer connections, so dropper is disabled 54 // - if not syncing and the peer count is close to the limit, it drops peers 55 // randomly every peerDropInterval to make space for new peers 56 // - peers are dropped separately from the inboud pool and from the dialed pool 57 type dropper struct { 58 maxDialPeers int // maximum number of dialed peers 59 maxInboundPeers int // maximum number of inbound peers 60 peersFunc getPeersFunc 61 syncingFunc getSyncingFunc 62 63 // peerDropTimer introduces churn if we are close to limit capacity. 64 // We handle Dialed and Inbound connections separately 65 peerDropTimer *time.Timer 66 67 wg sync.WaitGroup // wg for graceful shutdown 68 shutdownCh chan struct{} 69 } 70 71 // Callback type to get the list of connected peers. 72 type getPeersFunc func() []*p2p.Peer 73 74 // Callback type to get syncing status. 75 // Returns true while syncing, false when synced. 76 type getSyncingFunc func() bool 77 78 func newDropper(maxDialPeers, maxInboundPeers int) *dropper { 79 cm := &dropper{ 80 maxDialPeers: maxDialPeers, 81 maxInboundPeers: maxInboundPeers, 82 peerDropTimer: time.NewTimer(randomDuration(peerDropIntervalMin, peerDropIntervalMax)), 83 shutdownCh: make(chan struct{}), 84 } 85 if peerDropIntervalMin > peerDropIntervalMax { 86 panic("peerDropIntervalMin duration must be less than or equal to peerDropIntervalMax duration") 87 } 88 return cm 89 } 90 91 // Start the dropper. 92 func (cm *dropper) Start(srv *p2p.Server, syncingFunc getSyncingFunc) { 93 cm.peersFunc = srv.Peers 94 cm.syncingFunc = syncingFunc 95 cm.wg.Add(1) 96 go cm.loop() 97 } 98 99 // Stop the dropper. 100 func (cm *dropper) Stop() { 101 cm.peerDropTimer.Stop() 102 close(cm.shutdownCh) 103 cm.wg.Wait() 104 } 105 106 // dropRandomPeer selects one of the peers randomly and drops it from the peer pool. 107 func (cm *dropper) dropRandomPeer() bool { 108 peers := cm.peersFunc() 109 var numInbound int 110 for _, p := range peers { 111 if p.Inbound() { 112 numInbound++ 113 } 114 } 115 numDialed := len(peers) - numInbound 116 117 selectDoNotDrop := func(p *p2p.Peer) bool { 118 // Avoid dropping trusted and static peers, or recent peers. 119 // Only drop peers if their respective category (dialed/inbound) 120 // is close to limit capacity. 121 return p.Trusted() || p.StaticDialed() || 122 p.Lifetime() < mclock.AbsTime(doNotDropBefore) || 123 (p.DynDialed() && cm.maxDialPeers-numDialed > peerDropThreshold) || 124 (p.Inbound() && cm.maxInboundPeers-numInbound > peerDropThreshold) 125 } 126 127 droppable := slices.DeleteFunc(peers, selectDoNotDrop) 128 if len(droppable) > 0 { 129 p := droppable[mrand.Intn(len(droppable))] 130 log.Debug("Dropping random peer", "inbound", p.Inbound(), 131 "id", p.ID(), "duration", common.PrettyDuration(p.Lifetime()), "peercountbefore", len(peers)) 132 p.Disconnect(p2p.DiscUselessPeer) 133 if p.Inbound() { 134 droppedInbound.Mark(1) 135 } else { 136 droppedOutbound.Mark(1) 137 } 138 return true 139 } 140 return false 141 } 142 143 // randomDuration generates a random duration between min and max. 144 func randomDuration(min, max time.Duration) time.Duration { 145 if min > max { 146 panic("min duration must be less than or equal to max duration") 147 } 148 return time.Duration(mrand.Int63n(int64(max-min)) + int64(min)) 149 } 150 151 // loop is the main loop of the connection dropper. 152 func (cm *dropper) loop() { 153 defer cm.wg.Done() 154 155 for { 156 select { 157 case <-cm.peerDropTimer.C: 158 // Drop a random peer if we are not syncing and the peer count is close to the limit. 159 if !cm.syncingFunc() { 160 cm.dropRandomPeer() 161 } 162 cm.peerDropTimer.Reset(randomDuration(peerDropIntervalMin, peerDropIntervalMax)) 163 case <-cm.shutdownCh: 164 return 165 } 166 } 167 }