github.com/status-im/status-go@v1.1.0/services/ext/mailservers/connmanager.go (about) 1 package mailservers 2 3 import ( 4 "sync" 5 "time" 6 7 "github.com/ethereum/go-ethereum/event" 8 "github.com/ethereum/go-ethereum/log" 9 "github.com/ethereum/go-ethereum/p2p" 10 "github.com/ethereum/go-ethereum/p2p/enode" 11 12 "github.com/status-im/status-go/eth-node/types" 13 ) 14 15 const ( 16 peerEventsBuffer = 10 // sufficient buffer to avoid blocking a p2p feed. 17 whisperEventsBuffer = 20 // sufficient buffer to avod blocking a eventSub envelopes feed. 18 ) 19 20 // PeerAdderRemover is an interface for adding or removing peers. 21 type PeerAdderRemover interface { 22 AddPeer(node *enode.Node) 23 RemovePeer(node *enode.Node) 24 } 25 26 // PeerEventsSubscriber interface to subscribe for p2p.PeerEvent's. 27 type PeerEventsSubscriber interface { 28 SubscribeEvents(chan *p2p.PeerEvent) event.Subscription 29 } 30 31 // EnvelopeEventSubscriber interface to subscribe for types.EnvelopeEvent's. 32 type EnvelopeEventSubscriber interface { 33 SubscribeEnvelopeEvents(chan<- types.EnvelopeEvent) types.Subscription 34 } 35 36 type p2pServer interface { 37 PeerAdderRemover 38 PeerEventsSubscriber 39 } 40 41 // NewConnectionManager creates an instance of ConnectionManager. 42 func NewConnectionManager(server p2pServer, eventSub EnvelopeEventSubscriber, target, maxFailures int, timeout time.Duration) *ConnectionManager { 43 return &ConnectionManager{ 44 server: server, 45 eventSub: eventSub, 46 connectedTarget: target, 47 maxFailures: maxFailures, 48 notifications: make(chan []*enode.Node), 49 timeoutWaitAdded: timeout, 50 } 51 } 52 53 // ConnectionManager manages keeps target of peers connected. 54 type ConnectionManager struct { 55 wg sync.WaitGroup 56 quit chan struct{} 57 58 server p2pServer 59 eventSub EnvelopeEventSubscriber 60 61 notifications chan []*enode.Node 62 connectedTarget int 63 timeoutWaitAdded time.Duration 64 maxFailures int 65 } 66 67 // Notify sends a non-blocking notification about new nodes. 68 func (ps *ConnectionManager) Notify(nodes []*enode.Node) { 69 ps.wg.Add(1) 70 go func() { 71 select { 72 case ps.notifications <- nodes: 73 case <-ps.quit: 74 } 75 ps.wg.Done() 76 }() 77 78 } 79 80 // Start subscribes to a p2p server and handles new peers and state updates for those peers. 81 func (ps *ConnectionManager) Start() { 82 ps.quit = make(chan struct{}) 83 ps.wg.Add(1) 84 go func() { 85 state := newInternalState(ps.server, ps.connectedTarget, ps.timeoutWaitAdded) 86 events := make(chan *p2p.PeerEvent, peerEventsBuffer) 87 sub := ps.server.SubscribeEvents(events) 88 whisperEvents := make(chan types.EnvelopeEvent, whisperEventsBuffer) 89 whisperSub := ps.eventSub.SubscribeEnvelopeEvents(whisperEvents) 90 requests := map[types.Hash]struct{}{} 91 failuresPerServer := map[types.EnodeID]int{} 92 93 defer sub.Unsubscribe() 94 defer whisperSub.Unsubscribe() 95 defer ps.wg.Done() 96 for { 97 select { 98 case <-ps.quit: 99 return 100 case err := <-sub.Err(): 101 log.Error("retry after error subscribing to p2p events", "error", err) 102 return 103 case err := <-whisperSub.Err(): 104 log.Error("retry after error suscribing to eventSub events", "error", err) 105 return 106 case newNodes := <-ps.notifications: 107 state.processReplacement(newNodes, events) 108 case ev := <-events: 109 processPeerEvent(state, ev) 110 case ev := <-whisperEvents: 111 // TODO treat failed requests the same way as expired 112 switch ev.Event { 113 case types.EventMailServerRequestSent: 114 requests[ev.Hash] = struct{}{} 115 case types.EventMailServerRequestCompleted: 116 // reset failures count on first success 117 failuresPerServer[ev.Peer] = 0 118 delete(requests, ev.Hash) 119 case types.EventMailServerRequestExpired: 120 _, exist := requests[ev.Hash] 121 if !exist { 122 continue 123 } 124 failuresPerServer[ev.Peer]++ 125 log.Debug("request to a mail server expired, disconnect a peer", "address", ev.Peer) 126 if failuresPerServer[ev.Peer] >= ps.maxFailures { 127 state.nodeDisconnected(ev.Peer) 128 } 129 } 130 } 131 } 132 }() 133 } 134 135 // Stop gracefully closes all background goroutines and waits until they finish. 136 func (ps *ConnectionManager) Stop() { 137 if ps.quit == nil { 138 return 139 } 140 select { 141 case <-ps.quit: 142 return 143 default: 144 } 145 close(ps.quit) 146 ps.wg.Wait() 147 ps.quit = nil 148 } 149 150 func (state *internalState) processReplacement(newNodes []*enode.Node, events <-chan *p2p.PeerEvent) { 151 replacement := map[types.EnodeID]*enode.Node{} 152 for _, n := range newNodes { 153 replacement[types.EnodeID(n.ID())] = n 154 } 155 state.replaceNodes(replacement) 156 if state.ReachedTarget() { 157 log.Debug("already connected with required target", "target", state.target) 158 return 159 } 160 if state.timeout != 0 { 161 log.Debug("waiting defined timeout to establish connections", 162 "timeout", state.timeout, "target", state.target) 163 timer := time.NewTimer(state.timeout) 164 waitForConnections(state, timer.C, events) 165 timer.Stop() 166 } 167 } 168 169 func newInternalState(srv PeerAdderRemover, target int, timeout time.Duration) *internalState { 170 return &internalState{ 171 options: options{target: target, timeout: timeout}, 172 srv: srv, 173 connected: map[types.EnodeID]struct{}{}, 174 currentNodes: map[types.EnodeID]*enode.Node{}, 175 } 176 } 177 178 type options struct { 179 target int 180 timeout time.Duration 181 } 182 183 type internalState struct { 184 options 185 srv PeerAdderRemover 186 187 connected map[types.EnodeID]struct{} 188 currentNodes map[types.EnodeID]*enode.Node 189 } 190 191 func (state *internalState) ReachedTarget() bool { 192 return len(state.connected) >= state.target 193 } 194 195 func (state *internalState) replaceNodes(new map[types.EnodeID]*enode.Node) { 196 for nid, n := range state.currentNodes { 197 if _, exist := new[nid]; !exist { 198 delete(state.connected, nid) 199 state.srv.RemovePeer(n) 200 } 201 } 202 if !state.ReachedTarget() { 203 for _, n := range new { 204 state.srv.AddPeer(n) 205 } 206 } 207 state.currentNodes = new 208 } 209 210 func (state *internalState) nodeAdded(peer types.EnodeID) { 211 n, exist := state.currentNodes[peer] 212 if !exist { 213 return 214 } 215 if state.ReachedTarget() { 216 state.srv.RemovePeer(n) 217 } else { 218 state.connected[types.EnodeID(n.ID())] = struct{}{} 219 } 220 } 221 222 func (state *internalState) nodeDisconnected(peer types.EnodeID) { 223 n, exist := state.currentNodes[peer] // unrelated event 224 if !exist { 225 return 226 } 227 _, exist = state.connected[peer] // check if already disconnected 228 if !exist { 229 return 230 } 231 if len(state.currentNodes) == 1 { // keep node connected if we don't have another choice 232 return 233 } 234 state.srv.RemovePeer(n) // remove peer permanently, otherwise p2p.Server will try to reconnect 235 delete(state.connected, peer) 236 if !state.ReachedTarget() { // try to connect with any other selected (but not connected) node 237 for nid, n := range state.currentNodes { 238 _, exist := state.connected[nid] 239 if exist || peer == nid { 240 continue 241 } 242 state.srv.AddPeer(n) 243 } 244 } 245 } 246 247 func processPeerEvent(state *internalState, ev *p2p.PeerEvent) { 248 switch ev.Type { 249 case p2p.PeerEventTypeAdd: 250 log.Debug("connected to a mailserver", "address", ev.Peer) 251 state.nodeAdded(types.EnodeID(ev.Peer)) 252 case p2p.PeerEventTypeDrop: 253 log.Debug("mailserver disconnected", "address", ev.Peer) 254 state.nodeDisconnected(types.EnodeID(ev.Peer)) 255 } 256 } 257 258 func waitForConnections(state *internalState, timeout <-chan time.Time, events <-chan *p2p.PeerEvent) { 259 for { 260 select { 261 case ev := <-events: 262 processPeerEvent(state, ev) 263 if state.ReachedTarget() { 264 return 265 } 266 case <-timeout: 267 return 268 } 269 } 270 271 }