2021-06-09 14:37:08 +00:00
|
|
|
{.push raises: [Defect].}
|
2021-02-04 10:32:58 +00:00
|
|
|
|
|
|
|
import
|
2021-04-21 09:36:56 +00:00
|
|
|
std/[options, sets, sequtils, times],
|
2021-02-05 10:49:11 +00:00
|
|
|
chronos, chronicles, metrics,
|
2021-03-26 08:49:51 +00:00
|
|
|
./waku_peer_store,
|
|
|
|
../storage/peer/peer_storage
|
2021-02-04 10:32:58 +00:00
|
|
|
|
2021-03-26 08:49:51 +00:00
|
|
|
export waku_peer_store
|
2021-02-05 10:49:11 +00:00
|
|
|
|
|
|
|
declareCounter waku_peers_dials, "Number of peer dials", ["outcome"]
|
2021-03-26 08:49:51 +00:00
|
|
|
declarePublicGauge waku_peers_errors, "Number of peer manager errors", ["type"]
|
2021-02-05 10:49:11 +00:00
|
|
|
|
|
|
|
logScope:
|
|
|
|
topics = "wakupeers"
|
|
|
|
|
2021-02-04 10:32:58 +00:00
|
|
|
type
|
|
|
|
PeerManager* = ref object of RootObj
|
|
|
|
switch*: Switch
|
2021-02-12 08:53:52 +00:00
|
|
|
peerStore*: WakuPeerStore
|
2021-03-26 08:49:51 +00:00
|
|
|
storage: PeerStorage
|
2021-02-04 10:32:58 +00:00
|
|
|
|
2021-04-21 09:36:56 +00:00
|
|
|
let
|
|
|
|
defaultDialTimeout = chronos.minutes(1) # @TODO should this be made configurable?
|
2021-02-05 10:49:11 +00:00
|
|
|
|
2021-03-26 08:49:51 +00:00
|
|
|
####################
|
|
|
|
# Helper functions #
|
|
|
|
####################
|
|
|
|
|
2021-06-02 07:53:34 +00:00
|
|
|
proc toPeerInfo*(storedInfo: StoredInfo): PeerInfo =
|
2021-03-26 08:49:51 +00:00
|
|
|
PeerInfo.init(peerId = storedInfo.peerId,
|
|
|
|
addrs = toSeq(storedInfo.addrs),
|
|
|
|
protocols = toSeq(storedInfo.protos))
|
|
|
|
|
2021-04-21 09:36:56 +00:00
|
|
|
proc insertOrReplace(ps: PeerStorage,
|
|
|
|
peerId: PeerID,
|
2021-06-09 14:37:08 +00:00
|
|
|
storedInfo: StoredInfo,
|
|
|
|
connectedness: Connectedness,
|
2021-07-14 17:58:46 +00:00
|
|
|
disconnectTime: int64 = 0) =
|
2021-03-26 08:49:51 +00:00
|
|
|
# Insert peer entry into persistent storage, or replace existing entry with updated info
|
2021-04-21 09:36:56 +00:00
|
|
|
let res = ps.put(peerId, storedInfo, connectedness, disconnectTime)
|
2021-03-26 08:49:51 +00:00
|
|
|
if res.isErr:
|
|
|
|
warn "failed to store peers", err = res.error
|
|
|
|
waku_peers_errors.inc(labelValues = ["storage_failure"])
|
|
|
|
|
|
|
|
proc dialPeer(pm: PeerManager, peerId: PeerID,
|
|
|
|
addrs: seq[MultiAddress], proto: string,
|
|
|
|
dialTimeout = defaultDialTimeout): Future[Option[Connection]] {.async.} =
|
|
|
|
info "Dialing peer from manager", wireAddr = addrs[0], peerId = peerId
|
|
|
|
|
|
|
|
# Dial Peer
|
|
|
|
let dialFut = pm.switch.dial(peerId, addrs, proto)
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Attempt to dial remote peer
|
|
|
|
if (await dialFut.withTimeout(dialTimeout)):
|
|
|
|
waku_peers_dials.inc(labelValues = ["successful"])
|
|
|
|
return some(dialFut.read())
|
|
|
|
else:
|
|
|
|
# @TODO any redial attempts?
|
|
|
|
debug "Dialing remote peer timed out"
|
|
|
|
waku_peers_dials.inc(labelValues = ["timeout"])
|
|
|
|
|
|
|
|
pm.peerStore.connectionBook.set(peerId, CannotConnect)
|
|
|
|
if not pm.storage.isNil:
|
|
|
|
pm.storage.insertOrReplace(peerId, pm.peerStore.get(peerId), CannotConnect)
|
|
|
|
|
|
|
|
return none(Connection)
|
|
|
|
except CatchableError as e:
|
|
|
|
# @TODO any redial attempts?
|
|
|
|
debug "Dialing remote peer failed", msg = e.msg
|
|
|
|
waku_peers_dials.inc(labelValues = ["failed"])
|
|
|
|
|
|
|
|
pm.peerStore.connectionBook.set(peerId, CannotConnect)
|
|
|
|
if not pm.storage.isNil:
|
|
|
|
pm.storage.insertOrReplace(peerId, pm.peerStore.get(peerId), CannotConnect)
|
|
|
|
|
|
|
|
return none(Connection)
|
|
|
|
|
2021-07-14 17:58:46 +00:00
|
|
|
proc loadFromStorage(pm: PeerManager) =
|
2021-03-26 08:49:51 +00:00
|
|
|
# Load peers from storage, if available
|
2021-04-21 09:36:56 +00:00
|
|
|
proc onData(peerId: PeerID, storedInfo: StoredInfo, connectedness: Connectedness, disconnectTime: int64) =
|
2021-04-16 09:57:45 +00:00
|
|
|
if peerId == pm.switch.peerInfo.peerId:
|
|
|
|
# Do not manage self
|
|
|
|
return
|
|
|
|
|
2021-03-26 08:49:51 +00:00
|
|
|
pm.peerStore.addressBook.set(peerId, storedInfo.addrs)
|
|
|
|
pm.peerStore.protoBook.set(peerId, storedInfo.protos)
|
|
|
|
pm.peerStore.keyBook.set(peerId, storedInfo.publicKey)
|
|
|
|
pm.peerStore.connectionBook.set(peerId, NotConnected) # Reset connectedness state
|
2021-04-21 09:36:56 +00:00
|
|
|
pm.peerStore.disconnectBook.set(peerId, disconnectTime)
|
2021-03-26 08:49:51 +00:00
|
|
|
|
|
|
|
let res = pm.storage.getAll(onData)
|
|
|
|
if res.isErr:
|
|
|
|
warn "failed to load peers from storage", err = res.error
|
|
|
|
waku_peers_errors.inc(labelValues = ["storage_load_failure"])
|
|
|
|
|
|
|
|
##################
|
|
|
|
# Initialisation #
|
|
|
|
##################
|
|
|
|
|
2021-02-12 08:53:52 +00:00
|
|
|
proc onConnEvent(pm: PeerManager, peerId: PeerID, event: ConnEvent) {.async.} =
|
|
|
|
case event.kind
|
|
|
|
of ConnEventKind.Connected:
|
|
|
|
pm.peerStore.connectionBook.set(peerId, Connected)
|
2021-03-26 08:49:51 +00:00
|
|
|
if not pm.storage.isNil:
|
|
|
|
pm.storage.insertOrReplace(peerId, pm.peerStore.get(peerId), Connected)
|
2021-02-12 08:53:52 +00:00
|
|
|
return
|
|
|
|
of ConnEventKind.Disconnected:
|
|
|
|
pm.peerStore.connectionBook.set(peerId, CanConnect)
|
2021-03-26 08:49:51 +00:00
|
|
|
if not pm.storage.isNil:
|
2021-04-21 09:36:56 +00:00
|
|
|
pm.storage.insertOrReplace(peerId, pm.peerStore.get(peerId), CanConnect, getTime().toUnix)
|
2021-02-12 08:53:52 +00:00
|
|
|
return
|
|
|
|
|
2021-07-14 17:58:46 +00:00
|
|
|
proc new*(T: type PeerManager, switch: Switch, storage: PeerStorage = nil): PeerManager =
|
2021-02-12 08:53:52 +00:00
|
|
|
let pm = PeerManager(switch: switch,
|
2021-03-26 08:49:51 +00:00
|
|
|
peerStore: WakuPeerStore.new(),
|
|
|
|
storage: storage)
|
2021-02-12 08:53:52 +00:00
|
|
|
|
2021-06-15 08:55:47 +00:00
|
|
|
proc peerHook(peerInfo: PeerInfo, event: ConnEvent): Future[void] {.gcsafe.} =
|
|
|
|
onConnEvent(pm, peerInfo.peerId, event)
|
2021-02-12 08:53:52 +00:00
|
|
|
|
|
|
|
pm.switch.addConnEventHandler(peerHook, ConnEventKind.Connected)
|
|
|
|
pm.switch.addConnEventHandler(peerHook, ConnEventKind.Disconnected)
|
|
|
|
|
2021-03-26 08:49:51 +00:00
|
|
|
if not storage.isNil:
|
|
|
|
pm.loadFromStorage() # Load previously managed peers.
|
|
|
|
|
2021-02-12 08:53:52 +00:00
|
|
|
return pm
|
2021-02-04 10:32:58 +00:00
|
|
|
|
2021-02-11 08:58:25 +00:00
|
|
|
#####################
|
|
|
|
# Manager interface #
|
|
|
|
#####################
|
|
|
|
|
|
|
|
proc peers*(pm: PeerManager): seq[StoredInfo] =
|
|
|
|
# Return the known info for all peers
|
|
|
|
pm.peerStore.peers()
|
|
|
|
|
|
|
|
proc peers*(pm: PeerManager, proto: string): seq[StoredInfo] =
|
|
|
|
# Return the known info for all peers registered on the specified protocol
|
|
|
|
pm.peers.filterIt(it.protos.contains(proto))
|
|
|
|
|
2021-02-12 08:53:52 +00:00
|
|
|
proc connectedness*(pm: PeerManager, peerId: PeerId): Connectedness =
|
2021-02-11 08:58:25 +00:00
|
|
|
# Return the connection state of the given, managed peer
|
|
|
|
# @TODO the PeerManager should keep and update local connectedness state for peers, redial on disconnect, etc.
|
|
|
|
# @TODO richer return than just bool, e.g. add enum "CanConnect", "CannotConnect", etc. based on recent connection attempts
|
|
|
|
|
|
|
|
let storedInfo = pm.peerStore.get(peerId)
|
|
|
|
|
|
|
|
if (storedInfo == StoredInfo()):
|
|
|
|
# Peer is not managed, therefore not connected
|
2021-02-12 08:53:52 +00:00
|
|
|
return NotConnected
|
2021-02-11 08:58:25 +00:00
|
|
|
else:
|
2021-02-12 08:53:52 +00:00
|
|
|
pm.peerStore.connectionBook.get(peerId)
|
2021-02-11 08:58:25 +00:00
|
|
|
|
|
|
|
proc hasPeer*(pm: PeerManager, peerInfo: PeerInfo, proto: string): bool =
|
2021-02-08 09:17:20 +00:00
|
|
|
# Returns `true` if peer is included in manager for the specified protocol
|
2021-02-04 10:32:58 +00:00
|
|
|
|
2021-02-08 09:17:20 +00:00
|
|
|
pm.peerStore.get(peerInfo.peerId).protos.contains(proto)
|
|
|
|
|
2021-04-16 09:57:45 +00:00
|
|
|
proc hasPeers*(pm: PeerManager, proto: string): bool =
|
|
|
|
# Returns `true` if manager has any peers for the specified protocol
|
|
|
|
pm.peers.anyIt(it.protos.contains(proto))
|
|
|
|
|
2021-07-14 17:58:46 +00:00
|
|
|
proc addPeer*(pm: PeerManager, peerInfo: PeerInfo, proto: string) =
|
2021-02-08 09:17:20 +00:00
|
|
|
# Adds peer to manager for the specified protocol
|
|
|
|
|
2021-04-16 09:57:45 +00:00
|
|
|
if peerInfo.peerId == pm.switch.peerInfo.peerId:
|
|
|
|
# Do not attempt to manage our unmanageable self
|
|
|
|
return
|
|
|
|
|
2021-02-08 09:17:20 +00:00
|
|
|
debug "Adding peer to manager", peerId = peerInfo.peerId, addr = peerInfo.addrs[0], proto = proto
|
2021-02-04 10:32:58 +00:00
|
|
|
|
|
|
|
# ...known addresses
|
|
|
|
for multiaddr in peerInfo.addrs:
|
|
|
|
pm.peerStore.addressBook.add(peerInfo.peerId, multiaddr)
|
|
|
|
|
|
|
|
# ...public key
|
|
|
|
var publicKey: PublicKey
|
|
|
|
discard peerInfo.peerId.extractPublicKey(publicKey)
|
|
|
|
|
|
|
|
pm.peerStore.keyBook.set(peerInfo.peerId, publicKey)
|
|
|
|
|
|
|
|
# ...associated protocols
|
|
|
|
pm.peerStore.protoBook.add(peerInfo.peerId, proto)
|
|
|
|
|
2021-03-26 08:49:51 +00:00
|
|
|
# Add peer to storage. Entry will subsequently be updated with connectedness information
|
|
|
|
if not pm.storage.isNil:
|
|
|
|
pm.storage.insertOrReplace(peerInfo.peerId, pm.peerStore.get(peerInfo.peerId), NotConnected)
|
|
|
|
|
2021-02-11 08:58:25 +00:00
|
|
|
proc selectPeer*(pm: PeerManager, proto: string): Option[PeerInfo] =
|
|
|
|
# Selects the best peer for a given protocol
|
|
|
|
let peers = pm.peers.filterIt(it.protos.contains(proto))
|
|
|
|
|
|
|
|
if peers.len >= 1:
|
|
|
|
# @TODO proper heuristic here that compares peer scores and selects "best" one. For now the first peer for the given protocol is returned
|
|
|
|
let peerStored = peers[0]
|
|
|
|
|
|
|
|
return some(peerStored.toPeerInfo())
|
|
|
|
else:
|
|
|
|
return none(PeerInfo)
|
2021-02-08 09:17:20 +00:00
|
|
|
|
2021-04-21 09:36:56 +00:00
|
|
|
proc reconnectPeers*(pm: PeerManager, proto: string, backoff: chronos.Duration = chronos.seconds(0)) {.async.} =
|
2021-03-26 08:49:51 +00:00
|
|
|
## Reconnect to peers registered for this protocol. This will update connectedness.
|
|
|
|
## Especially useful to resume connections from persistent storage after a restart.
|
|
|
|
|
|
|
|
debug "Reconnecting peers", proto=proto
|
|
|
|
|
|
|
|
for storedInfo in pm.peers(proto):
|
2021-04-21 09:36:56 +00:00
|
|
|
# Check if peer is reachable.
|
|
|
|
if pm.peerStore.connectionBook.get(storedInfo.peerId) == CannotConnect:
|
|
|
|
debug "Not reconnecting to unreachable peer", peerId=storedInfo.peerId
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Respect optional backoff period where applicable.
|
|
|
|
let
|
|
|
|
disconnectTime = Moment.init(pm.peerStore.disconnectBook.get(storedInfo.peerId), Second) # Convert
|
|
|
|
currentTime = Moment.init(getTime().toUnix, Second) # Current time comparable to persisted value
|
|
|
|
backoffTime = disconnectTime + backoff - currentTime # Consider time elapsed since last disconnect
|
|
|
|
|
|
|
|
trace "Respecting backoff", backoff=backoff, disconnectTime=disconnectTime, currentTime=currentTime, backoffTime=backoffTime
|
|
|
|
|
|
|
|
if backoffTime > ZeroDuration:
|
|
|
|
debug "Backing off before reconnect...", peerId=storedInfo.peerId, backoffTime=backoffTime
|
|
|
|
# We disconnected recently and still need to wait for a backoff period before connecting
|
|
|
|
await sleepAsync(backoffTime)
|
|
|
|
|
2021-03-26 08:49:51 +00:00
|
|
|
trace "Reconnecting to peer", peerId=storedInfo.peerId
|
|
|
|
discard await pm.dialPeer(storedInfo.peerId, toSeq(storedInfo.addrs), proto)
|
|
|
|
|
2021-02-08 09:17:20 +00:00
|
|
|
####################
|
|
|
|
# Dialer interface #
|
|
|
|
####################
|
|
|
|
|
|
|
|
proc dialPeer*(pm: PeerManager, peerInfo: PeerInfo, proto: string, dialTimeout = defaultDialTimeout): Future[Option[Connection]] {.async.} =
|
|
|
|
# Dial a given peer and add it to the list of known peers
|
|
|
|
# @TODO check peer validity and score before continuing. Limit number of peers to be managed.
|
|
|
|
|
|
|
|
# First add dialed peer info to peer store, if it does not exist yet...
|
|
|
|
if not pm.hasPeer(peerInfo, proto):
|
|
|
|
trace "Adding newly dialed peer to manager", peerId = peerInfo.peerId, addr = peerInfo.addrs[0], proto = proto
|
|
|
|
pm.addPeer(peerInfo, proto)
|
2021-04-21 09:36:56 +00:00
|
|
|
|
|
|
|
if peerInfo.peerId == pm.switch.peerInfo.peerId:
|
|
|
|
# Do not attempt to dial self
|
|
|
|
return none(Connection)
|
2021-02-08 09:17:20 +00:00
|
|
|
|
2021-03-26 08:49:51 +00:00
|
|
|
return await pm.dialPeer(peerInfo.peerId, peerInfo.addrs, proto, dialTimeout)
|