Skip to content

Commit 0dc8290

Browse files
committed
Add gatekeeper DB liveness readiness
1 parent 169ffe8 commit 0dc8290

13 files changed

Lines changed: 249 additions & 2 deletions

File tree

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const config = {
4040
'^\\.\\/abstract-json\\.js$': '<rootDir>/packages/gatekeeper/src/db/abstract-json.ts',
4141
'^\\.\\/db-utils\\.js$': '<rootDir>/services/search-server/src/db/db-utils.ts',
4242
'^\\.\\/did-resolver\\.js$': '<rootDir>/packages/gatekeeper/src/did-resolver.ts',
43+
'^\\.\\/health\\.js$': '<rootDir>/packages/gatekeeper/src/db/health.ts',
4344
'^\\.\\/index-export\\.js$': '<rootDir>/packages/gatekeeper/src/db/index-export.ts',
4445
'^\\.\\/index-helpers\\.js$': '<rootDir>/services/search-server/src/index-helpers.ts',
4546
'^\\.\\/projections\\.js$': '<rootDir>/services/search-server/src/projections.ts',

packages/gatekeeper/src/db/abstract-json.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ export abstract class AbstractJson implements GatekeeperDb {
6666
return;
6767
}
6868

69+
async isReady(): Promise<boolean> {
70+
return true;
71+
}
72+
6973
private splitSuffix(did: string): string {
7074
if (!did) {
7175
throw new InvalidDIDError();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export const DB_HEALTH_TIMEOUT_MS = 1_000;
2+
3+
export async function withHealthCheckTimeout<T>(
4+
check: Promise<T>,
5+
message: string,
6+
timeoutMs: number = DB_HEALTH_TIMEOUT_MS
7+
): Promise<T> {
8+
let timeoutId: NodeJS.Timeout | undefined;
9+
10+
try {
11+
return await Promise.race([
12+
check,
13+
new Promise<T>((_, reject) => {
14+
timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
15+
}),
16+
]);
17+
}
18+
finally {
19+
if (timeoutId) {
20+
clearTimeout(timeoutId);
21+
}
22+
}
23+
}

packages/gatekeeper/src/db/mongo.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
normalizeIndexExportLimit,
2020
parseIndexExportCursor
2121
} from './index-export.js';
22+
import { DB_HEALTH_TIMEOUT_MS, withHealthCheckTimeout } from './health.js';
2223

2324
interface DidsDoc {
2425
id: string
@@ -105,6 +106,26 @@ export default class DbMongo implements GatekeeperDb {
105106
}
106107
}
107108

109+
async isReady(): Promise<boolean> {
110+
if (!this.client || !this.db) {
111+
return false;
112+
}
113+
114+
try {
115+
await withHealthCheckTimeout(
116+
this.client.db('admin').command(
117+
{ ping: 1 },
118+
{ timeoutMS: DB_HEALTH_TIMEOUT_MS }
119+
),
120+
'Mongo readiness check timed out'
121+
);
122+
return true;
123+
}
124+
catch {
125+
return false;
126+
}
127+
}
128+
108129
async resetDb(): Promise<void> {
109130
if (!this.db) {
110131
throw new Error(MONGO_NOT_STARTED_ERROR)

packages/gatekeeper/src/db/postgres.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
normalizeIndexExportLimit,
1818
parseIndexExportCursor
1919
} from './index-export.js';
20+
import { withHealthCheckTimeout } from './health.js';
2021

2122
interface EventRow {
2223
event: GatekeeperEvent | string | null;
@@ -263,6 +264,23 @@ export default class DbPostgres implements GatekeeperDb {
263264
}
264265
}
265266

267+
async isReady(): Promise<boolean> {
268+
if (!this.pool) {
269+
return false;
270+
}
271+
272+
try {
273+
await withHealthCheckTimeout(
274+
this.pool.query('SELECT 1'),
275+
'Postgres readiness check timed out'
276+
);
277+
return true;
278+
}
279+
catch {
280+
return false;
281+
}
282+
}
283+
266284
async resetDb(): Promise<void> {
267285
await this.withTx(async client => {
268286
await client.query('DELETE FROM gatekeeper_events WHERE namespace = $1', [this.dbName]);

packages/gatekeeper/src/db/redis.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
normalizeIndexExportLimit,
2020
parseIndexExportCursor
2121
} from './index-export.js';
22+
import { withHealthCheckTimeout } from './health.js';
2223

2324
const REDIS_NOT_STARTED_ERROR = 'Redis not started. Call start() first.';
2425
const log = childLogger({ service: 'gatekeeper-db', module: 'redis' });
@@ -71,6 +72,26 @@ export default class DbRedis implements GatekeeperDb {
7172
}
7273
}
7374

75+
async isReady(): Promise<boolean> {
76+
if (!this.redis) {
77+
return false;
78+
}
79+
if (this.redis.status !== 'ready') {
80+
return false;
81+
}
82+
83+
try {
84+
const response = await withHealthCheckTimeout(
85+
this.redis.ping(),
86+
'Redis readiness check timed out'
87+
);
88+
return response === 'PONG';
89+
}
90+
catch {
91+
return false;
92+
}
93+
}
94+
7495
private logRedisConnectionError(error: unknown): void {
7596
const now = Date.now();
7697
if (now - this.lastRedisErrorLogAt < 60_000) {

packages/gatekeeper/src/db/sqlite.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,20 @@ export default class DbSqlite implements GatekeeperDb {
149149
}
150150
}
151151

152+
async isReady(): Promise<boolean> {
153+
if (!this.db) {
154+
return false;
155+
}
156+
157+
try {
158+
await this.db.get('SELECT 1');
159+
return true;
160+
}
161+
catch {
162+
return false;
163+
}
164+
}
165+
152166

153167
async resetDb(): Promise<void> {
154168
if (!this.db) {

packages/gatekeeper/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export type IndexExportResponse = IndexExportSnapshotResponse | IndexExportChang
191191
export interface GatekeeperDb {
192192
start(): Promise<void>;
193193
stop(): Promise<void>;
194+
isReady(): Promise<boolean>;
194195
resetDb(): Promise<void | number | JsonDbFile>;
195196
addEvent(did: string, event: GatekeeperEvent): Promise<void | number>;
196197
getEvents(did: string): Promise<GatekeeperEvent[]>;

services/gatekeeper/server/src/gatekeeper-api.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
formatBytes,
2626
formatDuration,
2727
isRateLimitWhitelistedRequest,
28+
isGatekeeperReady,
2829
logRequest,
2930
parseIndexExportRequest,
3031
rateLimitWindowUnits,
@@ -170,7 +171,8 @@ let serverReady = false;
170171
*/
171172
v1router.get('/ready', async (req, res) => {
172173
try {
173-
res.json(serverReady);
174+
const ready = await isGatekeeperReady(serverReady, db);
175+
res.status(ready ? 200 : 503).json(ready);
174176
} catch (error: any) {
175177
res.status(500).send(error.toString());
176178
}

services/gatekeeper/server/src/helpers.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { NextFunction, Request, Response } from 'express';
22
import { BlockList, isIP } from 'net';
33

4-
import type { IndexExportRequest } from '@mdip/gatekeeper/types';
4+
import type { GatekeeperDb, IndexExportRequest } from '@mdip/gatekeeper/types';
55
import { childLogger } from '@mdip/common/logger';
66

77
const log = childLogger({ service: 'gatekeeper-server' });
@@ -129,6 +129,22 @@ export function shouldSkipRateLimitPath(req: Request, skipPaths: string[]): bool
129129
pathOnly === skipPath || pathOnly.startsWith(`${skipPath}/`));
130130
}
131131

132+
export async function isGatekeeperReady(
133+
serverReady: boolean,
134+
db: Pick<GatekeeperDb, 'isReady'>
135+
): Promise<boolean> {
136+
if (!serverReady) {
137+
return false;
138+
}
139+
140+
try {
141+
return await db.isReady();
142+
}
143+
catch {
144+
return false;
145+
}
146+
}
147+
132148
export function parseOptionalString(value: unknown, fieldName: string): string | null | undefined {
133149
if (value === undefined) {
134150
return undefined;

0 commit comments

Comments
 (0)