diff --git a/README.md b/README.md index 036ac481..0d449050 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Use a `.env` at root of the repository to set values for the environment variabl | IPFS_PORT | N | `5001` | Port of the `IPFS` node to use for metadata storage | | WATCHER_POLL_PERIOD_MS | N | `10000` | Number of ms between polling of service state | | WATCHER_TIMEOUT_MS | N | `2000` | Timeout period in ms for service state | +| INDEXER_TIMEOUT_MS | N | `30000` | Timeout period in ms for service state | | API_SWAGGER_BG_COLOR | N | `#fafafa` | CSS \_color\* val for UI bg ( try: [e4f2f3](https://coolors.co/e4f2f3) , [e7f6e6](https://coolors.co/e7f6e6) or [f8dddd](https://coolors.co/f8dddd) ) | | API_SWAGGER_TITLE | N | `IdentityAPI` | String used to customise the title of the html page | | API_SWAGGER_HEADING | N | `IdentityService` | String used to customise the H2 heading | diff --git a/package-lock.json b/package-lock.json index d88cb9da..f3e57dae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@digicatapult/sqnc-process-management": "^2.2.131", "@digicatapult/tsimp": "^2.0.12", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.15.0", @@ -426,6 +427,50 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@digicatapult/sqnc-process-management": { + "version": "2.2.131", + "resolved": "https://registry.npmjs.org/@digicatapult/sqnc-process-management/-/sqnc-process-management-2.2.131.tgz", + "integrity": "sha512-JKB1VKj+vHnZmIFJV3lBy9M+9EHW3ozh6JaPMwTp9hHJ0eeXE6tnUgBlAGPyxfgGKvLBEOGlF5+yoks6TBcH6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api": "^14.3.1", + "chalk": "^5.3.0", + "commander": "^12.1.0", + "typescript": "^5.6.3", + "zod": "^3.23.8" + }, + "bin": { + "process-management": "build/src/index.js" + }, + "engines": { + "node": ">=20.x.x", + "npm": ">=10.x.x" + } + }, + "node_modules/@digicatapult/sqnc-process-management/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@digicatapult/sqnc-process-management/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@digicatapult/tsimp": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/@digicatapult/tsimp/-/tsimp-2.0.12.tgz", @@ -8537,6 +8582,33 @@ } } }, + "@digicatapult/sqnc-process-management": { + "version": "2.2.131", + "resolved": "https://registry.npmjs.org/@digicatapult/sqnc-process-management/-/sqnc-process-management-2.2.131.tgz", + "integrity": "sha512-JKB1VKj+vHnZmIFJV3lBy9M+9EHW3ozh6JaPMwTp9hHJ0eeXE6tnUgBlAGPyxfgGKvLBEOGlF5+yoks6TBcH6A==", + "dev": true, + "requires": { + "@polkadot/api": "^14.3.1", + "chalk": "^5.3.0", + "commander": "^12.1.0", + "typescript": "^5.6.3", + "zod": "^3.23.8" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + }, + "commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true + } + } + }, "@digicatapult/tsimp": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/@digicatapult/tsimp/-/tsimp-2.0.12.tgz", diff --git a/package.json b/package.json index 6d29a331..96d7982d 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "db:migrate": "npm run db:cmd -- migrate:latest --knexfile src/lib/db/knexfile.ts", "db:rollback": "npm run db:cmd -- migrate:rollback --knexfile src/lib/db/knexfile.ts", "coverage": "c8 npm run test", - "flows": "npx @digicatapult/sqnc-process-management@latest create -h localhost -p 9944 -u //Alice -f processFlows.json" + "flows": "process-management create -h localhost -p 9944 -u //Alice -f processFlows.json" }, "repository": { "type": "git", @@ -38,6 +38,7 @@ }, "homepage": "https://github.com/digicatapult/sqnc-matchmaker-api#readme", "devDependencies": { + "@digicatapult/sqnc-process-management": "^2.2.131", "@digicatapult/tsimp": "^2.0.12", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.15.0", diff --git a/src/env.ts b/src/env.ts index 55605142..e3b3a148 100644 --- a/src/env.ts +++ b/src/env.ts @@ -26,6 +26,7 @@ const env = envalid.cleanEnv(process.env, { IPFS_PORT: envalid.port({ default: 5001 }), WATCHER_POLL_PERIOD_MS: envalid.num({ default: 10 * 1000 }), WATCHER_TIMEOUT_MS: envalid.num({ default: 2 * 1000 }), + INDEXER_TIMEOUT_MS: envalid.num({ default: 30 * 1000 }), API_SWAGGER_BG_COLOR: envalid.str({ default: '#fafafa' }), API_SWAGGER_TITLE: envalid.str({ default: 'MatchmakerAPI' }), API_SWAGGER_HEADING: envalid.str({ default: 'MatchmakerAPI' }), diff --git a/src/index.ts b/src/index.ts index 0fc989e5..5d2ef7a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import 'reflect-metadata' - import { Express } from 'express' import { container } from 'tsyringe' @@ -14,8 +13,18 @@ import { logger } from './lib/logger.js' if (env.ENABLE_INDEXER) { const node = container.resolve(ChainNode) + container.register('Indexer', { + useFactory: () => + new Indexer({ + db: new Database(), + logger, + node, + startupTime: new Date(), + env: env, + }), + }) + const indexer = container.resolve('Indexer') - const indexer = new Indexer({ db: new Database(), logger, node }) await indexer.start() indexer.processAllBlocks(await node.getLastFinalisedBlockHash()).then(() => node.watchFinalisedBlocks(async (hash) => { diff --git a/src/lib/indexer/__tests__/index.test.ts b/src/lib/indexer/__tests__/index.test.ts index b5724ee7..c719bfc4 100644 --- a/src/lib/indexer/__tests__/index.test.ts +++ b/src/lib/indexer/__tests__/index.test.ts @@ -1,3 +1,4 @@ +import 'reflect-metadata' import { describe, it, afterEach, beforeEach } from 'mocha' import sinon from 'sinon' import { expect } from 'chai' @@ -5,9 +6,11 @@ import { expect } from 'chai' import { withMockLogger } from './fixtures/logger.js' import { withLastProcessedBlocksByCall, withInitialLastProcessedBlock } from './fixtures/db.js' import { withHappyChainNode, withGetHeaderBoom } from './fixtures/chainNode.js' -import Indexer from '../index.js' +import Indexer, { getStatus } from '../index.js' +import env from '../../../env.js' describe('Indexer', function () { + const startupTime = new Date() let indexer: Indexer const logger = withMockLogger() @@ -21,7 +24,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) const result = await indexer.start() expect(result).to.equal(null) }) @@ -31,7 +34,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) const result = await indexer.start() expect(result).to.equal('1-hash') }) @@ -41,7 +44,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() expect(handleBlock.called).to.equal(false) }) @@ -53,7 +56,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() const result = await indexer.processNextBlock('1-hash') @@ -66,7 +69,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() const result = await indexer.processNextBlock('2-hash') @@ -80,7 +83,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() const result = await indexer.processNextBlock('3-hash') @@ -94,7 +97,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() await indexer.processNextBlock('3-hash') const result = await indexer.processNextBlock('3-hash') @@ -110,7 +113,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() await indexer.processNextBlock('2-hash') const result = await indexer.processNextBlock('2-hash') @@ -129,7 +132,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() await indexer.processNextBlock('5-hash') const result = await indexer.processNextBlock('5-hash') @@ -145,7 +148,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() await indexer.processNextBlock('3-hash') const result = await indexer.processNextBlock('2-hash') @@ -174,7 +177,7 @@ describe('Indexer', function () { ]), }) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() await indexer.processNextBlock('2-hash') @@ -209,7 +212,7 @@ describe('Indexer', function () { ]), }) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() await indexer.processNextBlock('2-hash') @@ -241,7 +244,7 @@ describe('Indexer', function () { const node = withGetHeaderBoom(1) const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() const p = indexer.processNextBlock('2-hash').then((s) => s) clock.tickAsync(1000) @@ -258,7 +261,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}).onCall(0).rejects(new Error('BOOM')) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() const p = indexer.processNextBlock('2-hash').then((s) => s) clock.tickAsync(1000) @@ -278,7 +281,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() const result = await indexer.processAllBlocks('3-hash') @@ -293,7 +296,7 @@ describe('Indexer', function () { const node = withHappyChainNode() const handleBlock = sinon.stub().resolves({}) - indexer = new Indexer({ db, node, logger, handleBlock }) + indexer = new Indexer({ db, node, logger, handleBlock, startupTime, env }) await indexer.start() const result = await indexer.processAllBlocks('1-hash') @@ -301,4 +304,88 @@ describe('Indexer', function () { expect(handleBlock.called).to.equal(false) }) }) + describe('getStatus tests', function () { + let clock: sinon.SinonFakeTimers + beforeEach(function () { + clock = sinon.useFakeTimers() + }) + + afterEach(function () { + clock.restore() + }) + it('should return service UP if within 30s of starting up', async function () { + const startupTime = new Date('2024-11-25T00:00:00.000Z') + clock.setSystemTime(new Date('2024-11-25T00:00:25.000Z')) + const result = await getStatus(30000, startupTime, null, null) + expect(result).to.have.property('status', 'up') + expect(result.detail).to.have.property('message', 'Service healthy. Starting up.') + }) + it('should return service DOWN if it has started up a while back', async function () { + const startupTime = new Date('2024-11-25T00:00:15.000Z') + clock.setSystemTime(new Date('2024-11-25T00:01:00.000Z')) + const result = await getStatus(30000, startupTime, null, null) + expect(result).to.have.property('status', 'down') + expect(result.detail).to.have.property( + 'message', + 'Last activity was more than 30s ago, no blocks were processed.' + ) + expect(result.detail).to.have.property('latestActivityTime', null) + }) + it('should return service UP because we are "catching up" on old blocks', async function () { + const currentTime = new Date('2024-11-25T00:01:00.000Z') + clock.setSystemTime(currentTime) + const startupTime = new Date(currentTime.getTime() - 30 * 1000) + // lastUnprocessedBlockTime: 2 seconds after current time + const lastUnprocessedBlockTime = new Date(currentTime.getTime() + 2 * 1000) + const result = await getStatus(30000, startupTime, null, lastUnprocessedBlockTime) + expect(result).to.have.property('status', 'up') + const latestActivityTime = result.detail?.latestActivityTime + expect(latestActivityTime).to.be.instanceOf(Date) + expect(result.detail).to.have.property('message', 'Service healthy. Running.') + }) + it('should return service UP because we are processing blocks', async function () { + const currentTime = new Date('2024-11-25T00:01:00.000Z') + clock.setSystemTime(currentTime) + const startupTime = new Date(currentTime.getTime() - 30 * 1000) + // lastProcessedBlockTime: 4 seconds after current time + const lastUnprocessedBlockTime = new Date(currentTime.getTime() + 2 * 1000) + const lastProcessedBlockTime = new Date(currentTime.getTime() + 4 * 1000) + const result = await getStatus(30000, startupTime, lastProcessedBlockTime, lastUnprocessedBlockTime) + expect(result).to.have.property('status', 'up') + const latestActivityTime = result.detail?.latestActivityTime + expect(latestActivityTime).to.be.instanceOf(Date) + expect(result.detail).to.have.property('message', 'Service healthy. Running.') + }) + it('should return service DOWN if last activity was more than 30s ago (catching up to blocks)', async function () { + const currentTime = new Date('2024-11-25T00:05:00.000Z') + clock.setSystemTime(currentTime) + // Startup time: 2 minutes before current time + const startupTime = new Date(currentTime.getTime() - 2 * 60 * 1000) + // lastProcessedBlockTime: 35 seconds before current time + const lastUnprocessedBlockTime = new Date(currentTime.getTime() - 35 * 1000) + const result = await getStatus(30000, startupTime, null, lastUnprocessedBlockTime) + expect(result).to.have.property('status', 'down') + const latestActivityTime = result.detail?.latestActivityTime + expect(latestActivityTime).to.be.instanceOf(Date) + expect(result.detail?.message).to.include( + 'Last activity was more than 30s ago. Last learned of block: Mon Nov 25 2024 00:04:25' + ) + }) + it('should return service DOWN if last activity was more than 30s ago (catching up to blocks)', async function () { + const currentTime = new Date('2024-11-25T00:05:00.000Z') + clock.setSystemTime(currentTime) + // Startup time: 2 minutes before current time + const startupTime = new Date(currentTime.getTime() - 2 * 60 * 1000) + // lastProcessedBlockTime: 35 seconds before current time + const lastUnprocessedBlockTime = new Date(currentTime.getTime() - 35 * 1000) + const lastProcessedBlockTime = new Date(currentTime.getTime() - 35 * 1000) + const result = await getStatus(30000, startupTime, lastProcessedBlockTime, lastUnprocessedBlockTime) + expect(result).to.have.property('status', 'down') + const latestActivityTime = result.detail?.latestActivityTime + expect(latestActivityTime).to.be.instanceOf(Date) + expect(result.detail?.message).to.include( + 'Last activity was more than 30s ago. Last processed block at : Mon Nov 25 2024 00:04:25 GMT+0000' + ) + }) + }) }) diff --git a/src/lib/indexer/index.ts b/src/lib/indexer/index.ts index 19f03309..42c28c96 100644 --- a/src/lib/indexer/index.ts +++ b/src/lib/indexer/index.ts @@ -6,6 +6,9 @@ import DefaultBlockHandler from './handleBlock.js' import { ChangeSet } from './changeSet.js' import { HEX } from '../../models/strings.js' import { DbBlock } from '../db/index.js' +import { injectable, singleton } from 'tsyringe' +import { serviceState, Status } from '../service-watcher/statusPoll.js' +import { Env } from '../../env.js' export type BlockHandler = (blockHash: HEX) => Promise @@ -13,24 +16,42 @@ export interface IndexerCtorArgs { db: Database logger: Logger node: ChainNode + startupTime: Date + env: Env handleBlock?: BlockHandler retryDelay?: number } +export interface BlockProcessingTimes { + startupTime: Date + lastProcessedBlockTime: Date | null + lastUnprocessedBlockTime: Date | null +} + +@singleton() +@injectable() export default class Indexer { private logger: Logger + private env: Env private db: Database private node: ChainNode private gen: AsyncGenerator private handleBlock: BlockHandler private retryDelay: number + private startupTime: Date + private lastProcessedBlockTime: Date | null // while we are running and up to date + private lastUnprocessedBlockTime: Date | null // only for when we are catching up on unprocessed blocks - constructor({ db, logger, node, handleBlock, retryDelay }: IndexerCtorArgs) { + constructor({ db, logger, node, handleBlock, retryDelay, startupTime, env }: IndexerCtorArgs) { this.logger = logger.child({ module: 'indexer' }) + this.env = env this.db = db this.node = node this.gen = this.nextBlockProcessor() this.retryDelay = retryDelay || 1000 + this.lastProcessedBlockTime = null + this.lastUnprocessedBlockTime = null + this.startupTime = startupTime if (handleBlock) { this.handleBlock = handleBlock return @@ -97,11 +118,13 @@ export default class Indexer { } await this.updateUnprocessedBlocks(lastProcessedBlock, lastKnownFinalised) + if (this.lastProcessedBlockTime === null) this.lastProcessedBlockTime = new Date() //once the above method has finished successfully we want to assign a value to lastProcessedBlockTime const nextUnprocessedBlockHash = await this.getNextUnprocessedBlockHash(lastProcessedBlock) if (nextUnprocessedBlockHash) { const changeSet = await this.handleBlock(nextUnprocessedBlockHash) await this.updateDbWithNewBlock(nextUnprocessedBlockHash, changeSet) + this.lastProcessedBlockTime = new Date() return nextUnprocessedBlockHash } @@ -171,6 +194,7 @@ export default class Indexer { parent: unprocessedBlock.parent, height: `${unprocessedBlock.height}`, }) + this.lastUnprocessedBlockTime = new Date() // time for when we have last leaned about a block parentHash = unprocessedBlock.parent } } @@ -254,4 +278,65 @@ export default class Indexer { } }) } + + async getStatus() { + return await getStatus( + this.env.INDEXER_TIMEOUT_MS, + this.startupTime, + this.lastProcessedBlockTime, + this.lastUnprocessedBlockTime + ) + } +} + +export const getStatus = async ( + indexerTimeout: number, + startupTime: Date, + lastProcessedBlockTime: Date | null, + lastUnprocessedBlockTime: Date | null +): Promise => { + const currentDate = new Date() + if (currentDate.getTime() - startupTime.getTime() < indexerTimeout) { + // if we started less than 30s ago -> PASS + return { + status: serviceState.UP, + detail: { + message: 'Service healthy. Starting up.', + startupTime: startupTime, + latestActivityTime: currentDate, + }, + } + } + const latestActivityTime = lastProcessedBlockTime || lastUnprocessedBlockTime + if (latestActivityTime === null) { + return { + status: serviceState.DOWN, + detail: { + message: 'Last activity was more than 30s ago, no blocks were processed.', + startupTime: startupTime, + latestActivityTime: null, + }, + } + } + if (currentDate.getTime() - latestActivityTime.getTime() < indexerTimeout) { + return { + status: serviceState.UP, + detail: { + message: 'Service healthy. Running.', + startupTime: startupTime, + latestActivityTime: latestActivityTime, + }, + } + } + const errMessage = lastProcessedBlockTime + ? `Last activity was more than 30s ago. Last processed block at : ${lastProcessedBlockTime}` + : `Last activity was more than 30s ago. Last learned of block: ${lastUnprocessedBlockTime}` + return { + status: serviceState.DOWN, + detail: { + message: errMessage, + startupTime: startupTime, + latestActivityTime: latestActivityTime, + }, + } } diff --git a/src/lib/service-watcher/index.ts b/src/lib/service-watcher/index.ts index 7c9e5843..6661c7ad 100644 --- a/src/lib/service-watcher/index.ts +++ b/src/lib/service-watcher/index.ts @@ -4,6 +4,7 @@ import startApiStatus from './apiStatus.js' import startIpfsStatus from './ipfsStatus.js' import startIdentityStatus from './identityStatus.js' import { buildCombinedHandler, SERVICE_STATE, Status } from './statusPoll.js' +import startIndexerStatus from './indexerStatus.js' @singleton() export class ServiceWatcher { @@ -27,14 +28,16 @@ export class ServiceWatcher { close: () => void }> => { const handlers = new Map() - const [apiStatus, ipfsStatus, identityStatus] = await Promise.all([ + const [apiStatus, ipfsStatus, identityStatus, indexerStatus] = await Promise.all([ startApiStatus(), startIpfsStatus(), startIdentityStatus(), + startIndexerStatus(), ]) handlers.set('api', apiStatus) handlers.set('ipfs', ipfsStatus) handlers.set('identity', identityStatus) + handlers.set('indexer', indexerStatus) return buildCombinedHandler(handlers) } diff --git a/src/lib/service-watcher/indexerStatus.ts b/src/lib/service-watcher/indexerStatus.ts new file mode 100644 index 00000000..1544a9df --- /dev/null +++ b/src/lib/service-watcher/indexerStatus.ts @@ -0,0 +1,19 @@ +import Indexer from '../indexer/index.js' +import env from '../../env.js' +import { startStatusHandler } from './statusPoll.js' +import Database from '../db/index.js' +import ChainNode from '../chainNode.js' +import { logger } from '../logger.js' + +const { WATCHER_POLL_PERIOD_MS, WATCHER_TIMEOUT_MS } = env +const node = new ChainNode(logger, env) +const indexer = new Indexer({ db: new Database(), logger, node, startupTime: new Date(), env }) + +const startIndexerStatus = () => + startStatusHandler({ + getStatus: indexer.getStatus.bind(indexer), + pollingPeriodMs: WATCHER_POLL_PERIOD_MS, + serviceTimeoutMs: WATCHER_TIMEOUT_MS, + }) + +export default startIndexerStatus diff --git a/test/helper/healthHelper.ts b/test/helper/healthHelper.ts index 0ff2cd2f..3c822c0d 100644 --- a/test/helper/healthHelper.ts +++ b/test/helper/healthHelper.ts @@ -1,5 +1,12 @@ export const responses = { - ok: (sqncRuntimeVersion: number, ipfsVersion: string, identityVersion: string) => ({ + ok: ( + sqncRuntimeVersion: number, + ipfsVersion: string, + identityVersion: string, + indexerStatus: string, + startupTime: Date, + latestActivityTime: Date + ) => ({ code: 200, body: { status: 'ok', @@ -33,10 +40,25 @@ export const responses = { version: identityVersion, }, }, + indexer: { + status: indexerStatus, + detail: { + message: 'Service healthy. Starting up.', + latestActivityTime: latestActivityTime, + startupTime: startupTime, + }, + }, }, }, }), - ipfsDown: (sqncRuntimeVersion: number, identityVersion: string) => ({ + + ipfsDown: ( + sqncRuntimeVersion: number, + identityVersion: string, + indexerStatus: string, + startupTime: Date, + latestActivityTime: Date + ) => ({ code: 503, body: { status: 'down', @@ -69,6 +91,14 @@ export const responses = { version: identityVersion, }, }, + indexer: { + status: indexerStatus, + detail: { + message: 'Service healthy. Starting up.', + latestActivityTime: latestActivityTime, + startupTime: startupTime, + }, + }, }, }, }), diff --git a/test/integration/offchain/healthcheck.test.ts b/test/integration/offchain/healthcheck.test.ts index 6575429e..1b55e6df 100644 --- a/test/integration/offchain/healthcheck.test.ts +++ b/test/integration/offchain/healthcheck.test.ts @@ -19,7 +19,15 @@ const getIpfsVersion = (actualResult: any) => { const getIdentityVersion = (actualResult: any) => { return actualResult?._body?.details?.identity?.detail?.version } - +const getIndexerStatus = (actualResult: any) => { + return actualResult?._body?.details?.indexer?.status +} +const getIndexerStartupTime = (actualResult: any) => { + return actualResult?._body?.details?.indexer?.detail?.startupTime +} +const getIndexerLatestActivityTime = (actualResult: any) => { + return actualResult?._body?.details?.indexer?.detail?.latestActivityTime +} describe('health check', () => { describe('happy path', function () { let app: Express @@ -41,7 +49,10 @@ describe('health check', () => { const response = healthResponses.ok( getSpecVersion(actualResult), getIpfsVersion(actualResult), - getIdentityVersion(actualResult) + getIdentityVersion(actualResult), + getIndexerStatus(actualResult), + getIndexerStartupTime(actualResult), + getIndexerLatestActivityTime(actualResult) ) expect(actualResult.status).to.equal(response.code) expect(actualResult.body).to.deep.equal(response.body) @@ -65,7 +76,13 @@ describe('health check', () => { it('service down', async function () { const actualResult = await get(app, '/health') - const response = healthResponses.ipfsDown(getSpecVersion(actualResult), getIdentityVersion(actualResult)) + const response = healthResponses.ipfsDown( + getSpecVersion(actualResult), + getIdentityVersion(actualResult), + getIndexerStatus(actualResult), + getIndexerStartupTime(actualResult), + getIndexerLatestActivityTime(actualResult) + ) expect(actualResult.status).to.equal(response.code) expect(actualResult.body).to.deep.equal(response.body) })