1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

Checking for readiness tests

This commit is contained in:
Gastón Fournier 2025-10-23 16:18:17 +02:00
parent 9cb27364a0
commit e4c322d633
No known key found for this signature in database
GPG Key ID: AF45428626E17A8E
4 changed files with 98 additions and 68 deletions

View File

@ -10,9 +10,9 @@ export const readyCheckSchema = {
properties: {
health: {
description:
'The readiness state this Unleash instance is in. GOOD if the server is up and running. It never returns BAD; if the server is unhealthy you will get an unsuccessful http response.',
'The readiness state this Unleash instance is in. GOOD if the server is up and running. If the server is unhealthy you will get an unsuccessful http response.',
type: 'string',
enum: ['GOOD', 'BAD'],
enum: ['GOOD'],
example: 'GOOD',
},
},

View File

@ -1,44 +0,0 @@
import supertest, { type Test } from 'supertest';
import { createServices } from '../services/index.js';
import { createTestConfig } from '../../test/config/test-config.js';
import createStores from '../../test/fixtures/store.js';
import getLogger from '../../test/fixtures/no-logger.js';
import getApp from '../app.js';
import type TestAgent from 'supertest/lib/agent.d.ts';
async function getSetup() {
const stores = createStores();
const config = createTestConfig();
const services = createServices(stores, config);
const app = await getApp(config, stores, services);
return {
request: supertest(app),
stores,
};
}
let request: TestAgent<Test>;
beforeEach(async () => {
const setup = await getSetup();
request = setup.request;
});
afterEach(() => {
getLogger.setMuteError(false);
});
test('should give 200 when ready', async () => {
await request.get('/ready').expect(200);
});
test('should give health=GOOD when ready', async () => {
expect.assertions(2);
await request
.get('/ready')
.expect(200)
.expect((res) => {
expect(res.status).toBe(200);
expect(res.body.health).toBe('GOOD');
});
});

View File

@ -1,4 +1,5 @@
import type { Request, Response } from 'express';
import type { PoolClient } from 'pg';
import type { IUnleashConfig } from '../types/option.js';
import type { IUnleashServices } from '../services/index.js';
import type { Db } from '../db/db.js';
@ -8,6 +9,7 @@ import Controller from './controller.js';
import { NONE } from '../types/permissions.js';
import { createResponseSchema } from '../openapi/util/create-response-schema.js';
import type { ReadyCheckSchema } from '../openapi/spec/ready-check-schema.js';
import { emptyResponse, parseEnvVarNumber } from '../server-impl.js';
export class ReadyCheckController extends Controller {
private logger: Logger;
@ -37,7 +39,7 @@ export class ReadyCheckController extends Controller {
'This operation returns information about whether this Unleash instance is ready to serve requests or not. Typically used by your deployment orchestrator (e.g. Kubernetes, Docker Swarm, Mesos, et al.).',
responses: {
200: createResponseSchema('readyCheckSchema'),
500: createResponseSchema('readyCheckSchema'),
503: emptyResponse,
},
}),
],
@ -47,16 +49,53 @@ export class ReadyCheckController extends Controller {
async getReady(_: Request, res: Response<ReadyCheckSchema>): Promise<void> {
if (this.config.checkDbOnReady && this.db) {
try {
await this.db.raw('select 1');
const timeoutMs = parseEnvVarNumber(
process.env.DATABASE_STATEMENT_TIMEOUT_MS,
200,
);
await this.runReadinessQuery(timeoutMs);
res.status(200).json({ health: 'GOOD' });
return;
} catch (err: any) {
this.logger.warn('Database readiness check failed', err);
res.status(500).json({ health: 'BAD' });
res.status(503).end();
return;
}
}
res.status(200).json({ health: 'GOOD' });
}
private async runReadinessQuery(timeoutMs: number): Promise<void> {
const client = getKnexClient(this.db!);
const pendingAcquire = client.pool.acquire();
const abortDelay = setTimeout(() => pendingAcquire.abort(), timeoutMs);
let connection: PoolClient | undefined;
try {
connection = (await pendingAcquire.promise) as PoolClient;
} finally {
clearTimeout(abortDelay);
}
try {
connection.query({
text: 'SELECT 1',
});
} finally {
if (connection) {
await client.releaseConnection(connection);
}
}
}
}
function getKnexClient(db: Db): Db['client'] {
if (db.client?.pool && typeof db.client.releaseConnection === 'function') {
return db.client as Db['client'];
}
throw new Error('Unsupported database handle for readiness check');
}

View File

@ -1,27 +1,62 @@
import { setupApp } from './helpers/test-helper.js';
import { setupAppWithCustomConfig } from './helpers/test-helper.js';
import dbInit, { type ITestDb } from './helpers/database-init.js';
import getLogger from '../fixtures/no-logger.js';
import type { IUnleashStores } from '../../lib/types/index.js';
let stores: IUnleashStores;
let db: ITestDb;
beforeAll(async () => {
db = await dbInit('ready_api', getLogger);
stores = db.stores;
describe('DB is up', () => {
beforeAll(async () => {
db = await dbInit();
});
test('when checkDb is disabled, returns ready', async () => {
const { request } = await setupAppWithCustomConfig(
db.stores,
undefined,
db.rawDatabase,
);
await request
.get('/ready')
.expect('Content-Type', /json/)
.expect(200)
.expect('{"health":"GOOD"}');
});
test('when checkDb is enabled, returns ready', async () => {
const { request } = await setupAppWithCustomConfig(
db.stores,
{ checkDbOnReady: true },
db.rawDatabase,
);
await request.get('/ready').expect(200).expect('{"health":"GOOD"}');
});
});
afterAll(async () => {
await db.destroy();
});
describe('DB is down', () => {
beforeAll(async () => {
db = await dbInit();
});
test('returns readiness good', async () => {
expect.assertions(0);
const { request, destroy } = await setupApp(stores);
await request
.get('/ready')
.expect('Content-Type', /json/)
.expect(200)
.expect('{"health":"GOOD"}');
await destroy();
test('when checkDb is disabled, returns readiness good', async () => {
const { request } = await setupAppWithCustomConfig(
db.stores,
undefined,
db.rawDatabase,
);
await db.destroy();
await request
.get('/ready')
.expect('Content-Type', /json/)
.expect(200)
.expect('{"health":"GOOD"}');
});
test('when checkDb is enabled, fails readiness check', async () => {
const { request } = await setupAppWithCustomConfig(
db.stores,
{ checkDbOnReady: true },
db.rawDatabase,
);
await db.destroy();
await request.get('/ready').expect(503);
});
});