@cityofzion/neo-js
Version:
Running NEO blockchain full node with Node.js and MongoDB.
280 lines (239 loc) • 8.62 kB
text/typescript
import { EventEmitter } from 'events'
import { Logger, LoggerOptions } from 'node-log-it'
import { merge, filter, remove, meanBy, round } from 'lodash'
import { RpcDelegate } from '../delegates/rpc-delegate'
import C from '../common/constants'
import { NeoValidator } from '../validators/neo-validator'
import { AxiosRequestConfig } from 'axios'
const MODULE_NAME = 'Node'
const DEFAULT_ID = 0
const DEFAULT_OPTIONS: NodeOptions = {
toLogReliability: false,
truncateRequestLogIntervalMs: 30 * 1000,
requestLogTtl: 5 * 60 * 1000, // In milliseconds
timeout: 30000,
loggerOptions: {},
}
export interface NodeMeta {
isActive: boolean | undefined
pendingRequests: number | undefined
latency: number | undefined
blockHeight: number | undefined
lastSeenTimestamp: number | undefined
userAgent: string | undefined
endpoint: string
}
export interface NodeOptions {
toLogReliability?: boolean
truncateRequestLogIntervalMs?: number
requestLogTtl?: number
timeout?: number
loggerOptions?: LoggerOptions
}
export class Node extends EventEmitter {
isActive: boolean | undefined
pendingRequests: number | undefined
latency: number | undefined // In milliseconds
blockHeight: number | undefined
lastPingTimestamp: number | undefined // Latest timestamp that node perform benchmark on
lastSeenTimestamp: number | undefined // Latest timestamp that node detected to be activated via benchmark
userAgent: string | undefined
endpoint: string
isBenchmarking = false
private options: NodeOptions
private logger: Logger
private requestLogs: object[] = []
private truncateRequestLogIntervalId?: NodeJS.Timer
constructor(endpoint: string, options: NodeOptions = {}) {
super()
// Associate required properties
this.endpoint = endpoint
// Associate optional properties
this.options = merge({}, DEFAULT_OPTIONS, options)
this.validateOptionalParameters()
// Bootstrapping
this.logger = new Logger(MODULE_NAME, this.options.loggerOptions)
if (this.options.toLogReliability) {
this.truncateRequestLogIntervalId = setInterval(() => this.truncateRequestLog(), this.options.truncateRequestLogIntervalMs!)
}
// Event handlers
this.on('query:init', this.queryInitHandler.bind(this))
this.on('query:complete', this.queryCompleteHandler.bind(this))
this.logger.debug('constructor completes.')
}
async getBlock(height: number, isVerbose: boolean = true): Promise<object> {
this.logger.debug('getBlock triggered.')
NeoValidator.validateHeight(height)
const verboseKey: number = isVerbose ? 1 : 0
return await this.query(C.rpc.getblock, [height, verboseKey])
}
async getBlockCount(): Promise<object> {
this.logger.debug('getBlockCount triggered.')
return await this.query(C.rpc.getblockcount)
}
async getVersion(): Promise<object> {
this.logger.debug('getVersion triggered.')
return await this.query(C.rpc.getversion)
}
async getTransaction(transactionId: string, isVerbose: boolean = true): Promise<object> {
this.logger.debug('transactionId triggered.')
const verboseKey: number = isVerbose ? 1 : 0
return await this.query(C.rpc.getrawtransaction, [transactionId, verboseKey])
}
getNodeMeta(): NodeMeta {
return {
isActive: this.isActive,
pendingRequests: this.pendingRequests,
latency: this.latency,
blockHeight: this.blockHeight,
lastSeenTimestamp: this.lastSeenTimestamp,
userAgent: this.userAgent,
endpoint: this.endpoint,
}
}
/**
* A float number between 0 an 1
*/
getNodeReliability(): number | undefined {
const requestCount = this.requestLogs.length
if (requestCount === 0) {
return undefined
}
const successCount = filter(this.requestLogs, (logObj: any) => logObj.isSuccess === true).length
return successCount / requestCount
}
getShapedLatency(): number | undefined {
this.logger.debug('getShapedLatency triggered.')
if (this.requestLogs.length === 0) {
return undefined
}
const logPool = filter(this.requestLogs, (logObj: any) => logObj.isSuccess === true && logObj.latency !== undefined)
if (logPool.length === 0) {
return undefined
}
const averageLatency = round(meanBy(logPool, (logObj: any) => logObj.latency), 0)
return averageLatency
}
close() {
this.logger.debug('close triggered.')
if (this.truncateRequestLogIntervalId) {
clearInterval(this.truncateRequestLogIntervalId)
}
}
private queryInitHandler(payload: object) {
this.logger.debug('queryInitHandler triggered.')
this.startBenchmark(payload)
}
private queryCompleteHandler(payload: any) {
this.logger.debug('queryCompleteHandler triggered.')
this.stopBenchmark(payload)
}
private validateOptionalParameters() {
// TODO
}
private startBenchmark(payload: any) {
this.logger.debug('startBenchmark triggered.')
this.increasePendingRequest()
// Perform latency benchmark when it's a getBlockCount() request
if (payload.method === C.rpc.getblockcount) {
if (this.isBenchmarking) {
this.logger.debug('An benchmarking schedule is already in place. Skipping... endpoint:', this.endpoint)
} else {
this.isBenchmarking = true
}
}
}
private stopBenchmark(payload: any) {
this.logger.debug('stopBenchmark triggered.')
this.decreasePendingRequest()
this.lastPingTimestamp = Date.now()
// Store latest active state base on existence of error
if (!payload.isSuccess) {
this.isActive = false
} else {
this.isActive = true
this.lastSeenTimestamp = Date.now()
}
// Store block height value if provided
if (payload.blockHeight) {
this.blockHeight = payload.blockHeight
}
// Store user agent value if provided
if (payload.userAgent) {
this.userAgent = payload.userAgent
}
// Perform benchmark when it's a getBlockCount() request
if (payload.method === C.rpc.getblockcount) {
if (!this.isBenchmarking) {
this.logger.debug('There are no running benchmarking schedule in place. Skipping... endpoint:', this.endpoint)
} else {
this.isBenchmarking = false
// Store latency value if provided
if (payload.latency) {
this.latency = payload.latency
}
// Reliability logging
if (this.options.toLogReliability) {
if (!payload.isSuccess) {
this.requestLogs.push({
timestamp: Date.now(),
isSuccess: payload.isSuccess,
})
} else {
this.requestLogs.push({
timestamp: Date.now(),
isSuccess: payload.isSuccess,
latency: this.latency,
})
}
}
}
}
}
private truncateRequestLog() {
this.logger.debug('truncateRequestLog triggered.')
const cutOffTimestamp = Date.now() - this.options.requestLogTtl!
this.requestLogs = remove(this.requestLogs, (logObj: any) => logObj.timestamp > cutOffTimestamp)
}
private async query(method: string, params: any[] = [], id: number = DEFAULT_ID): Promise<object> {
this.logger.debug('query triggered. method:', method)
this.emit('query:init', { method, params, id })
const requestConfig = this.getRequestConfig()
const t0 = Date.now()
try {
const res: any = await RpcDelegate.query(this.endpoint, method, params, id, requestConfig)
const latency = Date.now() - t0
const result = res.result
const blockHeight = method === C.rpc.getblockcount ? result : undefined
const userAgent = method === C.rpc.getversion ? result.useragent : undefined
this.emit('query:complete', { isSuccess: true, method, latency, blockHeight, userAgent })
return result
} catch (err) {
this.emit('query:complete', { isSuccess: false, method, error: err })
throw err
}
}
private increasePendingRequest() {
this.logger.debug('increasePendingRequest triggered.')
if (this.pendingRequests) {
this.pendingRequests += 1
} else {
this.pendingRequests = 1
}
}
private decreasePendingRequest() {
this.logger.debug('decreasePendingRequest triggered.')
if (this.pendingRequests) {
this.pendingRequests -= 1
} else {
this.pendingRequests = 0
}
}
private getRequestConfig(): AxiosRequestConfig {
const config: AxiosRequestConfig = {}
if (this.options.timeout) {
config.timeout = this.options.timeout
}
return config
}
}