Kaynağa Gözat

Server: refactored database connections to improve thread safety

Christian Kahlau 3 yıl önce
ebeveyn
işleme
01e0bc4e29

+ 0 - 4
server/src/ctrl/database.class.ts

@@ -488,8 +488,4 @@ export class Database extends SQLiteController {
 
     return Object.keys(errors).length ? errors : null;
   }
-
-  async close() {
-    return new Promise<void>((res, rej) => this.db.close(err => (err ? rej(err) : res())));
-  }
 }

+ 33 - 24
server/src/ctrl/http-check-controller.class.ts

@@ -7,15 +7,18 @@ import { Logger } from '../../../common/util/logger.class';
 import { Database, ServiceChangedStatus } from './database.class';
 import { Timer } from '../timer.class';
 
-type Subscriber = { id: number; interval: number; conf: HttpCheckConfig; db: Database };
+type Subscriber = { id: number; interval: number; conf: HttpCheckConfig };
 
 export class HttpCheckController {
   private subscriptions: Array<Subscriber> = [];
+  private db!: Database;
 
-  constructor(db: Database) {
+  constructor() {
+    this.db = new Database();
     (async () => {
       try {
-        const configs = await db.getHttpCheckConfigs();
+        await this.db.open();
+        const configs = await this.db.getHttpCheckConfigs();
 
         configs.forEach(async conf => {
           if (!conf) return;
@@ -25,7 +28,7 @@ export class HttpCheckController {
 
           process.nextTick(async () => {
             Logger.info('[INFO] Initial HTTP Service Check for', conf.title, '...');
-            await this.timerTick(conf, sub.db);
+            await this.timerTick(conf);
           });
         });
       } catch (err) {
@@ -48,38 +51,39 @@ export class HttpCheckController {
         await this.unscheduleCheck(subscriber);
         break;
       case ServiceChangedStatus.Rescheduled:
-        await this.unscheduleCheck(subscriber);
-        await this.scheduleCheck(conf);
+        await this.rescheduleCheck(conf, subscriber);
         break;
       default:
         break;
     }
   }
 
-  private async scheduleCheck(conf: HttpCheckConfig) {
-    const serverDB = new Database();
-    await serverDB.open();
-
+  private async scheduleCheck(conf: HttpCheckConfig, log = true) {
     let interval = Number(conf.interval);
     if (Number.isNaN(interval)) interval = defaults.serviceChecks.interval;
 
-    Logger.info(`[INFO] Starting HTTP Service Check Controller for "${conf.title}" with interval ${interval} seconds ...`);
-    const id = Timer.instance.subscribe(interval, async () => await this.timerTick(conf, serverDB));
-    const sub = { id, interval, conf, db: serverDB };
+    if (log) Logger.info(`[INFO] Starting HTTP Service Check Controller for "${conf.title}" with interval ${interval} seconds ...`);
+    const id = Timer.instance.subscribe(interval, async () => await this.timerTick(conf));
+    const sub = { id, interval, conf };
     this.subscriptions.push(sub);
     return sub;
   }
 
-  private async unscheduleCheck(sub?: Subscriber) {
+  private async rescheduleCheck(conf: HttpCheckConfig, sub?: Subscriber) {
+    Logger.info('[INFO] Rescheduling HTTP Service Check for', conf.title);
+    await this.unscheduleCheck(sub, false);
+    await this.scheduleCheck(conf, false);
+  }
+
+  private async unscheduleCheck(sub?: Subscriber, log = true) {
     if (!sub) return;
 
-    Logger.info('[INFO] Removing HTTP Service Check for', sub.conf.title);
+    if (log) Logger.info('[INFO] Removing HTTP Service Check for', sub.conf.title);
     Timer.instance.unsubscribe(sub.id);
-    await sub.db.close();
     this.subscriptions = this.subscriptions.filter(s => s.id !== sub.id);
   }
 
-  private async timerTick(conf: HttpCheckConfig, db: Database) {
+  private async timerTick(conf: HttpCheckConfig) {
     Logger.debug('[DEBUG] TICK', new Date(), JSON.stringify(conf));
 
     const now = new Date();
@@ -88,7 +92,7 @@ export class HttpCheckController {
       responseType: 'text'
     };
     try {
-      const current = await db.getHttpCheckConfigByID(conf.serverId ?? 0, conf.id);
+      const current = await this.db.getHttpCheckConfigByID(conf.serverId ?? 0, conf.id);
 
       if (!current) {
         Logger.warn(`[WARN] HealthCheckConfig(${conf.id}) not found in Database but still scheduled in Timer!`);
@@ -104,14 +108,14 @@ export class HttpCheckController {
         const reg = new RegExp(check, 'i');
         if (!reg.test(responseText)) {
           Logger.debug(`[DEBUG] Regular expression /${check}/i not found in response`);
-          await db.insertHealthCheckData(current.id, now, HttpCheckStatus.CheckFailed, `Regular expression /${check}/i not found in response`);
+          await this.db.insertHealthCheckData(current.id, now, HttpCheckStatus.CheckFailed, `Regular expression /${check}/i not found in response`);
           success = false;
         }
       }
 
       if (success) {
         Logger.debug(`[DEBUG] HTTP Service Check "${current.title}": OK.`);
-        await db.insertHealthCheckData(current.id, now, HttpCheckStatus.OK, 'OK');
+        await this.db.insertHealthCheckData(current.id, now, HttpCheckStatus.OK, 'OK');
       }
     } catch (err) {
       let log = false;
@@ -119,11 +123,11 @@ export class HttpCheckController {
         // err.code = 'ECONNREFUSED' | 'ECONNABORTED' | 'ERR_BAD_REQUEST' | 'ERR_BAD_RESPONSE' | ...?
         try {
           if (err.code === 'ECONNABORTED') {
-            await db.insertHealthCheckData(conf.id, now, HttpCheckStatus.Timeout, err.message);
+            await this.db.insertHealthCheckData(conf.id, now, HttpCheckStatus.Timeout, err.message);
           } else if (err.code && ['ERR_BAD_REQUEST', 'ERR_BAD_RESPONSE'].includes(err.code)) {
-            await db.insertHealthCheckData(conf.id, now, HttpCheckStatus.RequestFailed, `${err.response?.status} ${err.response?.statusText}`);
+            await this.db.insertHealthCheckData(conf.id, now, HttpCheckStatus.RequestFailed, `${err.response?.status} ${err.response?.statusText}`);
           } else {
-            await db.insertHealthCheckData(conf.id, now, HttpCheckStatus.RequestFailed, err.message);
+            await this.db.insertHealthCheckData(conf.id, now, HttpCheckStatus.RequestFailed, err.message);
           }
         } catch (insertErr) {
           Logger.error(`[ERROR] Inserting HealthCheckData on Error failed:`, insertErr);
@@ -131,7 +135,7 @@ export class HttpCheckController {
         }
       } else {
         try {
-          await db.insertHealthCheckData(conf.id, now, HttpCheckStatus.Unknown, new String(err).toString());
+          await this.db.insertHealthCheckData(conf.id, now, HttpCheckStatus.Unknown, new String(err).toString());
         } catch (insertErr) {
           Logger.error(`[ERROR] Inserting HealthCheckData on Error failed:`, insertErr);
         }
@@ -140,4 +144,9 @@ export class HttpCheckController {
       if (log) Logger.error('[ERROR] HTTP Service Check failed:', err);
     }
   }
+
+  async close() {
+    if (!this.db) return;
+    await this.db.close();
+  }
 }

+ 14 - 9
server/src/ctrl/server-connector.class.ts

@@ -8,26 +8,26 @@ import { Timer } from '../timer.class';
 
 export class ServerConnector {
   private subscriptions: Array<{ id: number; interval: number; server: Server }> = [];
+  private db!: Database;
 
-  constructor(db: Database) {
+  constructor() {
+    this.db = new Database();
     (async () => {
       try {
-        const serverList = await db.getAllServerConfigs();
+        await this.db.open();
+        const serverList = await this.db.getAllServerConfigs();
 
         serverList.forEach(async server => {
-          const serverDB = new Database();
-          await serverDB.open();
-
           let interval = Number(server.config['syncInterval']);
           if (Number.isNaN(interval)) interval = defaults.serverSync.interval;
 
           Logger.info('[INFO] Starting Server Sync Connector for', server.title, 'with interval', interval, 'seconds ...');
-          const id = Timer.instance.subscribe(interval, async () => await this.timerTick(server, serverDB));
+          const id = Timer.instance.subscribe(interval, async () => await this.timerTick(server));
           this.subscriptions.push({ id, interval, server });
 
           process.nextTick(async () => {
             Logger.info('[INFO] Initial Sync for', server.title, '...');
-            await this.timerTick(server, serverDB);
+            await this.timerTick(server);
             Logger.info('[SUCCESS] Initial Sync for', server.title, 'succeeded.');
           });
         });
@@ -39,7 +39,7 @@ export class ServerConnector {
     })();
   }
 
-  private async timerTick(server: Server, db: Database) {
+  private async timerTick(server: Server) {
     Logger.debug('[DEBUG] TICK', new Date(), JSON.stringify(server));
     if (process.env.DEV_MODE) return Logger.warn('[WARN] DEV_MODE active - sync inactive.');
 
@@ -54,7 +54,7 @@ export class ServerConnector {
       const data: ReducedData[] = response.data.data.map((entry: any) => ({ ...entry, time: new Date(entry.time) }));
 
       // Process data in DB
-      await db.insertServerData(server.id, data);
+      await this.db.insertServerData(server.id, data);
 
       // Commit Transaction
       await axios.patch(`http://${server.fqdn}:8890/${trxHdl}`, null, { responseType: 'json' });
@@ -72,4 +72,9 @@ export class ServerConnector {
       // if (!!db) await db.close();
     }
   }
+
+  async close() {
+    if (!this.db) return;
+    await this.db.close();
+  }
 }

+ 5 - 0
server/src/ctrl/sqlite-controller.base.ts

@@ -59,4 +59,9 @@ export abstract class SQLiteController {
   public async rollback() {
     await this.run('ROLLBACK;', []);
   }
+
+  async close() {
+    if (!this.db) return;
+    await new Promise<void>((res, rej) => this.db.close(err => (err ? rej(err) : res())));
+  }
 }

+ 2 - 2
server/src/index.ts

@@ -20,8 +20,8 @@ Logger.logLevel = LOG_LEVEL;
 
   const pool: ControllerPool = {
     db,
-    serverConnector: new ServerConnector(db),
-    httpChecks: new HttpCheckController(db)
+    serverConnector: new ServerConnector(),
+    httpChecks: new HttpCheckController()
   };
 
   Timer.instance.start();