Use maintenance component to throw 503s when some components are unavailable

This commit is contained in:
Alice Gaudon 2021-05-13 16:26:27 +02:00
parent a3ebf46b54
commit c9fed2d873
6 changed files with 30 additions and 28 deletions

View File

@ -214,11 +214,6 @@ export default abstract class Application implements Extendable<ApplicationCompo
}); });
}); });
// Start components
for (const component of this.components) {
await component.start?.(app);
}
// Components routes // Components routes
for (const component of this.components) { for (const component of this.components) {
if (component.initRoutes) { if (component.initRoutes) {
@ -234,6 +229,11 @@ export default abstract class Application implements Extendable<ApplicationCompo
component.setCurrentRouter(null); component.setCurrentRouter(null);
} }
// Start components
for (const component of this.components) {
await component.start?.(app);
}
// Routes // Routes
this.routes(initRouter, handleRouter); this.routes(initRouter, handleRouter);
@ -373,6 +373,10 @@ export default abstract class Application implements Extendable<ApplicationCompo
return this.cacheProvider || null; return this.cacheProvider || null;
} }
public getComponents(): ApplicationComponent[] {
return [...this.components];
}
public as<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): C { public as<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): C {
const module = this.components.find(component => component.constructor === type) || const module = this.components.find(component => component.constructor === type) ||
Object.values(this.webSocketListeners).find(listener => listener.constructor === type); Object.values(this.webSocketListeners).find(listener => listener.constructor === type);

View File

@ -15,14 +15,18 @@ export default abstract class ApplicationComponent {
public async init?(): Promise<void>; public async init?(): Promise<void>;
public async start?(expressApp: Express): Promise<void>;
public async initRoutes?(router: Router): Promise<void>; public async initRoutes?(router: Router): Promise<void>;
public async handleRoutes?(router: Router): Promise<void>; public async handleRoutes?(router: Router): Promise<void>;
public async start?(expressApp: Express): Promise<void>;
public async stop?(): Promise<void>; public async stop?(): Promise<void>;
public isReady(): boolean {
return true;
}
protected async prepare(name: string, prepare: () => Promise<void>): Promise<void> { protected async prepare(name: string, prepare: () => Promise<void>): Promise<void> {
let err; let err;
do { do {

View File

@ -21,6 +21,7 @@ import FormHelperComponent from "./components/FormHelperComponent.js";
import FrontendToolsComponent from "./components/FrontendToolsComponent.js"; import FrontendToolsComponent from "./components/FrontendToolsComponent.js";
import LogRequestsComponent from "./components/LogRequestsComponent.js"; import LogRequestsComponent from "./components/LogRequestsComponent.js";
import MailComponent from "./components/MailComponent.js"; import MailComponent from "./components/MailComponent.js";
import MaintenanceComponent from "./components/MaintenanceComponent.js";
import MysqlComponent from "./components/MysqlComponent.js"; import MysqlComponent from "./components/MysqlComponent.js";
import PreviousUrlComponent from "./components/PreviousUrlComponent.js"; import PreviousUrlComponent from "./components/PreviousUrlComponent.js";
import RedisComponent from "./components/RedisComponent.js"; import RedisComponent from "./components/RedisComponent.js";
@ -79,6 +80,9 @@ export default class TestApp extends Application {
// Static files // Static files
this.use(new ServeStaticDirectoryComponent('public')); this.use(new ServeStaticDirectoryComponent('public'));
// Maintenance
this.use(new MaintenanceComponent());
// Dynamic views and routes // Dynamic views and routes
const intermediateDirectory = 'build'; const intermediateDirectory = 'build';
this.use(new FrontendToolsComponent( this.use(new FrontendToolsComponent(

View File

@ -1,36 +1,29 @@
import config from "config"; import config from "config";
import {NextFunction, Request, Response, Router} from "express"; import {NextFunction, Request, Response, Router} from "express";
import Application from "../Application.js";
import ApplicationComponent from "../ApplicationComponent.js"; import ApplicationComponent from "../ApplicationComponent.js";
import {ServiceUnavailableHttpError} from "../HttpError.js"; import {ServiceUnavailableHttpError} from "../HttpError.js";
export default class MaintenanceComponent extends ApplicationComponent { export default class MaintenanceComponent extends ApplicationComponent {
private readonly application: Application; public async initRoutes(router: Router): Promise<void> {
private readonly canServe: () => boolean;
public constructor(application: Application, canServe: () => boolean) {
super();
this.application = application;
this.canServe = canServe;
}
public async handleRoutes(router: Router): Promise<void> {
router.use((req: Request, res: Response, next: NextFunction) => { router.use((req: Request, res: Response, next: NextFunction) => {
if (res.headersSent) { if (res.headersSent) {
return next(); return next();
} }
if (!this.application.isReady()) { if (!this.getApp().isReady()) {
res.header({'Retry-After': 60}); res.header({'Retry-After': 60});
res.locals.refresh_after = 5; res.locals.refresh_after = 5;
throw new ServiceUnavailableHttpError(`${config.get('app.name')} is readying up. Please wait a few seconds...`); throw new ServiceUnavailableHttpError(`${config.get('app.name')} is readying up. Please wait a few seconds...`);
} }
if (!this.canServe()) { for (const component of this.getApp().getComponents()) {
if (!component.isReady()) {
res.header({'Retry-After': 60});
res.locals.refresh_after = 30; res.locals.refresh_after = 30;
throw new ServiceUnavailableHttpError(`${config.get('app.name')} is unavailable due to failure of dependent services.`); throw new ServiceUnavailableHttpError(`${config.get('app.name')} is unavailable due to failure of dependent services.`);
} }
}
next(); next();
}); });

View File

@ -12,7 +12,7 @@ export default class MysqlComponent extends ApplicationComponent {
await MysqlConnectionManager.endPool(); await MysqlConnectionManager.endPool();
} }
public canServe(): boolean { public isReady(): boolean {
return MysqlConnectionManager.isReady(); return MysqlConnectionManager.isReady();
} }

View File

@ -9,7 +9,7 @@ import {logger} from "../Logger.js";
export default class RedisComponent extends ApplicationComponent implements CacheProvider { export default class RedisComponent extends ApplicationComponent implements CacheProvider {
private redisClient?: RedisClient; private redisClient?: RedisClient;
private store?: Store; private store: Store = new RedisStore(this);
public async start(_app: Express): Promise<void> { public async start(_app: Express): Promise<void> {
this.redisClient = redis.createClient(config.get('redis.port'), config.get('redis.host'), { this.redisClient = redis.createClient(config.get('redis.port'), config.get('redis.host'), {
@ -18,8 +18,6 @@ export default class RedisComponent extends ApplicationComponent implements Cach
this.redisClient.on('error', (err: Error) => { this.redisClient.on('error', (err: Error) => {
logger.error(err, 'An error occurred with redis.'); logger.error(err, 'An error occurred with redis.');
}); });
this.store = new RedisStore(this);
} }
public async stop(): Promise<void> { public async stop(): Promise<void> {
@ -30,11 +28,10 @@ export default class RedisComponent extends ApplicationComponent implements Cach
} }
public getStore(): Store { public getStore(): Store {
if (!this.store) throw `Redis store was not initialized.`;
return this.store; return this.store;
} }
public canServe(): boolean { public isReady(): boolean {
return this.redisClient !== undefined && this.redisClient.connected; return this.redisClient !== undefined && this.redisClient.connected;
} }