2019-08-16 17:30:03 +02:00
|
|
|
'use strict'
|
|
|
|
|
2020-02-14 13:42:23 +00:00
|
|
|
const errcode = require('err-code')
|
2019-12-12 10:19:57 +01:00
|
|
|
const mergeOptions = require('merge-options')
|
2020-04-24 16:10:40 +01:00
|
|
|
const LatencyMonitor = require('./latency-monitor')
|
2019-08-16 17:30:03 +02:00
|
|
|
const debug = require('debug')('libp2p:connection-manager')
|
2019-12-12 10:19:57 +01:00
|
|
|
const retimer = require('retimer')
|
2019-08-16 17:30:03 +02:00
|
|
|
|
2020-04-18 17:06:56 +02:00
|
|
|
const { EventEmitter } = require('events')
|
|
|
|
|
|
|
|
const PeerId = require('peer-id')
|
|
|
|
|
2020-02-14 13:42:23 +00:00
|
|
|
const {
|
|
|
|
ERR_INVALID_PARAMETERS
|
|
|
|
} = require('../errors')
|
|
|
|
|
2019-08-16 17:30:03 +02:00
|
|
|
const defaultOptions = {
|
2019-12-12 10:19:57 +01:00
|
|
|
maxConnections: Infinity,
|
|
|
|
minConnections: 0,
|
2019-08-16 17:30:03 +02:00
|
|
|
maxData: Infinity,
|
|
|
|
maxSentData: Infinity,
|
|
|
|
maxReceivedData: Infinity,
|
|
|
|
maxEventLoopDelay: Infinity,
|
|
|
|
pollInterval: 2000,
|
|
|
|
movingAverageInterval: 60000,
|
|
|
|
defaultPeerValue: 1
|
|
|
|
}
|
|
|
|
|
2020-04-18 17:06:56 +02:00
|
|
|
/**
|
|
|
|
* Responsible for managing known connections.
|
|
|
|
* @fires ConnectionManager#peer:connect Emitted when a new peer is connected.
|
|
|
|
* @fires ConnectionManager#peer:disconnect Emitted when a peer is disconnected.
|
|
|
|
*/
|
|
|
|
class ConnectionManager extends EventEmitter {
|
2019-12-12 10:19:57 +01:00
|
|
|
/**
|
|
|
|
* @constructor
|
|
|
|
* @param {Libp2p} libp2p
|
|
|
|
* @param {object} options
|
|
|
|
* @param {Number} options.maxConnections The maximum number of connections allowed. Default=Infinity
|
|
|
|
* @param {Number} options.minConnections The minimum number of connections to avoid pruning. Default=0
|
|
|
|
* @param {Number} options.maxData The max data (in and out), per average interval to allow. Default=Infinity
|
|
|
|
* @param {Number} options.maxSentData The max outgoing data, per average interval to allow. Default=Infinity
|
|
|
|
* @param {Number} options.maxReceivedData The max incoming data, per average interval to allow.. Default=Infinity
|
|
|
|
* @param {Number} options.maxEventLoopDelay The upper limit the event loop can take to run. Default=Infinity
|
|
|
|
* @param {Number} options.pollInterval How often, in milliseconds, metrics and latency should be checked. Default=2000
|
|
|
|
* @param {Number} options.movingAverageInterval How often, in milliseconds, to compute averages. Default=60000
|
|
|
|
* @param {Number} options.defaultPeerValue The value of the peer. Default=1
|
|
|
|
*/
|
2019-08-16 17:30:03 +02:00
|
|
|
constructor (libp2p, options) {
|
2020-04-18 17:06:56 +02:00
|
|
|
super()
|
|
|
|
|
2019-08-16 17:30:03 +02:00
|
|
|
this._libp2p = libp2p
|
2020-04-14 14:05:30 +02:00
|
|
|
this._peerId = libp2p.peerId.toB58String()
|
2020-04-18 17:06:56 +02:00
|
|
|
|
fix: conn mngr min/max connection values (#528)
Fixes the case when options are passed with `maxConnections` and/or `minConnections` set to `undefined`:
```console
{
defaultOptions: {
maxConnections: Infinity,
minConnections: 0,
maxData: Infinity,
maxSentData: Infinity,
maxReceivedData: Infinity,
maxEventLoopDelay: Infinity,
pollInterval: 2000,
movingAverageInterval: 60000,
defaultPeerValue: 1
},
options: {
minPeers: 25,
maxConnections: undefined,
minConnections: undefined
}
}
{ maxConnections: undefined, minConnections: undefined }
1) "before all" hook in "custom config"
(node:67176) UnhandledPromiseRejectionWarning: AssertionError [ERR_ASSERTION]: Connection Manager maxConnections must be greater than minConnections
at new ConnectionManager (node_modules/libp2p/src/connection-manager/index.js:43:5)
at new Libp2p (node_modules/libp2p/src/index.js:92:30)
at Object.module.exports [as libp2p] (src/core/components/libp2p.js:27:10)
at Proxy.start (src/core/components/start.js:48:31)
at async Daemon.start (src/cli/daemon.js:63:31)
at async startHttpAPI (test/http-api/routes.js:29:5)
at async Context.<anonymous> (test/http-api/routes.js:48:7)
```
2020-01-06 14:03:07 +00:00
|
|
|
this._options = mergeOptions.call({ ignoreUndefined: true }, defaultOptions, options)
|
2020-02-14 13:42:23 +00:00
|
|
|
if (this._options.maxConnections < this._options.minConnections) {
|
|
|
|
throw errcode(new Error('Connection Manager maxConnections must be greater than minConnections'), ERR_INVALID_PARAMETERS)
|
|
|
|
}
|
2019-08-16 17:30:03 +02:00
|
|
|
|
|
|
|
debug('options: %j', this._options)
|
|
|
|
|
2020-04-18 17:06:56 +02:00
|
|
|
this._libp2p = libp2p
|
2019-08-16 17:30:03 +02:00
|
|
|
|
2020-04-18 17:06:56 +02:00
|
|
|
/**
|
|
|
|
* Map of peer identifiers to their peer value for pruning connections.
|
|
|
|
* @type {Map<string, number>}
|
|
|
|
*/
|
2019-08-16 17:30:03 +02:00
|
|
|
this._peerValues = new Map()
|
2020-04-18 17:06:56 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Map of connections per peer
|
|
|
|
* @type {Map<string, Array<conn>>}
|
|
|
|
*/
|
|
|
|
this.connections = new Map()
|
|
|
|
|
2019-12-12 10:19:57 +01:00
|
|
|
this._timer = null
|
|
|
|
this._checkMetrics = this._checkMetrics.bind(this)
|
2019-08-16 17:30:03 +02:00
|
|
|
}
|
|
|
|
|
2020-04-18 17:06:56 +02:00
|
|
|
/**
|
|
|
|
* Get current number of open connections.
|
|
|
|
*/
|
|
|
|
get size () {
|
|
|
|
return Array.from(this.connections.values())
|
|
|
|
.reduce((accumulator, value) => accumulator + value.length, 0)
|
|
|
|
}
|
|
|
|
|
2019-12-12 10:19:57 +01:00
|
|
|
/**
|
|
|
|
* Starts the Connection Manager. If Metrics are not enabled on libp2p
|
|
|
|
* only event loop and connection limits will be monitored.
|
|
|
|
*/
|
2019-08-16 17:30:03 +02:00
|
|
|
start () {
|
2020-04-18 17:06:56 +02:00
|
|
|
if (this._libp2p.metrics) {
|
2019-12-12 10:19:57 +01:00
|
|
|
this._timer = this._timer || retimer(this._checkMetrics, this._options.pollInterval)
|
|
|
|
}
|
|
|
|
|
2019-08-16 17:30:03 +02:00
|
|
|
// latency monitor
|
|
|
|
this._latencyMonitor = new LatencyMonitor({
|
2019-12-12 10:19:57 +01:00
|
|
|
latencyCheckIntervalMs: this._options.pollInterval,
|
2019-08-16 17:30:03 +02:00
|
|
|
dataEmitIntervalMs: this._options.pollInterval
|
|
|
|
})
|
|
|
|
this._onLatencyMeasure = this._onLatencyMeasure.bind(this)
|
|
|
|
this._latencyMonitor.on('data', this._onLatencyMeasure)
|
2019-12-12 10:19:57 +01:00
|
|
|
debug('started')
|
2019-08-16 17:30:03 +02:00
|
|
|
}
|
|
|
|
|
2019-12-12 10:19:57 +01:00
|
|
|
/**
|
|
|
|
* Stops the Connection Manager
|
2020-04-18 17:06:56 +02:00
|
|
|
* @async
|
2019-12-12 10:19:57 +01:00
|
|
|
*/
|
2020-04-18 17:06:56 +02:00
|
|
|
async stop () {
|
2019-12-12 10:19:57 +01:00
|
|
|
this._timer && this._timer.clear()
|
|
|
|
this._latencyMonitor && this._latencyMonitor.removeListener('data', this._onLatencyMeasure)
|
2020-04-18 17:06:56 +02:00
|
|
|
|
|
|
|
await this._close()
|
2019-12-12 10:19:57 +01:00
|
|
|
debug('stopped')
|
2019-08-16 17:30:03 +02:00
|
|
|
}
|
|
|
|
|
2020-04-18 17:06:56 +02:00
|
|
|
/**
|
|
|
|
* Cleans up the connections
|
|
|
|
* @async
|
|
|
|
*/
|
|
|
|
async _close () {
|
|
|
|
// Close all connections we're tracking
|
|
|
|
const tasks = []
|
|
|
|
for (const connectionList of this.connections.values()) {
|
|
|
|
for (const connection of connectionList) {
|
|
|
|
tasks.push(connection.close())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await tasks
|
|
|
|
this.connections.clear()
|
|
|
|
}
|
|
|
|
|
2019-12-12 10:19:57 +01:00
|
|
|
/**
|
|
|
|
* Sets the value of the given peer. Peers with lower values
|
|
|
|
* will be disconnected first.
|
|
|
|
* @param {PeerId} peerId
|
|
|
|
* @param {number} value A number between 0 and 1
|
|
|
|
*/
|
2019-08-16 17:30:03 +02:00
|
|
|
setPeerValue (peerId, value) {
|
|
|
|
if (value < 0 || value > 1) {
|
|
|
|
throw new Error('value should be a number between 0 and 1')
|
|
|
|
}
|
2020-01-22 11:47:30 +01:00
|
|
|
if (peerId.toB58String) {
|
|
|
|
peerId = peerId.toB58String()
|
2019-08-16 17:30:03 +02:00
|
|
|
}
|
|
|
|
this._peerValues.set(peerId, value)
|
|
|
|
}
|
|
|
|
|
2019-12-12 10:19:57 +01:00
|
|
|
/**
|
|
|
|
* Checks the libp2p metrics to determine if any values have exceeded
|
|
|
|
* the configured maximums.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_checkMetrics () {
|
2020-04-18 17:06:56 +02:00
|
|
|
const movingAverages = this._libp2p.metrics.global.movingAverages
|
2019-12-12 10:19:57 +01:00
|
|
|
const received = movingAverages.dataReceived[this._options.movingAverageInterval].movingAverage()
|
2019-08-16 17:30:03 +02:00
|
|
|
this._checkLimit('maxReceivedData', received)
|
2019-12-12 10:19:57 +01:00
|
|
|
const sent = movingAverages.dataSent[this._options.movingAverageInterval].movingAverage()
|
2019-08-16 17:30:03 +02:00
|
|
|
this._checkLimit('maxSentData', sent)
|
|
|
|
const total = received + sent
|
|
|
|
this._checkLimit('maxData', total)
|
2019-12-12 10:19:57 +01:00
|
|
|
debug('metrics update', total)
|
|
|
|
this._timer.reschedule(this._options.pollInterval)
|
2019-08-16 17:30:03 +02:00
|
|
|
}
|
|
|
|
|
2019-12-12 10:19:57 +01:00
|
|
|
/**
|
|
|
|
* Tracks the incoming connection and check the connection limit
|
|
|
|
* @param {Connection} connection
|
|
|
|
*/
|
|
|
|
onConnect (connection) {
|
2020-05-07 10:46:00 +02:00
|
|
|
const peerId = connection.remotePeer
|
|
|
|
const peerIdStr = peerId.toB58String()
|
|
|
|
const storedConn = this.connections.get(peerIdStr)
|
2020-04-18 17:06:56 +02:00
|
|
|
|
2020-05-27 18:08:53 +02:00
|
|
|
this.emit('peer:connect', connection)
|
2020-04-18 17:06:56 +02:00
|
|
|
if (storedConn) {
|
|
|
|
storedConn.push(connection)
|
|
|
|
} else {
|
2020-05-07 10:46:00 +02:00
|
|
|
this.connections.set(peerIdStr, [connection])
|
2020-04-18 17:06:56 +02:00
|
|
|
}
|
|
|
|
|
2020-05-07 10:46:00 +02:00
|
|
|
this._libp2p.peerStore.keyBook.set(peerId, peerId.pubKey)
|
|
|
|
|
|
|
|
if (!this._peerValues.has(peerIdStr)) {
|
|
|
|
this._peerValues.set(peerIdStr, this._options.defaultPeerValue)
|
2019-08-16 17:30:03 +02:00
|
|
|
}
|
2020-04-18 17:06:56 +02:00
|
|
|
|
|
|
|
this._checkLimit('maxConnections', this.size)
|
2019-12-12 10:19:57 +01:00
|
|
|
}
|
2019-08-16 17:30:03 +02:00
|
|
|
|
2019-12-12 10:19:57 +01:00
|
|
|
/**
|
|
|
|
* Removes the connection from tracking
|
|
|
|
* @param {Connection} connection
|
|
|
|
*/
|
|
|
|
onDisconnect (connection) {
|
2020-04-18 17:06:56 +02:00
|
|
|
const peerId = connection.remotePeer.toB58String()
|
|
|
|
let storedConn = this.connections.get(peerId)
|
|
|
|
|
|
|
|
if (storedConn && storedConn.length > 1) {
|
|
|
|
storedConn = storedConn.filter((conn) => conn.id !== connection.id)
|
|
|
|
this.connections.set(peerId, storedConn)
|
|
|
|
} else if (storedConn) {
|
|
|
|
this.connections.delete(peerId)
|
|
|
|
this._peerValues.delete(connection.remotePeer.toB58String())
|
|
|
|
this.emit('peer:disconnect', connection)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a connection with a peer.
|
|
|
|
* @param {PeerId} peerId
|
|
|
|
* @returns {Connection}
|
|
|
|
*/
|
|
|
|
get (peerId) {
|
|
|
|
if (!PeerId.isPeerId(peerId)) {
|
|
|
|
throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS)
|
|
|
|
}
|
|
|
|
|
|
|
|
const id = peerId.toB58String()
|
|
|
|
const connections = this.connections.get(id)
|
|
|
|
|
|
|
|
// Return the first, open connection
|
|
|
|
if (connections) {
|
|
|
|
return connections.find(connection => connection.stat.status === 'open')
|
|
|
|
}
|
|
|
|
return null
|
2019-08-16 17:30:03 +02:00
|
|
|
}
|
|
|
|
|
2019-12-12 10:19:57 +01:00
|
|
|
/**
|
|
|
|
* If the event loop is slow, maybe close a connection
|
|
|
|
* @private
|
|
|
|
* @param {*} summary The LatencyMonitor summary
|
|
|
|
*/
|
2019-08-16 17:30:03 +02:00
|
|
|
_onLatencyMeasure (summary) {
|
|
|
|
this._checkLimit('maxEventLoopDelay', summary.avgMs)
|
|
|
|
}
|
|
|
|
|
2019-12-12 10:19:57 +01:00
|
|
|
/**
|
|
|
|
* If the `value` of `name` has exceeded its limit, maybe close a connection
|
|
|
|
* @private
|
|
|
|
* @param {string} name The name of the field to check limits for
|
|
|
|
* @param {number} value The current value of the field
|
|
|
|
*/
|
2019-08-16 17:30:03 +02:00
|
|
|
_checkLimit (name, value) {
|
|
|
|
const limit = this._options[name]
|
|
|
|
debug('checking limit of %s. current value: %d of %d', name, value, limit)
|
|
|
|
if (value > limit) {
|
|
|
|
debug('%s: limit exceeded: %s, %d', this._peerId, name, value)
|
|
|
|
this._maybeDisconnectOne()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-12 10:19:57 +01:00
|
|
|
/**
|
|
|
|
* If we have more connections than our maximum, close a connection
|
|
|
|
* to the lowest valued peer.
|
|
|
|
* @private
|
|
|
|
*/
|
2019-08-16 17:30:03 +02:00
|
|
|
_maybeDisconnectOne () {
|
2020-04-18 17:06:56 +02:00
|
|
|
if (this._options.minConnections < this.connections.size) {
|
2019-08-16 17:30:03 +02:00
|
|
|
const peerValues = Array.from(this._peerValues).sort(byPeerValue)
|
|
|
|
debug('%s: sorted peer values: %j', this._peerId, peerValues)
|
|
|
|
const disconnectPeer = peerValues[0]
|
|
|
|
if (disconnectPeer) {
|
|
|
|
const peerId = disconnectPeer[0]
|
|
|
|
debug('%s: lowest value peer is %s', this._peerId, peerId)
|
2019-12-12 10:19:57 +01:00
|
|
|
debug('%s: closing a connection to %j', this._peerId, peerId)
|
2020-04-18 17:06:56 +02:00
|
|
|
for (const connections of this.connections.values()) {
|
|
|
|
if (connections[0].remotePeer.toB58String() === peerId) {
|
|
|
|
connections[0].close()
|
2019-12-12 10:19:57 +01:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2019-08-16 17:30:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = ConnectionManager
|
|
|
|
|
|
|
|
function byPeerValue (peerValueEntryA, peerValueEntryB) {
|
|
|
|
return peerValueEntryA[1] - peerValueEntryB[1]
|
|
|
|
}
|