@cityofzion/neo-js
Version:
Running NEO blockchain full node with Node.js and MongoDB.
270 lines (225 loc) • 8.53 kB
text/typescript
import { EventEmitter } from 'events'
import { Logger, LoggerOptions } from 'node-log-it'
import { merge, filter, minBy, maxBy, random, find } from 'lodash'
import { Node } from './node'
const MODULE_NAME = 'Mesh'
const DEFAULT_OPTIONS: MeshOptions = {
startBenchmarkOnInit: true,
toFetchUserAgent: true,
benchmarkIntervalMs: 2000,
fetchMissingUserAgentIntervalMs: 5000,
refreshUserAgentIntervalMs: 5 * 60 * 1000,
minActiveNodesRequired: 2,
pendingRequestsThreshold: 5,
loggerOptions: {},
}
export interface MeshOptions {
startBenchmarkOnInit?: boolean
toFetchUserAgent?: boolean
benchmarkIntervalMs?: number
fetchMissingUserAgentIntervalMs?: number
refreshUserAgentIntervalMs?: number
minActiveNodesRequired?: number
pendingRequestsThreshold?: number
loggerOptions?: LoggerOptions
}
export class Mesh extends EventEmitter {
nodes: Node[] // Ensure there's at least 1 item in the array
private _isReady = false
private benchmarkIntervalId?: NodeJS.Timer
private fetchMissingUserAgentIntervalId?: NodeJS.Timer
private refreshUserAgentIntervalId?: NodeJS.Timer
private options: MeshOptions
private logger: Logger
constructor(nodes: Node[], options: MeshOptions = {}) {
super()
// Associate required properties
this.nodes = nodes
if (this.nodes.length === 0) {
throw new Error('Mesh must have 1 or more nodes.')
}
// Associate optional properties
this.options = merge({}, DEFAULT_OPTIONS, options)
this.validateOptionalParameters()
// Bootstrapping
this.logger = new Logger(MODULE_NAME, this.options.loggerOptions)
if (this.options.startBenchmarkOnInit) {
this.startBenchmark()
}
this.logger.debug('constructor completes.')
}
isReady(): boolean {
return this._isReady
}
startBenchmark() {
this.logger.debug('startBenchmark triggered.')
// Go through and ping all unknown nodes
const unknownNodes = filter(this.nodes, (n: Node) => n.isActive === undefined)
this.logger.debug('unknownNodes.length:', unknownNodes.length)
unknownNodes.forEach((n: Node) => {
n.getBlockCount()
.then(() => {
this.checkMeshReady()
})
.catch((err: any) => {
this.logger.info('node.getBlockCount() failed, but to continue. Endpoint:', n.endpoint, 'Message:', err.message)
})
})
// Fetch node version
if (this.options.toFetchUserAgent) {
unknownNodes.forEach((n: Node) => {
n.getVersion().catch((err: any) => {
this.logger.info('node.getVersion() failed, but to continue. Endpoint:', n.endpoint, 'Message:', err.message)
})
})
this.fetchMissingUserAgentIntervalId = setInterval(() => this.performFetchMissingUserAgent(), this.options.fetchMissingUserAgentIntervalMs!)
this.refreshUserAgentIntervalId = setInterval(() => this.performRefreshUserAgent(), this.options.refreshUserAgentIntervalMs!)
}
// Start timer
this.benchmarkIntervalId = setInterval(() => this.performBenchmark(), this.options.benchmarkIntervalMs!)
}
stopBenchmark() {
this.logger.debug('stopBenchmark triggered.')
if (this.benchmarkIntervalId) {
clearInterval(this.benchmarkIntervalId)
}
if (this.fetchMissingUserAgentIntervalId) {
clearInterval(this.fetchMissingUserAgentIntervalId)
}
if (this.refreshUserAgentIntervalId) {
clearInterval(this.refreshUserAgentIntervalId)
}
}
close() {
this.logger.debug('close triggered.')
this.stopBenchmark()
this.nodes.forEach((n: Node) => {
n.close()
})
}
getFastestNode(activeOnly = true): Node | undefined {
this.logger.debug('getFastestNode triggered.')
let nodePool = activeOnly ? this.listActiveNodes() : this.nodes
if (nodePool.length === 0) {
return undefined
}
nodePool = filter(nodePool, (n: Node) => n.latency !== undefined)
if (nodePool.length === 0) {
return undefined
}
return minBy(nodePool, 'latency')
}
getHighestNode(activeOnly = true): Node | undefined {
this.logger.debug('getHighestNode triggered.')
let nodePool = activeOnly ? this.listActiveNodes() : this.nodes
if (nodePool.length === 0) {
return undefined
}
nodePool = filter(nodePool, (n: Node) => n.blockHeight !== undefined)
if (nodePool.length === 0) {
return undefined
}
return maxBy(nodePool, 'blockHeight')
}
/**
* @param activeOnly Toggle to only pick node that is determined to be active.
*/
getRandomNode(activeOnly = true): Node | undefined {
this.logger.debug('getRandomNode triggered.')
const nodePool = activeOnly ? this.listActiveNodes() : this.nodes
if (nodePool.length === 0) {
return undefined
}
const randomIndex = random(0, nodePool.length - 1)
return nodePool[randomIndex]
}
/**
* An optimal node is defined as a fast node that has not exceed a pending threshold.
*/
getOptimalNode(height: number, activeOnly = true): Node | undefined {
this.logger.debug('getOptimalNode triggered.')
const nodePool = activeOnly ? this.listActiveNodes() : this.nodes
if (nodePool.length === 0) {
return undefined
}
// Filter nodes that has the required height
const qualifyHeightNodes = filter(this.nodes, (n: Node) => n.blockHeight !== undefined && n.blockHeight >= height)
if (qualifyHeightNodes.length === 0) {
return undefined
}
// Filter nodes that exceed pending threshold
const qualifyPendingNodes = filter(qualifyHeightNodes, (n: Node) => !n.pendingRequests || n.pendingRequests <= this.options.pendingRequestsThreshold!)
if (qualifyPendingNodes.length === 0) {
// If all qualify nodes exceeded pending threshold, then just pick a random one from qualifyHeightNodes
const randomIndex = random(0, qualifyHeightNodes.length - 1)
return qualifyHeightNodes[randomIndex]
}
// Pick the fastest node from qualifyPendingNodes
return minBy(qualifyPendingNodes, 'latency')
}
private validateOptionalParameters() {
// TODO
}
private performBenchmark() {
this.logger.debug('performBenchmark triggered.')
const node = this.getNodeToBenchmark()
if (node) {
node.getBlockCount().catch((err: any) => {
this.logger.info('node.getBlockCount error in performBenchmark(). Endpoint:', node.endpoint, 'Message:', err.message)
})
} else {
this.logger.info('Unable to find a suitable node to perform benchmark.')
}
}
private getNodeToBenchmark(): Node | undefined {
this.logger.debug('getNodeToBenchmark triggered.')
// Find nodes that's not currently running benchmarks
const nodePool = filter(this.nodes, (n: Node) => !n.isBenchmarking)
if (nodePool.length === 0) {
return undefined
}
// Attempt to find a node that hasn't been benchmarked at all
const unknownNode = find(nodePool, (n: Node) => n.lastPingTimestamp === undefined)
if (unknownNode) {
return unknownNode
}
// Attempt to find a node that last benchmark longest ago
const targetNode = minBy(nodePool, (n: Node) => n.lastPingTimestamp)
return targetNode
}
private performFetchMissingUserAgent() {
this.logger.debug('performBenchmark triggered.')
const nodePool = filter(this.nodes, (n: Node) => n.userAgent === undefined)
nodePool.forEach((n: Node) => {
n.getVersion().catch((err: any) => {
this.logger.info('node.getVersion() failed, but to continue. Endpoint:', n.endpoint, 'Message:', err.message)
})
})
}
private performRefreshUserAgent() {
this.logger.debug('performRefreshUserAgent triggered.')
this.nodes.forEach((n: Node) => {
n.getVersion().catch((err: any) => {
this.logger.info('node.getVersion() failed, but to continue. Endpoint:', n.endpoint, 'Message:', err.message)
})
})
}
private checkMeshReady() {
this.logger.debug('checkMeshReady triggered.')
const activeNodes = this.listActiveNodes()
if (!this.options.minActiveNodesRequired || activeNodes.length >= this.options.minActiveNodesRequired) {
if (!this._isReady) {
// First signal that mesh is considered as 'ready' state
this.setReady()
this.logger.debug('mesh is considered to be now ready.')
}
}
}
private setReady() {
this._isReady = true
this.emit('ready')
}
private listActiveNodes(): Node[] {
return filter(this.nodes, { isActive: true })
}
}