feat(core): Introduce DB health check (#10661)

This commit is contained in:
Iván Ovejero 2024-09-05 11:04:48 +02:00 committed by GitHub
parent 3a8078068e
commit a8e80d0c4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 59 additions and 3 deletions

View file

@ -119,11 +119,17 @@ export abstract class AbstractServer {
protected setupPushServer() {} protected setupPushServer() {}
private async setupHealthCheck() { 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) => { this.app.get('/healthz', async (_req, res) => {
res.send({ status: 'ok' }); res.send({ status: 'ok' });
}); });
this.app.get('/healthz/readiness', async (_req, res) => {
return Db.connectionState.connected && Db.connectionState.migrated
? res.status(200).send({ status: 'ok' })
: res.status(503).send({ status: 'error' });
});
const { connectionState } = Db; const { connectionState } = Db;
this.app.use((_req, res, next) => { this.app.use((_req, res, next) => {
if (connectionState.connected) { if (connectionState.connected) {

View file

@ -172,6 +172,12 @@ export class Worker extends BaseCommand {
const server = http.createServer(app); const server = http.createServer(app);
app.get('/healthz/readiness', async (_req, res) => {
return Db.connectionState.connected && Db.connectionState.migrated
? res.status(200).send({ status: 'ok' })
: res.status(503).send({ status: 'error' });
});
app.get( app.get(
'/healthz', '/healthz',

View file

@ -0,0 +1,22 @@
import * as testDb from './shared/test-db';
import { setupTestServer } from '@test-integration/utils';
const testServer = setupTestServer({ endpointGroups: ['health'] });
describe('HealthcheckController', () => {
it('should return ok when DB is connected and migrated', async () => {
const response = await testServer.restlessAgent.get('/healthz/readiness');
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/readiness');
expect(response.statusCode).toBe(503);
expect(response.body).toEqual({ status: 'error' });
});
});

View file

@ -39,11 +39,16 @@ export async function init() {
await Db.migrate(); await Db.migrate();
} }
export function isReady() {
return Db.connectionState.connected && Db.connectionState.migrated;
}
/** /**
* Drop test DB, closing bootstrap connection if existing. * Drop test DB, closing bootstrap connection if existing.
*/ */
export async function terminate() { export async function terminate() {
await Db.close(); 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 // Can't use `Object.keys(entities)` here because some entities have a `Entity` suffix, while the repositories don't

View file

@ -10,6 +10,7 @@ import type { LicenseMocker } from './license';
import type { Project } from '@/databases/entities/project'; import type { Project } from '@/databases/entities/project';
type EndpointGroup = type EndpointGroup =
| 'health'
| 'me' | 'me'
| 'users' | 'users'
| 'auth' | 'auth'
@ -54,6 +55,7 @@ export interface TestServer {
authAgentFor: (user: User) => TestAgent; authAgentFor: (user: User) => TestAgent;
publicApiAgentFor: (user: User) => TestAgent; publicApiAgentFor: (user: User) => TestAgent;
authlessAgent: TestAgent; authlessAgent: TestAgent;
restlessAgent: TestAgent;
license: LicenseMocker; license: LicenseMocker;
} }

View file

@ -44,9 +44,16 @@ function prefix(pathSegment: string) {
} }
const browserId = 'test-browser-id'; 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); 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) { if (options?.auth && options?.user) {
const token = Container.get(AuthService).issueJWT(options.user, browserId); const token = Container.get(AuthService).issueJWT(options.user, browserId);
agent.jar.setCookie(`${AUTH_COOKIE_NAME}=${token}`); agent.jar.setCookie(`${AUTH_COOKIE_NAME}=${token}`);
@ -89,6 +96,7 @@ export const setupTestServer = ({
httpServer: app.listen(0), httpServer: app.listen(0),
authAgentFor: (user: User) => createAgent(app, { auth: true, user }), authAgentFor: (user: User) => createAgent(app, { auth: true, user }),
authlessAgent: createAgent(app), authlessAgent: createAgent(app),
restlessAgent: createAgent(app, { auth: false, noRest: true }),
publicApiAgentFor: (user) => publicApiAgent(app, { user }), publicApiAgentFor: (user) => publicApiAgent(app, { user }),
license: new LicenseMocker(), license: new LicenseMocker(),
}; };
@ -119,6 +127,13 @@ export const setupTestServer = ({
app.use(...apiRouters); app.use(...apiRouters);
} }
if (endpointGroups?.includes('health')) {
app.get('/healthz/readiness', async (_req, res) => {
testDb.isReady()
? res.status(200).send({ status: 'ok' })
: res.status(503).send({ status: 'error' });
});
}
if (endpointGroups.length) { if (endpointGroups.length) {
for (const group of endpointGroups) { for (const group of endpointGroups) {
switch (group) { switch (group) {