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:
parent
9cb27364a0
commit
e4c322d633
@ -10,9 +10,9 @@ export const readyCheckSchema = {
|
|||||||
properties: {
|
properties: {
|
||||||
health: {
|
health: {
|
||||||
description:
|
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',
|
type: 'string',
|
||||||
enum: ['GOOD', 'BAD'],
|
enum: ['GOOD'],
|
||||||
example: 'GOOD',
|
example: 'GOOD',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
|
import type { PoolClient } from 'pg';
|
||||||
import type { IUnleashConfig } from '../types/option.js';
|
import type { IUnleashConfig } from '../types/option.js';
|
||||||
import type { IUnleashServices } from '../services/index.js';
|
import type { IUnleashServices } from '../services/index.js';
|
||||||
import type { Db } from '../db/db.js';
|
import type { Db } from '../db/db.js';
|
||||||
@ -8,6 +9,7 @@ import Controller from './controller.js';
|
|||||||
import { NONE } from '../types/permissions.js';
|
import { NONE } from '../types/permissions.js';
|
||||||
import { createResponseSchema } from '../openapi/util/create-response-schema.js';
|
import { createResponseSchema } from '../openapi/util/create-response-schema.js';
|
||||||
import type { ReadyCheckSchema } from '../openapi/spec/ready-check-schema.js';
|
import type { ReadyCheckSchema } from '../openapi/spec/ready-check-schema.js';
|
||||||
|
import { emptyResponse, parseEnvVarNumber } from '../server-impl.js';
|
||||||
|
|
||||||
export class ReadyCheckController extends Controller {
|
export class ReadyCheckController extends Controller {
|
||||||
private logger: Logger;
|
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.).',
|
'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: {
|
responses: {
|
||||||
200: createResponseSchema('readyCheckSchema'),
|
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> {
|
async getReady(_: Request, res: Response<ReadyCheckSchema>): Promise<void> {
|
||||||
if (this.config.checkDbOnReady && this.db) {
|
if (this.config.checkDbOnReady && this.db) {
|
||||||
try {
|
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' });
|
res.status(200).json({ health: 'GOOD' });
|
||||||
return;
|
return;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.warn('Database readiness check failed', err);
|
this.logger.warn('Database readiness check failed', err);
|
||||||
res.status(500).json({ health: 'BAD' });
|
res.status(503).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ health: 'GOOD' });
|
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');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 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;
|
let db: ITestDb;
|
||||||
|
|
||||||
beforeAll(async () => {
|
describe('DB is up', () => {
|
||||||
db = await dbInit('ready_api', getLogger);
|
beforeAll(async () => {
|
||||||
stores = db.stores;
|
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 () => {
|
describe('DB is down', () => {
|
||||||
await db.destroy();
|
beforeAll(async () => {
|
||||||
});
|
db = await dbInit();
|
||||||
|
});
|
||||||
|
|
||||||
test('returns readiness good', async () => {
|
test('when checkDb is disabled, returns readiness good', async () => {
|
||||||
expect.assertions(0);
|
const { request } = await setupAppWithCustomConfig(
|
||||||
const { request, destroy } = await setupApp(stores);
|
db.stores,
|
||||||
await request
|
undefined,
|
||||||
.get('/ready')
|
db.rawDatabase,
|
||||||
.expect('Content-Type', /json/)
|
);
|
||||||
.expect(200)
|
await db.destroy();
|
||||||
.expect('{"health":"GOOD"}');
|
await request
|
||||||
await destroy();
|
.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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user