2023-08-03 16:21:15 +00:00
package peermanager
2023-01-13 23:58:22 +00:00
// Adapted from github.com/libp2p/go-libp2p@v0.23.2/p2p/discovery/backoff/backoffconnector.go
import (
"context"
"errors"
"sync"
"time"
2023-06-05 18:36:47 +00:00
"github.com/ethereum/go-ethereum/p2p/enode"
2023-01-13 23:58:22 +00:00
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
2023-06-05 14:39:38 +00:00
2023-01-13 23:58:22 +00:00
"github.com/libp2p/go-libp2p/p2p/discovery/backoff"
"github.com/waku-org/go-waku/logging"
2023-08-03 16:21:15 +00:00
wps "github.com/waku-org/go-waku/waku/v2/peerstore"
2023-06-05 14:39:38 +00:00
2023-08-28 06:47:48 +00:00
"sync/atomic"
2023-01-13 23:58:22 +00:00
"go.uber.org/zap"
lru "github.com/hashicorp/golang-lru"
)
2023-08-03 16:21:15 +00:00
// PeerData contains information about a peer useful in establishing connections with it.
type PeerData struct {
Origin wps . Origin
AddrInfo peer . AddrInfo
ENR * enode . Node
}
2023-08-15 01:27:51 +00:00
// PeerConnectionStrategy is a utility to connect to peers,
// but only if we have not recently tried connecting to them already
2023-01-13 23:58:22 +00:00
type PeerConnectionStrategy struct {
sync . RWMutex
cache * lru . TwoQueueCache
host host . Host
2023-08-10 12:58:22 +00:00
pm * PeerManager
2023-01-13 23:58:22 +00:00
cancel context . CancelFunc
2023-08-28 06:47:48 +00:00
paused atomic . Bool
2023-01-13 23:58:22 +00:00
2023-07-07 12:35:22 +00:00
wg sync . WaitGroup
dialTimeout time . Duration
dialCh chan peer . AddrInfo
subscriptions [ ] <- chan PeerData
2023-01-13 23:58:22 +00:00
backoff backoff . BackoffFactory
mux sync . Mutex
logger * zap . Logger
}
2023-08-15 01:27:51 +00:00
// NewPeerConnectionStrategy creates a utility to connect to peers,
// but only if we have not recently tried connecting to them already.
//
2023-01-13 23:58:22 +00:00
// cacheSize is the size of a TwoQueueCache
// dialTimeout is how long we attempt to connect to a peer before giving up
// minPeers is the minimum number of peers that the node should have
// backoff describes the strategy used to decide how long to backoff after previously attempting to connect to a peer
2023-08-28 06:47:48 +00:00
func NewPeerConnectionStrategy ( cacheSize int , pm * PeerManager ,
2023-08-15 01:27:51 +00:00
dialTimeout time . Duration , backoff backoff . BackoffFactory ,
logger * zap . Logger ) ( * PeerConnectionStrategy , error ) {
2023-01-13 23:58:22 +00:00
cache , err := lru . New2Q ( cacheSize )
if err != nil {
return nil , err
}
2023-08-28 06:47:48 +00:00
pc := & PeerConnectionStrategy {
2023-01-13 23:58:22 +00:00
cache : cache ,
wg : sync . WaitGroup { } ,
dialTimeout : dialTimeout ,
2023-08-28 06:47:48 +00:00
pm : pm ,
2023-01-13 23:58:22 +00:00
backoff : backoff ,
logger : logger . Named ( "discovery-connector" ) ,
2023-08-28 06:47:48 +00:00
}
pm . SetPeerConnector ( pc )
return pc , nil
2023-01-13 23:58:22 +00:00
}
type connCacheData struct {
nextTry time . Time
strat backoff . BackoffStrategy
}
2023-07-07 12:35:22 +00:00
// Subscribe receives channels on which discovered peers should be pushed
func ( c * PeerConnectionStrategy ) Subscribe ( ctx context . Context , ch <- chan PeerData ) {
if c . cancel != nil {
c . wg . Add ( 1 )
go func ( ) {
defer c . wg . Done ( )
c . consumeSubscription ( ctx , ch )
} ( )
} else {
c . subscriptions = append ( c . subscriptions , ch )
}
}
func ( c * PeerConnectionStrategy ) consumeSubscription ( ctx context . Context , ch <- chan PeerData ) {
for {
2023-08-28 06:47:48 +00:00
// for returning from the loop when peerConnector is paused.
2023-07-07 12:35:22 +00:00
select {
case <- ctx . Done ( ) :
return
2023-08-28 06:47:48 +00:00
default :
}
//
if ! c . isPaused ( ) {
2023-07-07 12:35:22 +00:00
select {
case <- ctx . Done ( ) :
return
2023-08-28 06:47:48 +00:00
case p , ok := <- ch :
if ! ok {
return
}
c . pm . AddDiscoveredPeer ( p )
c . publishWork ( ctx , p . AddrInfo )
case <- time . After ( 1 * time . Second ) :
// This timeout is to not lock the goroutine
break
2023-07-07 12:35:22 +00:00
}
2023-08-28 06:47:48 +00:00
} else {
time . Sleep ( 1 * time . Second ) // sleep while the peerConnector is paused.
2023-07-07 12:35:22 +00:00
}
}
2023-01-13 23:58:22 +00:00
}
2023-08-15 01:27:51 +00:00
// SetHost sets the host to be able to mount or consume a protocol
2023-04-17 00:04:12 +00:00
func ( c * PeerConnectionStrategy ) SetHost ( h host . Host ) {
c . host = h
}
2023-08-15 01:27:51 +00:00
// Start attempts to connect to the peers passed in by peerCh.
// Will not connect to peers if they are within the backoff period.
2023-01-13 23:58:22 +00:00
func ( c * PeerConnectionStrategy ) Start ( ctx context . Context ) error {
if c . cancel != nil {
return errors . New ( "already started" )
}
ctx , cancel := context . WithCancel ( ctx )
c . cancel = cancel
c . dialCh = make ( chan peer . AddrInfo )
2023-08-28 06:47:48 +00:00
c . wg . Add ( 2 )
2023-01-13 23:58:22 +00:00
go c . shouldDialPeers ( ctx )
go c . dialPeers ( ctx )
2023-07-07 12:35:22 +00:00
c . consumeSubscriptions ( ctx )
2023-01-13 23:58:22 +00:00
return nil
}
2023-08-15 01:27:51 +00:00
// Stop terminates the peer-connector
2023-01-13 23:58:22 +00:00
func ( c * PeerConnectionStrategy ) Stop ( ) {
if c . cancel == nil {
return
}
c . cancel ( )
2023-08-28 06:47:48 +00:00
c . cancel = nil
2023-01-13 23:58:22 +00:00
c . wg . Wait ( )
close ( c . dialCh )
}
func ( c * PeerConnectionStrategy ) isPaused ( ) bool {
2023-08-28 06:47:48 +00:00
return c . paused . Load ( )
2023-01-13 23:58:22 +00:00
}
func ( c * PeerConnectionStrategy ) shouldDialPeers ( ctx context . Context ) {
defer c . wg . Done ( )
ticker := time . NewTicker ( 1 * time . Second )
defer ticker . Stop ( )
for {
select {
case <- ctx . Done ( ) :
return
case <- ticker . C :
2023-08-28 06:47:48 +00:00
_ , outRelayPeers := c . pm . getRelayPeers ( )
c . paused . Store ( outRelayPeers . Len ( ) >= c . pm . OutRelayPeersTarget ) // pause if no of OutPeers more than or eq to target
2023-01-13 23:58:22 +00:00
}
}
}
2023-08-28 06:47:48 +00:00
// it might happen Subscribe is called before peerConnector has started so store these subscriptions in subscriptions array and custom after c.cancel is set.
2023-07-07 12:35:22 +00:00
func ( c * PeerConnectionStrategy ) consumeSubscriptions ( ctx context . Context ) {
for _ , subs := range c . subscriptions {
c . wg . Add ( 1 )
go func ( s <- chan PeerData ) {
defer c . wg . Done ( )
c . consumeSubscription ( ctx , s )
} ( subs )
}
2023-08-28 06:47:48 +00:00
c . subscriptions = nil
2023-07-07 12:35:22 +00:00
}
2023-01-13 23:58:22 +00:00
func ( c * PeerConnectionStrategy ) publishWork ( ctx context . Context , p peer . AddrInfo ) {
select {
case c . dialCh <- p :
case <- ctx . Done ( ) :
return
}
}
2023-07-06 14:19:51 +00:00
const maxActiveDials = 5
2023-08-28 06:47:48 +00:00
// c.cache is thread safe
// only reason why mutex is used: if canDialPeer is queried twice for the same peer.
2023-08-15 01:27:51 +00:00
func ( c * PeerConnectionStrategy ) canDialPeer ( pi peer . AddrInfo ) bool {
c . mux . Lock ( )
2023-08-28 06:47:48 +00:00
defer c . mux . Unlock ( )
2023-08-15 01:27:51 +00:00
val , ok := c . cache . Get ( pi . ID )
var cachedPeer * connCacheData
if ok {
tv := val . ( * connCacheData )
now := time . Now ( )
if now . Before ( tv . nextTry ) {
return false
}
tv . nextTry = now . Add ( tv . strat . Delay ( ) )
} else {
cachedPeer = & connCacheData { strat : c . backoff ( ) }
cachedPeer . nextTry = time . Now ( ) . Add ( cachedPeer . strat . Delay ( ) )
c . cache . Add ( pi . ID , cachedPeer )
}
return true
}
2023-01-13 23:58:22 +00:00
func ( c * PeerConnectionStrategy ) dialPeers ( ctx context . Context ) {
defer c . wg . Done ( )
2023-07-06 14:19:51 +00:00
2023-08-28 06:47:48 +00:00
maxGoRoutines := c . pm . OutRelayPeersTarget
2023-07-06 14:19:51 +00:00
if maxGoRoutines > maxActiveDials {
maxGoRoutines = maxActiveDials
}
sem := make ( chan struct { } , maxGoRoutines )
2023-01-13 23:58:22 +00:00
for {
select {
case pi , ok := <- c . dialCh :
if ! ok {
return
}
2023-08-15 01:27:51 +00:00
if pi . ID == c . host . ID ( ) || pi . ID == "" ||
c . host . Network ( ) . Connectedness ( pi . ID ) == network . Connected {
2023-06-30 18:29:24 +00:00
continue
}
2023-08-15 01:27:51 +00:00
if c . canDialPeer ( pi ) {
sem <- struct { } { }
c . wg . Add ( 1 )
2023-08-28 06:47:48 +00:00
go c . dialPeer ( ctx , pi , sem )
2023-01-13 23:58:22 +00:00
}
case <- ctx . Done ( ) :
return
}
}
}
2023-08-15 01:27:51 +00:00
2023-08-28 06:47:48 +00:00
func ( c * PeerConnectionStrategy ) dialPeer ( ctx context . Context , pi peer . AddrInfo , sem chan struct { } ) {
2023-08-15 01:27:51 +00:00
defer c . wg . Done ( )
2023-08-28 06:47:48 +00:00
ctx , cancel := context . WithTimeout ( ctx , c . dialTimeout )
2023-08-15 01:27:51 +00:00
defer cancel ( )
err := c . host . Connect ( ctx , pi )
if err != nil && ! errors . Is ( err , context . Canceled ) {
c . host . Peerstore ( ) . ( wps . WakuPeerstore ) . AddConnFailure ( pi )
c . logger . Warn ( "connecting to peer" , logging . HostID ( "peerID" , pi . ID ) , zap . Error ( err ) )
}
<- sem
}