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  }