Use maintenance component to throw 503s when some components are unavailable
This commit is contained in:
parent
a3ebf46b54
commit
c9fed2d873
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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(
|
||||||
|
@ -1,35 +1,28 @@
|
|||||||
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()) {
|
||||||
res.locals.refresh_after = 30;
|
if (!component.isReady()) {
|
||||||
throw new ServiceUnavailableHttpError(`${config.get('app.name')} is unavailable due to failure of dependent services.`);
|
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();
|
next();
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user