Skip to content

Commit

Permalink
Merge pull request #148 from symbol/dev
Browse files Browse the repository at this point in the history
[statistics-service] task: Dev -> Main - release v1.1.7
  • Loading branch information
Baha authored May 12, 2022
2 parents 96c4bc5 + 96a1ede commit 48cf62f
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 17 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file.

The changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [v1.1.7][v1.1.7] - 12-May-2022

Package | Version | Link
---|---------|---
REST Core| v2.4.0 | [catapult-rest][[email protected]]
SDK Core| v2.0.0 | [symbol-sdk][[email protected]]

- Fixed node monitoring scheduled job hanging after a while issue [#145](https://github.com/symbol/statistics-service/pull/145)

## [v1.1.6][v1.1.6] - 23-Mar-2022

Package | Version | Link
Expand Down Expand Up @@ -175,6 +184,7 @@ REST Core| v2.1.0 | [catapult-rest](https://github.com/symbol/catapult-rest/rele
### Fixes
- Cors error. [#13](https://github.com/symbol/statistics-service/issues/13)

[v1.1.7]: https://github.com/symbol/statistics-service/releases/tag/v1.1.7
[v1.1.6]: https://github.com/symbol/statistics-service/releases/tag/v1.1.6
[v1.1.5]: https://github.com/symbol/statistics-service/releases/tag/v1.1.5
[v1.1.4]: https://github.com/symbol/statistics-service/releases/tag/v1.1.4
Expand Down
45 changes: 43 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "symbol-statistics-service",
"version": "1.1.6",
"version": "1.1.7",
"description": "",
"scripts": {
"dev": "nodemon",
Expand All @@ -24,6 +24,7 @@
"devDependencies": {
"@openapitools/openapi-generator-cli": "^2.4.15",
"@types/chai": "^4.3.0",
"@types/chai-as-promised": "^7.1.5",
"@types/humanize-duration": "^3.27.0",
"@types/mocha": "^9.0.0",
"@types/mongoose": "^5.7.14",
Expand All @@ -33,6 +34,7 @@
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"eslint": "^7.10.0",
"eslint-config-prettier": "^6.12.0",
"eslint-plugin-prettier": "^3.1.4",
Expand Down
4 changes: 2 additions & 2 deletions src/infrastructure/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ export class Logger {
filename: 'error.log',
});

static getLogger(name: string): winston.Logger {
static getLogger(name: string, fileTransport = true): winston.Logger {
return winston.createLogger({
format: winston.format.combine(winston.format.label({ label: name })),
transports: [Logger.transportsConsole, Logger.transportsFile],
transports: [Logger.transportsConsole, ...(fileTransport ? [Logger.transportsFile] : [])],
});
}

Expand Down
18 changes: 11 additions & 7 deletions src/services/ApiNodeService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { HTTP } from '@src/services/Http';
import * as winston from 'winston';
import { basename } from '@src/utils';
import { basename, promiseAllTimeout } from '@src/utils';
import { Logger } from '@src/infrastructure';
import { WebSocket } from 'ws';
import { monitor } from '@src/config';

const logger: winston.Logger = Logger.getLogger(basename(__filename));
const REQUEST_TIMEOUT = monitor.REQUEST_TIMEOUT;

const logger: winston.Logger = Logger.getLogger(basename(__filename), false);

interface NodeStatus {
apiNode: string;
Expand Down Expand Up @@ -98,11 +101,12 @@ export class ApiNodeService {
return apiStatus;
}

const [nodeInfo, nodeServer, nodeHealth] = await Promise.all([
ApiNodeService.getNodeInfo(hostUrl),
ApiNodeService.getNodeServer(hostUrl),
ApiNodeService.getNodeHealth(hostUrl),
]);
const [nodeInfo, nodeServer, nodeHealth] = await promiseAllTimeout(
[ApiNodeService.getNodeInfo(hostUrl), ApiNodeService.getNodeServer(hostUrl), ApiNodeService.getNodeHealth(hostUrl)],
REQUEST_TIMEOUT,
logger,
'getStatus',
);

if (nodeInfo) {
Object.assign(apiStatus, {
Expand Down
11 changes: 6 additions & 5 deletions src/services/Http.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios';
import { monitor } from '@src/config';

const REQEST_TIMEOUT = monitor.REQUEST_TIMEOUT;

export class HTTP {
static TIMEOUT_MSG = `HTTP get request failed. Timeout error`;

static get(url: string, config?: AxiosRequestConfig | undefined): Promise<AxiosResponse<any>> {
return new Promise<AxiosResponse<any>>((resolve, reject) => {
const options = { timeout: monitor.REQUEST_TIMEOUT, ...config };
const timeout = setTimeout(() => {
reject(Error(`HTTP get request failed. Timeout error`));
}, REQEST_TIMEOUT + REQEST_TIMEOUT * 0.1);
reject(Error(HTTP.TIMEOUT_MSG));
}, options.timeout * 1.1);

axios
.get(url, { timeout: REQEST_TIMEOUT, ...config })
.get(url, options)
.then((response) => {
clearTimeout(timeout);
resolve(response);
Expand Down
19 changes: 19 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,22 @@ export const splitByPredicate = <T>(predicate: (item: T) => boolean, arr: T[]):
{ filtered: [] as T[], unfiltered: [] as T[] },
);
};

const promiseTimeout = (ms: number, timeoutVal: any, logger: winston.Logger, loggingMethod: string) => {
return new Promise((resolve) =>
setTimeout(() => {
logger.info(`[${loggingMethod}] Promise timeout reached, returning ${timeoutVal}`);
resolve(timeoutVal);
}, ms),
);
};

export const promiseAllTimeout = (
promises: Promise<any>[],
timeout: number,
logger: winston.Logger,
loggingMethod: string,
timeOutVal?: any,
): Promise<any> => {
return Promise.all(promises.map((promise) => Promise.race([promise, promiseTimeout(timeout, timeOutVal, logger, loggingMethod)])));
};
113 changes: 113 additions & 0 deletions test/ApiNodeService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { expect } from 'chai';
import { stub, restore } from 'sinon';
import { ApiNodeService } from '@src/services/ApiNodeService';

describe('ApiNodeService', () => {
const host = 'localhost';
const hostURL = `https://${host}:3001`;
const webSocketStatus = { isAvailable: true, wss: true, url: `wss://${host}:3001` };
const serverInfo = {
restVersion: '2.4.0',
sdkVersion: '2.4.1',
deployment: {
deploymentTool: 'symbol-bootstrap',
deploymentToolVersion: '1.1.6',
lastUpdatedDate: '2022-03-16',
},
};
const chainInfo = {
scoreHigh: '0',
scoreLow: '3553661314979960997',
height: '355232',
latestFinalizedBlock: {
finalizationEpoch: 495,
finalizationPoint: 16,
height: '355204',
hash: 'ACE7C54D9DEB625151157D2E0A0EC4CD92235A95A0997FF87FA8DE743A2E46B8',
},
};

afterEach(() => {
restore();
});

it('getStatus returns data successfully', async () => {
// Arrange:
const nodeInfo = {
version: 16777987,
publicKey: '3460D29534CA997D6A74BEBB93F38356833B806E1A35F700EBE08D57FC8D3FED',
networkGenerationHashSeed: '7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836',
roles: 3,
port: 7900,
networkIdentifier: 152,
host: host,
friendlyName: host,
nodePublicKey: '7F95C44319D02FF33F852142D78D85679B1B82EB48194CCCFBB6749ED75653DC',
};

const nodeStatus = {
apiNode: 'up',
db: 'up',
};

const finalization = {
height: Number.parseInt(chainInfo.latestFinalizedBlock.height),
epoch: chainInfo.latestFinalizedBlock.finalizationEpoch,
point: chainInfo.latestFinalizedBlock.finalizationPoint,
hash: chainInfo.latestFinalizedBlock.hash,
};

stub(ApiNodeService, 'getNodeChainInfo').returns(Promise.resolve(chainInfo));
stub(ApiNodeService, 'webSocketStatus').returns(Promise.resolve(webSocketStatus));
stub(ApiNodeService, 'getNodeServer').returns(Promise.resolve(serverInfo));
stub(ApiNodeService, 'getNodeInfo').returns(Promise.resolve(nodeInfo));
stub(ApiNodeService, 'getNodeHealth').returns(Promise.resolve(nodeStatus));
stub(ApiNodeService, 'isHttpsEnabled').returns(Promise.resolve(true));

// Act:
const apiStatus = await ApiNodeService.getStatus(hostURL);

// Assert:
expect(apiStatus.webSocket).to.be.deep.equal(webSocketStatus);
expect(apiStatus.restGatewayUrl).to.be.equal(hostURL);
expect(apiStatus.isAvailable).to.be.equal(true);
expect(apiStatus.isHttpsEnabled).to.be.equal(true);
expect(apiStatus.nodeStatus).to.be.deep.equal(nodeStatus);
expect(apiStatus.chainHeight).to.equal(chainInfo.height);
expect(apiStatus.finalization).to.be.deep.equal(finalization);
expect(apiStatus.nodePublicKey).to.be.equal(nodeInfo.nodePublicKey);
expect(apiStatus.restVersion).to.be.equal(serverInfo.restVersion);
expect(apiStatus.lastStatusCheck).to.be.not.undefined;
});

it('getStatus proceeds even if some of the api calls hang', async () => {
// Arrange:
const webSocketStatus = { isAvailable: false, wss: false, url: undefined };

stub(ApiNodeService, 'getNodeChainInfo').returns(Promise.resolve(chainInfo));
stub(ApiNodeService, 'webSocketStatus').returns(Promise.resolve(webSocketStatus));

const hangingMethods = ['getNodeInfo', 'getNodeHealth'];

hangingMethods.map((method: any) =>
stub(ApiNodeService, method).returns(
new Promise((_, reject) => {
setTimeout(() => {
reject(Error('timeout'));
}, 6_000);
}),
),
);
stub(ApiNodeService, 'getNodeServer').returns(Promise.resolve(serverInfo));

// Act:
const apiStatus = await ApiNodeService.getStatus(hostURL);

// Assert:
expect(apiStatus.nodePublicKey).to.be.undefined;
expect(apiStatus.nodeStatus).to.be.undefined;
expect(apiStatus.webSocket).to.be.deep.equal(webSocketStatus);
expect(apiStatus.restVersion).to.be.equal(serverInfo.restVersion);
expect(apiStatus.chainHeight).to.be.equal(chainInfo.height);
}).timeout(10_000);
});
41 changes: 41 additions & 0 deletions test/Http.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { HTTP } from '@src/services/Http';
import { use, expect } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { stub, restore } from 'sinon';
import axios from 'axios';

use(chaiAsPromised);

describe('Http', () => {
afterEach(() => {
restore();
});

it('Get method timeouts after given time', async () => {
// Arrange:
stub(axios, 'get').returns(
new Promise((resolve) => {
setTimeout(() => {
resolve('success');
}, 5_000);
}),
);

// Act + Assert:
return expect(HTTP.get('SOME_FAKE_URL', { timeout: 2_000 })).to.eventually.be.rejectedWith(HTTP.TIMEOUT_MSG);
}).timeout(3000);

it('Get method returns data successfully', async () => {
// Arrange:
stub(axios, 'get').returns(
new Promise((resolve, _) => {
setTimeout(() => {
resolve('success');
}, 1_000);
}),
);

// Act + Assert:
return expect(HTTP.get('SOME_FAKE_URL', { timeout: 2_000 })).to.eventually.be.equal('success');
});
});
Loading

0 comments on commit 48cf62f

Please sign in to comment.