Skip to content

Commit

Permalink
feat(core): Introduce DB health check (#10661)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivov authored Sep 5, 2024
1 parent 3a80780 commit a8e80d0
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 3 deletions.
8 changes: 7 additions & 1 deletion packages/cli/src/abstract-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/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;
this.app.use((_req, res, next) => {
if (connectionState.connected) {
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/commands/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ export class Worker extends BaseCommand {

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(
'/healthz',

Expand Down
22 changes: 22 additions & 0 deletions packages/cli/test/integration/healthcheck.controller.test.ts
Original file line number Diff line number Diff line change
@@ -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' });
});
});
5 changes: 5 additions & 0 deletions packages/cli/test/integration/shared/test-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/test/integration/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { LicenseMocker } from './license';
import type { Project } from '@/databases/entities/project';

type EndpointGroup =
| 'health'
| 'me'
| 'users'
| 'auth'
Expand Down Expand Up @@ -54,6 +55,7 @@ export interface TestServer {
authAgentFor: (user: User) => TestAgent;
publicApiAgentFor: (user: User) => TestAgent;
authlessAgent: TestAgent;
restlessAgent: TestAgent;
license: LicenseMocker;
}

Expand Down
19 changes: 17 additions & 2 deletions packages/cli/test/integration/shared/utils/test-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down Expand Up @@ -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(),
};
Expand Down Expand Up @@ -119,6 +127,13 @@ export const setupTestServer = ({
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) {
for (const group of endpointGroups) {
switch (group) {
Expand Down

0 comments on commit a8e80d0

Please sign in to comment.