diff --git a/package-lock.json b/package-lock.json index 53e0e21..c069b6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,12 @@ { "name": "symbol-statistics-service", - "version": "1.1.3", + "version": "1.1.4", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "1.1.3", + "name": "symbol-statistics-service", + "version": "1.1.4", "dependencies": { "@types/cors": "^2.8.6", "@types/express": "^4.17.6", @@ -15,6 +16,7 @@ "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", + "humanize-duration": "^3.27.1", "module-alias": "^2.2.2", "mongoose": "^5.9.16", "symbol-sdk": "^1.0.3", @@ -25,6 +27,7 @@ }, "devDependencies": { "@openapitools/openapi-generator-cli": "^2.4.15", + "@types/humanize-duration": "^3.27.0", "@types/mongoose": "^5.7.14", "@types/node": "^14.14.10", "@types/winston": "^2.4.4", @@ -518,6 +521,12 @@ "@types/range-parser": "*" } }, + "node_modules/@types/humanize-duration": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.0.tgz", + "integrity": "sha512-ivv1EIdXz20vHPB9xftPvogUEjGLSSlgz2fipK2yyHQZvZeIgUQBZc23Kcuxa4zGbiUcRtr36Sw96CF+TO30Fw==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", @@ -2960,6 +2969,11 @@ "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", "dev": true }, + "node_modules/humanize-duration": { + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.1.tgz", + "integrity": "sha512-jCVkMl+EaM80rrMrAPl96SGG4NRac53UyI1o/yAzebDntEY6K6/Fj2HOjdPg8omTqIe5Y0wPBai2q5xXrIbarA==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6683,6 +6697,12 @@ "@types/range-parser": "*" } }, + "@types/humanize-duration": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.0.tgz", + "integrity": "sha512-ivv1EIdXz20vHPB9xftPvogUEjGLSSlgz2fipK2yyHQZvZeIgUQBZc23Kcuxa4zGbiUcRtr36Sw96CF+TO30Fw==", + "dev": true + }, "@types/json-schema": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", @@ -8534,6 +8554,11 @@ "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", "dev": true }, + "humanize-duration": { + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.1.tgz", + "integrity": "sha512-jCVkMl+EaM80rrMrAPl96SGG4NRac53UyI1o/yAzebDntEY6K6/Fj2HOjdPg8omTqIe5Y0wPBai2q5xXrIbarA==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/package.json b/package.json index aae1727..e28de5c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "devDependencies": { "@openapitools/openapi-generator-cli": "^2.4.15", + "@types/humanize-duration": "^3.27.0", "@types/mongoose": "^5.7.14", "@types/node": "^14.14.10", "@types/winston": "^2.4.4", @@ -49,6 +50,7 @@ "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", + "humanize-duration": "^3.27.1", "module-alias": "^2.2.2", "mongoose": "^5.9.16", "symbol-sdk": "^1.0.3", diff --git a/src/config/config.json b/src/config/config.json index f9dc274..be448ae 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -17,6 +17,7 @@ "PEER_NODE_PORT": 7900, "REQUEST_TIMEOUT": 5000, "NUMBER_OF_NODE_REQUEST_CHUNK": 10, + "NODE_PEERS_REQUEST_CHUNK_SIZE": 50, "PREFERRED_NODES": ["*.symboldev.network"], "MIN_PARTNER_NODE_VERSION": 16777728 } diff --git a/src/config/index.ts b/src/config/index.ts index 4724bab..233456e 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -18,6 +18,7 @@ interface Symbol { interface Monitor { NODE_MONITOR_SCHEDULE_INTERVAL: number; NUMBER_OF_NODE_REQUEST_CHUNK: number; + NODE_PEERS_REQUEST_CHUNK_SIZE: number; CHAIN_HEIGHT_MONITOR_SCHEDULE_INTERVAL: number; GEOLOCATION_MONITOR_SCHEDULE_INTERVAL: number; API_NODE_PORT: number; @@ -49,6 +50,7 @@ export const symbol: Symbol = { export const monitor: Monitor = { NODE_MONITOR_SCHEDULE_INTERVAL: Number(process.env.NODE_MONITOR_SCHEDULE_INTERVAL) || config.NODE_MONITOR_SCHEDULE_INTERVAL, NUMBER_OF_NODE_REQUEST_CHUNK: Number(process.env.NUMBER_OF_NODE_REQUEST_CHUNK) || config.NUMBER_OF_NODE_REQUEST_CHUNK, + NODE_PEERS_REQUEST_CHUNK_SIZE: Number(process.env.NODE_PEERS_REQUEST_CHUNK_SIZE) || config.NODE_PEERS_REQUEST_CHUNK_SIZE, CHAIN_HEIGHT_MONITOR_SCHEDULE_INTERVAL: Number(process.env.CHAIN_HEIGHT_MONITOR_SCHEDULE_INTERVAL) || config.CHAIN_HEIGHT_MONITOR_SCHEDULE_INTERVAL, GEOLOCATION_MONITOR_SCHEDULE_INTERVAL: @@ -85,6 +87,9 @@ export const verifyConfig = (cfg: Config): boolean => { if (isNaN(cfg.monitor.NUMBER_OF_NODE_REQUEST_CHUNK) || cfg.monitor.NUMBER_OF_NODE_REQUEST_CHUNK < 0) error = 'Invalid "NUMBER_OF_NODE_REQUEST_CHUNK"'; + if (isNaN(cfg.monitor.NODE_PEERS_REQUEST_CHUNK_SIZE) || cfg.monitor.NODE_PEERS_REQUEST_CHUNK_SIZE < 0) + error = 'Invalid "NODE_PEERS_REQUEST_CHUNK_SIZE"'; + if (isNaN(cfg.monitor.API_NODE_PORT) || cfg.monitor.API_NODE_PORT <= 0 || cfg.monitor.API_NODE_PORT >= 10000) error = 'Invalid "API_NODE_PORT"'; diff --git a/src/models/Node.ts b/src/models/Node.ts index 014c92f..580fe41 100644 --- a/src/models/Node.ts +++ b/src/models/Node.ts @@ -50,6 +50,7 @@ const NodeSchema: Schema = new Schema({ type: Number, required: false, }, + required: false, }, apiStatus: { webSocket: { @@ -236,7 +237,9 @@ NodeSchema.set('toObject', { export const Node = mongoose.model('Node', NodeSchema); export const validateNodeModel = (node: any): boolean => { - if (!node || typeof node !== 'object') return false; + if (!node || typeof node !== 'object') { + return false; + } return !new Node(node).validateSync(); }; diff --git a/src/services/ApiNodeService.ts b/src/services/ApiNodeService.ts index e4dd96f..5b7c6c6 100644 --- a/src/services/ApiNodeService.ts +++ b/src/services/ApiNodeService.ts @@ -163,7 +163,7 @@ export class ApiNodeService { try { return (await HTTP.get(`${hostUrl}/node/info`)).data; } catch (e) { - logger.error(`Fail to request /node/info: ${hostUrl}`, e); + logger.error(`[getNodeInfo] Fail to request /node/info: ${hostUrl}`, e); return null; } }; @@ -172,7 +172,7 @@ export class ApiNodeService { try { return (await HTTP.get(`${hostUrl}/chain/info`)).data; } catch (e) { - logger.error(`Fail to request /chain/info: ${hostUrl}`, e); + logger.error(`[getNodeChainInfo] Fail to request /chain/info: ${hostUrl}`, e); return null; } }; @@ -183,7 +183,7 @@ export class ApiNodeService { return nodeServerInfo.serverInfo; } catch (e) { - logger.error(`Fail to request /node/server: ${hostUrl}`, e); + logger.error(`[getNodeServer] Fail to request /node/server: ${hostUrl}`, e); return null; } }; @@ -194,7 +194,7 @@ export class ApiNodeService { return health.status; } catch (e) { - logger.error(`Fail to request /node/health: ${hostUrl}`, e); + logger.error(`[getNodeHealth] Fail to request /node/health: ${hostUrl}`, e); return null; } }; diff --git a/src/services/ChainHeightMonitor.ts b/src/services/ChainHeightMonitor.ts index eb112cd..57ac996 100644 --- a/src/services/ChainHeightMonitor.ts +++ b/src/services/ChainHeightMonitor.ts @@ -61,7 +61,7 @@ export class ChainHeightMonitor { try { this.nodeList = (await DataBase.getNodeList()).filter((node) => isAPIRole(node.roles)); } catch (e) { - logger.error('Failed to get node list. Use nodes from config'); + logger.error('[getNodeList] Failed to get node list. Use nodes from config'); for (const node of symbol.NODES) { const url = new URL(node); const hostUrl = await ApiNodeService.buildHostUrl(url.hostname); diff --git a/src/services/DataBase.ts b/src/services/DataBase.ts index 9b91526..48ddcf0 100644 --- a/src/services/DataBase.ts +++ b/src/services/DataBase.ts @@ -109,28 +109,30 @@ export class DataBase { collectionName: string, ) => { const prevState = await model.find().exec(); - let error = Error(); try { await model.deleteMany(); } catch (e) { - logger.error(`Update collection "${collectionName}" failed. Error during "model.deleteMany()". ${error.message}`); - throw error; + const msg = `Update collection "${collectionName}" failed. Error during "model.deleteMany()". ${e.message}`; + + logger.error(msg); + throw new Error(msg); } try { await model.insertMany(documents); } catch (e) { - logger.error(`Update collection "${collectionName}" failed. Error during "model.insertMany()". ${error.message}`); - await model.insertMany(prevState); - throw error; + const msg = `Update collection "${collectionName}" failed. Error during "model.insertMany()". ${e.message}`; + + logger.error(msg); + throw new Error(msg); } const currentState = await model.find().exec(); if (documents.length !== currentState.length) { logger.error( - `Update collection "${collectionName}" failed. Collectin.length(${currentState.length}) !== documentsToInsert.length(${documents.length})`, + `Update collection "${collectionName}" failed. Collection.length(${currentState.length}) !== documentsToInsert.length(${documents.length})`, ); await model.insertMany(prevState); throw new Error(`Failed to update collection "${collectionName}. Length verification failed`); diff --git a/src/services/HostInfo.ts b/src/services/HostInfo.ts index e929d69..bc2f499 100644 --- a/src/services/HostInfo.ts +++ b/src/services/HostInfo.ts @@ -43,7 +43,7 @@ export class HostInfo { zip: data.zip, }; } catch (e) { - logger.error(`Failed to get host ${host} info ${e.message}`); + logger.error(`[getHostDetail] Failed to get host ${host} info ${e.message}`); return null; } }; diff --git a/src/services/NodeMonitor.ts b/src/services/NodeMonitor.ts index a7a58df..e8cb3f8 100644 --- a/src/services/NodeMonitor.ts +++ b/src/services/NodeMonitor.ts @@ -13,7 +13,7 @@ import { Logger } from '@src/infrastructure'; import { INode, validateNodeModel } from '@src/models/Node'; import { symbol, monitor } from '@src/config'; -import { isAPIRole, isPeerRole, basename, splitArray, sleep } from '@src/utils'; +import { isAPIRole, isPeerRole, basename, splitArray, showDuration } from '@src/utils'; const logger: winston.Logger = Logger.getLogger(basename(__filename)); @@ -24,6 +24,7 @@ export class NodeMonitor { private isRunning: boolean; private interval: number; private nodeInfoChunks: number; + private nodePeersChunkSize: number; private nodeInfoDelay: number; private networkIdentifier: number; private generationHashSeed: string; @@ -39,6 +40,7 @@ export class NodeMonitor { this.isRunning = false; this.interval = _interval || 300000; this.nodeInfoChunks = monitor.NUMBER_OF_NODE_REQUEST_CHUNK; + this.nodePeersChunkSize = monitor.NODE_PEERS_REQUEST_CHUNK_SIZE; this.nodeInfoDelay = 1000; this.networkIdentifier = 0; this.generationHashSeed = ''; @@ -81,31 +83,29 @@ export class NodeMonitor { }; private getNodeList = async (): Promise => { - logger.info(`Getting node list`); + logger.info(`[getNodeList] Getting node list...`); + const startTime = new Date().getTime(); // Fetch node list from config nodes + logger.info(`[getNodeList] Initial node list: ${symbol.NODES.join(', ')}`); for (const nodeUrl of symbol.NODES) { const peers = await this.fetchNodePeersByURL(nodeUrl); this.addNodesToList(peers); } - // Nested fetch node list from current nodeList[] - const nodeListPromises = this.nodeList.map(async (node) => { - if (isAPIRole(node.roles)) { - const hostUrl = await ApiNodeService.buildHostUrl(node.host); + // Fetch node list from database + const nodesFromDb = (await DataBase.getNodeList().then((nodes) => nodes.map((n) => n.toJSON()))) || []; - return this.fetchNodePeersByURL(hostUrl); - } - - return []; - }); - - const arrayOfNodeList = await Promise.all(nodeListPromises); - const nodeList: INode[] = arrayOfNodeList.reduce((accumulator, value) => accumulator.concat(value), []); + logger.info(`[getNodeList] Nodes count from DB: ${nodesFromDb.length}`); + // adding the nodes from DB to the node list + this.addNodesToList(nodesFromDb); - this.addNodesToList(nodeList); + await this.fetchAndAddNodeListPeers(); + logger.info( + `[getNodeList] Total node count: ${this.nodeList.length}, time elapsed: [${showDuration(startTime - new Date().getTime())}]`, + ); return Promise.resolve(); }; @@ -124,49 +124,102 @@ export class NodeMonitor { if (Array.isArray(nodePeers.data)) nodeList = [...nodePeers.data]; } catch (e) { - logger.error(`FetchNodePeersByURL. Failed to get /node/peers from "${hostUrl}". ${e.message}`); + logger.error(`[FetchNodePeersByURL] Failed to get /node/peers from "${hostUrl}". ${e.message}`); } return nodeList; }; + /** + * Fetch peers from current node list and add them to the node list. + */ + private fetchAndAddNodeListPeers = async (): Promise => { + const apiNodeList = this.nodeList.filter((node) => isAPIRole(node.roles)); + + logger.info( + `[fetchAndAddNodeListPeers] Getting peers from nodes, total nodes: ${this.nodeList.length}, api nodes: ${apiNodeList.length}`, + ); + const nodeListChunks = splitArray(apiNodeList, this.nodePeersChunkSize); + + let numOfNodesProcessed = 0; + + for (const nodes of nodeListChunks) { + logger.info( + `[fetchAndAddNodeListPeers] Getting peer list for chunk of ${nodes.length} nodes, progress: ${ + numOfNodesProcessed + '/' + apiNodeList.length + }`, + ); + const nodePeersPromises = [...nodes].map(async (node) => + this.fetchNodePeersByURL(await ApiNodeService.buildHostUrl(node.host)), + ); + const arrayOfPeerList = await Promise.all(nodePeersPromises); + const peers: INode[] = arrayOfPeerList.reduce((accumulator, value) => accumulator.concat(value), []); + + this.addNodesToList(peers); + numOfNodesProcessed += nodes.length; + } + }; + private getNodeListInfo = async () => { - logger.info(`Getting node from peers total ${this.nodeList.length} nodes`); + const startTime = new Date().getTime(); + const nodeCount = this.nodeList.length; + + logger.info(`[getNodeListInfo] Getting node from peers, total nodes: ${nodeCount}`); const nodeListChunks = splitArray(this.nodeList, this.nodeInfoChunks); this.nodeList = []; - for (const nodes of nodeListChunks) { - logger.info(`Getting node info for chunk of ${nodes.length} nodes`); + let numOfNodesProcessed = 0; + for (const nodes of nodeListChunks) { + logger.info( + `[getNodeListInfo] Getting node info for chunk of ${nodes.length} nodes, progress: ${ + numOfNodesProcessed + '/' + nodeCount + }`, + ); const nodeInfoPromises = [...nodes].map((node) => this.getNodeInfo(node)); + const arrayOfNodeInfo = await Promise.all(nodeInfoPromises); + + logger.info(`[getNodeListInfo] Number of nodeInfo:${arrayOfNodeInfo.length} in the chunk of ${nodes.length}`); + this.addNodesToList(arrayOfNodeInfo); + numOfNodesProcessed += nodes.length; - this.addNodesToList((await Promise.all(nodeInfoPromises)) as INode[]); - await sleep(this.nodeInfoDelay); + //await sleep(this.nodeInfoDelay); } this.nodeList.forEach((node) => this.nodesStats.addToStats(node)); + logger.info( + `[getNodeListInfo] Total node count(after nodeInfo): ${this.nodeList.length}, time elapsed: [${showDuration( + startTime - new Date().getTime(), + )}]`, + ); }; private async getNodeInfo(node: INode): Promise { let nodeWithInfo: INode = { ...node }; + const nodeHost = node.host; try { - const hostDetail = await HostInfo.getHostDetailCached(node.host); + const hostDetail = await HostInfo.getHostDetailCached(nodeHost); - if (hostDetail) nodeWithInfo.hostDetail = hostDetail; + if (hostDetail) { + nodeWithInfo.hostDetail = hostDetail; + } if (isPeerRole(nodeWithInfo.roles)) { - nodeWithInfo.peerStatus = await PeerNodeService.getStatus(node.host, node.port); + nodeWithInfo.peerStatus = await PeerNodeService.getStatus(nodeHost, node.port); } if (isAPIRole(nodeWithInfo.roles)) { - const hostUrl = await ApiNodeService.buildHostUrl(nodeWithInfo.host); + const hostUrl = await ApiNodeService.buildHostUrl(nodeHost); // Get node info and overwrite info from /node/peers const nodeStatus = await ApiNodeService.getNodeInfo(hostUrl); if (nodeStatus) { Object.assign(nodeWithInfo, nodeStatus); + if (!nodeWithInfo.host) { + nodeWithInfo.host = nodeHost; + } } // Request API Status, if node belong to the network @@ -178,9 +231,11 @@ export class NodeMonitor { } } } catch (e) { - logger.error(`GetNodeInfo. Failed to fetch info for "${nodeWithInfo.host}". ${e.message}`); + logger.error(`[getNodeInfo] Failed to fetch info for "${nodeWithInfo.host}". ${e.message}`); } - + logger.info( + `[getNodeInfo] NodeHost values before:${nodeHost} and after:${nodeWithInfo.host} and hostDetail.host:${nodeWithInfo.hostDetail?.host}`, + ); return nodeWithInfo; } @@ -248,9 +303,9 @@ export class NodeMonitor { node.networkGenerationHashSeed !== this.generationHashSeed || !!this.nodeList.find((addedNode) => addedNode.publicKey === node.publicKey) || !validateNodeModel(node) - ) + ) { return; - + } this.nodeList.push(node); }); }; diff --git a/src/utils.ts b/src/utils.ts index 18a9918..63ec6c9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import { INode } from '@src/models/Node'; import * as path from 'path'; +import * as humanizeDuration from 'humanize-duration'; export const stringToArray = (str: string | undefined): Array => { let result = null; @@ -58,3 +59,7 @@ export const splitArray = (array: Array, chunks: number): Array => all[ch] = [].concat(all[ch] || [], one); return all; }, []); + +export const showDuration = (durationMs: number): string => { + return humanizeDuration(durationMs); +};