Просмотр исходного кода

initialize maridb-controller instances per request and timer; removed old onError handlers

Christian Kahlau 2 лет назад
Родитель
Сommit
6b8e87081a

+ 3 - 2
server/src/ctrl/controller-pool.interface.ts

@@ -1,9 +1,10 @@
-import { DataProvider } from './data-provider.interface';
+import { Pool } from 'mysql';
+
 import { HttpCheckController } from './http-check-controller.class';
 import { ServerConnector } from './server-connector.class';
 
 export interface ControllerPool {
-  db: DataProvider;
+  connectionPool: Pool;
   serverConnector: ServerConnector;
   httpChecks: HttpCheckController;
 }

+ 0 - 1
server/src/ctrl/data-provider.interface.ts

@@ -3,7 +3,6 @@ import { ServiceChangedStatus } from '../lib/service-changed-status.enum';
 import { HealthCheckDataProvider } from './health-check-data-provider.interface';
 
 export interface DataProvider extends HealthCheckDataProvider {
-  set onError(listener: (error: any) => void);
   open(migrate?: boolean): Promise<void>;
   getAllServerConfigs(): Promise<Server[]>;
   insertServerData(serverID: number, data: ReducedData[]): Promise<void>;

+ 5 - 4
server/src/ctrl/http-check-controller.class.ts

@@ -1,5 +1,6 @@
 import axios, { AxiosError, AxiosRequestConfig } from 'axios';
 import moment from 'moment';
+import { Pool } from 'mysql';
 
 import defaults from '../../../common/defaults.module';
 import { HttpCheckStatus } from '../../../common/lib/http-check-data.module';
@@ -7,19 +8,19 @@ import { Logger } from '../../../common/util/logger.class';
 
 import { ServiceChangedStatus } from '../lib/service-changed-status.enum';
 import { Timer } from '../timer.class';
-import { SQLiteDatabase } from './sqlite-database.class';
 import { FCMController } from './fcm-controller.class';
 import { HealthCheckDataProvider } from './health-check-data-provider.interface';
+import { MariaDBDatabase } from './mariadb-database.class';
 
 type Subscriber = { id: number; interval: number; conf: HttpCheckConfig };
 type ContentCheckError = { type: 'contentCheck'; status: HttpCheckStatus; message: string };
 
 export class HttpCheckController {
   private subscriptions: Array<Subscriber> = [];
-  private db!: SQLiteDatabase;
+  private db!: MariaDBDatabase;
 
-  constructor() {
-    this.db = new SQLiteDatabase();
+  constructor(pool: Pool) {
+    this.db = new MariaDBDatabase(pool);
     (async () => {
       try {
         await this.db.open();

+ 2 - 8
server/src/ctrl/mariadb-connector.class.ts

@@ -5,14 +5,9 @@ const MAX_CONNECTION_RETRIES = 60;
 
 export class MariaDBConnector {
   private _conn?: PoolConnection;
-  private _onError = (err: any) => {};
 
   constructor(private pool: Pool) {}
 
-  public set onError(e: (err: any) => void) {
-    this._onError = e;
-  }
-
   public query(options: string | QueryOptions, values: any): Promise<Array<object | object[]>> {
     return new Promise(async (resolve, reject) => {
       try {
@@ -62,7 +57,7 @@ export class MariaDBConnector {
     let lasterror: any | null = null;
     while (retries <= MAX_CONNECTION_RETRIES) {
       try {
-        Logger.info(`[INFO] Connecting mariadb connection pool (${retries}/${MAX_CONNECTION_RETRIES}) ...`);
+        Logger.debug(`[DEBUG] Connecting mariadb connection pool (${retries}/${MAX_CONNECTION_RETRIES}) ...`);
         await this._connect();
         lasterror = null;
         break;
@@ -83,8 +78,6 @@ export class MariaDBConnector {
           return this.pool.getConnection((err, conn) => {
             if (err) return reject(err);
             this._conn = conn;
-            this._conn.on('error', this._onError);
-
             resolve();
           });
         }
@@ -99,6 +92,7 @@ export class MariaDBConnector {
     return new Promise(async (resolve, reject) => {
       try {
         if (this._conn && ['connected', 'authenticated'].includes(this._conn?.state)) {
+          Logger.debug(`[DEBUG] Closing mariadb connection.`);
           this._conn.release();
         }
         resolve();

+ 4 - 6
server/src/ctrl/mariadb-database.class.ts

@@ -15,19 +15,13 @@ import { MariaDBConnector } from './mariadb-connector.class';
 
 export class MariaDBDatabase implements DataProvider {
   private db: MariaDBConnector;
-  private _onError: (error: any) => void = err => console.error('[DB.ONERROR]', err);
 
   constructor(pool: Pool) {
     this.db = new MariaDBConnector(pool);
   }
 
-  public set onError(listener: (error: any) => void) {
-    this._onError = listener;
-  }
-
   public async open(migrate = false) {
     try {
-      this.db.onError = this._onError.bind(this);
       await this.db.connect();
 
       Logger.info('[INFO] Opened MariaDB Connection');
@@ -630,4 +624,8 @@ export class MariaDBDatabase implements DataProvider {
 
     return Object.keys(errors).length ? errors : null;
   }
+
+  public async close(): Promise<void> {
+    await this.db.close();
+  }
 }

+ 5 - 0
server/src/ctrl/mariadb-importer.class.ts

@@ -198,4 +198,9 @@ export class MariaDBImporter {
       pageSize = Math.min(pageSize, count - offset);
     }
   }
+
+  public async close(): Promise<void> {
+    await this.newDb.close();
+    await this.oldDb.close();
+  }
 }

+ 4 - 0
server/src/ctrl/mariadb-poolfactory.class.ts

@@ -22,4 +22,8 @@ export class MariaDBPoolFactory {
     };
     return createPool(connectionData);
   }
+
+  public static end(pool: Pool) {
+    return new Promise<void>((res, rej) => pool.end(err => (err ? rej(err) : res())));
+  }
 }

+ 5 - 4
server/src/ctrl/server-connector.class.ts

@@ -1,19 +1,20 @@
 import axios, { AxiosError } from 'axios';
 import moment from 'moment';
+import { Pool } from 'mysql';
 
 import defaults from '../../../common/defaults.module';
 import { Logger } from '../../../common/util/logger.class';
 
-import { SQLiteDatabase } from './sqlite-database.class';
 import { FCMController } from './fcm-controller.class';
 import { Timer } from '../timer.class';
+import { MariaDBDatabase } from './mariadb-database.class';
 
 export class ServerConnector {
   private subscriptions: Array<{ id: number; interval: number; server: Server }> = [];
-  private db!: SQLiteDatabase;
+  private db!: MariaDBDatabase;
 
-  constructor() {
-    this.db = new SQLiteDatabase();
+  constructor(pool: Pool) {
+    this.db = new MariaDBDatabase(pool);
     (async () => {
       try {
         await this.db.open();

+ 27 - 15
server/src/index.ts

@@ -1,6 +1,8 @@
 import fs from 'fs';
 import fsp from 'fs/promises';
 import path from 'path';
+import moment from 'moment';
+import { Pool } from 'mysql';
 
 import { Logger, LogLevel } from '../../common/util/logger.class';
 
@@ -12,7 +14,6 @@ import { MariaDBPoolFactory } from './ctrl/mariadb-poolfactory.class';
 import { ServerConnector } from './ctrl/server-connector.class';
 import { Webserver } from './webserver.class';
 import { Timer } from './timer.class';
-import moment from 'moment';
 
 const LOG_LEVEL: LogLevel = (process.env.LOG_LEVEL as LogLevel) || 'INFO';
 Logger.logLevel = LOG_LEVEL;
@@ -22,25 +23,28 @@ process.on('SIGQUIT', exitGracefully);
 process.on('SIGTERM', exitGracefully);
 
 let pool: ControllerPool;
+let db: MariaDBDatabase;
+let mig: MariaDBImporter;
+let connectionPool: Pool;
 (async () => {
-  const connPool = await MariaDBPoolFactory.createConnectionPool();
+  connectionPool = await MariaDBPoolFactory.createConnectionPool();
   const sqliteDir = path.resolve(process.cwd(), process.env.DATA_DIR || 'data');
   const doneFile = path.resolve(sqliteDir, 'import_done.txt');
 
   if (fs.existsSync(sqliteDir) && !fs.existsSync(doneFile)) {
-    const mig = new MariaDBImporter(connPool);
+    mig = new MariaDBImporter(connectionPool);
     await mig.connect();
     await mig.runImport();
     await fsp.writeFile(doneFile, moment().format('YYYY-MM-DD[T]HH:mm:ss.SSSZZ'), { encoding: 'utf-8' });
   }
 
-  const db = new MariaDBDatabase(connPool);
+  db = new MariaDBDatabase(connectionPool);
   await db.open();
 
   pool = {
-    db,
-    serverConnector: new ServerConnector(),
-    httpChecks: new HttpCheckController()
+    connectionPool,
+    serverConnector: new ServerConnector(connectionPool),
+    httpChecks: new HttpCheckController(connectionPool)
   };
 
   Timer.instance.start();
@@ -49,20 +53,28 @@ let pool: ControllerPool;
 
 async function exitGracefully(...args: any[]) {
   Logger.info(`[EXITING] Graceful exit, received ${JSON.stringify(args)}`);
-  if (pool) {
-    try {
-      Logger.info(`[EXITING] Tear down ServerConnector ...`);
+  try {
+    Logger.info(`[EXITING] Tear down ServerConnector ...`);
+    if (pool) {
       await pool.serverConnector.close();
 
       Logger.info(`[EXITING] Tear down HttpCheckController ...`);
       await pool.httpChecks.close();
+    }
 
-      Logger.info(`[EXITING] Tear down Main Database Controller ...`);
-      await pool.serverConnector.close();
-    } catch (err) {
-      Logger.error(`[ERROR] Tear down sequence failed:`, err);
-      process.exit(2);
+    if (mig) {
+      Logger.info(`[EXITING] Tear down MariaDBImporter instance ...`);
+      await mig.close();
     }
+
+    Logger.info(`[EXITING] Tear down Main Database Controller ...`);
+    if (db) await db.close();
+
+    Logger.info(`[EXITING] Tear down MariaDB Connection Pool ...`);
+    if (connectionPool) await MariaDBPoolFactory.end(connectionPool);
+  } catch (err) {
+    Logger.error(`[ERROR] Tear down sequence failed:`, err);
+    process.exit(2);
   }
   process.exit(0);
 }

+ 4 - 4
server/src/webhdl/server-api-handler.class.ts

@@ -13,7 +13,7 @@ export class ServerAPIHandler extends WebHandler {
 
     this.router.get('/', async (req, res, next) => {
       try {
-        const serverConfigs = await this.ctrlPool.db.getAllServerConfigs();
+        const serverConfigs = await req.db.getAllServerConfigs();
         res.send(serverConfigs);
       } catch (err) {
         next(err);
@@ -28,7 +28,7 @@ export class ServerAPIHandler extends WebHandler {
           throw new HttpStatusException(`Not a valid server id: ${req.params.serverID}`, 400);
         }
 
-        const serverDataTypes = await this.ctrlPool.db.getServerDataTypes(serverID);
+        const serverDataTypes = await req.db.getServerDataTypes(serverID);
         res.send(serverDataTypes);
       } catch (err) {
         next(err);
@@ -55,7 +55,7 @@ export class ServerAPIHandler extends WebHandler {
           throw new HttpStatusException("QueryParams 'start' and 'end' must be parseable dates or unix epoch timestamps (ms).", 400);
         }
 
-        const data = await this.ctrlPool.db.queryServerData(serverID, qType, start, end);
+        const data = await req.db.queryServerData(serverID, qType, start, end);
         res.send({
           start,
           end,
@@ -86,7 +86,7 @@ export class ServerAPIHandler extends WebHandler {
           throw new HttpStatusException("QueryParams 'start' and 'end' must be parseable dates or unix epoch timestamps (ms).", 400);
         }
 
-        const data = await this.ctrlPool.db.queryServerStats(serverID, qType, start, end);
+        const data = await req.db.queryServerStats(serverID, qType, start, end);
         res.send({
           start,
           end,

+ 6 - 6
server/src/webhdl/services-api-handler.class.ts

@@ -18,7 +18,7 @@ export class ServicesAPIHandler extends WebHandler {
     this.router.get('/:serverID', async (req, res, next) => {
       try {
         const serverID = this.validateNumber(req.params.serverID, 'server id');
-        const services = await this.ctrlPool.db.getHttpCheckConfigs(serverID);
+        const services = await req.db.getHttpCheckConfigs(serverID);
 
         res.send(services);
       } catch (err) {
@@ -30,7 +30,7 @@ export class ServicesAPIHandler extends WebHandler {
       try {
         const serverID = this.validateNumber(req.params.serverID, 'server id');
 
-        const result = await this.ctrlPool.db.saveHttpCheckConfig(serverID, req.body);
+        const result = await req.db.saveHttpCheckConfig(serverID, req.body);
 
         if (result.status !== ServiceChangedStatus.None) {
           await this.ctrlPool.httpChecks.updateCheck(result.status, result.result);
@@ -47,7 +47,7 @@ export class ServicesAPIHandler extends WebHandler {
         const serverID = this.validateNumber(req.params.serverID, 'server id');
         const serviceID = this.validateNumber(req.params.serviceID, 'service id');
 
-        const deleted = await this.ctrlPool.db.deleteHealthCheckConfig(serverID, serviceID);
+        const deleted = await req.db.deleteHealthCheckConfig(serverID, serviceID);
 
         if (deleted) {
           await this.ctrlPool.httpChecks.updateCheck(ServiceChangedStatus.Deactivated, { id: serviceID } as HttpCheckConfig);
@@ -64,7 +64,7 @@ export class ServicesAPIHandler extends WebHandler {
         const serverID = this.validateNumber(req.params.serverID, 'server id');
         const serviceID = this.validateNumber(req.params.serviceID, 'service id');
 
-        const result = await this.ctrlPool.db.getHttpCheckConfigByID(serverID, serviceID);
+        const result = await req.db.getHttpCheckConfigByID(serverID, serviceID);
 
         res.send(result);
       } catch (err) {
@@ -88,7 +88,7 @@ export class ServicesAPIHandler extends WebHandler {
           throw new HttpStatusException("QueryParams 'start' and 'end' must be parseable dates or unix epoch timestamps (ms).", 400);
         }
 
-        const data = await this.ctrlPool.db.queryServiceCheckData(serverID, serviceID, start, end);
+        const data = await req.db.queryServiceCheckData(serverID, serviceID, start, end);
         res.send({
           start,
           end,
@@ -115,7 +115,7 @@ export class ServicesAPIHandler extends WebHandler {
           throw new HttpStatusException("QueryParams 'start' and 'end' must be parseable dates or unix epoch timestamps (ms).", 400);
         }
 
-        const data = await this.ctrlPool.db.queryServiceCheckLogs(serverID, serviceID, start, end);
+        const data = await req.db.queryServiceCheckLogs(serverID, serviceID, start, end);
         res.send({
           start,
           end,

+ 38 - 0
server/src/webserver.class.ts

@@ -5,17 +5,55 @@ import { HttpStatusException } from '../../common/lib/http-status.exception';
 import { Logger } from '../../common/util/logger.class';
 
 import { ControllerPool } from './ctrl/controller-pool.interface';
+import { MariaDBDatabase } from './ctrl/mariadb-database.class';
 import { ValidationException } from './lib/validation-exception.class';
 import { FCMAPIHandler } from './webhdl/fcm-api-handler.class';
 import { ServerAPIHandler } from './webhdl/server-api-handler.class';
 import { ServicesAPIHandler } from './webhdl/services-api-handler.class';
 
+declare global {
+  namespace Express {
+    interface Request {
+      db: MariaDBDatabase;
+    }
+  }
+}
+
 export class Webserver {
   private app: Express;
 
   constructor(private port: number, ctrlPool: ControllerPool) {
     this.app = express();
 
+    // -- GLOBAL MIDDLEWARES --
+    // Create a DatabaseController with a connection from the pool for every request
+    this.app.use(async (req, res, next) => {
+      try {
+        req.db = new MariaDBDatabase(ctrlPool.connectionPool);
+        await req.db.open();
+        next();
+      } catch (e) {
+        next(e);
+      }
+    });
+    // Destroy DatabaseController & release pool connection
+    this.app.use((req, res, next) => {
+      const afterResponse = async () => {
+        res.removeListener('finish', afterResponse);
+        res.removeListener('close', afterResponse);
+        try {
+          await req.db.close();
+        } catch (error) {
+          console.error('[ERROR] Destroying DatabaseController failed:', error);
+        }
+      };
+
+      res.on('finish', afterResponse);
+      res.on('close', afterResponse);
+
+      next();
+    });
+
     const fcmApi = new FCMAPIHandler(ctrlPool);
     this.app.use('/fcm', fcmApi.router);
     const serverApi = new ServerAPIHandler(ctrlPool);