-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* docs: test commit * chore: test commit revert * feat: implement first version of node health check * docs: added doco to two methods * fix: seperated into integration & unit tests * feat: added additional unit tests with mocks * chore: updated yarn.lock * chore: updated yarn.lock * fix: use the predefined Thor Solo node address * test: get code coverage to 100% * fix: renamed constant * docs: added missing doc * fix: moved constant thorest/blocks.ts * fix: referenced BlockDetail instead of local interface * refactor: from a utility method to a class and integrate with ThorClient/HTTPClient * refactor: change 2 variable types to reduce visibility in the class * docs: improve comments * fix: expect specific error type * fix: use specific error type for invlaid block returned * fix: re-organised unit and integration tests * fix: regenerated yarn.lock using node version 18.16.0 * fix: rename node into 'nodes' and some minors * fix: same work for block and blocks * fix: use blocks client in order to not repeat * fix: remove redundant endpoint form thorest.ts to * fix: remove redundant cose and move NODE_HEALTHCHECK_TOLERANCE_IN_SECONDS in constants * fix: add data in error --------- Co-authored-by: rodolfopietro97 <[email protected]>
- Loading branch information
1 parent
dd88b92
commit d6470ca
Showing
11 changed files
with
339 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
export * from './types.d'; | ||
export * from './block-client'; | ||
export * from './blocks-client'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
export * from './accounts'; | ||
export * from './blocks'; | ||
export * from './logs'; | ||
export * from './nodes'; | ||
export * from './transactions'; | ||
export * from './thor-client'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './nodes-client'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { type HttpClient } from '../../http'; | ||
import { type BlockDetail, BlocksClient } from '../blocks'; | ||
import { buildError, DATA } from '@vechain-sdk/errors'; | ||
import { NODE_HEALTHCHECK_TOLERANCE_IN_SECONDS } from '../../../utils'; | ||
|
||
/** | ||
* Provides utility method for checking the health of a node. | ||
*/ | ||
class NodesClient { | ||
/** | ||
* Internal blocks client instance used for interacting with block-related endpoints. | ||
*/ | ||
private readonly blocksClient: BlocksClient; | ||
|
||
/** | ||
* Initializes a new instance of the `NodeClient` class. | ||
* @param httpClient - The HTTP client instance used for making HTTP requests. | ||
*/ | ||
constructor(readonly httpClient: HttpClient) { | ||
this.blocksClient = new BlocksClient(httpClient); | ||
} | ||
|
||
/** | ||
* Checks the health of a node using the following algorithm: | ||
* 1. Make an HTTP GET request to retrieve the last block timestamp. | ||
* 2. Calculates the difference between the current time and the last block timestamp. | ||
* 3. If the difference is less than the tolerance, the node is healthy. | ||
* Note, we could also check '/node/network/peers since' but the difficulty with this approach is | ||
* if you consider a scenario where the node is connected to 20+ peers, which is healthy, and it receives the new blocks as expected. | ||
* But what if the node's disk is full, and it's not writing the new blocks to its database? In this case the node is off-sync even | ||
* though it's technically alive and connected | ||
* @returns A boolean indicating whether the node is healthy. | ||
* @throws {InvalidDataTypeError} - if the timestamp key does not exist in the response from the API call to the node | ||
* @throws {InvalidDataTypeError} - if the timestamp key exists in the response from the API call to the node but the value is not a number | ||
* @throws {InvalidDataTypeError} - if the response from the API call to the node is not an object | ||
* @throws {InvalidDataTypeError} - if the response from the API call to the node is null or undefined | ||
*/ | ||
public async isHealthy(): Promise<boolean> { | ||
/** | ||
* @internal | ||
* Perform an HTTP GET request using the SimpleNet instance to get the latest block | ||
*/ | ||
const response = await this.blocksClient.getBestBlock(); | ||
|
||
/** | ||
* timestamp from the last block | ||
* @internal | ||
*/ | ||
const lastBlockTimestamp: number = this.getTimestampFromBlock(response); | ||
|
||
/** | ||
* seconds elapsed since the timestamp of the last block | ||
* @internal | ||
*/ | ||
const secondsSinceLastBlock = | ||
Math.floor(Date.now() / 1000) - lastBlockTimestamp; | ||
|
||
return ( | ||
Math.abs(secondsSinceLastBlock) < | ||
NODE_HEALTHCHECK_TOLERANCE_IN_SECONDS | ||
); | ||
} | ||
|
||
/** | ||
* Extracts the timestamp from the block | ||
* @remarks | ||
* This function throws an error if the timestamp key does not exist in the response from the API call to the node | ||
* @param response the response from the API call to the node | ||
* @returns the timestamp from the block | ||
* @throws {InvalidDataTypeError} - if the timestamp key does not exist in the response from the API call to the node | ||
* @throws {InvalidDataTypeError} - if the timestamp key exists in the response from the API call to the node but the value is not a number | ||
* @throws {InvalidDataTypeError} - if the response from the API call to the node is not an object | ||
* @throws {InvalidDataTypeError} - if the response from the API call to the node is null or undefined | ||
*/ | ||
private readonly getTimestampFromBlock = ( | ||
response: BlockDetail | null | ||
): number => { | ||
if ( | ||
response === null || | ||
response === undefined || | ||
typeof response !== 'object' || | ||
!('timestamp' in response) || | ||
typeof response.timestamp !== 'number' | ||
) { | ||
throw buildError( | ||
DATA.INVALID_DATA_TYPE, | ||
'Invalid block format returned from node. The block must be an object with a timestamp key present of type number', | ||
{ response } | ||
); | ||
} | ||
|
||
return response.timestamp; | ||
}; | ||
} | ||
|
||
export { NodesClient }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './http-client'; | ||
export * from './nodes'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
* Node healtcheck Tolerance in seconds. | ||
* @example When set to 30, it means that we consider a node healthy even when it's off-sync by roughly 3 blocks. | ||
* | ||
* @public | ||
*/ | ||
const NODE_HEALTHCHECK_TOLERANCE_IN_SECONDS = 30; | ||
|
||
export { NODE_HEALTHCHECK_TOLERANCE_IN_SECONDS }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/** | ||
* @internal | ||
* Block with a timestamp much older than the current time | ||
*/ | ||
const blockWithOldTimeStamp = { | ||
number: 16935885, | ||
id: '0x01026bcde286e4c5b55507477edc666bb79b41ea97b6e78d65726fe557131533', | ||
size: 361, | ||
parentID: | ||
'0x01026bcc5214fdc936b9afd15460479dfe35972219f78f322e23cc8184a035ab', | ||
timestamp: 16993933000, | ||
gasLimit: 30000000, | ||
beneficiary: '0xb4094c25f86d628fdd571afc4077f0d0196afb48', | ||
gasUsed: 0, | ||
totalScore: 131653862, | ||
txsRoot: | ||
'0x45b0cfc220ceec5b7c1c62c4d4193d38e4eba48e8815729ce75f9c0ab0e4c1c0', | ||
txsFeatures: 1, | ||
stateRoot: | ||
'0x0b43423ced22d182d73728e47fef395169bff38c725dfdb84589e3cedfad2db2', | ||
receiptsRoot: | ||
'0x45b0cfc220ceec5b7c1c62c4d4193d38e4eba48e8815729ce75f9c0ab0e4c1c0', | ||
com: true, | ||
signer: '0xd6fab81fd54b989655b42d51b0344ddcb5007a5a', | ||
isTrunk: true, | ||
isFinalized: false, | ||
transactions: [] | ||
}; | ||
|
||
/** | ||
* @internal | ||
* Block with a missing timestamp | ||
*/ | ||
const blockWithMissingTimeStamp = { | ||
number: 16935885, | ||
id: '0x01026bcde286e4c5b55507477edc666bb79b41ea97b6e78d65726fe557131533', | ||
size: 361, | ||
parentID: | ||
'0x01026bcc5214fdc936b9afd15460479dfe35972219f78f322e23cc8184a035ab', | ||
gasLimit: 30000000, | ||
beneficiary: '0xb4094c25f86d628fdd571afc4077f0d0196afb48', | ||
gasUsed: 0, | ||
totalScore: 131653862, | ||
txsRoot: | ||
'0x45b0cfc220ceec5b7c1c62c4d4193d38e4eba48e8815729ce75f9c0ab0e4c1c0', | ||
txsFeatures: 1, | ||
stateRoot: | ||
'0x0b43423ced22d182d73728e47fef395169bff38c725dfdb84589e3cedfad2db2', | ||
receiptsRoot: | ||
'0x45b0cfc220ceec5b7c1c62c4d4193d38e4eba48e8815729ce75f9c0ab0e4c1c0', | ||
com: true, | ||
signer: '0xd6fab81fd54b989655b42d51b0344ddcb5007a5a', | ||
isTrunk: true, | ||
isFinalized: false, | ||
transactions: [] | ||
}; | ||
|
||
/** | ||
* @internal | ||
* Block with an invalid timestamp format | ||
*/ | ||
const blockWithInvalidTimeStampFormat = { | ||
number: 16935885, | ||
id: '0x01026bcde286e4c5b55507477edc666bb79b41ea97b6e78d65726fe557131533', | ||
size: 361, | ||
parentID: | ||
'0x01026bcc5214fdc936b9afd15460479dfe35972219f78f322e23cc8184a035ab', | ||
timestamp: 'bad timestamp type', | ||
gasLimit: 30000000, | ||
beneficiary: '0xb4094c25f86d628fdd571afc4077f0d0196afb48', | ||
gasUsed: 0, | ||
totalScore: 131653862, | ||
txsRoot: | ||
'0x45b0cfc220ceec5b7c1c62c4d4193d38e4eba48e8815729ce75f9c0ab0e4c1c0', | ||
txsFeatures: 1, | ||
stateRoot: | ||
'0x0b43423ced22d182d73728e47fef395169bff38c725dfdb84589e3cedfad2db2', | ||
receiptsRoot: | ||
'0x45b0cfc220ceec5b7c1c62c4d4193d38e4eba48e8815729ce75f9c0ab0e4c1c0', | ||
com: true, | ||
signer: '0xd6fab81fd54b989655b42d51b0344ddcb5007a5a', | ||
isTrunk: true, | ||
isFinalized: false, | ||
transactions: [] | ||
}; | ||
|
||
export { | ||
blockWithOldTimeStamp, | ||
blockWithMissingTimeStamp, | ||
blockWithInvalidTimeStampFormat | ||
}; |
48 changes: 48 additions & 0 deletions
48
packages/network/tests/client/thor-client/node/node.integration.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { describe, expect, test } from '@jest/globals'; | ||
import { thorSoloClient } from '../../../fixture'; | ||
import { HttpClient, ThorClient } from '../../../../src'; | ||
import { HTTPClientError } from '@vechain-sdk/errors'; | ||
|
||
/** | ||
* Node integration tests | ||
* @group integration/node | ||
*/ | ||
describe('Integration tests to check the Node health check for different scenarios', () => { | ||
test('valid URL but inaccessible VeChain node', async () => { | ||
/** | ||
* client required to access a node | ||
* @internal | ||
*/ | ||
const thorClient = new ThorClient(new HttpClient('www.google.ie')); | ||
await expect(thorClient.nodes.isHealthy()).rejects.toThrowError( | ||
HTTPClientError | ||
); | ||
}); | ||
|
||
test('invalid URL', async () => { | ||
/** | ||
* client required to access a node | ||
* @internal | ||
*/ | ||
const thorClient = new ThorClient(new HttpClient('INVALID_URL')); | ||
await expect(thorClient.nodes.isHealthy()).rejects.toThrowError( | ||
HTTPClientError | ||
); | ||
}); | ||
|
||
test('valid and available synchronized node', async () => { | ||
const healtyNode = await thorSoloClient.nodes.isHealthy(); | ||
expect(healtyNode).toBe(true); | ||
}); | ||
|
||
test('null or empty URL or blank URL', async () => { | ||
let thorClient = new ThorClient(new HttpClient('')); | ||
await expect(thorClient.nodes.isHealthy()).rejects.toThrowError( | ||
HTTPClientError | ||
); | ||
thorClient = new ThorClient(new HttpClient(' ')); | ||
await expect(thorClient.nodes.isHealthy()).rejects.toThrowError( | ||
HTTPClientError | ||
); | ||
}); | ||
}); |
Oops, something went wrong.
d6470ca
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test Coverage
Summary