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
for (const component of this.components) {
if (component.initRoutes) {
@ -234,6 +229,11 @@ export default abstract class Application implements Extendable<ApplicationCompo
component.setCurrentRouter(null);
}
// Start components
for (const component of this.components) {
await component.start?.(app);
}
// Routes
this.routes(initRouter, handleRouter);
@ -373,6 +373,10 @@ export default abstract class Application implements Extendable<ApplicationCompo
return this.cacheProvider || null;
}
public getComponents(): ApplicationComponent[] {
return [...this.components];
}
public as<C extends ApplicationComponent | WebSocketListener<Application>>(type: Type<C>): C {
const module = this.components.find(component => component.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 start?(expressApp: Express): Promise<void>;
public async initRoutes?(router: Router): Promise<void>;
public async handleRoutes?(router: Router): Promise<void>;
public async start?(expressApp: Express): Promise<void>;
public async stop?(): Promise<void>;
public isReady(): boolean {
return true;
}
protected async prepare(name: string, prepare: () => Promise<void>): Promise<void> {
let err;
do {

View File

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

View File

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

View File

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

View File

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