X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FBootstrap.ts;h=2c7f56fe4de94c1d7524e99957e68c1b8503c83c;hb=f23be6aad227f8942bb140d286585423fe84f60b;hp=37fe0e6b5ae7c5506df07f5d17b94af3b37abfcf;hpb=8cc482a9324a0989516b6eb6db85a16258c4b4d1;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts index 37fe0e6b..2c7f56fe 100644 --- a/src/charging-station/Bootstrap.ts +++ b/src/charging-station/Bootstrap.ts @@ -2,16 +2,16 @@ import { EventEmitter } from 'node:events'; import { dirname, extname, join } from 'node:path'; +import process, { exit } from 'node:process'; import { fileURLToPath } from 'node:url'; -import { isMainThread } from 'node:worker_threads'; import chalk from 'chalk'; import { availableParallelism } from 'poolifier'; -import { waitChargingStationEvents } from './ChargingStationUtils'; +import { waitChargingStationEvents } from './Helpers'; import type { AbstractUIServer } from './ui-server/AbstractUIServer'; import { UIServerFactory } from './ui-server/UIServerFactory'; -import { version } from '../../package.json' assert { type: 'json' }; +import { version } from '../../package.json'; import { BaseError } from '../exception'; import { type Storage, StorageFactory } from '../performance'; import { @@ -45,8 +45,10 @@ import { type WorkerAbstract, WorkerFactory } from '../worker'; const moduleName = 'Bootstrap'; enum exitCodes { + succeeded = 0, missingChargingStationsConfiguration = 1, noChargingStationTemplates = 2, + gracefulShutdownError = 3, } export class Bootstrap extends EventEmitter { @@ -54,7 +56,7 @@ export class Bootstrap extends EventEmitter { public numberOfChargingStations!: number; public numberOfChargingStationTemplates!: number; private workerImplementation: WorkerAbstract | null; - private readonly uiServer!: AbstractUIServer | null; + private readonly uiServer: AbstractUIServer | null; private readonly storage!: Storage; private numberOfStartedChargingStations!: number; private readonly version: string = version; @@ -67,7 +69,7 @@ export class Bootstrap extends EventEmitter { private constructor() { super(); for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) { - process.on(signal, this.gracefulShutdown); + process.on(signal, this.gracefulShutdown.bind(this)); } // Enable unconditionally for now handleUnhandledRejection(); @@ -82,11 +84,9 @@ export class Bootstrap extends EventEmitter { dirname(fileURLToPath(import.meta.url)), `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`, ); - const uiServerConfiguration = Configuration.getConfigurationSection( - ConfigurationSection.uiServer, + this.uiServer = UIServerFactory.getUIServerImplementation( + Configuration.getConfigurationSection(ConfigurationSection.uiServer), ); - uiServerConfiguration.enabled === true && - (this.uiServer = UIServerFactory.getUIServerImplementation(uiServerConfiguration)); const performanceStorageConfiguration = Configuration.getConfigurationSection( ConfigurationSection.performanceStorage, @@ -97,7 +97,7 @@ export class Bootstrap extends EventEmitter { performanceStorageConfiguration.uri!, this.logPrefix(), )); - Configuration.setConfigurationChangeCallback(async () => Bootstrap.getInstance().restart()); + Configuration.configurationChangeCallback = async () => Bootstrap.getInstance().restart(false); } public static getInstance(): Bootstrap { @@ -108,9 +108,6 @@ export class Bootstrap extends EventEmitter { } public async start(): Promise { - if (!isMainThread) { - throw new Error('Cannot start charging stations simulator from worker thread'); - } if (this.started === false) { if (this.starting === false) { this.starting = true; @@ -121,7 +118,8 @@ export class Bootstrap extends EventEmitter { this.initializeWorkerImplementation(workerConfiguration); await this.workerImplementation?.start(); await this.storage?.open(); - this.uiServer?.start(); + Configuration.getConfigurationSection(ConfigurationSection.uiServer) + .enabled === true && this.uiServer?.start(); // Start ChargingStation object instance in worker thread for (const stationTemplateUrl of Configuration.getStationTemplateUrls()!) { try { @@ -160,7 +158,7 @@ export class Bootstrap extends EventEmitter { Configuration.workerDynamicPoolInUse() && console.warn( chalk.yellow( - 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using static pool or worker set mode instead', + 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using fixed pool or worker set mode instead', ), ); console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info); @@ -174,36 +172,20 @@ export class Bootstrap extends EventEmitter { } } - public async stop(): Promise { - if (!isMainThread) { - throw new Error('Cannot stop charging stations simulator from worker thread'); - } + public async stop(stopChargingStations = true): Promise { if (this.started === true) { if (this.stopping === false) { this.stopping = true; - await this.uiServer?.sendInternalRequest( - this.uiServer.buildProtocolRequest( - generateUUID(), - ProcedureName.STOP_CHARGING_STATION, - Constants.EMPTY_FREEZED_OBJECT, - ), - ); - await Promise.race([ - waitChargingStationEvents( - this, - ChargingStationWorkerMessageEvents.stopped, - this.numberOfChargingStations, - ), - new Promise((resolve) => { - setTimeout(() => { - const message = `Timeout reached ${formatDurationMilliSeconds( - Constants.STOP_SIMULATOR_TIMEOUT, - )} at stopping charging stations simulator`; - console.warn(chalk.yellow(message)); - resolve(message); - }, Constants.STOP_SIMULATOR_TIMEOUT); - }), - ]); + if (stopChargingStations === true) { + await this.uiServer?.sendInternalRequest( + this.uiServer.buildProtocolRequest( + generateUUID(), + ProcedureName.STOP_CHARGING_STATION, + Constants.EMPTY_FROZEN_OBJECT, + ), + ); + await this.waitChargingStationsStopped(); + } await this.workerImplementation?.stop(); this.workerImplementation = null; this.uiServer?.stop(); @@ -220,17 +202,36 @@ export class Bootstrap extends EventEmitter { } } - public async restart(): Promise { - await this.stop(); + public async restart(stopChargingStations?: boolean): Promise { + await this.stop(stopChargingStations); await this.start(); } + private async waitChargingStationsStopped(): Promise { + await Promise.race([ + waitChargingStationEvents( + this, + ChargingStationWorkerMessageEvents.stopped, + this.numberOfChargingStations, + ), + new Promise((resolve) => { + setTimeout(() => { + const message = `Timeout ${formatDurationMilliSeconds( + Constants.STOP_SIMULATOR_TIMEOUT, + )} reached at stopping charging stations simulator`; + console.warn(chalk.yellow(message)); + resolve(message); + }, Constants.STOP_SIMULATOR_TIMEOUT); + }), + ]); + } + private initializeWorkerImplementation(workerConfiguration: WorkerConfiguration): void { let elementsPerWorker: number | undefined; if (workerConfiguration?.elementsPerWorker === 'auto') { elementsPerWorker = this.numberOfChargingStations > availableParallelism() - ? Math.round(this.numberOfChargingStations / availableParallelism()) + ? Math.round(this.numberOfChargingStations / (availableParallelism() * 1.5)) : 1; } this.workerImplementation === null && @@ -256,13 +257,10 @@ export class Bootstrap extends EventEmitter { // logger.debug( // `${this.logPrefix()} ${moduleName}.messageHandler: Worker channel message received: ${JSON.stringify( // msg, - // null, + // undefined, // 2, // )}`, // ); - if (isNullOrUndefined(msg?.event)) { - return; - } try { switch (msg.event) { case ChargingStationWorkerMessageEvents.started: @@ -284,9 +282,20 @@ export class Bootstrap extends EventEmitter { msg.data as Statistics, ); break; + case ChargingStationWorkerMessageEvents.startWorkerElementError: + logger.error( + `${this.logPrefix()} ${moduleName}.messageHandler: Error occured while starting worker element:`, + msg.data, + ); + this.emit(ChargingStationWorkerMessageEvents.startWorkerElementError, msg.data); + break; + case ChargingStationWorkerMessageEvents.startedWorkerElement: + break; default: throw new BaseError( - `Unknown event type: '${msg.event}' for data: ${JSON.stringify(msg.data, null, 2)}`, + `Unknown charging station worker event: '${ + msg.event + }' received with data: ${JSON.stringify(msg.data, undefined, 2)}`, ); } } catch (error) { @@ -344,13 +353,13 @@ export class Bootstrap extends EventEmitter { console.warn( chalk.yellow("'stationTemplateUrls' not defined or empty in configuration, exiting"), ); - process.exit(exitCodes.missingChargingStationsConfiguration); + exit(exitCodes.missingChargingStationsConfiguration); } if (this.numberOfChargingStations === 0) { console.warn( chalk.yellow('No charging station template enabled in configuration, exiting'), ); - process.exit(exitCodes.noChargingStationTemplates); + exit(exitCodes.noChargingStationTemplates); } this.initializedCounters = true; } @@ -377,17 +386,25 @@ export class Bootstrap extends EventEmitter { }); } - private gracefulShutdown = (): void => { - console.info(`${chalk.green('Graceful shutdown')}`); + private gracefulShutdown(): void { this.stop() .then(() => { - process.exit(0); + console.info(`${chalk.green('Graceful shutdown')}`); + // stop() asks for charging stations to stop by default + this.waitChargingStationsStopped() + .then(() => { + exit(exitCodes.succeeded); + }) + .catch((error) => { + console.error(chalk.red('Error while waiting for charging stations to stop: '), error); + exit(exitCodes.gracefulShutdownError); + }); }) .catch((error) => { console.error(chalk.red('Error while shutdowning charging stations simulator: '), error); - process.exit(1); + exit(exitCodes.gracefulShutdownError); }); - }; + } private logPrefix = (): string => { return logPrefix(' Bootstrap |');