From 68e48103ebcc9f31992e7924e95353a9d33e6f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 4 Sep 2024 12:56:04 +0200 Subject: [PATCH 1/2] feat(core): Introduce DB health check --- packages/cli/src/abstract-server.ts | 8 ++++++- packages/cli/src/commands/worker.ts | 6 +++++ .../cli/test/integration/healthcheck.test.ts | 22 +++++++++++++++++++ .../cli/test/integration/shared/test-db.ts | 5 +++++ packages/cli/test/integration/shared/types.ts | 2 ++ .../integration/shared/utils/test-server.ts | 19 ++++++++++++++-- 6 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 packages/cli/test/integration/healthcheck.test.ts diff --git a/packages/cli/src/abstract-server.ts b/packages/cli/src/abstract-server.ts index 9bac826aceb7d..2f3eefce85bf2 100644 --- a/packages/cli/src/abstract-server.ts +++ b/packages/cli/src/abstract-server.ts @@ -119,11 +119,17 @@ export abstract class AbstractServer { protected setupPushServer() {} private async setupHealthCheck() { - // health check should not care about DB connections + // main health check should not care about DB connections this.app.get('/healthz', async (_req, res) => { res.send({ status: 'ok' }); }); + this.app.get('/healthz/db', async (_req, res) => { + return Db.connectionState.connected && Db.connectionState.migrated + ? res.status(200).send({ status: 'ok' }) + : res.status(500).send({ status: 'error' }); + }); + const { connectionState } = Db; this.app.use((_req, res, next) => { if (connectionState.connected) { diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index 7db79b850874f..5d7496578c01c 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -172,6 +172,12 @@ export class Worker extends BaseCommand { const server = http.createServer(app); + app.get('/healthz/db', async (_req, res) => { + return Db.connectionState.connected && Db.connectionState.migrated + ? res.status(200).send({ status: 'ok' }) + : res.status(500).send({ status: 'error' }); + }); + app.get( '/healthz', diff --git a/packages/cli/test/integration/healthcheck.test.ts b/packages/cli/test/integration/healthcheck.test.ts new file mode 100644 index 0000000000000..319bba3a3ea8c --- /dev/null +++ b/packages/cli/test/integration/healthcheck.test.ts @@ -0,0 +1,22 @@ +import * as testDb from './shared/test-db'; +import { setupTestServer } from '@test-integration/utils'; + +const testServer = setupTestServer({ endpointGroups: ['health'] }); + +describe('DB health check', () => { + it('should return ok when DB is connected and migrated', async () => { + const response = await testServer.restlessAgent.get('/healthz/db'); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + + it('should return error when DB is not connected', async () => { + await testDb.terminate(); + + const response = await testServer.restlessAgent.get('/healthz/db'); + + expect(response.statusCode).toBe(500); + expect(response.body).toEqual({ status: 'error' }); + }); +}); diff --git a/packages/cli/test/integration/shared/test-db.ts b/packages/cli/test/integration/shared/test-db.ts index 06b1adb962500..1bb6944911944 100644 --- a/packages/cli/test/integration/shared/test-db.ts +++ b/packages/cli/test/integration/shared/test-db.ts @@ -39,11 +39,16 @@ export async function init() { await Db.migrate(); } +export function isReady() { + return Db.connectionState.connected && Db.connectionState.migrated; +} + /** * Drop test DB, closing bootstrap connection if existing. */ export async function terminate() { await Db.close(); + Db.connectionState.connected = false; } // Can't use `Object.keys(entities)` here because some entities have a `Entity` suffix, while the repositories don't diff --git a/packages/cli/test/integration/shared/types.ts b/packages/cli/test/integration/shared/types.ts index 60fb6860824c3..e285c5aab67e1 100644 --- a/packages/cli/test/integration/shared/types.ts +++ b/packages/cli/test/integration/shared/types.ts @@ -10,6 +10,7 @@ import type { LicenseMocker } from './license'; import type { Project } from '@/databases/entities/project'; type EndpointGroup = + | 'health' | 'me' | 'users' | 'auth' @@ -54,6 +55,7 @@ export interface TestServer { authAgentFor: (user: User) => TestAgent; publicApiAgentFor: (user: User) => TestAgent; authlessAgent: TestAgent; + restlessAgent: TestAgent; license: LicenseMocker; } diff --git a/packages/cli/test/integration/shared/utils/test-server.ts b/packages/cli/test/integration/shared/utils/test-server.ts index a3cac9b4134c8..5fc4ffbede505 100644 --- a/packages/cli/test/integration/shared/utils/test-server.ts +++ b/packages/cli/test/integration/shared/utils/test-server.ts @@ -44,9 +44,16 @@ function prefix(pathSegment: string) { } const browserId = 'test-browser-id'; -function createAgent(app: express.Application, options?: { auth: boolean; user: User }) { +function createAgent( + app: express.Application, + options?: { auth: boolean; user?: User; noRest?: boolean }, +) { const agent = request.agent(app); - void agent.use(prefix(REST_PATH_SEGMENT)); + + const withRestSegment = !options?.noRest; + + if (withRestSegment) void agent.use(prefix(REST_PATH_SEGMENT)); + if (options?.auth && options?.user) { const token = Container.get(AuthService).issueJWT(options.user, browserId); agent.jar.setCookie(`${AUTH_COOKIE_NAME}=${token}`); @@ -89,6 +96,7 @@ export const setupTestServer = ({ httpServer: app.listen(0), authAgentFor: (user: User) => createAgent(app, { auth: true, user }), authlessAgent: createAgent(app), + restlessAgent: createAgent(app, { auth: false, noRest: true }), publicApiAgentFor: (user) => publicApiAgent(app, { user }), license: new LicenseMocker(), }; @@ -119,6 +127,13 @@ export const setupTestServer = ({ app.use(...apiRouters); } + if (endpointGroups?.includes('health')) { + app.get('/healthz/db', async (_req, res) => { + testDb.isReady() + ? res.status(200).send({ status: 'ok' }) + : res.status(500).send({ status: 'error' }); + }); + } if (endpointGroups.length) { for (const group of endpointGroups) { switch (group) { From e4d281e27c1af37221ceb429c5fce1d510715cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 4 Sep 2024 17:08:28 +0200 Subject: [PATCH 2/2] Rename --- packages/cli/src/abstract-server.ts | 4 ++-- packages/cli/src/commands/worker.ts | 4 ++-- ...healthcheck.test.ts => healthcheck.controller.test.ts} | 8 ++++---- packages/cli/test/integration/shared/utils/test-server.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) rename packages/cli/test/integration/{healthcheck.test.ts => healthcheck.controller.test.ts} (85%) diff --git a/packages/cli/src/abstract-server.ts b/packages/cli/src/abstract-server.ts index 2f3eefce85bf2..46d10f71af171 100644 --- a/packages/cli/src/abstract-server.ts +++ b/packages/cli/src/abstract-server.ts @@ -124,10 +124,10 @@ export abstract class AbstractServer { res.send({ status: 'ok' }); }); - this.app.get('/healthz/db', async (_req, res) => { + this.app.get('/healthz/readiness', async (_req, res) => { return Db.connectionState.connected && Db.connectionState.migrated ? res.status(200).send({ status: 'ok' }) - : res.status(500).send({ status: 'error' }); + : res.status(503).send({ status: 'error' }); }); const { connectionState } = Db; diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index 5d7496578c01c..c1cff69330216 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -172,10 +172,10 @@ export class Worker extends BaseCommand { const server = http.createServer(app); - app.get('/healthz/db', async (_req, res) => { + app.get('/healthz/readiness', async (_req, res) => { return Db.connectionState.connected && Db.connectionState.migrated ? res.status(200).send({ status: 'ok' }) - : res.status(500).send({ status: 'error' }); + : res.status(503).send({ status: 'error' }); }); app.get( diff --git a/packages/cli/test/integration/healthcheck.test.ts b/packages/cli/test/integration/healthcheck.controller.test.ts similarity index 85% rename from packages/cli/test/integration/healthcheck.test.ts rename to packages/cli/test/integration/healthcheck.controller.test.ts index 319bba3a3ea8c..61a8f164bb18b 100644 --- a/packages/cli/test/integration/healthcheck.test.ts +++ b/packages/cli/test/integration/healthcheck.controller.test.ts @@ -3,9 +3,9 @@ import { setupTestServer } from '@test-integration/utils'; const testServer = setupTestServer({ endpointGroups: ['health'] }); -describe('DB health check', () => { +describe('HealthcheckController', () => { it('should return ok when DB is connected and migrated', async () => { - const response = await testServer.restlessAgent.get('/healthz/db'); + const response = await testServer.restlessAgent.get('/healthz/readiness'); expect(response.statusCode).toBe(200); expect(response.body).toEqual({ status: 'ok' }); @@ -14,9 +14,9 @@ describe('DB health check', () => { it('should return error when DB is not connected', async () => { await testDb.terminate(); - const response = await testServer.restlessAgent.get('/healthz/db'); + const response = await testServer.restlessAgent.get('/healthz/readiness'); - expect(response.statusCode).toBe(500); + expect(response.statusCode).toBe(503); expect(response.body).toEqual({ status: 'error' }); }); }); diff --git a/packages/cli/test/integration/shared/utils/test-server.ts b/packages/cli/test/integration/shared/utils/test-server.ts index 5fc4ffbede505..b2b86daefcf3c 100644 --- a/packages/cli/test/integration/shared/utils/test-server.ts +++ b/packages/cli/test/integration/shared/utils/test-server.ts @@ -128,10 +128,10 @@ export const setupTestServer = ({ } if (endpointGroups?.includes('health')) { - app.get('/healthz/db', async (_req, res) => { + app.get('/healthz/readiness', async (_req, res) => { testDb.isReady() ? res.status(200).send({ status: 'ok' }) - : res.status(500).send({ status: 'error' }); + : res.status(503).send({ status: 'error' }); }); } if (endpointGroups.length) {