fix: wait for charging stations to be stopped at simulator stop
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 26 May 2023 10:55:09 +0000 (12:55 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 26 May 2023 10:55:09 +0000 (12:55 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/charging-station/Bootstrap.ts
src/charging-station/ui-server/AbstractUIServer.ts
src/charging-station/ui-server/ui-services/AbstractUIService.ts

index 4a6bd75c2d7a2ebbce2e6ebb24c3ceab9019dba6..db884ed874285adae8c532ff6b3fccb175ec5fbb 100644 (file)
@@ -1,5 +1,6 @@
 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
 
+import { EventEmitter } from 'node:events';
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 import { type Worker, isMainThread } from 'node:worker_threads';
@@ -31,7 +32,7 @@ enum exitCodes {
   noChargingStationTemplates = 2,
 }
 
-export class Bootstrap {
+export class Bootstrap extends EventEmitter {
   private static instance: Bootstrap | null = null;
   public numberOfChargingStations!: number;
   public numberOfChargingStationTemplates!: number;
@@ -45,10 +46,9 @@ export class Bootstrap {
   private readonly workerScript: string;
 
   private constructor() {
+    super();
     for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) {
-      process.on(signal, () => {
-        this.gracefulShutdown().catch(Constants.EMPTY_FUNCTION);
-      });
+      process.on(signal, this.gracefulShutdown);
     }
     // Enable unconditionally for now
     ErrorUtils.handleUnhandledRejection();
@@ -130,10 +130,13 @@ export class Bootstrap {
   public async stop(): Promise<void> {
     if (isMainThread && this.started === true) {
       await this.uiServer?.sendBroadcastChannelRequest(
-        Utils.generateUUID(),
-        ProcedureName.STOP_CHARGING_STATION,
-        Constants.EMPTY_FREEZED_OBJECT
+        this.uiServer.buildProtocolRequest(
+          Utils.generateUUID(),
+          ProcedureName.STOP_CHARGING_STATION,
+          Constants.EMPTY_FREEZED_OBJECT
+        )
       );
+      await this.waitForChargingStationsStopped();
       await this.workerImplementation?.stop();
       this.workerImplementation = null;
       this.uiServer?.stop();
@@ -183,15 +186,22 @@ export class Bootstrap {
       switch (msg.id) {
         case ChargingStationWorkerMessageEvents.started:
           this.workerEventStarted(msg.data as ChargingStationData);
+          this.emit(ChargingStationWorkerMessageEvents.started, msg.data as ChargingStationData);
           break;
         case ChargingStationWorkerMessageEvents.stopped:
           this.workerEventStopped(msg.data as ChargingStationData);
+          this.emit(ChargingStationWorkerMessageEvents.stopped, msg.data as ChargingStationData);
           break;
         case ChargingStationWorkerMessageEvents.updated:
           this.workerEventUpdated(msg.data as ChargingStationData);
+          this.emit(ChargingStationWorkerMessageEvents.updated, msg.data as ChargingStationData);
           break;
         case ChargingStationWorkerMessageEvents.performanceStatistics:
           this.workerEventPerformanceStatistics(msg.data as Statistics);
+          this.emit(
+            ChargingStationWorkerMessageEvents.performanceStatistics,
+            msg.data as Statistics
+          );
           break;
         default:
           throw new BaseError(
@@ -282,14 +292,28 @@ export class Bootstrap {
     });
   }
 
-  private gracefulShutdown = async (): Promise<void> => {
+  private gracefulShutdown = (): void => {
     console.info(`${chalk.green('Graceful shutdown')}`);
-    try {
-      await this.stop();
-      process.exit(0);
-    } catch (error) {
-      process.exit(1);
-    }
+    this.stop()
+      .then(() => {
+        process.exit(0);
+      })
+      .catch((error) => {
+        console.error(chalk.red('Error while stopping charging stations simulator:'), error);
+        process.exit(1);
+      });
+  };
+
+  private waitForChargingStationsStopped = async (): Promise<void> => {
+    return new Promise((resolve) => {
+      let stoppedEvents = 0;
+      this.on(ChargingStationWorkerMessageEvents.stopped, () => {
+        ++stoppedEvents;
+        if (stoppedEvents === this.numberOfChargingStations) {
+          resolve();
+        }
+      });
+    });
   };
 
   private logPrefix = (): string => {
index 2f6f950b938fd94e74557e4b4f3206d5ae0e2939..af5c01295d16ce39d78906f429f15c22424917d5 100644 (file)
@@ -11,7 +11,7 @@ import {
   type ProcedureName,
   type ProtocolRequest,
   type ProtocolResponse,
-  type ProtocolVersion,
+  ProtocolVersion,
   type RequestPayload,
   type ResponsePayload,
   type UIServerConfiguration,
@@ -46,14 +46,10 @@ export abstract class AbstractUIServer {
     this.chargingStations.clear();
   }
 
-  public async sendBroadcastChannelRequest(
-    id: string,
-    procedureName: ProcedureName,
-    requestPayload: RequestPayload
-  ): Promise<void> {
-    for (const uiService of this.uiServices.values()) {
-      await uiService.requestHandler(this.buildProtocolRequest(id, procedureName, requestPayload));
-    }
+  public async sendBroadcastChannelRequest(request: ProtocolRequest): Promise<ProtocolResponse> {
+    const protocolVersion = ProtocolVersion['0.0.1'];
+    this.registerProtocolVersionUIService(protocolVersion);
+    return this.uiServices.get(protocolVersion)?.requestHandler(request);
   }
 
   protected startHttpServer(): void {
index 541966afbc98f2ad9317d126185ac15e5e4eace8..392a63edf296fb9179359fbb086ecf21e35f2821 100644 (file)
@@ -5,6 +5,7 @@ import {
   ProcedureName,
   type ProtocolRequest,
   type ProtocolRequestHandler,
+  type ProtocolResponse,
   type ProtocolVersion,
   type RequestPayload,
   type ResponsePayload,
@@ -65,11 +66,11 @@ export abstract class AbstractUIService {
     this.broadcastChannelRequests = new Map<string, number>();
   }
 
-  public async requestHandler(request: ProtocolRequest): Promise<void> {
+  public async requestHandler(request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
     let messageId: string;
     let command: ProcedureName;
     let requestPayload: RequestPayload | undefined;
-    let responsePayload: ResponsePayload;
+    let responsePayload: ResponsePayload | undefined;
     try {
       [messageId, command, requestPayload] = request;
 
@@ -98,11 +99,11 @@ export abstract class AbstractUIService {
         errorStack: (error as Error).stack,
         errorDetails: (error as OCPPError).details,
       };
-    } finally {
-      // Send response for payload not forwarded to broadcast channel
-      if (!Utils.isNullOrUndefined(responsePayload)) {
-        this.sendResponse(messageId, responsePayload);
-      }
+    }
+    // Send response
+    if (!Utils.isNullOrUndefined(responsePayload)) {
+      this.sendResponse(messageId, responsePayload);
+      return this.uiServer.buildProtocolResponse(messageId, responsePayload);
     }
   }