github.com/lbryio/lbcd@v0.22.119/connmgr/connmanager.go (about)

     1  // Copyright (c) 2016 The btcsuite developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package connmgr
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"net"
    11  	"sync"
    12  	"sync/atomic"
    13  	"time"
    14  )
    15  
    16  // maxFailedAttempts is the maximum number of successive failed connection
    17  // attempts after which network failure is assumed and new connections will
    18  // be delayed by the configured retry duration.
    19  const maxFailedAttempts = 25
    20  
    21  var (
    22  	//ErrDialNil is used to indicate that Dial cannot be nil in the configuration.
    23  	ErrDialNil = errors.New("Config: Dial cannot be nil")
    24  
    25  	// maxRetryDuration is the max duration of time retrying of a persistent
    26  	// connection is allowed to grow to.  This is necessary since the retry
    27  	// logic uses a backoff mechanism which increases the interval base times
    28  	// the number of retries that have been done.
    29  	maxRetryDuration = time.Minute * 5
    30  
    31  	// defaultRetryDuration is the default duration of time for retrying
    32  	// persistent connections.
    33  	defaultRetryDuration = time.Second * 5
    34  
    35  	// defaultTargetOutbound is the default number of outbound connections to
    36  	// maintain.
    37  	defaultTargetOutbound = uint32(8)
    38  )
    39  
    40  // ConnState represents the state of the requested connection.
    41  type ConnState uint8
    42  
    43  // ConnState can be either pending, established, disconnected or failed.  When
    44  // a new connection is requested, it is attempted and categorized as
    45  // established or failed depending on the connection result.  An established
    46  // connection which was disconnected is categorized as disconnected.
    47  const (
    48  	ConnPending ConnState = iota
    49  	ConnFailing
    50  	ConnCanceled
    51  	ConnEstablished
    52  	ConnDisconnected
    53  )
    54  
    55  // ConnReq is the connection request to a network address. If permanent, the
    56  // connection will be retried on disconnection.
    57  type ConnReq struct {
    58  	// The following variables must only be used atomically.
    59  	id uint64
    60  
    61  	Addr      net.Addr
    62  	Permanent bool
    63  
    64  	conn       net.Conn
    65  	state      ConnState
    66  	stateMtx   sync.RWMutex
    67  	retryCount uint32
    68  }
    69  
    70  // updateState updates the state of the connection request.
    71  func (c *ConnReq) updateState(state ConnState) {
    72  	c.stateMtx.Lock()
    73  	c.state = state
    74  	c.stateMtx.Unlock()
    75  }
    76  
    77  // ID returns a unique identifier for the connection request.
    78  func (c *ConnReq) ID() uint64 {
    79  	return atomic.LoadUint64(&c.id)
    80  }
    81  
    82  // State is the connection state of the requested connection.
    83  func (c *ConnReq) State() ConnState {
    84  	c.stateMtx.RLock()
    85  	state := c.state
    86  	c.stateMtx.RUnlock()
    87  	return state
    88  }
    89  
    90  // String returns a human-readable string for the connection request.
    91  func (c *ConnReq) String() string {
    92  	if c.Addr == nil || c.Addr.String() == "" {
    93  		return fmt.Sprintf("reqid %d", atomic.LoadUint64(&c.id))
    94  	}
    95  	return fmt.Sprintf("%s (reqid %d)", c.Addr, atomic.LoadUint64(&c.id))
    96  }
    97  
    98  // Config holds the configuration options related to the connection manager.
    99  type Config struct {
   100  	// Listeners defines a slice of listeners for which the connection
   101  	// manager will take ownership of and accept connections.  When a
   102  	// connection is accepted, the OnAccept handler will be invoked with the
   103  	// connection.  Since the connection manager takes ownership of these
   104  	// listeners, they will be closed when the connection manager is
   105  	// stopped.
   106  	//
   107  	// This field will not have any effect if the OnAccept field is not
   108  	// also specified.  It may be nil if the caller does not wish to listen
   109  	// for incoming connections.
   110  	Listeners []net.Listener
   111  
   112  	// OnAccept is a callback that is fired when an inbound connection is
   113  	// accepted.  It is the caller's responsibility to close the connection.
   114  	// Failure to close the connection will result in the connection manager
   115  	// believing the connection is still active and thus have undesirable
   116  	// side effects such as still counting toward maximum connection limits.
   117  	//
   118  	// This field will not have any effect if the Listeners field is not
   119  	// also specified since there couldn't possibly be any accepted
   120  	// connections in that case.
   121  	OnAccept func(net.Conn)
   122  
   123  	// TargetOutbound is the number of outbound network connections to
   124  	// maintain. Defaults to 8.
   125  	TargetOutbound uint32
   126  
   127  	// RetryDuration is the duration to wait before retrying connection
   128  	// requests. Defaults to 5s.
   129  	RetryDuration time.Duration
   130  
   131  	// OnConnection is a callback that is fired when a new outbound
   132  	// connection is established.
   133  	OnConnection func(*ConnReq, net.Conn)
   134  
   135  	// OnDisconnection is a callback that is fired when an outbound
   136  	// connection is disconnected.
   137  	OnDisconnection func(*ConnReq)
   138  
   139  	// GetNewAddress is a way to get an address to make a network connection
   140  	// to.  If nil, no new connections will be made automatically.
   141  	GetNewAddress func() (net.Addr, error)
   142  
   143  	// Dial connects to the address on the named network. It cannot be nil.
   144  	Dial func(net.Addr) (net.Conn, error)
   145  }
   146  
   147  // registerPending is used to register a pending connection attempt. By
   148  // registering pending connection attempts we allow callers to cancel pending
   149  // connection attempts before their successful or in the case they're not
   150  // longer wanted.
   151  type registerPending struct {
   152  	c    *ConnReq
   153  	done chan struct{}
   154  }
   155  
   156  // handleConnected is used to queue a successful connection.
   157  type handleConnected struct {
   158  	c    *ConnReq
   159  	conn net.Conn
   160  }
   161  
   162  // handleDisconnected is used to remove a connection.
   163  type handleDisconnected struct {
   164  	id    uint64
   165  	retry bool
   166  }
   167  
   168  // handleFailed is used to remove a pending connection.
   169  type handleFailed struct {
   170  	c   *ConnReq
   171  	err error
   172  }
   173  
   174  // ConnManager provides a manager to handle network connections.
   175  type ConnManager struct {
   176  	// The following variables must only be used atomically.
   177  	connReqCount uint64
   178  	start        int32
   179  	stop         int32
   180  
   181  	cfg            Config
   182  	wg             sync.WaitGroup
   183  	failedAttempts uint64
   184  	requests       chan interface{}
   185  	quit           chan struct{}
   186  }
   187  
   188  // handleFailedConn handles a connection failed due to a disconnect or any
   189  // other failure. If permanent, it retries the connection after the configured
   190  // retry duration. Otherwise, if required, it makes a new connection request.
   191  // After maxFailedConnectionAttempts new connections will be retried after the
   192  // configured retry duration.
   193  func (cm *ConnManager) handleFailedConn(c *ConnReq) {
   194  	if atomic.LoadInt32(&cm.stop) != 0 {
   195  		return
   196  	}
   197  	if c.Permanent {
   198  		c.retryCount++
   199  		d := time.Duration(c.retryCount) * cm.cfg.RetryDuration
   200  		if d > maxRetryDuration {
   201  			d = maxRetryDuration
   202  		}
   203  		log.Debugf("Retrying connection to %v in %v", c, d)
   204  		time.AfterFunc(d, func() {
   205  			cm.Connect(c)
   206  		})
   207  	} else if cm.cfg.GetNewAddress != nil {
   208  		cm.failedAttempts++
   209  		if cm.failedAttempts >= maxFailedAttempts {
   210  			log.Debugf("Max failed connection attempts reached: [%d] "+
   211  				"-- retrying connection in: %v", maxFailedAttempts,
   212  				cm.cfg.RetryDuration)
   213  			time.AfterFunc(cm.cfg.RetryDuration, func() {
   214  				cm.NewConnReq()
   215  			})
   216  		} else {
   217  			go cm.NewConnReq()
   218  		}
   219  	}
   220  }
   221  
   222  // connHandler handles all connection related requests.  It must be run as a
   223  // goroutine.
   224  //
   225  // The connection handler makes sure that we maintain a pool of active outbound
   226  // connections so that we remain connected to the network.  Connection requests
   227  // are processed and mapped by their assigned ids.
   228  func (cm *ConnManager) connHandler() {
   229  
   230  	var (
   231  		// pending holds all registered conn requests that have yet to
   232  		// succeed.
   233  		pending = make(map[uint64]*ConnReq)
   234  
   235  		// conns represents the set of all actively connected peers.
   236  		conns = make(map[uint64]*ConnReq, cm.cfg.TargetOutbound)
   237  	)
   238  
   239  out:
   240  	for {
   241  		select {
   242  		case req := <-cm.requests:
   243  			switch msg := req.(type) {
   244  
   245  			case registerPending:
   246  				connReq := msg.c
   247  				connReq.updateState(ConnPending)
   248  				pending[msg.c.id] = connReq
   249  				close(msg.done)
   250  
   251  			case handleConnected:
   252  				connReq := msg.c
   253  
   254  				if _, ok := pending[connReq.id]; !ok {
   255  					if msg.conn != nil {
   256  						msg.conn.Close()
   257  					}
   258  					log.Debugf("Ignoring connection for "+
   259  						"canceled connreq=%v", connReq)
   260  					continue
   261  				}
   262  
   263  				connReq.updateState(ConnEstablished)
   264  				connReq.conn = msg.conn
   265  				conns[connReq.id] = connReq
   266  				log.Debugf("Connected to %v", connReq)
   267  				connReq.retryCount = 0
   268  				cm.failedAttempts = 0
   269  
   270  				delete(pending, connReq.id)
   271  
   272  				if cm.cfg.OnConnection != nil {
   273  					go cm.cfg.OnConnection(connReq, msg.conn)
   274  				}
   275  
   276  			case handleDisconnected:
   277  				connReq, ok := conns[msg.id]
   278  				if !ok {
   279  					connReq, ok = pending[msg.id]
   280  					if !ok {
   281  						log.Errorf("Unknown connid=%d",
   282  							msg.id)
   283  						continue
   284  					}
   285  
   286  					// Pending connection was found, remove
   287  					// it from pending map if we should
   288  					// ignore a later, successful
   289  					// connection.
   290  					connReq.updateState(ConnCanceled)
   291  					log.Debugf("Canceling: %v", connReq)
   292  					delete(pending, msg.id)
   293  					continue
   294  
   295  				}
   296  
   297  				// An existing connection was located, mark as
   298  				// disconnected and execute disconnection
   299  				// callback.
   300  				log.Debugf("Disconnected from %v", connReq)
   301  				delete(conns, msg.id)
   302  
   303  				if connReq.conn != nil {
   304  					connReq.conn.Close()
   305  				}
   306  
   307  				if cm.cfg.OnDisconnection != nil {
   308  					go cm.cfg.OnDisconnection(connReq)
   309  				}
   310  
   311  				// All internal state has been cleaned up, if
   312  				// this connection is being removed, we will
   313  				// make no further attempts with this request.
   314  				if !msg.retry {
   315  					connReq.updateState(ConnDisconnected)
   316  					continue
   317  				}
   318  
   319  				// Otherwise, we will attempt a reconnection if
   320  				// we do not have enough peers, or if this is a
   321  				// persistent peer. The connection request is
   322  				// re added to the pending map, so that
   323  				// subsequent processing of connections and
   324  				// failures do not ignore the request.
   325  				if uint32(len(conns)) < cm.cfg.TargetOutbound ||
   326  					connReq.Permanent {
   327  
   328  					connReq.updateState(ConnPending)
   329  					log.Debugf("Reconnecting to %v",
   330  						connReq)
   331  					pending[msg.id] = connReq
   332  					cm.handleFailedConn(connReq)
   333  				}
   334  
   335  			case handleFailed:
   336  				connReq := msg.c
   337  
   338  				if _, ok := pending[connReq.id]; !ok {
   339  					log.Debugf("Ignoring connection for "+
   340  						"canceled conn req: %v", connReq)
   341  					continue
   342  				}
   343  
   344  				connReq.updateState(ConnFailing)
   345  				log.Debugf("Failed to connect to %v: %v",
   346  					connReq, msg.err)
   347  				cm.handleFailedConn(connReq)
   348  			}
   349  
   350  		case <-cm.quit:
   351  			break out
   352  		}
   353  	}
   354  
   355  	cm.wg.Done()
   356  	log.Trace("Connection handler done")
   357  }
   358  
   359  // NewConnReq creates a new connection request and connects to the
   360  // corresponding address.
   361  func (cm *ConnManager) NewConnReq() {
   362  	if atomic.LoadInt32(&cm.stop) != 0 {
   363  		return
   364  	}
   365  	if cm.cfg.GetNewAddress == nil {
   366  		return
   367  	}
   368  
   369  	c := &ConnReq{}
   370  	atomic.StoreUint64(&c.id, atomic.AddUint64(&cm.connReqCount, 1))
   371  
   372  	// Submit a request of a pending connection attempt to the connection
   373  	// manager. By registering the id before the connection is even
   374  	// established, we'll be able to later cancel the connection via the
   375  	// Remove method.
   376  	done := make(chan struct{})
   377  	select {
   378  	case cm.requests <- registerPending{c, done}:
   379  	case <-cm.quit:
   380  		return
   381  	}
   382  
   383  	// Wait for the registration to successfully add the pending conn req to
   384  	// the conn manager's internal state.
   385  	select {
   386  	case <-done:
   387  	case <-cm.quit:
   388  		return
   389  	}
   390  
   391  	addr, err := cm.cfg.GetNewAddress()
   392  	if err != nil {
   393  		select {
   394  		case cm.requests <- handleFailed{c, err}:
   395  		case <-cm.quit:
   396  		}
   397  		return
   398  	}
   399  
   400  	c.Addr = addr
   401  
   402  	cm.Connect(c)
   403  }
   404  
   405  // Connect assigns an id and dials a connection to the address of the
   406  // connection request.
   407  func (cm *ConnManager) Connect(c *ConnReq) {
   408  	if atomic.LoadInt32(&cm.stop) != 0 {
   409  		return
   410  	}
   411  
   412  	// During the time we wait for retry there is a chance that
   413  	// this connection was already cancelled
   414  	if c.State() == ConnCanceled {
   415  		log.Debugf("Ignoring connect for canceled connreq=%v", c)
   416  		return
   417  	}
   418  
   419  	if atomic.LoadUint64(&c.id) == 0 {
   420  		atomic.StoreUint64(&c.id, atomic.AddUint64(&cm.connReqCount, 1))
   421  
   422  		// Submit a request of a pending connection attempt to the
   423  		// connection manager. By registering the id before the
   424  		// connection is even established, we'll be able to later
   425  		// cancel the connection via the Remove method.
   426  		done := make(chan struct{})
   427  		select {
   428  		case cm.requests <- registerPending{c, done}:
   429  		case <-cm.quit:
   430  			return
   431  		}
   432  
   433  		// Wait for the registration to successfully add the pending
   434  		// conn req to the conn manager's internal state.
   435  		select {
   436  		case <-done:
   437  		case <-cm.quit:
   438  			return
   439  		}
   440  	}
   441  
   442  	log.Debugf("Attempting to connect to %v", c)
   443  
   444  	conn, err := cm.cfg.Dial(c.Addr)
   445  	if err != nil {
   446  		select {
   447  		case cm.requests <- handleFailed{c, err}:
   448  		case <-cm.quit:
   449  		}
   450  		return
   451  	}
   452  
   453  	select {
   454  	case cm.requests <- handleConnected{c, conn}:
   455  	case <-cm.quit:
   456  	}
   457  }
   458  
   459  // Disconnect disconnects the connection corresponding to the given connection
   460  // id. If permanent, the connection will be retried with an increasing backoff
   461  // duration.
   462  func (cm *ConnManager) Disconnect(id uint64) {
   463  	if atomic.LoadInt32(&cm.stop) != 0 {
   464  		return
   465  	}
   466  
   467  	select {
   468  	case cm.requests <- handleDisconnected{id, true}:
   469  	case <-cm.quit:
   470  	}
   471  }
   472  
   473  // Remove removes the connection corresponding to the given connection id from
   474  // known connections.
   475  //
   476  // NOTE: This method can also be used to cancel a lingering connection attempt
   477  // that hasn't yet succeeded.
   478  func (cm *ConnManager) Remove(id uint64) {
   479  	if atomic.LoadInt32(&cm.stop) != 0 {
   480  		return
   481  	}
   482  
   483  	select {
   484  	case cm.requests <- handleDisconnected{id, false}:
   485  	case <-cm.quit:
   486  	}
   487  }
   488  
   489  // listenHandler accepts incoming connections on a given listener.  It must be
   490  // run as a goroutine.
   491  func (cm *ConnManager) listenHandler(listener net.Listener) {
   492  	log.Infof("Server listening on %s", listener.Addr())
   493  	for atomic.LoadInt32(&cm.stop) == 0 {
   494  		conn, err := listener.Accept()
   495  		if err != nil {
   496  			// Only log the error if not forcibly shutting down.
   497  			if atomic.LoadInt32(&cm.stop) == 0 {
   498  				log.Errorf("Can't accept connection: %v", err)
   499  			}
   500  			continue
   501  		}
   502  		go cm.cfg.OnAccept(conn)
   503  	}
   504  
   505  	cm.wg.Done()
   506  	log.Tracef("Listener handler done for %s", listener.Addr())
   507  }
   508  
   509  // Start launches the connection manager and begins connecting to the network.
   510  func (cm *ConnManager) Start() {
   511  	// Already started?
   512  	if atomic.AddInt32(&cm.start, 1) != 1 {
   513  		return
   514  	}
   515  
   516  	log.Trace("Connection manager started")
   517  	cm.wg.Add(1)
   518  	go cm.connHandler()
   519  
   520  	// Start all the listeners so long as the caller requested them and
   521  	// provided a callback to be invoked when connections are accepted.
   522  	if cm.cfg.OnAccept != nil {
   523  		for _, listner := range cm.cfg.Listeners {
   524  			cm.wg.Add(1)
   525  			go cm.listenHandler(listner)
   526  		}
   527  	}
   528  
   529  	for i := atomic.LoadUint64(&cm.connReqCount); i < uint64(cm.cfg.TargetOutbound); i++ {
   530  		go cm.NewConnReq()
   531  	}
   532  }
   533  
   534  // Wait blocks until the connection manager halts gracefully.
   535  func (cm *ConnManager) Wait() {
   536  	cm.wg.Wait()
   537  }
   538  
   539  // Stop gracefully shuts down the connection manager.
   540  func (cm *ConnManager) Stop() {
   541  	if atomic.AddInt32(&cm.stop, 1) != 1 {
   542  		log.Warnf("Connection manager already stopped")
   543  		return
   544  	}
   545  
   546  	// Stop all the listeners.  There will not be any listeners if
   547  	// listening is disabled.
   548  	for _, listener := range cm.cfg.Listeners {
   549  		// Ignore the error since this is shutdown and there is no way
   550  		// to recover anyways.
   551  		_ = listener.Close()
   552  	}
   553  
   554  	close(cm.quit)
   555  	log.Trace("Connection manager stopped")
   556  }
   557  
   558  // New returns a new connection manager.
   559  // Use Start to start connecting to the network.
   560  func New(cfg *Config) (*ConnManager, error) {
   561  	if cfg.Dial == nil {
   562  		return nil, ErrDialNil
   563  	}
   564  	// Default to sane values
   565  	if cfg.RetryDuration <= 0 {
   566  		cfg.RetryDuration = defaultRetryDuration
   567  	}
   568  	if cfg.TargetOutbound == 0 {
   569  		cfg.TargetOutbound = defaultTargetOutbound
   570  	}
   571  	cm := ConnManager{
   572  		cfg:      *cfg, // Copy so caller can't mutate
   573  		requests: make(chan interface{}),
   574  		quit:     make(chan struct{}),
   575  	}
   576  	return &cm, nil
   577  }